Compare commits

...

80 Commits

Author SHA1 Message Date
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
110 changed files with 7376 additions and 2287 deletions

View File

@ -63,7 +63,7 @@ jobs:
# - name: Run Browserless Chrome # - name: Run Browserless Chrome
# run: docker run -d -p 3000:3000 --network host browserless/chrome:1.57-chrome-stable # run: docker run -d -p 3000:3000 --network host browserless/chrome:1.57-chrome-stable
- name: Run Chrome - 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 - name: Run openvidu-local-deployment
uses: OpenVidu/actions/start-openvidu-local-deployment@main uses: OpenVidu/actions/start-openvidu-local-deployment@main
- name: Start OpenVidu Call backend - name: Start OpenVidu Call backend
@ -100,7 +100,7 @@ jobs:
# - name: Run Browserless Chrome # - name: Run Browserless Chrome
# run: docker run -d -p 3000:3000 --network host browserless/chrome:1.57-chrome-stable # run: docker run -d -p 3000:3000 --network host browserless/chrome:1.57-chrome-stable
- name: Run Chrome - 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 - name: Run openvidu-local-deployment
uses: OpenVidu/actions/start-openvidu-local-deployment@main uses: OpenVidu/actions/start-openvidu-local-deployment@main
- name: Start OpenVidu Call backend - name: Start OpenVidu Call backend
@ -133,7 +133,7 @@ jobs:
# - name: Run Browserless Chrome # - name: Run Browserless Chrome
# run: docker run -d -p 3000:3000 --network host browserless/chrome:1.57-chrome-stable # run: docker run -d -p 3000:3000 --network host browserless/chrome:1.57-chrome-stable
- name: Run Chrome - 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 - name: Run openvidu-local-deployment
uses: OpenVidu/actions/start-openvidu-local-deployment@main uses: OpenVidu/actions/start-openvidu-local-deployment@main
- name: Start OpenVidu Call backend - name: Start OpenVidu Call backend
@ -166,7 +166,7 @@ jobs:
# - name: Run Browserless Chrome # - name: Run Browserless Chrome
# run: docker run -d -p 3000:3000 --network host browserless/chrome:1.57-chrome-stable # run: docker run -d -p 3000:3000 --network host browserless/chrome:1.57-chrome-stable
- name: Run Chrome - 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 - name: Run openvidu-local-deployment
uses: OpenVidu/actions/start-openvidu-local-deployment@main uses: OpenVidu/actions/start-openvidu-local-deployment@main
- name: Start OpenVidu Call backend - name: Start OpenVidu Call backend
@ -180,6 +180,38 @@ jobs:
- name: Cleanup - name: Cleanup
if: always() if: always()
uses: OpenVidu/actions/cleanup@main 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: e2e_chat:
needs: test_setup needs: test_setup
@ -199,7 +231,7 @@ jobs:
# - name: Run Browserless Chrome # - name: Run Browserless Chrome
# run: docker run -d -p 3000:3000 --network host browserless/chrome:1.57-chrome-stable # run: docker run -d -p 3000:3000 --network host browserless/chrome:1.57-chrome-stable
- name: Run Chrome - 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 - name: Run openvidu-local-deployment
uses: OpenVidu/actions/start-openvidu-local-deployment@main uses: OpenVidu/actions/start-openvidu-local-deployment@main
- name: Start OpenVidu Call backend - name: Start OpenVidu Call backend
@ -232,7 +264,7 @@ jobs:
# - name: Run Browserless Chrome # - name: Run Browserless Chrome
# run: docker run -d -p 3000:3000 --network host browserless/chrome:1.57-chrome-stable # run: docker run -d -p 3000:3000 --network host browserless/chrome:1.57-chrome-stable
- name: Run Chrome - 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 - name: Run openvidu-local-deployment
uses: OpenVidu/actions/start-openvidu-local-deployment@main uses: OpenVidu/actions/start-openvidu-local-deployment@main
- name: Start OpenVidu Call backend - name: Start OpenVidu Call backend
@ -265,7 +297,7 @@ jobs:
# - name: Run Browserless Chrome # - name: Run Browserless Chrome
# run: docker run -d -p 3000:3000 --network host browserless/chrome:1.57-chrome-stable # run: docker run -d -p 3000:3000 --network host browserless/chrome:1.57-chrome-stable
- name: Run Chrome - 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 - name: Run openvidu-local-deployment
uses: OpenVidu/actions/start-openvidu-local-deployment@main uses: OpenVidu/actions/start-openvidu-local-deployment@main
- name: Start OpenVidu Call backend - name: Start OpenVidu Call backend
@ -298,7 +330,7 @@ jobs:
# - name: Run Browserless Chrome # - name: Run Browserless Chrome
# run: docker run -d -p 3000:3000 --network host browserless/chrome:1.57-chrome-stable # run: docker run -d -p 3000:3000 --network host browserless/chrome:1.57-chrome-stable
- name: Run Chrome - 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 - name: Run openvidu-local-deployment
uses: OpenVidu/actions/start-openvidu-local-deployment@main uses: OpenVidu/actions/start-openvidu-local-deployment@main
- name: Start OpenVidu Call backend - name: Start OpenVidu Call backend
@ -331,7 +363,7 @@ jobs:
# - name: Run Browserless Chrome # - name: Run Browserless Chrome
# run: docker run -d -p 3000:3000 --network host browserless/chrome:1.57-chrome-stable # run: docker run -d -p 3000:3000 --network host browserless/chrome:1.57-chrome-stable
- name: Run Chrome - 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 - name: Run openvidu-local-deployment
uses: OpenVidu/actions/start-openvidu-local-deployment@main uses: OpenVidu/actions/start-openvidu-local-deployment@main
- name: Start OpenVidu Call backend - name: Start OpenVidu Call backend
@ -364,7 +396,7 @@ jobs:
# - name: Run Browserless Chrome # - name: Run Browserless Chrome
# run: docker run -d -p 3000:3000 --network host browserless/chrome:1.57-chrome-stable # run: docker run -d -p 3000:3000 --network host browserless/chrome:1.57-chrome-stable
- name: Run Chrome - 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 - name: Run openvidu-local-deployment
uses: OpenVidu/actions/start-openvidu-local-deployment@main uses: OpenVidu/actions/start-openvidu-local-deployment@main
- name: Start OpenVidu Call backend - name: Start OpenVidu Call backend
@ -397,7 +429,7 @@ jobs:
# - name: Run Browserless Chrome # - name: Run Browserless Chrome
# run: docker run -d -p 3000:3000 --network host browserless/chrome:1.57-chrome-stable # run: docker run -d -p 3000:3000 --network host browserless/chrome:1.57-chrome-stable
- name: Run Chrome - 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 - name: Run openvidu-local-deployment
uses: OpenVidu/actions/start-openvidu-local-deployment@main uses: OpenVidu/actions/start-openvidu-local-deployment@main
- name: Start OpenVidu Call backend - name: Start OpenVidu Call backend

View File

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

View File

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

View File

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

View File

@ -35,7 +35,7 @@
"@types/node": "20.12.14", "@types/node": "20.12.14",
"@types/selenium-webdriver": "4.1.16", "@types/selenium-webdriver": "4.1.16",
"@types/ws": "^8.5.12", "@types/ws": "^8.5.12",
"chromedriver": "136.0.2", "chromedriver": "138.0.0",
"concat": "^1.0.3", "concat": "^1.0.3",
"cross-env": "^7.0.3", "cross-env": "^7.0.3",
"eslint-config-prettier": "^9.1.0", "eslint-config-prettier": "^9.1.0",
@ -8126,9 +8126,9 @@
} }
}, },
"node_modules/chromedriver": { "node_modules/chromedriver": {
"version": "136.0.2", "version": "138.0.0",
"resolved": "https://registry.npmjs.org/chromedriver/-/chromedriver-136.0.2.tgz", "resolved": "https://registry.npmjs.org/chromedriver/-/chromedriver-138.0.0.tgz",
"integrity": "sha512-yJ52GN01edLYWYK/OspYBv3plzF08Ucdq4ukYigJGOX8dWr/tP5PXSZPWFPVarmbmcO57pNLP9Im8hsYljMEjw==", "integrity": "sha512-bJ/DNm5Y0TbqM71ARaAohTWVwcQ2SsWciYC5Q9Ul7DC/oTxm6B1vI2h6WscFCOOi49ul4tXZVjA/LOruljjmjA==",
"dev": true, "dev": true,
"hasInstallScript": true, "hasInstallScript": true,
"license": "Apache-2.0", "license": "Apache-2.0",
@ -8145,7 +8145,7 @@
"chromedriver": "bin/chromedriver" "chromedriver": "bin/chromedriver"
}, },
"engines": { "engines": {
"node": ">=18" "node": ">=20"
} }
}, },
"node_modules/cli-cursor": { "node_modules/cli-cursor": {

View File

@ -27,7 +27,7 @@
"@types/node": "20.12.14", "@types/node": "20.12.14",
"@types/selenium-webdriver": "4.1.16", "@types/selenium-webdriver": "4.1.16",
"@types/ws": "^8.5.12", "@types/ws": "^8.5.12",
"chromedriver": "136.0.2", "chromedriver": "138.0.0",
"concat": "^1.0.3", "concat": "^1.0.3",
"cross-env": "^7.0.3", "cross-env": "^7.0.3",
"eslint-config-prettier": "^9.1.0", "eslint-config-prettier": "^9.1.0",
@ -89,6 +89,7 @@
"e2e:nested-structural-directives": "tsc --project ./e2e && npx jasmine --fail-fast ./e2e/dist/nested-components/structural-directives.test.js", "e2e:nested-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-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-directives": "tsc --project ./e2e && npx jasmine --fail-fast ./e2e/dist/api-directives.test.js",
"e2e:lib-internal-directives": "tsc --project ./e2e && npx jasmine --fail-fast ./e2e/dist/internal-directives.test.js",
"e2e:lib-chat": "tsc --project ./e2e && npx jasmine --fail-fast ./e2e/dist/chat.test.js", "e2e:lib-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-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-media-devices": "tsc --project ./e2e && npx jasmine --fail-fast ./e2e/dist/media-devices.test.js",

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -72,6 +72,372 @@
text-align: center; 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 { .recording-message {
color: var(--ov-text-surface-color); color: var(--ov-text-surface-color);
} }
@ -80,14 +446,84 @@
color: var(--ov-error-color); color: var(--ov-error-color);
font-weight: 600; font-weight: 600;
} }
.recording-name {
font-size: 14px; .recording-error {
font-weight: bold; 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 { .disable-recording-btn {
font-size: 12px !important; background-color: var(--ov-secondary-action-color) !important;
font-style: italic; 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 { .not-allowed-message {
@ -96,25 +532,44 @@
} }
.recording-action-buttons { .recording-action-buttons {
margin-top: 20px; margin: 5px 0px;
margin-bottom: 20px;
} }
#start-recording-btn { #start-recording-btn {
width: 100%; width: 100%;
background-color: var(--ov-primary-action-color); background-color: var(--ov-primary-action-color);
color: var(--ov-secondary-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 { #stop-recording-btn {
width: 100%; width: 100%;
background-color: var(--ov-error-color); background-color: var(--ov-error-color);
color: var(--ov-secondary-action-color); color: var(--ov-secondary-action-color);
border-radius: var(--ov-surface-radius);
} }
#reset-recording-status-btn { #reset-recording-status-btn {
width: 100%; width: 100%;
background-color: var(--ov-secondary-action-color); background-color: var(--ov-accent-action-color);
border-radius: var(--ov-surface-radius);
} }
.recording-item { .recording-item {

View File

@ -1,5 +1,5 @@
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, EventEmitter, Input, OnInit, Output } from '@angular/core'; import { ChangeDetectionStrategy, ChangeDetectorRef, Component, EventEmitter, Input, OnInit, OnDestroy, Output } from '@angular/core';
import { Subscription } from 'rxjs'; import { Subject, takeUntil } from 'rxjs';
import { import {
RecordingDeleteRequestedEvent, RecordingDeleteRequestedEvent,
RecordingDownloadClickedEvent, RecordingDownloadClickedEvent,
@ -16,6 +16,7 @@ import { RecordingService } from '../../../../services/recording/recording.servi
import { OpenViduService } from '../../../../services/openvidu/openvidu.service'; import { OpenViduService } from '../../../../services/openvidu/openvidu.service';
import { ILogger } from '../../../../models/logger.model'; import { ILogger } from '../../../../models/logger.model';
import { LoggerService } from '../../../../services/logger/logger.service'; 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. * 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 add more than one recording type
// TODO: Allow to choose where the recording is stored (s3, google cloud, etc) // TODO: Allow to choose where the recording is stored (s3, google cloud, etc)
// TODO: Allow to choose the layout of the recording // TODO: Allow to choose the layout of the recording
export class RecordingActivityComponent implements OnInit { export class RecordingActivityComponent implements OnInit, OnDestroy {
/** /**
* @internal * @internal
*/ */
@ -67,6 +68,20 @@ export class RecordingActivityComponent implements OnInit {
*/ */
@Output() onRecordingPlayClicked: EventEmitter<RecordingPlayClickedEvent> = new EventEmitter<RecordingPlayClickedEvent>(); @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 * @internal
*/ */
@ -99,12 +114,53 @@ export class RecordingActivityComponent implements OnInit {
*/ */
recordingError: any; recordingError: any;
/**
* @internal
*/
hasRoomTracksPublished: boolean = false;
/** /**
* @internal * @internal
*/ */
mouseHovering: boolean = false; 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 log: ILogger;
private recordingStatusSubscription: Subscription; private destroy$ = new Subject<void>();
/** /**
* @internal * @internal
@ -115,7 +171,8 @@ export class RecordingActivityComponent implements OnInit {
private actionService: ActionService, private actionService: ActionService,
private openviduService: OpenViduService, private openviduService: OpenViduService,
private cd: ChangeDetectorRef, private cd: ChangeDetectorRef,
private loggerSrv: LoggerService private loggerSrv: LoggerService,
private libService: OpenViduComponentsConfigService
) { ) {
this.log = this.loggerSrv.get('RecordingActivityComponent'); this.log = this.loggerSrv.get('RecordingActivityComponent');
} }
@ -125,13 +182,23 @@ export class RecordingActivityComponent implements OnInit {
*/ */
ngOnInit(): void { ngOnInit(): void {
this.subscribeToRecordingStatus(); this.subscribeToRecordingStatus();
this.subscribeToTracksChanges();
this.subscribeToConfigChanges();
} }
/** /**
* @internal * @internal
*/ */
ngOnDestroy() { ngOnDestroy() {
if (this.recordingStatusSubscription) this.recordingStatusSubscription.unsubscribe(); this.destroy$.next();
this.destroy$.complete();
}
/**
* @internal
*/
trackByRecordingId(index: number, recording: RecordingInfo): string | undefined {
return recording.id;
} }
/** /**
@ -226,11 +293,105 @@ export class RecordingActivityComponent implements OnInit {
this.recordingService.playRecording(recording); 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() { 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; const { status, recordingList, error } = event;
this.recordingStatus = status; this.recordingStatus = status;
this.recordingList = recordingList; if (this.showRecordingList) {
this.recordingList = recordingList;
} else {
// Avoid showing recordings
this.recordingList = [];
}
this.recordingError = error; this.recordingError = error;
this.recordingAlive = this.recordingStatus === RecordingStatus.STARTED; this.recordingAlive = this.recordingStatus === RecordingStatus.STARTED;
if (this.recordingStatus !== RecordingStatus.FAILED) { if (this.recordingStatus !== RecordingStatus.FAILED) {
@ -239,4 +400,24 @@ export class RecordingActivityComponent implements OnInit {
this.cd.markForCheck(); 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-container" id="background-effects-container" [class.prejoin-mode]="mode === 'prejoin'">
<div class="panel-header-container"> @if (mode === 'meeting') {
<h3 class="panel-title">{{ 'PANEL.BACKGROUND.TITLE' | translate }}</h3> <div class="panel-header-container">
<button class="panel-close-button" mat-icon-button matTooltip="{{ 'PANEL.CLOSE' | translate }}" (click)="close()"> <h3 class="panel-title">{{ 'PANEL.BACKGROUND.TITLE' | translate }}</h3>
<mat-icon>close</mat-icon>
<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> </button>
</div> }
<div class="effects-container" fxFlex="100%" fxLayoutAlign="space-evenly none"> <div class="effects-container" fxFlex="100%" fxLayoutAlign="space-evenly none">
<div> <div>

View File

@ -1,5 +1,11 @@
.prejoin-mode {
margin: 0 10px 0px 10px;
max-height: 100%;
min-height: 100%;
}
.background-title { .background-title {
color: var(--ov-text-surface-color); color: var(--ov-text-surface-color);
margin: 10px 0;
} }
.effects-container { .effects-container {
display: block !important; 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 { Subscription } from 'rxjs';
import { BackgroundEffect, EffectType } from '../../../models/background-effect.model'; import { BackgroundEffect, EffectType } from '../../../models/background-effect.model';
import { PanelType } from '../../../models/panel.model'; import { PanelType } from '../../../models/panel.model';
@ -16,6 +16,9 @@ import { VirtualBackgroundService } from '../../../services/virtual-background/v
standalone: false standalone: false
}) })
export class BackgroundEffectsPanelComponent implements OnInit { export class BackgroundEffectsPanelComponent implements OnInit {
@Input() mode: 'prejoin' | 'meeting' = 'meeting';
@Output() onClose = new EventEmitter<void>();
backgroundSelectedId: string; backgroundSelectedId: string;
effectType = EffectType; effectType = EffectType;
backgroundImages: BackgroundEffect[] = []; backgroundImages: BackgroundEffect[] = [];
@ -53,7 +56,11 @@ export class BackgroundEffectsPanelComponent implements OnInit {
} }
close() { 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) { async applyBackground(effect: BackgroundEffect) {

View File

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

View File

@ -8,7 +8,7 @@ import {
Output, Output,
TemplateRef TemplateRef
} from '@angular/core'; } from '@angular/core';
import { skip, Subscription } from 'rxjs'; import { skip, Subject, takeUntil } from 'rxjs';
import { import {
ActivitiesPanelDirective, ActivitiesPanelDirective,
AdditionalPanelsDirective, AdditionalPanelsDirective,
@ -25,6 +25,7 @@ import {
} from '../../models/panel.model'; } from '../../models/panel.model';
import { PanelService } from '../../services/panel/panel.service'; import { PanelService } from '../../services/panel/panel.service';
import { BackgroundEffect } from '../../models/background-effect.model'; 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) @ContentChild(ParticipantsPanelDirective)
set externalParticipantPanel(externalParticipantsPanel: ParticipantsPanelDirective) { set externalParticipantPanel(externalParticipantsPanel: ParticipantsPanelDirective) {
// This directive will has value only when PARTICIPANTS PANEL component tagged with '*ovParticipantsPanel' this._externalParticipantPanel = externalParticipantsPanel;
// is inside of the PANEL component tagged with '*ovPanel'
if (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 * @ignore
*/ */
@ContentChild(ActivitiesPanelDirective) @ContentChild(ActivitiesPanelDirective)
set externalActivitiesPanel(externalActivitiesPanel: ActivitiesPanelDirective) { set externalActivitiesPanel(externalActivitiesPanel: ActivitiesPanelDirective) {
// This directive will has value only when ACTIVITIES PANEL component tagged with '*ovActivitiesPanel' this._externalActivitiesPanel = externalActivitiesPanel;
// is inside of the PANEL component tagged with '*ovPanel'
if (externalActivitiesPanel) { if (externalActivitiesPanel) {
this.activitiesPanelTemplate = externalActivitiesPanel.template; this.updateTemplatesAndMarkForCheck();
} }
} }
@ -119,10 +98,9 @@ export class PanelComponent implements OnInit {
*/ */
@ContentChild(ChatPanelDirective) @ContentChild(ChatPanelDirective)
set externalChatPanel(externalChatPanel: ChatPanelDirective) { set externalChatPanel(externalChatPanel: ChatPanelDirective) {
// This directive will has value only when CHAT PANEL component tagged with '*ovChatPanel' this._externalChatPanel = externalChatPanel;
// is inside of the PANEL component tagged with '*ovPanel'
if (externalChatPanel) { if (externalChatPanel) {
this.chatPanelTemplate = externalChatPanel.template; this.updateTemplatesAndMarkForCheck();
} }
} }
@ -131,10 +109,9 @@ export class PanelComponent implements OnInit {
*/ */
@ContentChild(AdditionalPanelsDirective) @ContentChild(AdditionalPanelsDirective)
set externalAdditionalPanels(externalAdditionalPanels: AdditionalPanelsDirective) { set externalAdditionalPanels(externalAdditionalPanels: AdditionalPanelsDirective) {
// This directive will has value only when ADDITIONAL PANELS component tagged with '*ovPanelAdditionalPanels' this._externalAdditionalPanels = externalAdditionalPanels;
// is inside of the PANEL component tagged with '*ovPanel'
if (externalAdditionalPanels) { if (externalAdditionalPanels) {
this.additionalPanelsTemplate = externalAdditionalPanels.template; this.updateTemplatesAndMarkForCheck();
} }
} }
@ -195,7 +172,20 @@ export class PanelComponent implements OnInit {
* @internal * @internal
*/ */
isExternalPanelOpened: boolean; 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< private panelEmitersHandler: Map<
PanelType, PanelType,
@ -207,30 +197,78 @@ export class PanelComponent implements OnInit {
*/ */
constructor( constructor(
private panelService: PanelService, private panelService: PanelService,
private cd: ChangeDetectorRef private cd: ChangeDetectorRef,
private templateManagerService: TemplateManagerService
) {} ) {}
/** /**
* @ignore * @ignore
*/ */
ngOnInit(): void { ngOnInit(): void {
this.setupTemplates();
this.subscribeToPanelToggling(); this.subscribeToPanelToggling();
this.panelEmitersHandler.set(PanelType.CHAT, this.onChatPanelStatusChanged); this.panelEmitersHandler.set(PanelType.CHAT, this.onChatPanelStatusChanged);
this.panelEmitersHandler.set(PanelType.PARTICIPANTS, this.onParticipantsPanelStatusChanged); this.panelEmitersHandler.set(PanelType.PARTICIPANTS, this.onParticipantsPanelStatusChanged);
this.panelEmitersHandler.set(PanelType.SETTINGS, this.onSettingsPanelStatusChanged); this.panelEmitersHandler.set(PanelType.SETTINGS, this.onSettingsPanelStatusChanged);
this.panelEmitersHandler.set(PanelType.ACTIVITIES, this.onActivitiesPanelStatusChanged); 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 * @ignore
*/ */
ngOnDestroy() { ngOnDestroy() {
this.isChatPanelOpened = false; this.isChatPanelOpened = false;
this.isParticipantsPanelOpened = false; this.isParticipantsPanelOpened = false;
if (this.panelSubscription) this.panelSubscription.unsubscribe(); this.destroy$.next();
this.destroy$.complete();
} }
private subscribeToPanelToggling() { 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.isChatPanelOpened = ev.isOpened && ev.panelType === PanelType.CHAT;
this.isParticipantsPanelOpened = ev.isOpened && ev.panelType === PanelType.PARTICIPANTS; this.isParticipantsPanelOpened = ev.isOpened && ev.panelType === PanelType.PARTICIPANTS;
this.isBackgroundEffectsPanelOpened = ev.isOpened && ev.panelType === PanelType.BACKGROUND_EFFECTS; this.isBackgroundEffectsPanelOpened = ev.isOpened && ev.panelType === PanelType.BACKGROUND_EFFECTS;

View File

@ -1,33 +1,71 @@
<mat-list> <mat-list>
<mat-list-item> <mat-list-item>
<div matListItemIcon class="participant-avatar" [style.background-color]="_participant.colorProfile"> <!-- Main participant container with improved structure -->
<mat-icon>person</mat-icon> <div class="participant-container" [attr.data-participant-id]="_participant?.sid">
</div> <!-- Avatar section with dynamic color -->
<h3 matListItemTitle class="participant-name">{{ _participant.name }} <div
<span *ngIf="_participant.isLocal"> ({{ 'PANEL.PARTICIPANTS.YOU' | translate }})</span> class="participant-avatar"
</h3> [style.background-color]="_participant?.colorProfile"
<p matListItemLine class="participant-subtitle">{{ _participant | tracksPublishedTypes }}</p> [attr.aria-label]="'Avatar for ' + participantDisplayName"
<!-- <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"
> >
<mat-icon *ngIf="!_participant.isMutedForcibly">volume_up</mat-icon> <mat-icon>person</mat-icon>
<mat-icon *ngIf="_participant.isMutedForcibly">volume_off</mat-icon> </div>
</button>
<!-- External item elements --> <!-- Content section with name and status -->
<ng-container *ngIf="participantPanelItemElementsTemplate"> <div class="participant-content">
<ng-container *ngTemplateOutlet="participantPanelItemElementsTemplate"></ng-container> <div class="participant-name">
</ng-container> {{ 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> </div>
</mat-list-item> </mat-list-item>
</mat-list> </mat-list>

View File

@ -1,68 +1,443 @@
:host { :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 { .participant-avatar {
display: inherit; display: flex;
align-items: center;
justify-content: center;
width: 40px;
height: 40px;
border-radius: var(--ov-surface-radius); border-radius: var(--ov-surface-radius);
margin: auto !important; margin-right: 12px;
padding: 10px; padding: 0;
color: #000000; 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 { // Main content area
font-style: italic; .participant-content {
font-size: 11px !important; flex: 1;
margin: 0; display: flex;
color: var(--ov-text-surface-color); flex-direction: column;
min-width: 0; // Allows text truncation
margin-right: 8px;
} }
// Participant name styling
.participant-name { .participant-name {
font-weight: bold !important; font-weight: 600 !important;
color: var(--ov-text-surface-color); 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 { .participant-action-buttons {
display: flex; display: flex;
align-items: center;
gap: 4px;
flex-shrink: 0;
margin-left: auto;
} }
::ng-deep .participant-action-buttons > *:not(#mute-btn) { // Mute button styling
display: contents; #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) > * { // Participant badges container
margin: auto; .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 { ::ng-deep .mat-mdc-list-item {
height: max-content !important; height: auto !important;
padding-bottom: 10px !important; padding: 0 !important;
} min-height: auto !important;
border-radius: var(--ov-surface-radius, 8px);
::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;
} }
::ng-deep .mdc-list-item__content { ::ng-deep .mdc-list-item__content {
padding-left: 10px !important; padding: 0 !important;
align-self: center !important; align-self: stretch !important;
width: 100%;
} }
::ng-deep .mat-mdc-list-base { ::ng-deep .mat-mdc-list-base {
--mdc-list-list-item-hover-label-text-color: unset; --mdc-list-list-item-hover-label-text-color: unset;
--mdc-list-list-item-hover-leading-icon-color: unset; --mdc-list-list-item-hover-leading-icon-color: unset;
padding: 0;
} }
#mute-btn { ::ng-deep .mat-mdc-list-item:hover {
border-radius: 50%; background-color: transparent !important;
color: var(--ov-text-surface-color);
} }
.warn-btn { // Animations
/* background-color: var(--ov-error-color) !important; */ @keyframes fadeIn {
color: var(--ov-error-color); 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 { ChangeDetectionStrategy, ChangeDetectorRef, Component, ContentChild, Input, OnDestroy, OnInit, TemplateRef } from '@angular/core';
import { Subscription } from 'rxjs'; import { Subscription } from 'rxjs';
import { ParticipantPanelItemElementsDirective } from '../../../../directives/template/openvidu-components-angular.directive'; import { ParticipantPanelItemElementsDirective } from '../../../../directives/template/openvidu-components-angular.directive';
import { ParticipantPanelParticipantBadgeDirective } from '../../../../directives/template/internals.directive';
import { ParticipantModel } from '../../../../models/participant.model'; import { ParticipantModel } from '../../../../models/participant.model';
import { OpenViduComponentsConfigService } from '../../../../services/config/directive-config.service'; import { OpenViduComponentsConfigService } from '../../../../services/config/directive-config.service';
import { ParticipantService } from '../../../../services/participant/participant.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}. * 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({ @Component({
selector: 'ov-participant-panel-item', selector: 'ov-participant-panel-item',
templateUrl: './participant-panel-item.component.html', templateUrl: './participant-panel-item.component.html',
@ -35,40 +36,69 @@ export class ParticipantPanelItemComponent implements OnInit, OnDestroy {
*/ */
@ContentChild(ParticipantPanelItemElementsDirective) @ContentChild(ParticipantPanelItemElementsDirective)
set externalItemElements(externalItemElements: ParticipantPanelItemElementsDirective) { set externalItemElements(externalItemElements: ParticipantPanelItemElementsDirective) {
// This directive will has value only when ITEM ELEMENTS component tagget with '*ovParticipantPanelItemElements' directive this._externalItemElements = externalItemElements;
// is inside of the P PANEL ITEM component tagged with '*ovParticipantPanelItem' directive
if (externalItemElements) { if (externalItemElements) {
this.participantPanelItemElementsTemplate = externalItemElements.template; this.updateTemplatesAndMarkForCheck();
} }
} }
/** /**
* The participant to be displayed
* @ignore * @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() @Input()
set participant(participant: ParticipantModel) { set participant(participant: ParticipantModel) {
this._participant = participant; this._participant = participant;
this.cd.markForCheck();
} }
/** /**
* @ignore * @internal
* Current participant being displayed
*/ */
_participant: ParticipantModel; _participant: ParticipantModel;
/**
* Whether to show the mute button for remote participants
*/
@Input()
muteButton: boolean = true;
/** /**
* @ignore * @ignore
*/ */
constructor( constructor(
private libService: OpenViduComponentsConfigService, private libService: OpenViduComponentsConfigService,
private participantService: ParticipantService, private participantService: ParticipantService,
private cd: ChangeDetectorRef private cd: ChangeDetectorRef,
private templateManagerService: TemplateManagerService
) {} ) {}
/** /**
* @ignore * @ignore
*/ */
ngOnInit(): void { ngOnInit(): void {
this.setupTemplates();
this.subscribeToParticipantPanelItemDirectives(); this.subscribeToParticipantPanelItemDirectives();
} }
@ -80,14 +110,72 @@ export class ParticipantPanelItemComponent implements OnInit, OnDestroy {
} }
/** /**
* @ignore * Toggles the mute state of a remote participant
*/ */
toggleMuteForcibly() { toggleMuteForcibly() {
if (this._participant) { if (this._participant && !this._participant.isLocal) {
this.participantService.setRemoteMutedForcibly(this._participant.sid, !this._participant.isMutedForcibly); 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() { private subscribeToParticipantPanelItemDirectives() {
this.muteButtonSub = this.libService.participantItemMuteButton$.subscribe((value: boolean) => { this.muteButtonSub = this.libService.participantItemMuteButton$.subscribe((value: boolean) => {
this.showMuteButton = value; this.showMuteButton = value;

View File

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

View File

@ -13,8 +13,10 @@ import {
import { ParticipantService } from '../../../../services/participant/participant.service'; import { ParticipantService } from '../../../../services/participant/participant.service';
import { PanelService } from '../../../../services/panel/panel.service'; import { PanelService } from '../../../../services/panel/panel.service';
import { ParticipantPanelItemDirective } from '../../../../directives/template/openvidu-components-angular.directive'; 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 { 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}. * 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>; @ContentChild('participantPanelItem', { read: TemplateRef }) participantPanelItemTemplate: TemplateRef<any>;
/**
* @ignore
*/
@ContentChild('participantPanelAfterLocalParticipant', { read: TemplateRef })
participantPanelAfterLocalParticipantTemplate: TemplateRef<any>;
/** /**
* @ignore * @ignore
*/ */
@ContentChild(ParticipantPanelItemDirective) @ContentChild(ParticipantPanelItemDirective)
set externalParticipantPanelItem(externalParticipantPanelItem: ParticipantPanelItemDirective) { set externalParticipantPanelItem(externalParticipantPanelItem: ParticipantPanelItemDirective) {
// This directive will has value only when PARTICIPANT PANEL ITEM component tagged with '*ovParticipantPanelItem' this._externalParticipantPanelItem = externalParticipantPanelItem;
// is inside of the PARTICIPANTS PANEL component tagged with '*ovParticipantsPanel'
if (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 * @ignore
@ -69,32 +84,26 @@ export class ParticipantsPanelComponent implements OnInit, OnDestroy, AfterViewI
constructor( constructor(
private participantService: ParticipantService, private participantService: ParticipantService,
private panelService: PanelService, private panelService: PanelService,
private cd: ChangeDetectorRef private cd: ChangeDetectorRef,
private templateManagerService: TemplateManagerService,
private libService: OpenViduComponentsConfigService
) {} ) {}
/** /**
* @ignore * @ignore
*/ */
ngOnInit(): void { ngOnInit(): void {
this.localParticipantSubs = this.participantService.localParticipant$.subscribe((p: ParticipantModel | undefined) => { this.setupTemplates();
if (p) {
this.localParticipant = p;
this.cd.markForCheck();
}
});
this.remoteParticipantsSubs = this.participantService.remoteParticipants$.subscribe((p: ParticipantModel[]) => { this.subscribeToParticipantsChanges();
this.remoteParticipants = p;
this.cd.markForCheck();
});
} }
/** /**
* @ignore * @ignore
*/ */
ngOnDestroy() { ngOnDestroy() {
if (this.localParticipantSubs) this.localParticipantSubs.unsubscribe(); this.destroy$.next();
if (this.remoteParticipantsSubs) this.remoteParticipantsSubs.unsubscribe; 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 * @ignore
*/ */

View File

@ -54,7 +54,7 @@
::ng-deep .lang-selector .expand-more-icon, ::ng-deep .lang-selector .expand-more-icon,
::ng-deep .lang-selector mat-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, ::ng-deep .lang-selector div,

View File

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

View File

@ -1,64 +1,118 @@
<div class="container" id="prejoin-container"> <div class="prejoin-container" id="prejoin-container">
<!-- Top Language Toolbar -->
<div *ngIf="isLoading" id="loading-container"> <div class="top-toolbar" *ngIf="!isMinimal">
<mat-spinner [diameter]="50"></mat-spinner> <ov-lang-selector [compact]="false" class="language-selector" (onLangChanged)="onLangChanged.emit($event)"> </ov-lang-selector>
<span>{{ 'PREJOIN.PREPARING' | translate }}</span>
</div> </div>
<div *ngIf="!isLoading" id="prejoin-card"> <!-- Loading State -->
<ov-lang-selector *ngIf="!isMinimal" [compact]="true" class="lang-btn" (onLangChanged)="onLangChanged.emit($event)"> @if (isLoading) {
</ov-lang-selector> <div class="loading-overlay">
<div class="loading-content">
<div> <mat-spinner [diameter]="40"></mat-spinner>
<div class="video-container"> <span class="loading-text">{{ 'PREJOIN.PREPARING' | translate }}</span>
<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>
</div> </div>
</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)"
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 -->
<div class="background-control" *ngIf="backgroundEffectEnabled">
<button
mat-icon-button
class="background-button"
(click)="toggleBackgroundPanel()"
[matTooltip]="'Virtual Backgrounds'"
[disabled]="!isVideoEnabled"
>
<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="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">
<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"
[disabled]="showParticipantName && !participantName"
>
<mat-icon class="join-icon">videocam</mat-icon>
{{ 'PREJOIN.JOIN' | translate }}
</button>
</div>
</div>
}
</div>
</div>
}
</div> </div>

View File

@ -1,159 +1,411 @@
:host { :host {
.container { display: block;
height: 100%; width: 100%;
background-color: var(--ov-background-color); height: 100%;
display: flex;
justify-content: center;
align-items: center;
}
#loading-container { .prejoin-container {
position: absolute; min-height: 100vh;
top: 40%; background: var(--ov-background-color);
left: 0; display: flex;
right: 0; align-items: center;
text-align: center; justify-content: center;
color: var(--ov-text-primary-color); padding: 20px;
.mat-mdc-progress-spinner { box-sizing: border-box;
margin: auto; position: relative;
transition: all 0.3s ease;
.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; display: flex;
flex-direction: column; flex-direction: column;
align-items: center; box-shadow: 0 8px 32px rgba(0, 0, 0, 0.12);
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;
} }
::ng-deep .lang-btn { // Video Preview Section
position: absolute; .video-preview-section {
top: 10px; padding: 0;
right: 10px; .video-preview-container {
height: 25px !important; position: relative;
font-size: 14px !important; 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 { .vb-container {
color: var(--ov-text-surface-color) !important; height: fit-content;
}
.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);
overflow: hidden; overflow: hidden;
} }
.media-controls-container { // Configuration Section
.configuration-section {
padding: 24px 24px 24px; // Added top padding since video has no padding
display: flex; display: flex;
flex-wrap: wrap; flex-direction: column;
justify-content: space-between; gap: 20px;
width: 100%;
margin-top: 15px;
height: auto;
}
.participant-name-container { .input-section {
display: block !important; ::ng-deep .name-input {
width: 100%; .mat-mdc-form-field {
margin: 10px 0; width: 100%;
}
.video-controls-container, .mat-mdc-text-field-wrapper {
.audio-controls-container { border-radius: var(--ov-surface-radius);
width: calc(50% - 10px); background-color: var(--ov-input-background, #f8f9fa);
margin: 5px 0; border: 1px solid var(--ov-border-color, #e0e0e0);
} transition: all 0.2s ease;
.join-btn-container { &:hover {
width: 100%; border-color: var(--ov-primary-action-color, #4285f4);
margin-top: 15px; }
}
#join-button { &.mdc-text-field--focused {
background-color: var(--ov-primary-action-color); border-color: var(--ov-primary-action-color, #4285f4);
color: var(--ov-secondary-action-color); box-shadow: 0 0 0 3px rgba(66, 133, 244, 0.1);
font-weight: bold; }
border-radius: var(--ov-surface-radius); }
width: 100%;
height: 50px;
transition: background-color 0.3s;
}
// #join-button:hover { .mat-mdc-form-field-subscript-wrapper {
// background-color: lighten(var(--ov-primary-action-color), 10%); display: none;
// } }
.error { input {
font-size: 12px; font-size: 16px;
font-weight: bold; font-weight: 500;
font-style: italic; color: var(--ov-text-primary-color, #333);
color: var(--ov-error-color); padding: 16px;
margin-top: 5px;
}
@media (max-width: 768px) { &::placeholder {
#prejoin-card { color: var(--ov-text-secondary-color, #666);
padding: 10px; font-weight: 400;
}
}
}
}
} }
.video-container { .error-message {
height: 40vh; display: flex;
}
.media-controls-container {
flex-direction: column;
align-items: center; 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, .join-section {
.audio-controls-container { .join-button {
width: 100%; 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) { // Responsive Design
.media-controls-container { @media (max-width: 640px) {
flex-direction: row; .prejoin-container {
justify-content: space-between; padding: 16px;
min-height: 100vh;
} }
.video-controls-container, .prejoin-main {
.audio-controls-container { max-width: 100%;
width: 48%; 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) { @media (max-width: 480px) {
.video-container { .prejoin-container {
height: 30vh; padding: 12px;
} }
.media-controls-container { .configuration-section {
height: auto; 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, OnInit,
Output Output
} from '@angular/core'; } from '@angular/core';
import { Subscription } from 'rxjs'; import { animate, state, style, transition, trigger } from '@angular/animations';
import { filter, Subject, takeUntil, tap } from 'rxjs';
import { ILogger } from '../../models/logger.model'; import { ILogger } from '../../models/logger.model';
import { CdkOverlayService } from '../../services/cdk-overlay/cdk-overlay.service'; import { CdkOverlayService } from '../../services/cdk-overlay/cdk-overlay.service';
import { OpenViduComponentsConfigService } from '../../services/config/directive-config.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 { LocalTrack } from 'livekit-client';
import { CustomDevice } from '../../models/device.model'; import { CustomDevice } from '../../models/device.model';
import { LangOption } from '../../models/lang.model'; import { LangOption } from '../../models/lang.model';
import { StorageService } from '../../services/storage/storage.service';
/** /**
* @internal * @internal
@ -29,7 +29,47 @@ import { StorageService } from '../../services/storage/storage.service';
templateUrl: './pre-join.component.html', templateUrl: './pre-join.component.html',
styleUrls: ['./pre-join.component.scss'], styleUrls: ['./pre-join.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush, 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 { export class PreJoinComponent implements OnInit, OnDestroy {
@Input() set error(error: { name: string; message: string } | undefined) { @Input() set error(error: { name: string; message: string } | undefined) {
@ -43,7 +83,6 @@ export class PreJoinComponent implements OnInit, OnDestroy {
@Output() onReadyToJoin = new EventEmitter<any>(); @Output() onReadyToJoin = new EventEmitter<any>();
_error: string | undefined; _error: string | undefined;
windowSize: number; windowSize: number;
isLoading = true; isLoading = true;
participantName: string | undefined = ''; participantName: string | undefined = '';
@ -57,15 +96,16 @@ export class PreJoinComponent implements OnInit, OnDestroy {
showLogo: boolean = true; showLogo: boolean = true;
showParticipantName: boolean = true; showParticipantName: boolean = true;
// Future feature preparation
backgroundEffectEnabled: boolean = true; // Enable virtual backgrounds by default
showBackgroundPanel: boolean = false;
videoTrack: LocalTrack | undefined; videoTrack: LocalTrack | undefined;
audioTrack: LocalTrack | undefined; audioTrack: LocalTrack | undefined;
isVideoEnabled: boolean = false;
private tracks: LocalTrack[]; private tracks: LocalTrack[];
private log: ILogger; private log: ILogger;
private cameraButtonSub: Subscription; private destroy$ = new Subject<void>();
private microphoneButtonSub: Subscription;
private minimalSub: Subscription;
private displayLogoSub: Subscription;
private displayParticipantNameSub: Subscription;
private shouldRemoveTracksWhenComponentIsDestroyed: boolean = true; private shouldRemoveTracksWhenComponentIsDestroyed: boolean = true;
@HostListener('window:resize') @HostListener('window:resize')
@ -78,7 +118,6 @@ export class PreJoinComponent implements OnInit, OnDestroy {
private libService: OpenViduComponentsConfigService, private libService: OpenViduComponentsConfigService,
private cdkSrv: CdkOverlayService, private cdkSrv: CdkOverlayService,
private openviduService: OpenViduService, private openviduService: OpenViduService,
private storageService: StorageService,
private translateService: TranslateService, private translateService: TranslateService,
private changeDetector: ChangeDetectorRef private changeDetector: ChangeDetectorRef
) { ) {
@ -99,29 +138,19 @@ export class PreJoinComponent implements OnInit, OnDestroy {
// } // }
async ngOnDestroy() { async ngOnDestroy() {
this.destroy$.next();
this.destroy$.complete();
this.cdkSrv.setSelector('body'); 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) { if (this.shouldRemoveTracksWhenComponentIsDestroyed) {
this.tracks.forEach((track) => { this.tracks?.forEach((track) => {
track.stop(); track.stop();
}); });
} }
} }
private async initializeDevices() { private async initializeDevices() {
try { await this.initializeDevicesWithRetry();
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);
}
} }
onDeviceSelectorClicked() { onDeviceSelectorClicked() {
@ -130,65 +159,94 @@ export class PreJoinComponent implements OnInit, OnDestroy {
this.cdkSrv.setSelector('#prejoin-container'); this.cdkSrv.setSelector('#prejoin-container');
} }
joinSession() { join() {
if (this.showParticipantName && !this.participantName) { if (this.showParticipantName && !this.participantName?.trim()) {
this._error = this.translateService.translate('PREJOIN.NICKNAME_REQUIRED'); this._error = this.translateService.translate('PREJOIN.NICKNAME_REQUIRED');
return; return;
} }
// Clear any previous errors
this._error = undefined;
// Mark tracks as permanent for avoiding to be removed in ngOnDestroy // Mark tracks as permanent for avoiding to be removed in ngOnDestroy
this.shouldRemoveTracksWhenComponentIsDestroyed = false; this.shouldRemoveTracksWhenComponentIsDestroyed = false;
// Assign participant name to the observable if it is defined // 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(); // Wait for the next tick to ensure the participant name propagates
// through the observable before emitting onReadyToJoin
this.libService.participantName$
.pipe(
takeUntil(this.destroy$),
filter((name) => name === this.participantName?.trim()),
tap(() => this.onReadyToJoin.emit())
)
.subscribe();
} else {
// No participant name to set, emit immediately
this.onReadyToJoin.emit();
}
} }
onParticipantNameChanged(name: string) { 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() { onEnterPressed() {
this.joinSession(); this.join();
} }
private subscribeToPrejoinDirectives() { private subscribeToPrejoinDirectives() {
this.minimalSub = this.libService.minimal$.subscribe((value: boolean) => { this.libService.minimal$.pipe(takeUntil(this.destroy$)).subscribe((value: boolean) => {
this.isMinimal = value; this.isMinimal = value;
this.changeDetector.markForCheck(); 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.showCameraButton = value;
this.changeDetector.markForCheck(); 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.showMicrophoneButton = value;
this.changeDetector.markForCheck(); 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.showLogo = value;
this.changeDetector.markForCheck(); this.changeDetector.markForCheck();
}); });
this.libService.participantName$.subscribe((value: string) => {
this.libService.participantName$.pipe(takeUntil(this.destroy$)).subscribe((value: string) => {
if (value) { if (value) {
this.participantName = value; this.participantName = value;
this.changeDetector.markForCheck(); 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.showParticipantName = value;
this.changeDetector.markForCheck(); this.changeDetector.markForCheck();
}); });
} }
async videoEnabledChanged(enabled: boolean) { 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); const newVideoTrack = await this.openviduService.createLocalTracks(true, false);
this.videoTrack = newVideoTrack[0]; this.videoTrack = newVideoTrack[0];
this.tracks.push(this.videoTrack); this.tracks.push(this.videoTrack);
this.openviduService.setLocalTracks(this.tracks); this.openviduService.setLocalTracks(this.tracks);
} }
this.onVideoEnabledChanged.emit(enabled); this.onVideoEnabledChanged.emit(enabled);
} }
@ -201,4 +259,68 @@ export class PreJoinComponent implements OnInit, OnDestroy {
} }
this.onAudioEnabledChanged.emit(enabled); 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 { ILogger } from '../../models/logger.model';
import { animate, style, transition, trigger } from '@angular/animations'; import { animate, style, transition, trigger } from '@angular/animations';
import { MatDrawerContainer, MatSidenav } from '@angular/material/sidenav'; 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 { SidenavMode } from '../../models/layout.model';
import { PanelStatusInfo, PanelType } from '../../models/panel.model'; import { PanelStatusInfo, PanelType } from '../../models/panel.model';
import { DataTopic } from '../../models/data-topic.model'; import { DataTopic } from '../../models/data-topic.model';
@ -48,6 +48,7 @@ import {
} from 'livekit-client'; } from 'livekit-client';
import { ParticipantLeftEvent, ParticipantLeftReason, ParticipantModel } from '../../models/participant.model'; import { ParticipantLeftEvent, ParticipantLeftReason, ParticipantModel } from '../../models/participant.model';
import { RecordingStatus } from '../../models/recording.model'; import { RecordingStatus } from '../../models/recording.model';
import { TemplateManagerService, SessionTemplateConfiguration } from '../../services/template/template-manager.service';
/** /**
* @internal * @internal
@ -82,7 +83,7 @@ export class SessionComponent implements OnInit, OnDestroy {
/** /**
* Provides event notifications that fire when participant is disconnected from Room. * 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>(); @Output() onRoomDisconnected: EventEmitter<void> = new EventEmitter<void>();
@ -103,12 +104,16 @@ export class SessionComponent implements OnInit, OnDestroy {
drawer: MatDrawerContainer; drawer: MatDrawerContainer;
loading: boolean = true; loading: boolean = true;
/**
* @internal
* Template configuration managed by the service
*/
templateConfig: SessionTemplateConfiguration = {};
private shouldDisconnectRoomWhenComponentIsDestroyed: boolean = true; private shouldDisconnectRoomWhenComponentIsDestroyed: boolean = true;
private readonly SIDENAV_WIDTH_LIMIT_MODE = 790; private readonly SIDENAV_WIDTH_LIMIT_MODE = 790;
private menuSubscription: Subscription; private destroy$ = new Subject<void>();
private layoutWidthSubscription: Subscription;
private updateLayoutInterval: NodeJS.Timeout; private updateLayoutInterval: NodeJS.Timeout;
private captionLanguageSubscription: Subscription;
private log: ILogger; private log: ILogger;
constructor( constructor(
@ -125,9 +130,11 @@ export class SessionComponent implements OnInit, OnDestroy {
private translateService: TranslateService, private translateService: TranslateService,
// private captionService: CaptionService, // private captionService: CaptionService,
private backgroundService: VirtualBackgroundService, private backgroundService: VirtualBackgroundService,
private cd: ChangeDetectorRef private cd: ChangeDetectorRef,
private templateManagerService: TemplateManagerService
) { ) {
this.log = this.loggerSrv.get('SessionComponent'); this.log = this.loggerSrv.get('SessionComponent');
this.setupTemplates();
} }
@HostListener('window:beforeunload') @HostListener('window:beforeunload')
@ -191,7 +198,29 @@ export class SessionComponent implements OnInit, OnDestroy {
async ngOnInit() { async ngOnInit() {
this.shouldDisconnectRoomWhenComponentIsDestroyed = true; 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.subscribeToCaptionLanguage();
this.subcribeToActiveSpeakersChanged(); this.subcribeToActiveSpeakersChanged();
@ -204,14 +233,15 @@ export class SessionComponent implements OnInit, OnDestroy {
// this.subscribeToParticipantNameChanged(); // this.subscribeToParticipantNameChanged();
this.subscribeToDataMessage(); this.subscribeToDataMessage();
this.subscribeToReconnection(); this.subscribeToReconnection();
this.subscribeToVirtualBackground();
if (this.libService.isRecordingEnabled()) { // if (this.libService.isRecordingEnabled()) {
// this.subscribeToRecordingEvents(); // this.subscribeToRecordingEvents();
} // }
if (this.libService.isBroadcastingEnabled()) { // if (this.libService.isBroadcastingEnabled()) {
// this.subscribeToBroadcastingEvents(); // this.subscribeToBroadcastingEvents();
} // }
try { try {
await this.participantService.connect(); await this.participantService.connect();
// Send room created after participant connect for avoiding to send incomplete room payload // 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() { async ngOnDestroy() {
if (this.shouldDisconnectRoomWhenComponentIsDestroyed) { if (this.shouldDisconnectRoomWhenComponentIsDestroyed) {
await this.disconnectRoom(ParticipantLeftReason.LEAVE); await this.disconnectRoom(ParticipantLeftReason.LEAVE);
@ -237,8 +279,8 @@ export class SessionComponent implements OnInit, OnDestroy {
if (this.room) this.room.removeAllListeners(); if (this.room) this.room.removeAllListeners();
this.participantService.clear(); this.participantService.clear();
// this.room = undefined; // this.room = undefined;
if (this.menuSubscription) this.menuSubscription.unsubscribe(); this.destroy$.next();
if (this.layoutWidthSubscription) this.layoutWidthSubscription.unsubscribe(); this.destroy$.complete();
// if (this.captionLanguageSubscription) this.captionLanguageSubscription.unsubscribe(); // if (this.captionLanguageSubscription) this.captionLanguageSubscription.unsubscribe();
} }
@ -248,7 +290,8 @@ export class SessionComponent implements OnInit, OnDestroy {
await this.openviduService.disconnectRoom(() => { await this.openviduService.disconnectRoom(() => {
this.onParticipantLeft.emit({ this.onParticipantLeft.emit({
roomName: this.openviduService.getRoomName(), roomName: this.openviduService.getRoomName(),
participantName: this.participantService.getLocalParticipant()?.identity || '', participantName: this.participantService.getLocalParticipant()?.name || '',
identity: this.participantService.getLocalParticipant()?.identity || '',
reason reason
}); });
}, false); }, false);
@ -268,7 +311,7 @@ export class SessionComponent implements OnInit, OnDestroy {
this.startUpdateLayoutInterval(); 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) { if (this.sideMenu) {
this.settingsPanelOpened = ev.isOpened && ev.panelType === PanelType.SETTINGS; this.settingsPanelOpened = ev.isOpened && ev.panelType === PanelType.SETTINGS;
@ -287,7 +330,7 @@ export class SessionComponent implements OnInit, OnDestroy {
} }
private subscribeToLayoutWidth() { 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; 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}`); this.log.d(`Data event received: ${topic}`);
switch (topic) { switch (topic) {
case DataTopic.CHAT: case DataTopic.CHAT:
const participantName = participant?.identity || 'Unknown'; const participantName = participant?.name || 'Unknown';
this.chatService.addRemoteMessage(event.message, participantName); this.chatService.addRemoteMessage(event.message, participantName);
break; break;
case DataTopic.RECORDING_STARTING: case DataTopic.RECORDING_STARTING:
@ -457,7 +500,9 @@ export class SessionComponent implements OnInit, OnDestroy {
case DataTopic.ROOM_STATUS: case DataTopic.ROOM_STATUS:
const { recordingList, isRecordingStarted, isBroadcastingStarted, broadcastingId } = event as RoomStatusData; const { recordingList, isRecordingStarted, isBroadcastingStarted, broadcastingId } = event as RoomStatusData;
this.recordingService.setRecordingList(recordingList); if (this.libService.showRecordingActivityRecordingsList()) {
this.recordingService.setRecordingList(recordingList);
}
if (isRecordingStarted) { if (isRecordingStarted) {
const recordingActive = recordingList.find((recording) => recording.status === RecordingStatus.STARTED); const recordingActive = recordingList.find((recording) => recording.status === RecordingStatus.STARTED);
this.recordingService.setRecordingStarted(recordingActive); this.recordingService.setRecordingStarted(recordingActive);
@ -490,9 +535,11 @@ export class SessionComponent implements OnInit, OnDestroy {
this.room.on(RoomEvent.Disconnected, async (reason: DisconnectReason | undefined) => { this.room.on(RoomEvent.Disconnected, async (reason: DisconnectReason | undefined) => {
this.shouldDisconnectRoomWhenComponentIsDestroyed = false; this.shouldDisconnectRoomWhenComponentIsDestroyed = false;
this.actionService.closeConnectionDialog();
const participantLeftEvent: ParticipantLeftEvent = { const participantLeftEvent: ParticipantLeftEvent = {
roomName: this.openviduService.getRoomName(), roomName: this.openviduService.getRoomName(),
participantName: this.participantService.getLocalParticipant()?.identity || '', participantName: this.participantService.getLocalParticipant()?.name || '',
identity: this.participantService.getLocalParticipant()?.identity || '',
reason: ParticipantLeftReason.NETWORK_DISCONNECT reason: ParticipantLeftReason.NETWORK_DISCONNECT
}; };
const messageErrorKey = 'ERRORS.DISCONNECT'; const messageErrorKey = 'ERRORS.DISCONNECT';
@ -532,7 +579,7 @@ export class SessionComponent implements OnInit, OnDestroy {
this.log.d('Participant disconnected', participantLeftEvent); this.log.d('Participant disconnected', participantLeftEvent);
this.onParticipantLeft.emit(participantLeftEvent); this.onParticipantLeft.emit(participantLeftEvent);
this.onRoomDisconnected.emit(); this.onRoomDisconnected.emit();
if (descriptionErrorKey) { if (this.libService.getShowDisconnectionDialog() && descriptionErrorKey) {
this.actionService.openDialog( this.actionService.openDialog(
this.translateService.translate(messageErrorKey), this.translateService.translate(messageErrorKey),
this.translateService.translate(descriptionErrorKey) 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() { private startUpdateLayoutInterval() {
this.updateLayoutInterval = setInterval(() => { this.updateLayoutInterval = setInterval(() => {
this.layoutService.update(); this.layoutService.update();

View File

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

View File

@ -1,103 +1,29 @@
$ov-selection-color-btn: #afafaf; @use '../device-selector-shared' as shared;
$ov-selection-color: #cccccc;
:host { :host {
.device-container-element { display: flex;
border-radius: var(--ov-surface-radius); align-items: center;
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;
}
#audio-devices-not-found { .audio-device-selector {
font-size: 13px; @include shared.device-selector-base();
}
#microphone-button { // Audio-specific overrides for normal mode
color:#000000 &:not(.compact) {
} .normal-device-selector {
.device-input-selector {
::ng-deep .mat-mdc-text-field-wrapper, &:not(.disabled) {
::ng-deep .mat-mdc-form-field-flex, .selector-button {
::ng-deep .mat-mdc-select-trigger { // Audio-specific hover effect (simpler than video)
height: 50px !important; &:hover:not([disabled]) {
} border-color: var(--ov-primary-action-color, #4285f4);
}
::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;
} }
} }
::ng-deep .mat-mdc-select-panel {
background-color: #ffffff !important;
}
::ng-deep .mat-mdc-option { // Include shared device menu styles
padding: 10px 10px !important; @include shared.device-menu-styles();
}
::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;
}

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 { Subscription } from 'rxjs';
import { CustomDevice } from '../../../models/device.model'; import { CustomDevice } from '../../../models/device.model';
import { DeviceService } from '../../../services/device/device.service'; import { DeviceService } from '../../../services/device/device.service';
@ -16,6 +16,7 @@ import { ParticipantModel } from '../../../models/participant.model';
standalone: false standalone: false
}) })
export class AudioDevicesComponent implements OnInit, OnDestroy { export class AudioDevicesComponent implements OnInit, OnDestroy {
@Input() compact: boolean = false;
@Output() onAudioDeviceChanged = new EventEmitter<CustomDevice>(); @Output() onAudioDeviceChanged = new EventEmitter<CustomDevice>();
@Output() onAudioEnabledChanged = new EventEmitter<boolean>(); @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"> <div class="language-selector-container">
<mat-icon>translate</mat-icon> @if (compact) {
</button> <!-- Compact version (icon only) -->
<button *ngIf="!compact" mat-flat-button [matMenuTriggerFor]="menu" class="lang-button" id="lang-btn"> <button mat-icon-button [matMenuTriggerFor]="langMenu" class="compact-lang-button" [matTooltip]="'Change language'" disableRipple>
<span id="lang-selected-name">{{ langSelected?.name }}</span> <mat-icon>translate</mat-icon>
<mat-icon class="expand-more-icon">expand_more</mat-icon> </button>
</button> } @else {
<mat-menu #menu="matMenu"> <!-- Full version (with text) -->
<button <button mat-flat-button [matMenuTriggerFor]="langMenu" class="full-lang-button">
mat-menu-item <!-- <mat-icon class="lang-icon">translate</mat-icon> -->
*ngFor="let lang of languages" <span class="lang-name">
(click)="onLangSelected(lang.lang)" {{ langSelected?.name }}
[attr.id]="'lang-opt-' + lang.lang" <mat-icon class="expand-icon">expand_more</mat-icon>
class="lang-menu-opt" </span>
> </button>
<span>{{ lang.name }}</span> }
</button>
</mat-menu> <!-- 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 { .language-selector-container {
background-color: var(--ov-primary-action-color) !important; .compact-lang-button {
color: var(--ov-secondary-action-color) !important; width: 40px;
} height: 40px;
.lang-button .mat-icon { background: rgba(255, 255, 255, 0.9);
color: var(--ov-secondary-action-color); 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);
} mat-icon {
::ng-deep .mat-mdc-menu-panel { font-size: 18px;
border-radius: var(--ov-surface-radius) !important; width: 18px;
background-color: $ov-surface-color-lighter !important; height: 18px;
box-shadow: 1px 1px 5px 0px rgba(0, 0, 0, 0.2) !important; }
}
.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, ::ng-deep .language-menu.mat-mdc-menu-panel {
.mat-mdc-menu-item:visited, border-radius: 12px;
.mat-mdc-menu-item:link { box-shadow: 0 8px 32px rgba(0, 0, 0, 0.12);
color: var(--ov-text-surface-color) !important; 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 }"> <div class="participant-name-input-container" [class.error]="error">
<mat-form-field id="name-form" [ngClass]="{ error: error }"> <div class="input-wrapper">
<mat-select-trigger> <mat-icon class="input-icon">person</mat-icon>
<button mat-flat-button disabled>
<mat-icon>person</mat-icon>
</button>
</mat-select-trigger>
<input <input
id="name-input" id="name-input"
matInput
(change)="updateName()"
type="text" type="text"
maxlength="20" maxlength="20"
[(ngModel)]="name" [(ngModel)]="name"
autocomplete="off" autocomplete="off"
[disabled]="!isPrejoinPage" [disabled]="!isPrejoinPage"
(change)="updateName()"
(keypress)="eventKeyPress($event)" (keypress)="eventKeyPress($event)"
[placeholder]="'PREJOIN.NICKNAME' | translate"
class="name-input-field"
/> />
</mat-form-field> </div>
</div> </div>

View File

@ -1,67 +1,71 @@
$ov-selection-color-btn: #afafaf;
$ov-selection-color: #cccccc;
:host { :host {
#name-input-container { display: block;
height: 70px; width: 100%;
border-radius: var(--ov-surface-radius);
}
#name-input-container mat-form-field { .participant-name-input-container {
width: 100%; width: 100%;
color: var(--ov-secondary-action-color);
}
::ng-deep .mat-mdc-form-field-infix { .input-wrapper {
display: inline-flex; display: flex;
padding: 0px !important; align-items: center;
} background: var(--ov-input-background, #f8f9fa);
::ng-deep .mat-mdc-text-field-wrapper { border: 2px solid var(--ov-border-color, #e0e0e0);
padding: 0; border-radius: 12px;
height: 70px; padding: 0;
background-color: $ov-selection-color !important; transition: all 0.2s ease;
border-radius: var(--ov-surface-radius); position: relative;
} overflow: hidden;
::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;
}
.error { &:focus-within {
::ng-deep .mdc-button--unelevated { border-color: var(--ov-primary-action-color, #4285f4);
background-color: var(--ov-error-color) !important; 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,72 @@
<div class="device-container-element" [class.mute-btn]="!isCameraEnabled"> <div class="video-device-selector" [class.compact]="compact">
<mat-form-field id="video-devices-form" *ngIf="cameras.length > 0"> <!-- Unified Device Button (Compact Mode) -->
<mat-select @if (compact) {
[disabled]="!hasVideoDevices" <div class="unified-device-button">
[compareWith]="compareObjectDevices" <!-- Main toggle button -->
[value]="cameraSelected" <button
(selectionChange)="onCameraSelected($event)" mat-flat-button
> class="toggle-section"
<mat-select-trigger id="mat-select-trigger"> [disabled]="!hasVideoDevices || cameraStatusChanging"
<button [class.device-enabled]="isCameraEnabled"
mat-flat-button [class.device-disabled]="!isCameraEnabled"
id="camera-button" (click)="toggleCam($event)"
[disableRipple]="true" [matTooltip]="isCameraEnabled ? ('TOOLBAR.STOP_VIDEO' | translate) : ('TOOLBAR.START_VIDEO' | translate)"
[disabled]="!hasVideoDevices || cameraStatusChanging" [matTooltipDisabled]="!hasVideoDevices"
[class.mute-btn]="!isCameraEnabled" >
(click)="toggleCam($event)" <mat-icon>{{ isCameraEnabled ? 'videocam' : 'videocam_off' }}</mat-icon>
[matTooltip]="isCameraEnabled ? ('TOOLBAR.STOP_VIDEO' | translate) : ('TOOLBAR.START_VIDEO' | translate)"
[matTooltipDisabled]="!hasVideoDevices"
>
<mat-icon *ngIf="isCameraEnabled" id="videocam"> videocam </mat-icon>
<mat-icon *ngIf="!isCameraEnabled" id="videocam_off"> 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> </button>
<span id="video-devices-not-found"> {{ 'PREJOIN.NO_VIDEO_DEVICE' | translate }} </span>
<!-- Dropdown section -->
@if (isCameraEnabled && cameras.length > 1) {
<button mat-flat-button class="dropdown-section" [matMenuTriggerFor]="cameraMenu" [disabled]="cameraStatusChanging">
<mat-icon>expand_more</mat-icon>
</button>
}
</div> </div>
</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
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 {
<!-- 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>
}
</div>
}
<!-- Device Selection Menu (Shared) -->
<mat-menu #cameraMenu="matMenu" class="device-menu">
@for (camera of cameras; track camera.device) {
<button mat-menu-item (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>
<!-- No Camera Available -->
@if (cameras.length === 0) {
<div class="no-device-message">
<mat-icon class="warning-icon">warning</mat-icon>
<span>{{ 'PREJOIN.NO_VIDEO_DEVICE' | translate }}</span>
</div>
}
</div> </div>

View File

@ -1,106 +1,51 @@
@use '../device-selector-shared' as shared;
$ov-selection-color-btn: #afafaf; $ov-selection-color-btn: #afafaf;
$ov-selection-color: #cccccc; $ov-selection-color: #cccccc;
:host { :host {
.device-container-element { display: flex;
border-radius: var(--ov-surface-radius); align-items: center;
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;
}
#video-devices-not-found { .video-device-selector {
font-size: 13px; @include shared.device-selector-base();
}
#camera-button { // Video-specific overrides for compact mode
color: #000000; &.compact {
} .unified-device-button {
.toggle-section {
display: flex-end; // Video-specific styling
}
}
}
::ng-deep .mat-mdc-text-field-wrapper, // Video-specific overrides for normal mode
::ng-deep .mat-mdc-form-field-flex, &:not(.compact) {
::ng-deep .mat-mdc-select-trigger { .normal-device-selector {
height: 50px !important; .device-input-selector {
} &:not(.disabled) {
.selector-button {
::ng-deep .mat-mdc-form-field-subscript-wrapper { // Video-specific hover effect with box-shadow
display: none !important; &:hover:not([disabled]) {
} background-color: white !important;
border-color: var(--ov-primary-action-color);
::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;
} }
} }
::ng-deep .mat-mdc-select-panel {
background-color: var(--ov-surface-color) !important;
}
::ng-deep .mat-mdc-select-panel { // Include shared device menu styles
background-color: #e2e2e2 !important; @include shared.device-menu-styles();
}
::ng-deep .mat-mdc-option { // Video-specific additional styles
padding: 10px 10px !important;
}
::ng-deep .mat-mdc-form-field.mat-focused .mat-mdc-select-arrow {
color: var(--ov-primary-action-color-lighter) !important;
}
::ng-deep .mdc-text-field--filled:not(.mdc-text-field--disabled) .mdc-line-ripple::after { ::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; 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) { ::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; 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 { Subscription } from 'rxjs';
import { CustomDevice } from '../../../models/device.model'; import { CustomDevice } from '../../../models/device.model';
import { DeviceService } from '../../../services/device/device.service'; import { DeviceService } from '../../../services/device/device.service';
@ -16,6 +16,7 @@ import { ParticipantModel } from '../../../models/participant.model';
standalone: false standalone: false
}) })
export class VideoDevicesComponent implements OnInit, OnDestroy { export class VideoDevicesComponent implements OnInit, OnDestroy {
@Input() compact: boolean = false;
@Output() onVideoDeviceChanged = new EventEmitter<CustomDevice>(); @Output() onVideoDeviceChanged = new EventEmitter<CustomDevice>();
@Output() onVideoEnabledChanged = new EventEmitter<boolean>(); @Output() onVideoEnabledChanged = new EventEmitter<boolean>();

View File

@ -1,6 +1,6 @@
import { Component, ElementRef, Input, OnDestroy, OnInit, ViewChild } from '@angular/core'; import { Component, ElementRef, Input, OnDestroy, OnInit, ViewChild } from '@angular/core';
import { MatMenuPanel, MatMenuTrigger } from '@angular/material/menu'; 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 { CdkOverlayService } from '../../services/cdk-overlay/cdk-overlay.service';
import { OpenViduComponentsConfigService } from '../../services/config/directive-config.service'; import { OpenViduComponentsConfigService } from '../../services/config/directive-config.service';
import { LayoutService } from '../../services/layout/layout.service'; import { LayoutService } from '../../services/layout/layout.service';
@ -92,10 +92,7 @@ export class StreamComponent implements OnInit, OnDestroy {
} }
private _streamContainer: ElementRef; private _streamContainer: ElementRef;
private minimalSub: Subscription; private destroy$ = new Subject<void>();
private displayParticipantNameSub: Subscription;
private displayAudioDetectionSub: Subscription;
private videoControlsSub: Subscription;
private readonly HOVER_TIMEOUT = 3000; private readonly HOVER_TIMEOUT = 3000;
/** /**
@ -113,11 +110,9 @@ export class StreamComponent implements OnInit, OnDestroy {
} }
ngOnDestroy() { ngOnDestroy() {
this.destroy$.next();
this.destroy$.complete();
this.cdkSrv.setSelector('body'); 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() { private subscribeToStreamDirectives() {
this.minimalSub = this.libService.minimal$.subscribe((value: boolean) => { this.libService.minimal$
this.isMinimal = value; .pipe(takeUntil(this.destroy$))
}); .subscribe((value: boolean) => {
this.displayParticipantNameSub = this.libService.displayParticipantName$.subscribe((value: boolean) => { this.isMinimal = value;
this.showParticipantName = value; });
// this.cd.markForCheck();
}); this.libService.displayParticipantName$
this.displayAudioDetectionSub = this.libService.displayAudioDetection$.subscribe((value: boolean) => { .pipe(takeUntil(this.destroy$))
this.showAudioDetection = value; .subscribe((value: boolean) => {
// this.cd.markForCheck(); this.showParticipantName = value;
}); // this.cd.markForCheck();
this.videoControlsSub = this.libService.streamVideoControls$.subscribe((value: boolean) => { });
this.showVideoControls = 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" id="session-info-container"
[class.collapsed]="recordingStatus === _recordingStatus.STARTED || broadcastingStatus === _broadcastingStatus.STARTED" [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 <div
id="activities-tag" id="activities-tag"
*ngIf="recordingStatus === _recordingStatus.STARTED || broadcastingStatus === _broadcastingStatus.STARTED" *ngIf="recordingStatus === _recordingStatus.STARTED || broadcastingStatus === _broadcastingStatus.STARTED"
> >
<div *ngIf="recordingStatus === _recordingStatus.STARTED" id="recording-tag" class="recording-tag"> @if (recordingStatus === _recordingStatus.STARTED) {
<mat-icon class="blink">radio_button_checked</mat-icon> <div id="recording-tag" class="recording-tag" (click)="openRecordingActivityPanel()">
<span class="blink">REC</span> <mat-icon class="blink">radio_button_checked</mat-icon>
<span *ngIf="recordingTime"> | {{ recordingTime | date: 'H:mm:ss' }}</span> <span class="blink">REC</span>
</div> <span *ngIf="recordingTime"> | {{ recordingTime | date: 'H:mm:ss' }}</span>
<div *ngIf="broadcastingStatus === _broadcastingStatus.STARTED" id="broadcasting-tag" class="broadcasting-tag"> </div>
<mat-icon class="blink">sensors</mat-icon> }
<span class="blink">LIVE</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> </div>
</div> </div>
@ -120,18 +126,36 @@
*ngIf="!isMinimal && showRecordingButton" *ngIf="!isMinimal && showRecordingButton"
mat-menu-item mat-menu-item
id="recording-btn" 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()" (click)="toggleRecording()"
> >
<mat-icon color="warn">radio_button_checked</mat-icon> <mat-icon color="warn">radio_button_checked</mat-icon>
<span *ngIf="recordingStatus === _recordingStatus.STOPPED || recordingStatus === _recordingStatus.STOPPING"> @if (
{{ 'TOOLBAR.START_RECORDING' | translate }} recordingStatus === _recordingStatus.STOPPED ||
</span> recordingStatus === _recordingStatus.STOPPING ||
<span *ngIf="recordingStatus === _recordingStatus.STARTED || recordingStatus === _recordingStatus.STARTING"> recordingStatus === _recordingStatus.FAILED
{{ 'TOOLBAR.STOP_RECORDING' | translate }} ) {
</span> <span class="blink">
{{ 'TOOLBAR.START_RECORDING' | translate }}
</span>
} @else if (recordingStatus === _recordingStatus.STARTED || recordingStatus === _recordingStatus.STARTING) {
<span>{{ 'TOOLBAR.STOP_RECORDING' | translate }}</span>
}
</button> </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 --> <!-- Broadcasting button -->
<button <button
*ngIf="!isMinimal && showBroadcastingButton" *ngIf="!isMinimal && showBroadcastingButton"

View File

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

View File

@ -12,7 +12,7 @@ import {
TemplateRef, TemplateRef,
ViewChild ViewChild
} from '@angular/core'; } from '@angular/core';
import { fromEvent, skip, Subscription } from 'rxjs'; import { fromEvent, skip, Subject, takeUntil } from 'rxjs';
import { ChatService } from '../../services/chat/chat.service'; import { ChatService } from '../../services/chat/chat.service';
import { DocumentService } from '../../services/document/document.service'; import { DocumentService } from '../../services/document/document.service';
import { PanelService } from '../../services/panel/panel.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 { PlatformService } from '../../services/platform/platform.service';
import { RecordingService } from '../../services/recording/recording.service'; import { RecordingService } from '../../services/recording/recording.service';
import { StorageService } from '../../services/storage/storage.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 { TranslateService } from '../../services/translate/translate.service';
import { CdkOverlayService } from '../../services/cdk-overlay/cdk-overlay.service'; import { CdkOverlayService } from '../../services/cdk-overlay/cdk-overlay.service';
import { ParticipantLeftEvent, ParticipantLeftReason, ParticipantModel } from '../../models/participant.model'; import { ParticipantLeftEvent, ParticipantLeftReason, ParticipantModel } from '../../models/participant.model';
@ -77,10 +78,9 @@ export class ToolbarComponent implements OnInit, OnDestroy, AfterViewInit {
*/ */
@ContentChild(ToolbarAdditionalButtonsDirective) @ContentChild(ToolbarAdditionalButtonsDirective)
set externalAdditionalButtons(externalAdditionalButtons: ToolbarAdditionalButtonsDirective) { set externalAdditionalButtons(externalAdditionalButtons: ToolbarAdditionalButtonsDirective) {
// This directive will has value only when ADDITIONAL BUTTONS component (tagged with '*ovToolbarAdditionalButtons' directive) this._externalAdditionalButtons = externalAdditionalButtons;
// is inside of the TOOLBAR component tagged with '*ovToolbar' directive
if (externalAdditionalButtons) { if (externalAdditionalButtons) {
this.toolbarAdditionalButtonsTemplate = externalAdditionalButtons.template; this.updateTemplatesAndMarkForCheck();
} }
} }
@ -89,16 +89,15 @@ export class ToolbarComponent implements OnInit, OnDestroy, AfterViewInit {
*/ */
@ContentChild(ToolbarAdditionalPanelButtonsDirective) @ContentChild(ToolbarAdditionalPanelButtonsDirective)
set externalAdditionalPanelButtons(externalAdditionalPanelButtons: ToolbarAdditionalPanelButtonsDirective) { set externalAdditionalPanelButtons(externalAdditionalPanelButtons: ToolbarAdditionalPanelButtonsDirective) {
// This directive will has value only when ADDITIONAL PANEL BUTTONS component tagged with '*ovToolbarAdditionalPanelButtons' directive this._externalAdditionalPanelButtons = externalAdditionalPanelButtons;
// is inside of the TOOLBAR component tagged with '*ovToolbar' directive
if (externalAdditionalPanelButtons) { if (externalAdditionalPanelButtons) {
this.toolbarAdditionalPanelButtonsTemplate = externalAdditionalPanelButtons.template; this.updateTemplatesAndMarkForCheck();
} }
} }
/** /**
* This event is emitted when the room has been disconnected. * 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>(); @Output() onRoomDisconnected: EventEmitter<void> = new EventEmitter<void>();
@ -145,6 +144,12 @@ export class ToolbarComponent implements OnInit, OnDestroy, AfterViewInit {
@Output() onBroadcastingStopRequested: EventEmitter<BroadcastingStopRequestedEvent> = @Output() onBroadcastingStopRequested: EventEmitter<BroadcastingStopRequestedEvent> =
new 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 * @ignore
*/ */
@ -240,6 +245,11 @@ export class ToolbarComponent implements OnInit, OnDestroy, AfterViewInit {
*/ */
showRecordingButton: boolean = true; showRecordingButton: boolean = true;
/**
* @ignore
*/
showViewRecordingsButton: boolean = false;
/** /**
* @ignore * @ignore
*/ */
@ -280,7 +290,12 @@ export class ToolbarComponent implements OnInit, OnDestroy, AfterViewInit {
/** /**
* @ignore * @ignore
*/ */
showSessionName: boolean = true; showRoomName: boolean = true;
/**
* @ignore
*/
roomName: string = '';
/** /**
* @ignore * @ignore
@ -312,6 +327,11 @@ export class ToolbarComponent implements OnInit, OnDestroy, AfterViewInit {
*/ */
recordingStatus: RecordingStatus = RecordingStatus.STOPPED; recordingStatus: RecordingStatus = RecordingStatus.STOPPED;
/**
* @ignore
*/
isRecordingReadOnlyMode: boolean = false;
/** /**
* @ignore * @ignore
*/ */
@ -339,31 +359,18 @@ export class ToolbarComponent implements OnInit, OnDestroy, AfterViewInit {
*/ */
recordingTime: Date; 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 log: ILogger;
private minimalSub: Subscription; private destroy$ = new Subject<void>();
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 currentWindowHeight = window.innerHeight; private currentWindowHeight = window.innerHeight;
/** /**
@ -386,7 +393,8 @@ export class ToolbarComponent implements OnInit, OnDestroy, AfterViewInit {
private broadcastingService: BroadcastingService, private broadcastingService: BroadcastingService,
private translateService: TranslateService, private translateService: TranslateService,
private storageSrv: StorageService, private storageSrv: StorageService,
private cdkOverlayService: CdkOverlayService private cdkOverlayService: CdkOverlayService,
private templateManagerService: TemplateManagerService
) { ) {
this.log = this.loggerSrv.get('ToolbarComponent'); this.log = this.loggerSrv.get('ToolbarComponent');
} }
@ -416,10 +424,12 @@ export class ToolbarComponent implements OnInit, OnDestroy, AfterViewInit {
async ngOnInit() { async ngOnInit() {
this.room = this.openviduService.getRoom(); this.room = this.openviduService.getRoom();
this.evalAndSetRoomName(this.libService.getRoomName());
this.hasVideoDevices = this.oVDevicesService.hasVideoDeviceAvailable(); this.hasVideoDevices = this.oVDevicesService.hasVideoDeviceAvailable();
this.hasAudioDevices = this.oVDevicesService.hasAudioDeviceAvailable(); this.hasAudioDevices = this.oVDevicesService.hasAudioDeviceAvailable();
this.setupTemplates();
this.subscribeToToolbarDirectives(); this.subscribeToToolbarDirectives();
this.subscribeToUserMediaProperties(); this.subscribeToUserMediaProperties();
@ -437,34 +447,55 @@ export class ToolbarComponent implements OnInit, OnDestroy, AfterViewInit {
ngOnDestroy(): void { ngOnDestroy(): void {
this.panelService.clear(); this.panelService.clear();
if (this.panelTogglingSubscription) this.panelTogglingSubscription.unsubscribe(); this.destroy$.next();
if (this.chatMessagesSubscription) this.chatMessagesSubscription.unsubscribe(); this.destroy$.complete();
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.isFullscreenActive = false; this.isFullscreenActive = false;
this.cdkOverlayService.setSelector('body'); 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 * @ignore
*/ */
@ -527,7 +558,8 @@ export class ToolbarComponent implements OnInit, OnDestroy, AfterViewInit {
await this.openviduService.disconnectRoom(() => { await this.openviduService.disconnectRoom(() => {
this.onParticipantLeft.emit({ this.onParticipantLeft.emit({
roomName: this.openviduService.getRoomName(), roomName: this.openviduService.getRoomName(),
participantName: this.participantService.getLocalParticipant()?.identity || '', participantName: this.participantService.getLocalParticipant()?.name || '',
identity: this.participantService.getLocalParticipant()?.identity || '',
reason: ParticipantLeftReason.LEAVE reason: ParticipantLeftReason.LEAVE
}); });
this.onRoomDisconnected.emit(); 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 * @ignore
*/ */
toggleRecording() { toggleRecording() {
if (this.recordingStatus === RecordingStatus.FAILED) {
this.openRecordingActivityPanel();
return;
}
const payload: RecordingStartRequestedEvent = { const payload: RecordingStartRequestedEvent = {
roomName: this.openviduService.getRoomName() roomName: this.openviduService.getRoomName()
}; };
@ -551,9 +606,7 @@ export class ToolbarComponent implements OnInit, OnDestroy, AfterViewInit {
this.onRecordingStopRequested.emit(payload); this.onRecordingStopRequested.emit(payload);
} else if (this.recordingStatus === RecordingStatus.STOPPED) { } else if (this.recordingStatus === RecordingStatus.STOPPED) {
this.onRecordingStartRequested.emit(payload); this.onRecordingStartRequested.emit(payload);
if (this.showActivitiesPanelButton && !this.isActivitiesOpened) { this.openRecordingActivityPanel();
this.toggleActivitiesPanel('recording');
}
} }
} }
@ -570,9 +623,7 @@ export class ToolbarComponent implements OnInit, OnDestroy, AfterViewInit {
this.onBroadcastingStopRequested.emit(payload); this.onBroadcastingStopRequested.emit(payload);
this.broadcastingService.setBroadcastingStopped(); this.broadcastingService.setBroadcastingStopped();
} else if (this.broadcastingStatus === BroadcastingStatus.STOPPED) { } else if (this.broadcastingStatus === BroadcastingStatus.STOPPED) {
if (this.showActivitiesPanelButton && !this.isActivitiesOpened) { this.openBroadcastingActivityPanel();
this.toggleActivitiesPanel('broadcasting');
}
} }
} }
@ -625,7 +676,11 @@ export class ToolbarComponent implements OnInit, OnDestroy, AfterViewInit {
this.documentService.toggleFullscreen('session-container'); this.documentService.toggleFullscreen('session-container');
} }
private toggleActivitiesPanel(expandPanel: string) { /**
* @internal
* @param expandPanel
*/
toggleActivitiesPanel(expandPanel: string) {
this.panelService.togglePanel(PanelType.ACTIVITIES, expandPanel); this.panelService.togglePanel(PanelType.ACTIVITIES, expandPanel);
} }
@ -640,21 +695,23 @@ export class ToolbarComponent implements OnInit, OnDestroy, AfterViewInit {
} }
private subscribeToFullscreenChanged() { private subscribeToFullscreenChanged() {
this.fullscreenChangeSubscription = fromEvent(document, 'fullscreenchange').subscribe(() => { fromEvent(document, 'fullscreenchange')
const isFullscreen = Boolean(document.fullscreenElement); .pipe(takeUntil(this.destroy$))
if (isFullscreen) { .subscribe(() => {
this.cdkOverlayService.setSelector('#session-container'); const isFullscreen = Boolean(document.fullscreenElement);
} else { if (isFullscreen) {
this.cdkOverlayService.setSelector('body'); this.cdkOverlayService.setSelector('#session-container');
} } else {
this.isFullscreenActive = isFullscreen; this.cdkOverlayService.setSelector('body');
this.onFullscreenEnabledChanged.emit(this.isFullscreenActive); }
this.cd.detectChanges(); this.isFullscreenActive = isFullscreen;
}); this.onFullscreenEnabledChanged.emit(this.isFullscreenActive);
this.cd.detectChanges();
});
} }
private subscribeToMenuToggling() { 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.isChatOpened = ev.isOpened && ev.panelType === PanelType.CHAT;
this.isParticipantsOpened = ev.isOpened && ev.panelType === PanelType.PARTICIPANTS; this.isParticipantsOpened = ev.isOpened && ev.panelType === PanelType.PARTICIPANTS;
this.isActivitiesOpened = ev.isOpened && ev.panelType === PanelType.ACTIVITIES; this.isActivitiesOpened = ev.isOpened && ev.panelType === PanelType.ACTIVITIES;
@ -666,7 +723,7 @@ export class ToolbarComponent implements OnInit, OnDestroy, AfterViewInit {
} }
private subscribeToChatMessages() { 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()) { if (!this.panelService.isChatPanelOpened()) {
this.unreadMessages++; this.unreadMessages++;
} }
@ -675,7 +732,7 @@ export class ToolbarComponent implements OnInit, OnDestroy, AfterViewInit {
}); });
} }
private subscribeToUserMediaProperties() { 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 (p) {
if (this.isCameraEnabled !== p.isCameraEnabled) { if (this.isCameraEnabled !== p.isCameraEnabled) {
this.onVideoEnabledChanged.emit(p.isCameraEnabled); this.onVideoEnabledChanged.emit(p.isCameraEnabled);
@ -699,8 +756,13 @@ export class ToolbarComponent implements OnInit, OnDestroy, AfterViewInit {
} }
private subscribeToRecordingStatus() { private subscribeToRecordingStatus() {
this.recordingSubscription = this.recordingService.recordingStatusObs.subscribe((event: RecordingStatusInfo) => { this.libService.recordingActivityReadOnly$.pipe(takeUntil(this.destroy$)).subscribe((readOnly: boolean) => {
const { status, recordingElapsedTime } = event; this.isRecordingReadOnlyMode = readOnly;
this.cd.markForCheck();
});
this.recordingService.recordingStatusObs.pipe(takeUntil(this.destroy$)).subscribe((event: RecordingStatusInfo) => {
const { status, startedAt } = event;
this.recordingStatus = status; this.recordingStatus = status;
if (status === RecordingStatus.STARTED) { if (status === RecordingStatus.STARTED) {
this.startedRecording = event.recordingList.find((rec) => rec.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; this.startedRecording = undefined;
} }
if (recordingElapsedTime) { if (startedAt) {
this.recordingTime = recordingElapsedTime; this.recordingTime = startedAt;
} }
this.cd.markForCheck(); this.cd.markForCheck();
}); });
} }
private subscribeToBroadcastingStatus() { private subscribeToBroadcastingStatus() {
this.broadcastingSubscription = this.broadcastingService.broadcastingStatusObs.subscribe((ev: BroadcastingStatusInfo) => { this.broadcastingService.broadcastingStatusObs.pipe(takeUntil(this.destroy$)).subscribe((ev: BroadcastingStatusInfo) => {
if (!!ev) { if (!!ev) {
this.broadcastingStatus = ev.status; this.broadcastingStatus = ev.status;
this.broadcastingId = ev.broadcastingId; this.broadcastingId = ev.broadcastingId;
@ -726,86 +788,97 @@ export class ToolbarComponent implements OnInit, OnDestroy, AfterViewInit {
} }
private subscribeToToolbarDirectives() { private subscribeToToolbarDirectives() {
this.minimalSub = this.libService.minimal$.subscribe((value: boolean) => { this.libService.minimal$.pipe(takeUntil(this.destroy$)).subscribe((value: boolean) => {
this.isMinimal = value; this.isMinimal = value;
this.cd.markForCheck(); 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.brandingLogo = value;
this.cd.markForCheck(); 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.showCameraButton = value;
this.cd.markForCheck(); 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.showMicrophoneButton = value;
this.cd.markForCheck(); 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.showScreenshareButton = value && !this.platformService.isMobile();
this.cd.markForCheck(); 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.showFullscreenButton = value;
this.checkDisplayMoreOptions(); this.checkDisplayMoreOptions();
this.cd.markForCheck(); 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.showLeaveButton = value;
this.cd.markForCheck(); 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.showRecordingButton = value;
this.checkDisplayMoreOptions(); this.checkDisplayMoreOptions();
this.cd.markForCheck(); 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.showBroadcastingButton = value;
this.checkDisplayMoreOptions(); this.checkDisplayMoreOptions();
this.cd.markForCheck(); 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.showSettingsButton = value;
this.checkDisplayMoreOptions(); this.checkDisplayMoreOptions();
this.cd.markForCheck(); 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.showChatPanelButton = value;
this.cd.markForCheck(); 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.showParticipantsPanelButton = value;
this.cd.markForCheck(); 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.showActivitiesPanelButton = value;
this.cd.markForCheck(); 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.showBackgroundEffectsButton = value;
this.checkDisplayMoreOptions(); this.checkDisplayMoreOptions();
this.cd.markForCheck(); 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.showLogo = value;
this.cd.markForCheck(); this.cd.markForCheck();
}); });
this.displayRoomNameSub = this.libService.displayRoomName$.subscribe((value: boolean) => { this.libService.displayRoomName$.pipe(takeUntil(this.destroy$)).subscribe((value: boolean) => {
this.showSessionName = value; this.showRoomName = value;
this.cd.markForCheck();
});
this.captionsSubs = this.libService.captionsButton$.subscribe((value: boolean) => {
this.showCaptionsButton = value;
this.cd.markForCheck(); this.cd.markForCheck();
}); });
this.additionalButtonsPositionSub = this.libService.toolbarAdditionalButtonsPosition$.subscribe( this.libService.roomName$.pipe(takeUntil(this.destroy$)).subscribe((value: string) => {
(value: ToolbarAdditionalButtonsPosition) => { 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. // 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. // 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, // 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.additionalButtonsPosition = value;
this.cd.markForCheck(); this.cd.markForCheck();
}); });
} });
);
} }
private subscribeToCaptionsToggling() { private subscribeToCaptionsToggling() {
this.captionsSubs = this.layoutService.captionsTogglingObs.subscribe((value: boolean) => { this.layoutService.captionsTogglingObs.pipe(takeUntil(this.destroy$)).subscribe((value: boolean) => {
this.captionsEnabled = value; this.captionsEnabled = value;
this.cd.markForCheck(); this.cd.markForCheck();
}); });
@ -834,4 +906,14 @@ export class ToolbarComponent implements OnInit, OnDestroy, AfterViewInit {
this.showBroadcastingButton || this.showBroadcastingButton ||
this.showSettingsButton; this.showSettingsButton;
} }
private evalAndSetRoomName(value: string) {
if (!!value) {
this.roomName = value;
} else if (!!this.room && this.room.name) {
this.roomName = this.room.name;
} else {
this.roomName = '';
}
}
} }

View File

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

View File

@ -1,6 +1,17 @@
import { animate, style, transition, trigger } from '@angular/animations'; import { animate, style, transition, trigger } from '@angular/animations';
import { AfterViewInit, Component, ContentChild, EventEmitter, OnDestroy, Output, TemplateRef, ViewChild } from '@angular/core'; import {
import { Subscription, filter, skip, take } from 'rxjs'; AfterViewInit,
ChangeDetectionStrategy,
ChangeDetectorRef,
Component,
ContentChild,
EventEmitter,
OnDestroy,
Output,
TemplateRef,
ViewChild
} from '@angular/core';
import { Subject, filter, skip, take, takeUntil } from 'rxjs';
import { import {
ActivitiesPanelDirective, ActivitiesPanelDirective,
AdditionalPanelsDirective, AdditionalPanelsDirective,
@ -16,12 +27,19 @@ import {
ToolbarDirective ToolbarDirective
} from '../../directives/template/openvidu-components-angular.directive'; } from '../../directives/template/openvidu-components-angular.directive';
import { ILogger } from '../../models/logger.model'; import { ILogger } from '../../models/logger.model';
import { VideoconferenceState, VideoconferenceStateInfo } from '../../models/videoconference-state.model';
import { ActionService } from '../../services/action/action.service'; import { ActionService } from '../../services/action/action.service';
import { OpenViduComponentsConfigService } from '../../services/config/directive-config.service'; import { OpenViduComponentsConfigService } from '../../services/config/directive-config.service';
import { DeviceService } from '../../services/device/device.service'; import { DeviceService } from '../../services/device/device.service';
import { LoggerService } from '../../services/logger/logger.service'; import { LoggerService } from '../../services/logger/logger.service';
import { OpenViduService } from '../../services/openvidu/openvidu.service'; import { OpenViduService } from '../../services/openvidu/openvidu.service';
import { StorageService } from '../../services/storage/storage.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 { Room } from 'livekit-client';
import { ParticipantLeftEvent, ParticipantModel } from '../../models/participant.model'; import { ParticipantLeftEvent, ParticipantModel } from '../../models/participant.model';
import { CustomDevice } from '../../models/device.model'; import { CustomDevice } from '../../models/device.model';
@ -40,6 +58,11 @@ import {
} from '../../models/recording.model'; } from '../../models/recording.model';
import { BroadcastingStartRequestedEvent, BroadcastingStopRequestedEvent } from '../../models/broadcasting.model'; import { BroadcastingStartRequestedEvent, BroadcastingStopRequestedEvent } from '../../models/broadcasting.model';
import { LangOption } from '../../models/lang.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. * The **VideoconferenceComponent** is the parent of all OpenVidu components.
@ -51,68 +74,285 @@ import { LangOption } from '../../models/lang.model';
styleUrls: ['./videoconference.component.scss'], styleUrls: ['./videoconference.component.scss'],
animations: [ animations: [
trigger('inOutAnimation', [ 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 }))]) // transition(':leave', [style({ opacity: 1 }), animate('50ms ease-in', style({ opacity: 0.9 }))])
]) ])
], ],
standalone: false standalone: false
}) })
export class VideoconferenceComponent implements OnDestroy, AfterViewInit { 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 *** // *** Toolbar ***
private _externalToolbar?: ToolbarDirective;
/** /**
* @internal * @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 * @internal
*/ */
@ContentChild(ToolbarAdditionalButtonsDirective) externalToolbarAdditionalButtons: ToolbarAdditionalButtonsDirective; @ContentChild(ToolbarAdditionalButtonsDirective)
set externalToolbarAdditionalButtons(value: ToolbarAdditionalButtonsDirective) {
this._externalToolbarAdditionalButtons = value;
this.setupTemplates();
}
/** /**
* @internal * @internal
*/ */
@ContentChild(ToolbarAdditionalPanelButtonsDirective) externalToolbarAdditionalPanelButtons: ToolbarAdditionalPanelButtonsDirective; get externalToolbarAdditionalButtons(): ToolbarAdditionalButtonsDirective | undefined {
return this._externalToolbarAdditionalButtons;
}
private _externalToolbarAdditionalPanelButtons?: ToolbarAdditionalPanelButtonsDirective;
/** /**
* @internal * @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 *** // *** Panels ***
/** private _externalPanel?: PanelDirective;
* @internal
*/
@ContentChild(PanelDirective) externalPanel: PanelDirective;
/**
* @internal
*/
@ContentChild(ChatPanelDirective) externalChatPanel: ChatPanelDirective;
/**
* @internal
*/
@ContentChild(ActivitiesPanelDirective) externalActivitiesPanel: ActivitiesPanelDirective;
/** /**
* @internal * @internal
*/ */
@ContentChild(ParticipantsPanelDirective) externalParticipantsPanel: ParticipantsPanelDirective; @ContentChild(PanelDirective)
set externalPanel(value: PanelDirective) {
this._externalPanel = value;
this.setupTemplates();
}
/** /**
* @internal * @internal
*/ */
@ContentChild(ParticipantPanelItemDirective) externalParticipantPanelItem: ParticipantPanelItemDirective; get externalPanel(): PanelDirective | undefined {
return this._externalPanel;
}
private _externalChatPanel?: ChatPanelDirective;
/** /**
* @internal * @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 *** // *** Layout ***
private _externalLayout?: LayoutDirective;
/** /**
* @internal * @internal
*/ */
@ContentChild(LayoutDirective) externalLayout: LayoutDirective; @ContentChild(LayoutDirective)
set externalLayout(value: LayoutDirective) {
this._externalLayout = value;
this.setupTemplates();
}
/** /**
* @internal * @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 * @internal
@ -182,6 +422,10 @@ export class VideoconferenceComponent implements OnDestroy, AfterViewInit {
* @internal * @internal
*/ */
openviduAngularAdditionalPanelsTemplate: TemplateRef<any>; openviduAngularAdditionalPanelsTemplate: TemplateRef<any>;
/**
* @internal
*/
openviduAngularParticipantPanelAfterLocalParticipantTemplate: TemplateRef<any>;
/** /**
* @internal * @internal
*/ */
@ -198,6 +442,20 @@ export class VideoconferenceComponent implements OnDestroy, AfterViewInit {
* @internal * @internal
*/ */
openviduAngularStreamTemplate: TemplateRef<any>; 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. * 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. * 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>(); @Output() onRoomDisconnected: EventEmitter<void> = new EventEmitter<void>();
@ -315,6 +573,13 @@ export class VideoconferenceComponent implements OnDestroy, AfterViewInit {
*/ */
@Output() onRecordingPlayClicked: EventEmitter<RecordingPlayClickedEvent> = new EventEmitter<RecordingPlayClickedEvent>(); @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}. * Provides event notifications that fire when download recording button is clicked from {@link ActivitiesPanelComponent}.
* It provides the {@link RecordingDownloadClickedEvent} payload as event data. * It provides the {@link RecordingDownloadClickedEvent} payload as event data.
@ -335,6 +600,12 @@ export class VideoconferenceComponent implements OnDestroy, AfterViewInit {
@Output() onBroadcastingStopRequested: EventEmitter<BroadcastingStopRequestedEvent> = @Output() onBroadcastingStopRequested: EventEmitter<BroadcastingStopRequestedEvent> =
new 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. * 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. * 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 * @internal
* Centralized state management for the videoconference component
*/ */
error: boolean = false; componentState: VideoconferenceStateInfo = {
/** state: VideoconferenceState.INITIALIZING,
* @internal showPrejoin: true,
*/ isRoomReady: false,
errorMessage: string = ''; isConnected: false,
/** hasAudioDevices: false,
* @internal hasVideoDevices: false,
*/ hasUserInitiatedJoin: false,
showPrejoin: boolean = true; wasPrejoinShown: false,
isLoading: true,
error: {
hasError: false,
message: '',
tokenError: null
}
};
/** private destroy$ = new Subject<void>();
* @internal
*/
isRoomReady: boolean = false;
/**
* @internal
*/
loading = true;
/**
* @internal
*/
_tokenError: any;
private prejoinSub: Subscription;
private tokenSub: Subscription;
private tokenErrorSub: Subscription;
private participantNameSub: Subscription;
private log: ILogger; private log: ILogger;
private latestParticipantName: string | undefined; 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 * @internal
*/ */
@ -395,17 +684,29 @@ export class VideoconferenceComponent implements OnDestroy, AfterViewInit {
private deviceSrv: DeviceService, private deviceSrv: DeviceService,
private openviduService: OpenViduService, private openviduService: OpenViduService,
private actionService: ActionService, private actionService: ActionService,
private libService: OpenViduComponentsConfigService private libService: OpenViduComponentsConfigService,
private templateManagerService: TemplateManagerService
) { ) {
this.log = this.loggerSrv.get('VideoconferenceComponent'); 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(); this.subscribeToVideconferenceDirectives();
} }
ngOnDestroy() { ngOnDestroy() {
if (this.prejoinSub) this.prejoinSub.unsubscribe(); this.destroy$.next();
if (this.participantNameSub) this.participantNameSub.unsubscribe(); this.destroy$.complete();
if (this.tokenSub) this.tokenSub.unsubscribe();
if (this.tokenErrorSub) this.tokenErrorSub.unsubscribe();
this.deviceSrv.clear(); this.deviceSrv.clear();
} }
@ -413,116 +714,203 @@ export class VideoconferenceComponent implements OnDestroy, AfterViewInit {
* @internal * @internal
*/ */
ngAfterViewInit() { ngAfterViewInit() {
//Add material icons to the page if not already present this.setupTemplates();
const existingLink = document.querySelector('link[href*="Material+Symbols+Outlined"]'); this.deviceSrv.initializeDevices().then(() => {
if (!existingLink) { this.updateComponentState({
const link = document.createElement('link'); isLoading: false
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));
} }
/** /**
* @internal * @internal
*/ */
_onReadyToJoin() { private addMaterialIconsIfNeeded(): void {
this.openviduService.initRoom(); //Add material icons to the page if not already present
const participantName = this.latestParticipantName; const existingLink = document.querySelector(VideoconferenceComponent.MATERIAL_ICONS_SELECTOR);
if (participantName) this.onTokenRequested.emit(participantName); if (!existingLink) {
// Emits onReadyToJoin event only if prejoin page has been shown const link = document.createElement('link');
if (this.showPrejoin) this.onReadyToJoin.emit(); 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 * @internal
*/ */
_onParticipantLeft(event: ParticipantLeftEvent) { _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); this.onParticipantLeft.emit(event);
} }
private subscribeToVideconferenceDirectives() { 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 { try {
if (!token) { if (!token) {
this.log.e('Token is empty'); this.log.e('Token is empty');
@ -532,27 +920,61 @@ export class VideoconferenceComponent implements OnDestroy, AfterViewInit {
const livekitUrl = this.libService.getLivekitUrl(); const livekitUrl = this.libService.getLivekitUrl();
this.openviduService.initializeAndSetToken(token, livekitUrl); this.openviduService.initializeAndSetToken(token, livekitUrl);
this.log.d('Token has been successfully set. Room is ready to join'); this.log.d('Token has been successfully set. Room is ready to join');
this.isRoomReady = true;
this.showPrejoin = false; // Only update showPrejoin if user hasn't initiated join process yet
// This prevents prejoin from showing again after user clicked join
if (!this.hasUserInitiatedJoin()) {
this.updateComponentState({
state: VideoconferenceState.PREJOIN_SHOWN,
isRoomReady: true,
showPrejoin: this.libService.showPrejoin()
});
} else {
// User has initiated join, proceed to hide prejoin and continue
this.log.d('User has initiated join, hiding prejoin and proceeding');
this.updateComponentState({
state: VideoconferenceState.READY_TO_CONNECT,
isRoomReady: true,
showPrejoin: false
});
}
} catch (error) { } catch (error) {
this.log.e('Error trying to set token', 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; if (!error) return;
this.log.e('Token error received', error); 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.actionService.openDialog(error.name, error.message, false);
} }
}); });
this.prejoinSub = this.libService.prejoin$.subscribe((value: boolean) => {
this.showPrejoin = value; this.libService.prejoin$.pipe(takeUntil(this.destroy$)).subscribe((value: boolean) => {
if (!this.showPrejoin) { this.updateComponentState({
showPrejoin: value
});
if (!value) {
// Emit token ready if the prejoin page won't be shown // Emit token ready if the prejoin page won't be shown
// Ensure we have a participant name before proceeding with the join // Ensure we have a participant name before proceeding with the join
@ -563,10 +985,11 @@ export class VideoconferenceComponent implements OnDestroy, AfterViewInit {
this._onReadyToJoin(); this._onReadyToJoin();
} else { } else {
// No name yet - set up a one-time subscription to wait for it // No name yet - set up a one-time subscription to wait for it
const waitForNameSub = this.libService.participantName$ this.libService.participantName$
.pipe( .pipe(
filter((name) => !!name), filter((name) => !!name),
take(1) take(1),
takeUntil(this.destroy$)
) )
.subscribe(() => { .subscribe(() => {
// Now we have the name in latestParticipantName // Now we have the name in latestParticipantName
@ -576,24 +999,35 @@ export class VideoconferenceComponent implements OnDestroy, AfterViewInit {
setTimeout(() => { setTimeout(() => {
if (!this.latestParticipantName) { if (!this.latestParticipantName) {
this.log.w('No participant name received after timeout, proceeding anyway'); this.log.w('No participant name received after timeout, proceeding anyway');
waitForNameSub.unsubscribe();
const storedName = this.storageSrv.getParticipantName(); const storedName = this.storageSrv.getParticipantName();
if (storedName) { if (storedName) {
this.latestParticipantName = storedName; this.latestParticipantName = storedName;
this.libService.setParticipantName(storedName); this.libService.updateGeneralConfig({ participantName: storedName });
} }
this._onReadyToJoin(); 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) { if (name) {
this.latestParticipantName = name; this.latestParticipantName = name;
this.storageSrv.setParticipantName(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) { update(value: boolean) {
if (this.libService.showRecordingActivity() !== value) { this.libService.updateRecordingActivityConfig({ enabled: value });
this.libService.setRecordingActivity(value);
}
} }
} }
@ -103,8 +101,6 @@ export class ActivitiesPanelBroadcastingActivityDirective implements AfterViewIn
} }
update(value: boolean) { 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 standalone: false
}) })
export class AdminDashboardRecordingsListDirective implements AfterViewInit, OnDestroy { export class AdminDashboardRecordingsListDirective implements AfterViewInit, OnDestroy {
@Input() set recordingsList(value: RecordingInfo[]) { @Input() set recordingsList(value: RecordingInfo[]) {
this.recordingsValue = value; this.recordingsValue = value;
this.update(this.recordingsValue); this.update(this.recordingsValue);
} }
recordingsValue: RecordingInfo [] = []; recordingsValue: RecordingInfo[] = [];
constructor(public elementRef: ElementRef, private libService: OpenViduComponentsConfigService) {} constructor(
public elementRef: ElementRef,
private libService: OpenViduComponentsConfigService
) {}
ngAfterViewInit() { ngAfterViewInit() {
this.update(this.recordingsValue); this.update(this.recordingsValue);
@ -38,9 +40,7 @@ export class AdminDashboardRecordingsListDirective implements AfterViewInit, OnD
} }
update(value: RecordingInfo[]) { update(value: RecordingInfo[]) {
if (this.libService.getAdminRecordingsList() !== value) { this.libService.updateAdminConfig({ recordingsList: value });
this.libService.setAdminRecordingsList(value);
}
} }
} }
@ -58,7 +58,6 @@ export class AdminDashboardRecordingsListDirective implements AfterViewInit, OnD
standalone: false standalone: false
}) })
export class AdminDashboardTitleDirective implements AfterViewInit, OnDestroy { export class AdminDashboardTitleDirective implements AfterViewInit, OnDestroy {
@Input() set navbarTitle(value: string) { @Input() set navbarTitle(value: string) {
this.navbarTitleValue = value; this.navbarTitleValue = value;
this.update(this.navbarTitleValue); this.update(this.navbarTitleValue);
@ -66,7 +65,10 @@ export class AdminDashboardTitleDirective implements AfterViewInit, OnDestroy {
navbarTitleValue: string = 'OpenVidu Dashboard'; navbarTitleValue: string = 'OpenVidu Dashboard';
constructor(public elementRef: ElementRef, private libService: OpenViduComponentsConfigService) {} constructor(
public elementRef: ElementRef,
private libService: OpenViduComponentsConfigService
) {}
ngAfterViewInit() { ngAfterViewInit() {
this.update(this.navbarTitleValue); this.update(this.navbarTitleValue);
@ -80,13 +82,10 @@ export class AdminDashboardTitleDirective implements AfterViewInit, OnDestroy {
} }
update(value: any) { update(value: any) {
if (this.libService.getAdminDashboardTitle() !== value) { this.libService.updateAdminConfig({ dashboardTitle: value });
this.libService.setAdminDashboardTitle(value);
}
} }
} }
/** /**
* The **navbarTitle** directive allows customize the title of the navbar in {@link AdminLoginComponent}. * 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 standalone: false
}) })
export class AdminLoginTitleDirective implements AfterViewInit, OnDestroy { export class AdminLoginTitleDirective implements AfterViewInit, OnDestroy {
@Input() set navbarTitle(value: any) { @Input() set navbarTitle(value: any) {
this.navbarTitleValue = value; this.navbarTitleValue = value;
this.update(this.navbarTitleValue); this.update(this.navbarTitleValue);
@ -109,7 +107,10 @@ export class AdminLoginTitleDirective implements AfterViewInit, OnDestroy {
navbarTitleValue: any = null; navbarTitleValue: any = null;
constructor(public elementRef: ElementRef, private libService: OpenViduComponentsConfigService) {} constructor(
public elementRef: ElementRef,
private libService: OpenViduComponentsConfigService
) {}
ngAfterViewInit() { ngAfterViewInit() {
this.update(this.navbarTitleValue); this.update(this.navbarTitleValue);
@ -123,14 +124,10 @@ export class AdminLoginTitleDirective implements AfterViewInit, OnDestroy {
} }
update(value: any) { update(value: any) {
if (this.libService.getAdminLoginTitle() !== value) { this.libService.updateAdminConfig({ loginTitle: value });
this.libService.setAdminLoginTitle(value);
}
} }
} }
/** /**
* The **error** directive allows show the authentication error in {@link AdminLoginComponent}. * 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> * <ov-admin-login [error]="error"></ov-admin-login>
* *
*/ */
@Directive({ @Directive({
selector: 'ov-admin-login[error]', selector: 'ov-admin-login[error]',
standalone: false standalone: false
}) })
export class AdminLoginErrorDirective implements AfterViewInit, OnDestroy { export class AdminLoginErrorDirective implements AfterViewInit, OnDestroy {
@Input() set error(value: any) { @Input() set error(value: any) {
this.errorValue = value; this.errorValue = value;
this.update(this.errorValue); this.update(this.errorValue);
@ -153,7 +149,10 @@ export class AdminLoginErrorDirective implements AfterViewInit, OnDestroy {
errorValue: any = null; errorValue: any = null;
constructor(public elementRef: ElementRef, private libService: OpenViduComponentsConfigService) {} constructor(
public elementRef: ElementRef,
private libService: OpenViduComponentsConfigService
) {}
ngAfterViewInit() { ngAfterViewInit() {
this.update(this.errorValue); this.update(this.errorValue);
@ -167,9 +166,6 @@ export class AdminLoginErrorDirective implements AfterViewInit, OnDestroy {
} }
update(value: any) { update(value: any) {
if (this.libService.getAdminLoginError() !== value) { this.libService.updateAdminConfig({ loginError: value });
this.libService.setAdminLoginError(value);
}
} }
} }

View File

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

View File

@ -122,7 +122,7 @@ export class ToolbarBrandingLogoDirective implements AfterViewInit, OnDestroy {
} }
private update(value: string) { 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) { 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; muteValue: boolean = true;
constructor(public elementRef: ElementRef, private libService: OpenViduComponentsConfigService) {} constructor(
public elementRef: ElementRef,
private libService: OpenViduComponentsConfigService
) {}
ngAfterViewInit() { ngAfterViewInit() {
this.update(this.muteValue); this.update(this.muteValue);
@ -46,8 +49,6 @@ export class ParticipantPanelItemMuteButtonDirective implements AfterViewInit, O
} }
update(value: boolean) { update(value: boolean) {
if (this.libService.showParticipantItemMuteButton() !== value) { this.libService.updateStreamConfig({ participantItemMuteButton: value });
this.libService.setParticipantItemMuteButton(value);
}
} }
} }

View File

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

View File

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

View File

@ -55,7 +55,7 @@ export class LivekitUrlDirective implements OnDestroy {
* @ignore * @ignore
*/ */
update(value: string) { update(value: string) {
this.libService.setLivekitUrl(value); this.libService.updateGeneralConfig({ livekitUrl: value });
} }
} }
@ -108,7 +108,7 @@ export class TokenDirective implements OnDestroy {
* @ignore * @ignore
*/ */
update(value: string) { update(value: string) {
this.libService.setToken(value); this.libService.updateGeneralConfig({ token: value });
} }
} }
@ -160,7 +160,7 @@ export class TokenErrorDirective implements OnDestroy {
* @ignore * @ignore
*/ */
update(value: any) { update(value: any) {
this.libService.setTokenError(value); this.libService.updateGeneralConfig({ tokenError: value });
} }
} }
@ -212,9 +212,7 @@ export class MinimalDirective implements OnDestroy {
* @ignore * @ignore
*/ */
update(value: boolean) { update(value: boolean) {
if (this.libService.isMinimal() !== value) { this.libService.updateGeneralConfig({ minimal: value });
this.libService.setMinimal(value);
}
} }
} }
@ -538,7 +536,7 @@ export class ParticipantNameDirective implements AfterViewInit, OnDestroy {
* @ignore * @ignore
*/ */
update(value: string) { 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 * @ignore
*/ */
update(value: boolean) { update(value: boolean) {
if (this.libService.isPrejoin() !== value) { this.libService.updateGeneralConfig({ prejoin: value });
this.libService.setPrejoin(value);
}
} }
} }
@ -663,7 +659,7 @@ export class VideoEnabledDirective implements OnDestroy {
// Ensure libService state is consistent with the final enabled state // Ensure libService state is consistent with the final enabled state
if (this.libService.isVideoEnabled() !== finalEnabledState) { 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); this.storageService.setMicrophoneEnabled(finalEnabledState);
if (this.libService.isAudioEnabled() !== enabled) { 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 * @ignore
*/ */
update(value: string) { 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, ActivitiesPanelDirective,
BackgroundEffectsPanelDirective BackgroundEffectsPanelDirective
} from './openvidu-components-angular.directive'; } from './openvidu-components-angular.directive';
import {
LayoutAdditionalElementsDirective,
ParticipantPanelAfterLocalParticipantDirective,
ParticipantPanelParticipantBadgeDirective,
PreJoinDirective
} from './internals.directive';
@NgModule({ @NgModule({
declarations: [ declarations: [
@ -29,6 +35,10 @@ import {
ToolbarAdditionalPanelButtonsDirective, ToolbarAdditionalPanelButtonsDirective,
ParticipantPanelItemElementsDirective, ParticipantPanelItemElementsDirective,
ActivitiesPanelDirective, ActivitiesPanelDirective,
PreJoinDirective,
ParticipantPanelAfterLocalParticipantDirective,
LayoutAdditionalElementsDirective,
ParticipantPanelParticipantBadgeDirective
// BackgroundEffectsPanelDirective // BackgroundEffectsPanelDirective
], ],
exports: [ exports: [
@ -44,6 +54,10 @@ import {
ToolbarAdditionalPanelButtonsDirective, ToolbarAdditionalPanelButtonsDirective,
ParticipantPanelItemElementsDirective, ParticipantPanelItemElementsDirective,
ActivitiesPanelDirective, ActivitiesPanelDirective,
PreJoinDirective,
ParticipantPanelAfterLocalParticipantDirective,
LayoutAdditionalElementsDirective,
ParticipantPanelParticipantBadgeDirective
// BackgroundEffectsPanelDirective // BackgroundEffectsPanelDirective
] ]
}) })

View File

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

View File

@ -55,7 +55,9 @@
"LEAVE": "离开会议", "LEAVE": "离开会议",
"PARTICIPANTS": "参与者", "PARTICIPANTS": "参与者",
"CHAT": "聊天", "CHAT": "聊天",
"ACTIVITIES": "活动" "ACTIVITIES": "活动",
"NO_TRACKS_PUBLISHED": "请分享音频或视频以开始录制。",
"VIEW_RECORDINGS": "查看录像"
}, },
"STREAM": { "STREAM": {
"SETTINGS": "设置", "SETTINGS": "设置",
@ -89,7 +91,9 @@
"MICROPHONE": "麦克风", "MICROPHONE": "麦克风",
"SCREEN": "屏幕", "SCREEN": "屏幕",
"NO_STREAMS": "无", "NO_STREAMS": "无",
"YOU": "你" "YOU": "你",
"MUTE": "静音",
"UNMUTE": "取消静音"
}, },
"SETTINGS": { "SETTINGS": {
"TITLE": "设置", "TITLE": "设置",
@ -114,6 +118,10 @@
"SUBTITLE": "为后人记录你的会议", "SUBTITLE": "为后人记录你的会议",
"CONTENT_TITLE": "记录你的视频通话", "CONTENT_TITLE": "记录你的视频通话",
"CONTENT_SUBTITLE": "当录音完成后,你将可以轻松地下载它", "CONTENT_SUBTITLE": "当录音完成后,你将可以轻松地下载它",
"VIEW_ONLY_SUBTITLE": "查看和访问房间录音",
"VIEW_ONLY_CONTENT_TITLE": "视频通话录音",
"VIEW_ONLY_CONTENT_SUBTITLE": "在这里您可以访问所有可用的录音",
"WATCH": "观看",
"STARTING": "开始录音", "STARTING": "开始录音",
"STOPPING": "停止录制", "STOPPING": "停止录制",
"IN_PROGRESS": "录音中", "IN_PROGRESS": "录音中",
@ -124,7 +132,10 @@
"DELETE_QUESTION": "您确定要删除录音吗", "DELETE_QUESTION": "您确定要删除录音吗",
"DOWNLOAD": "下载", "DOWNLOAD": "下载",
"RECORDINGS": "录制", "RECORDINGS": "录制",
"NO_MODERATOR": "只有主持人可以开始录音" "NO_MODERATOR": "只有主持人可以开始录音",
"NO_TRACKS_PUBLISHED": "请分享音频或视频以开始录制。",
"NO_RECORDINGS_AVAILABLE": "目前没有可用的录音",
"ERROR_STARTING": "开始录音时出错"
}, },
"STREAMING": { "STREAMING": {
"TITLE": "直播", "TITLE": "直播",

View File

@ -55,7 +55,8 @@
"LEAVE": "Die Raum verlassen", "LEAVE": "Die Raum verlassen",
"PARTICIPANTS": "Teilnehmer", "PARTICIPANTS": "Teilnehmer",
"CHAT": "Chat", "CHAT": "Chat",
"ACTIVITIES": "Aktivitäten" "ACTIVITIES": "Aktivitäten",
"NO_TRACKS_PUBLISHED": "Teile Audio oder Video, um mit der Aufnahme zu beginnen."
}, },
"STREAM": { "STREAM": {
"SETTINGS": "Einstellungen", "SETTINGS": "Einstellungen",
@ -89,7 +90,9 @@
"MICROPHONE": "MIKROFON", "MICROPHONE": "MIKROFON",
"SCREEN": "BILDSCHIRM", "SCREEN": "BILDSCHIRM",
"NO_STREAMS": "KEINE", "NO_STREAMS": "KEINE",
"YOU": "Sie" "YOU": "Sie",
"MUTE": "Stummschalten",
"UNMUTE": "Stummschaltung aufheben"
}, },
"SETTINGS": { "SETTINGS": {
"TITLE": "Einstellungen", "TITLE": "Einstellungen",
@ -124,7 +127,9 @@
"DELETE_QUESTION": "Möchten Sie die Aufzeichnung wirklich löschen?", "DELETE_QUESTION": "Möchten Sie die Aufzeichnung wirklich löschen?",
"DOWNLOAD": "Download", "DOWNLOAD": "Download",
"RECORDINGS": "AUFZEICHNUNGEN", "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": { "STREAMING": {
"TITLE": "Streaming", "TITLE": "Streaming",

View File

@ -55,7 +55,9 @@
"LEAVE": "Leave the room", "LEAVE": "Leave the room",
"PARTICIPANTS": "Participants", "PARTICIPANTS": "Participants",
"CHAT": "Chat", "CHAT": "Chat",
"ACTIVITIES": "Activities" "ACTIVITIES": "Activities",
"NO_TRACKS_PUBLISHED": "Share audio or video to start recording.",
"VIEW_RECORDINGS": "View recordings"
}, },
"STREAM": { "STREAM": {
"SETTINGS": "Settings", "SETTINGS": "Settings",
@ -89,7 +91,9 @@
"MICROPHONE": "MICROPHONE", "MICROPHONE": "MICROPHONE",
"SCREEN": "SCREEN", "SCREEN": "SCREEN",
"NO_STREAMS": "NONE", "NO_STREAMS": "NONE",
"YOU": "You" "YOU": "You",
"MUTE": "Mute",
"UNMUTE": "Unmute"
}, },
"SETTINGS": { "SETTINGS": {
"TITLE": "Settings", "TITLE": "Settings",
@ -114,6 +118,13 @@
"SUBTITLE": "Record your meeting for posterity", "SUBTITLE": "Record your meeting for posterity",
"CONTENT_TITLE": "Record your video call", "CONTENT_TITLE": "Record your video call",
"CONTENT_SUBTITLE": "When recording has finished you will be able to download it with ease", "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", "STARTING": "Starting recording",
"STOPPING": "Stopping recording", "STOPPING": "Stopping recording",
"IN_PROGRESS": "Recording in progress ...", "IN_PROGRESS": "Recording in progress ...",
@ -124,7 +135,11 @@
"DELETE_QUESTION": "Are you sure you want to delete the recording?", "DELETE_QUESTION": "Are you sure you want to delete the recording?",
"DOWNLOAD": "Download", "DOWNLOAD": "Download",
"RECORDINGS": "RECORDINGS", "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": { "STREAMING": {
"TITLE": "Streaming", "TITLE": "Streaming",

View File

@ -55,7 +55,9 @@
"LEAVE": "Salir de la sala", "LEAVE": "Salir de la sala",
"PARTICIPANTS": "Participantes", "PARTICIPANTS": "Participantes",
"CHAT": "Chat", "CHAT": "Chat",
"ACTIVITIES": "Actividades" "ACTIVITIES": "Actividades",
"NO_TRACKS_PUBLISHED": "Comparte audio o video para poder empezar a grabar.",
"VIEW_RECORDINGS": "Ver grabaciones"
}, },
"STREAM": { "STREAM": {
"SETTINGS": "Ajustes", "SETTINGS": "Ajustes",
@ -89,7 +91,9 @@
"MICROPHONE": "MICRÓFONO", "MICROPHONE": "MICRÓFONO",
"SCREEN": "PANTALLA", "SCREEN": "PANTALLA",
"NO_STREAMS": "NINGUNO", "NO_STREAMS": "NINGUNO",
"YOU": "Tú" "YOU": "Tú",
"MUTE": "Silenciar",
"UNMUTE": "Activar audio"
}, },
"SETTINGS": { "SETTINGS": {
"TITLE": "Configuración", "TITLE": "Configuración",
@ -114,6 +118,10 @@
"SUBTITLE": "Graba tus llamadas para la posteridad", "SUBTITLE": "Graba tus llamadas para la posteridad",
"CONTENT_TITLE": "Graba tu video conferencia", "CONTENT_TITLE": "Graba tu video conferencia",
"CONTENT_SUBTITLE": "Cuando la grabación haya finalizado, podrás descargarla con facilidad", "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...", "STARTING": "Iniciando grabación...",
"STOPPING": "Parando grabación", "STOPPING": "Parando grabación",
"IN_PROGRESS": "Grabación en curso", "IN_PROGRESS": "Grabación en curso",
@ -124,7 +132,10 @@
"DELETE_QUESTION": "¿Estás seguro/a de que deseas borrar la grabación?", "DELETE_QUESTION": "¿Estás seguro/a de que deseas borrar la grabación?",
"DOWNLOAD": "Descargar", "DOWNLOAD": "Descargar",
"RECORDINGS": "GRABACIONES", "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": { "STREAMING": {
"TITLE": "Streaming", "TITLE": "Streaming",

View File

@ -55,7 +55,9 @@
"LEAVE": "Quitter la salle", "LEAVE": "Quitter la salle",
"PARTICIPANTS": "Participants", "PARTICIPANTS": "Participants",
"CHAT": "Chat", "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": { "STREAM": {
"SETTINGS": "Paramètres", "SETTINGS": "Paramètres",
@ -89,7 +91,9 @@
"MICROPHONE": "MICROPHONE", "MICROPHONE": "MICROPHONE",
"SCREEN": "ÉCRAN", "SCREEN": "ÉCRAN",
"NO_STREAMS": "PAS_DE_FLUX", "NO_STREAMS": "PAS_DE_FLUX",
"YOU": "Vous" "YOU": "Vous",
"MUTE": "Couper le son",
"UNMUTE": "Désactiver le son"
}, },
"SETTINGS": { "SETTINGS": {
"TITLE": "Paramètres", "TITLE": "Paramètres",
@ -114,6 +118,10 @@
"SUBTITLE": "Enregistrez votre réunion pour la postérité", "SUBTITLE": "Enregistrez votre réunion pour la postérité",
"CONTENT_TITLE": "Enregistrez votre appel vidéo", "CONTENT_TITLE": "Enregistrez votre appel vidéo",
"CONTENT_SUBTITLE": "Une fois l'enregistrement terminé, vous pourrez le télécharger facilement", "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", "STARTING": "Début de l'enregistrement",
"STOPPING": "Arrêt de l'enregistrement", "STOPPING": "Arrêt de l'enregistrement",
"IN_PROGRESS": "Enregistrement en cours", "IN_PROGRESS": "Enregistrement en cours",
@ -124,7 +132,10 @@
"DELETE_QUESTION": "Voulez-vous vraiment supprimer l'enregistrement ?", "DELETE_QUESTION": "Voulez-vous vraiment supprimer l'enregistrement ?",
"DOWNLOAD": "Télécharger", "DOWNLOAD": "Télécharger",
"RECORDINGS": "ENREGISTREMENTS", "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": { "STREAMING": {
"TITLE": "Streaming", "TITLE": "Streaming",

View File

@ -55,7 +55,9 @@
"LEAVE": "कमरा छोड़ें", "LEAVE": "कमरा छोड़ें",
"PARTICIPANTS": "सदस्य", "PARTICIPANTS": "सदस्य",
"CHAT": "बातचीत", "CHAT": "बातचीत",
"ACTIVITIES": "गतिविधियाँ" "ACTIVITIES": "गतिविधियाँ",
"NO_TRACKS_PUBLISHED": "रिकॉर्डिंग शुरू करने के लिए ऑडियो या वीडियो साझा करें।",
"VIEW_RECORDINGS": "रिकॉर्डिंग देखें"
}, },
"STREAM": { "STREAM": {
"SETTINGS": "सेटिंग्स", "SETTINGS": "सेटिंग्स",
@ -89,7 +91,9 @@
"MICROPHONE": "माइक्रोफ़ोन", "MICROPHONE": "माइक्रोफ़ोन",
"SCREEN": "स्क्रीन", "SCREEN": "स्क्रीन",
"NO_STREAMS": "कोई_स्ट्रीम_नहीं", "NO_STREAMS": "कोई_स्ट्रीम_नहीं",
"YOU": "आप" "YOU": "आप",
"MUTE": "मौन",
"UNMUTE": "अनमौन"
}, },
"SETTINGS": { "SETTINGS": {
"TITLE": "सेटिंग्स", "TITLE": "सेटिंग्स",
@ -114,6 +118,10 @@
"SUBTITLE": "अपनी बैठक को भावी पीढ़ी के लिए रिकॉर्ड करें", "SUBTITLE": "अपनी बैठक को भावी पीढ़ी के लिए रिकॉर्ड करें",
"CONTENT_TITLE": "अपना वीडियो कॉल रिकॉर्ड करें", "CONTENT_TITLE": "अपना वीडियो कॉल रिकॉर्ड करें",
"CONTENT_SUBTITLE": "रिकॉर्डिंग समाप्त हो जाने पर आप इसे आसानी से डाउनलोड कर सकेंगे", "CONTENT_SUBTITLE": "रिकॉर्डिंग समाप्त हो जाने पर आप इसे आसानी से डाउनलोड कर सकेंगे",
"VIEW_ONLY_SUBTITLE": "कमरे की रिकॉर्डिंग देखें और एक्सेस करें",
"VIEW_ONLY_CONTENT_TITLE": "वीडियो कॉल रिकॉर्डिंग",
"VIEW_ONLY_CONTENT_SUBTITLE": "यहाँ आप सभी उपलब्ध रिकॉर्डिंग तक पहुँच सकते हैं",
"WATCH": "देखना",
"STARTING": "रिकॉर्डिंग शुरू कर रहा है", "STARTING": "रिकॉर्डिंग शुरू कर रहा है",
"STOPPING": "रिकॉर्डिंग बंद करना", "STOPPING": "रिकॉर्डिंग बंद करना",
"IN_PROGRESS": "रिकॉर्डिंग चल रही है", "IN_PROGRESS": "रिकॉर्डिंग चल रही है",
@ -124,7 +132,10 @@
"DELETE_QUESTION": "क्या आप वाकई रिकॉर्डिंग हटाना चाहते हैं", "DELETE_QUESTION": "क्या आप वाकई रिकॉर्डिंग हटाना चाहते हैं",
"DOWNLOAD": "डाउनलोड", "DOWNLOAD": "डाउनलोड",
"RECORDINGS": "रिकॉर्डिंग", "RECORDINGS": "रिकॉर्डिंग",
"NO_MODERATOR": "केवल मॉडरेटर ही रिकॉर्डिंग शुरू कर सकता है" "NO_MODERATOR": "केवल मॉडरेटर ही रिकॉर्डिंग शुरू कर सकता है",
"NO_TRACKS_PUBLISHED": "रिकॉर्डिंग शुरू करने के लिए ऑडियो या वीडियो साझा करें।",
"NO_RECORDINGS_AVAILABLE": "इस समय कोई रिकॉर्डिंग उपलब्ध नहीं है",
"ERROR_STARTING": "रिकॉर्डिंग शुरू करने में त्रुटि"
}, },
"STREAMING": { "STREAMING": {
"TITLE": "स्ट्रीमिंग", "TITLE": "स्ट्रीमिंग",

View File

@ -55,7 +55,9 @@
"LEAVE": "Abbandona la stanza", "LEAVE": "Abbandona la stanza",
"PARTICIPANTS": "Partecipanti", "PARTICIPANTS": "Partecipanti",
"CHAT": "Chat", "CHAT": "Chat",
"ACTIVITIES": "Attività" "ACTIVITIES": "Attività",
"NO_TRACKS_PUBLISHED": "Condividi audio o video per iniziare la registrazione.",
"VIEW_RECORDINGS": "Visualizza registrazioni"
}, },
"STREAM": { "STREAM": {
"SETTINGS": "Impostazioni", "SETTINGS": "Impostazioni",
@ -89,7 +91,9 @@
"MICROPHONE": "MICROFONO", "MICROPHONE": "MICROFONO",
"SCREEN": "SCREEN", "SCREEN": "SCREEN",
"NO_STREAMS": "NESSUNO", "NO_STREAMS": "NESSUNO",
"YOU": "Tu" "YOU": "Tu",
"MUTE": "Disattiva l'audio",
"UNMUTE": "Attiva l'audio"
}, },
"SETTINGS": { "SETTINGS": {
"TITLE": "Impostazioni", "TITLE": "Impostazioni",
@ -114,6 +118,10 @@
"SUBTITLE": "Registra la tua riunione per i posteri", "SUBTITLE": "Registra la tua riunione per i posteri",
"CONTENT_TITLE": "Registra la tua videochiamata", "CONTENT_TITLE": "Registra la tua videochiamata",
"CONTENT_SUBTITLE": "Al termine della registrazione potrete scaricarla con facilità", "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", "STARTING": "Avvio della registrazione",
"STOPPING": "Interruzione della registrazione", "STOPPING": "Interruzione della registrazione",
"IN_PROGRESS": "Registrazione in corso", "IN_PROGRESS": "Registrazione in corso",
@ -124,7 +132,10 @@
"DELETE_QUESTION": "Sei sicuro di voler eliminare la registrazione?", "DELETE_QUESTION": "Sei sicuro di voler eliminare la registrazione?",
"DOWNLOAD": "Scarica", "DOWNLOAD": "Scarica",
"RECORDINGS": "REGISTRAZIONI", "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": { "STREAMING": {
"TITLE": "Streaming", "TITLE": "Streaming",

View File

@ -55,7 +55,9 @@
"LEAVE": "ルームを終了する", "LEAVE": "ルームを終了する",
"PARTICIPANTS": "参加者", "PARTICIPANTS": "参加者",
"CHAT": "チャット", "CHAT": "チャット",
"ACTIVITIES": "アクティビティ" "ACTIVITIES": "アクティビティ",
"NO_TRACKS_PUBLISHED": "録音を開始するには、音声または動画を共有してください。",
"VIEW_RECORDINGS": "録画を表示"
}, },
"STREAM": { "STREAM": {
"SETTINGS": "設定", "SETTINGS": "設定",
@ -89,7 +91,9 @@
"MICROPHONE": "マイクロフォン", "MICROPHONE": "マイクロフォン",
"SCREEN": "スクリーン", "SCREEN": "スクリーン",
"NO_STREAMS": "ストリームなし", "NO_STREAMS": "ストリームなし",
"YOU": "あなた" "YOU": "あなた",
"MUTE": "ミュート",
"UNMUTE": "ミュート解除"
}, },
"SETTINGS": { "SETTINGS": {
"TITLE": "設定", "TITLE": "設定",
@ -114,6 +118,10 @@
"SUBTITLE": "会議を録画して保存する", "SUBTITLE": "会議を録画して保存する",
"CONTENT_TITLE": "ビデオ通話を録音する", "CONTENT_TITLE": "ビデオ通話を録音する",
"CONTENT_SUBTITLE": "録画が完了したら、簡単にダウンロードできます", "CONTENT_SUBTITLE": "録画が完了したら、簡単にダウンロードできます",
"VIEW_ONLY_SUBTITLE": "ルームの録画を表示してアクセスする",
"VIEW_ONLY_CONTENT_TITLE": "ビデオ通話の録画",
"VIEW_ONLY_CONTENT_SUBTITLE": "ここで利用可能なすべての録画にアクセスできます",
"WATCH": "視聴する",
"STARTING": "録画開始", "STARTING": "録画開始",
"STOPPING": "録音停止", "STOPPING": "録音停止",
"IN_PROGRESS": "録画中", "IN_PROGRESS": "録画中",
@ -124,7 +132,10 @@
"DELETE_QUESTION": "録画を削除してもよろしいですか", "DELETE_QUESTION": "録画を削除してもよろしいですか",
"DOWNLOAD": "保存", "DOWNLOAD": "保存",
"RECORDINGS": "録画", "RECORDINGS": "録画",
"NO_MODERATOR": "録画を開始できるのは、モデレーターのみです" "NO_MODERATOR": "録画を開始できるのは、モデレーターのみです",
"NO_TRACKS_PUBLISHED": "録音を開始するには、音声または動画を共有してください。",
"NO_RECORDINGS_AVAILABLE": "現在利用可能な録画はありません",
"ERROR_STARTING": "録画開始エラー"
}, },
"STREAMING": { "STREAMING": {
"TITLE": "ストリーミング", "TITLE": "ストリーミング",

View File

@ -55,7 +55,9 @@
"LEAVE": "Verlaat de kamer", "LEAVE": "Verlaat de kamer",
"PARTICIPANTS": "Deelnemers", "PARTICIPANTS": "Deelnemers",
"CHAT": "Chat", "CHAT": "Chat",
"ACTIVITIES": "Activiteiten" "ACTIVITIES": "Activiteiten",
"NO_TRACKS_PUBLISHED": "Deel audio of video om met opnemen te beginnen.",
"VIEW_RECORDINGS": "Opnames bekijken"
}, },
"STREAM": { "STREAM": {
"SETTINGS": "Instellingen", "SETTINGS": "Instellingen",
@ -89,7 +91,9 @@
"MICROPHONE": "MICROFOON", "MICROPHONE": "MICROFOON",
"SCREEN": "SCHERM", "SCREEN": "SCHERM",
"NO_STREAMS": "GEEN", "NO_STREAMS": "GEEN",
"YOU": "Jij" "YOU": "Jij",
"MUTE": "Dempen",
"UNMUTE": "Dempen opheffen"
}, },
"SETTINGS": { "SETTINGS": {
"TITLE": "Instellingen", "TITLE": "Instellingen",
@ -114,6 +118,10 @@
"SUBTITLE": "Neem uw vergadering op voor het nageslacht", "SUBTITLE": "Neem uw vergadering op voor het nageslacht",
"CONTENT_TITLE": "Neem uw videogesprek op", "CONTENT_TITLE": "Neem uw videogesprek op",
"CONTENT_SUBTITLE": "Als de opname klaar is kunt u deze met gemak downloaden", "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", "STARTING": "Beginnen met opnemen",
"STOPPING": "Opname stoppen", "STOPPING": "Opname stoppen",
"IN_PROGRESS": "Opname in uitvoering", "IN_PROGRESS": "Opname in uitvoering",
@ -124,7 +132,10 @@
"DELETE_QUESTION": "Weet je zeker dat je de opname wilt verwijderen?", "DELETE_QUESTION": "Weet je zeker dat je de opname wilt verwijderen?",
"DOWNLOAD": "Downloaden", "DOWNLOAD": "Downloaden",
"RECORDINGS": "OPNAME", "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": { "STREAMING": {
"TITLE": "Streaming", "TITLE": "Streaming",

View File

@ -55,7 +55,9 @@
"LEAVE": "Sair da sala", "LEAVE": "Sair da sala",
"PARTICIPANTS": "Participantes", "PARTICIPANTS": "Participantes",
"CHAT": "Chat", "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": { "STREAM": {
"SETTINGS": "Configurações", "SETTINGS": "Configurações",
@ -89,7 +91,9 @@
"MICROPHONE": "MICROFONE", "MICROPHONE": "MICROFONE",
"SCREEN": "TELA", "SCREEN": "TELA",
"NO_STREAMS": "NENHUM", "NO_STREAMS": "NENHUM",
"YOU": "Você (eu)" "YOU": "Você (eu)",
"MUTE": "Silenciar",
"UNMUTE": "Ativar som"
}, },
"SETTINGS": { "SETTINGS": {
"TITLE": "Configurações", "TITLE": "Configurações",
@ -114,6 +118,10 @@
"SUBTITLE": "Grave a sua reunião para a posteridade", "SUBTITLE": "Grave a sua reunião para a posteridade",
"CONTENT_TITLE": "Grave a sua videochamada", "CONTENT_TITLE": "Grave a sua videochamada",
"CONTENT_SUBTITLE": "Quando a gravação tiver terminado, poderá descarregá-la com facilidade", "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", "STARTING": "Começar a gravação",
"STOPPING": "Parando a gravação", "STOPPING": "Parando a gravação",
"IN_PROGRESS": "Gravação em andamento", "IN_PROGRESS": "Gravação em andamento",
@ -124,7 +132,10 @@
"DELETE_QUESTION": "Tem certeza de que deseja excluir a gravação?", "DELETE_QUESTION": "Tem certeza de que deseja excluir a gravação?",
"DOWNLOAD": "Download", "DOWNLOAD": "Download",
"RECORDINGS": "GRAVAÇÕES", "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": { "STREAMING": {
"TITLE": "Streaming", "TITLE": "Streaming",

View File

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

View File

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

View File

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

View File

@ -9,7 +9,36 @@ export enum StorageKeys {
CAMERA_ENABLED = 'cameraEnabled', CAMERA_ENABLED = 'cameraEnabled',
LANG = 'lang', LANG = 'lang',
CAPTION_LANG = 'captionLang', 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-'; export const STORAGE_PREFIX = 'ovComponents-';

View File

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

View File

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

View File

@ -1,6 +1,5 @@
import { Injectable } from '@angular/core'; 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'; import { GlobalConfigService } from '../config/global-config.service';
/** /**
@ -10,42 +9,87 @@ import { GlobalConfigService } from '../config/global-config.service';
providedIn: 'root' providedIn: 'root'
}) })
export class LoggerService implements ILogService { export class LoggerService implements ILogService {
public log; private log: Console;
public LOG_FNS = []; private LOG_FNS: Function[] = [];
public MSG_PREFIXES = [ private MSG_PREFIXES: string[][] = [
['[', ']'], ['[', '] DEBUG: '],
['[', '] VERBOSE: '],
['[', '] WARN: '], ['[', '] WARN: '],
['[', '] ERROR: '] ['[', '] ERROR: ']
]; ];
private loggerCache: Map<string, ILogger> = new Map();
constructor(private globalService: GlobalConfigService) { constructor(private globalService: GlobalConfigService) {
this.initializeLogger();
} }
private getLoggerFns(prefix: string) { private initializeLogger(): void {
this.log = window.console; this.log = window.console;
this.LOG_FNS = [this.log.log, this.log.warn, this.log.error]; this.LOG_FNS = [
const loggerFns = this.LOG_FNS.map((logTemplFn, i) => { this.log.log.bind(this.log),
return logTemplFn.bind(this.log, this.MSG_PREFIXES[i][0] + prefix + this.MSG_PREFIXES[i][1]); this.log.debug.bind(this.log),
}); this.log.warn.bind(this.log),
return loggerFns; 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 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);
}, const debugFn = (...args: any[]): void => {
e: function(...args: any[]) { if (!prodMode) {
loggerService.getLoggerFns(prefix)[2].apply(this.log, arguments); // 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 * @internal
*/ */
initRoom(): void { 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 videoDeviceId = this.deviceService.getCameraSelected()?.device ?? undefined;
const audioDeviceId = this.deviceService.getMicrophoneSelected()?.device ?? undefined; const audioDeviceId = this.deviceService.getMicrophoneSelected()?.device ?? undefined;
const roomOptions: RoomOptions = { const roomOptions: RoomOptions = {
@ -88,6 +94,7 @@ export class OpenViduService {
disconnectOnPageLeave: true disconnectOnPageLeave: true
}; };
this.room = new Room(roomOptions); this.room = new Room(roomOptions);
this.log.d('Room initialized successfully');
} }
/** /**
@ -130,12 +137,20 @@ export class OpenViduService {
*/ */
getRoom(): Room { getRoom(): Room {
if (!this.room) { if (!this.room) {
this.log.e('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'); throw new Error('Room is not initialized. Make sure token is set before accessing the room.');
} }
return this.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 * Returns the room name
*/ */
@ -151,6 +166,14 @@ export class OpenViduService {
return this.room?.state === ConnectionState.Connected; 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 * @internal
*/ */
@ -163,6 +186,13 @@ export class OpenViduService {
this.log.e('LiveKit URL is not defined. Please, check the livekitUrl parameter of the VideoConferenceComponent'); this.log.e('LiveKit URL is not defined. Please, check the livekitUrl parameter of the VideoConferenceComponent');
throw new Error('Livekit URL is not defined'); throw new Error('Livekit URL is not defined');
} }
// Initialize room if it doesn't exist yet
// This ensures that getRoom() won't fail if token is set before onTokenRequested
if (!this.room) {
this.log.d('Room not initialized yet, initializing room due to token assignment');
this.initRoom();
}
// return this.room.prepareConnection(this.livekitUrl, this.livekitToken); // return this.room.prepareConnection(this.livekitUrl, this.livekitToken);
} }
@ -208,23 +238,20 @@ export class OpenViduService {
videoDeviceId: string | boolean | undefined = undefined, videoDeviceId: string | boolean | undefined = undefined,
audioDeviceId: string | boolean | undefined = undefined audioDeviceId: string | boolean | undefined = undefined
): Promise<LocalTrack[]> { ): Promise<LocalTrack[]> {
// If video and audio device IDs are not provided, check if they are enabled and use the default devices // Default values: true if device is enabled, false otherwise
if (videoDeviceId === undefined) videoDeviceId = this.deviceService.isCameraEnabled(); videoDeviceId ??= this.deviceService.isCameraEnabled();
if (audioDeviceId === undefined) audioDeviceId = this.deviceService.isMicrophoneEnabled(); audioDeviceId ??= this.deviceService.isMicrophoneEnabled();
let options: CreateLocalTracksOptions = { const options: CreateLocalTracksOptions = {
audio: { echoCancellation: true, noiseSuppression: true }, audio: { echoCancellation: true, noiseSuppression: true },
video: {} video: {}
}; };
// Video device // Video device
if (videoDeviceId === true) { if (videoDeviceId === true) {
if (this.deviceService.hasVideoDeviceAvailable()) { options.video = this.deviceService.hasVideoDeviceAvailable()
videoDeviceId = this.deviceService.getCameraSelected()?.device || 'default'; ? { deviceId: this.deviceService.getCameraSelected()?.device || 'default' }
(options.video as VideoCaptureOptions).deviceId = videoDeviceId; : false;
} else {
options.video = false;
}
} else if (videoDeviceId === false) { } else if (videoDeviceId === false) {
options.video = false; options.video = false;
} else { } else {
@ -249,13 +276,13 @@ export class OpenViduService {
if (options.audio || options.video) { if (options.audio || options.video) {
this.log.d('Creating local tracks with options', options); this.log.d('Creating local tracks with options', options);
newLocalTracks = await createLocalTracks(options); newLocalTracks = await createLocalTracks(options);
// Mute tracks if devices are disabled
if (!this.deviceService.isCameraEnabled()) { if (!this.deviceService.isCameraEnabled()) {
const videoTrack = newLocalTracks.find((track) => track.kind === Track.Kind.Video); newLocalTracks.find((t) => t.kind === Track.Kind.Video)?.mute();
if (videoTrack) videoTrack.mute();
} }
if (!this.deviceService.isMicrophoneEnabled()) { if (!this.deviceService.isMicrophoneEnabled()) {
const audioTrack = newLocalTracks.find((track) => track.kind === Track.Kind.Audio); newLocalTracks.find((t) => t.kind === Track.Kind.Audio)?.mute();
if (audioTrack) audioTrack.mute();
} }
} }
return newLocalTracks; return newLocalTracks;
@ -301,7 +328,7 @@ export class OpenViduService {
return this.deviceService.isCameraEnabled(); return this.deviceService.isCameraEnabled();
} }
const videoTrack = this.localTracks.find((track) => track.kind === Track.Kind.Video); const videoTrack = this.localTracks.find((track) => track.kind === Track.Kind.Video);
return !!videoTrack && !videoTrack.isMuted; return !!videoTrack && !videoTrack.isMuted && videoTrack?.mediaStreamTrack?.enabled;
} }
/** /**
@ -314,7 +341,7 @@ export class OpenViduService {
return this.deviceService.isMicrophoneEnabled(); return this.deviceService.isMicrophoneEnabled();
} }
const audioTrack = this.localTracks.find((track) => track.kind === Track.Kind.Audio); const audioTrack = this.localTracks.find((track) => track.kind === Track.Kind.Audio);
return !!audioTrack && !audioTrack.isMuted; return !!audioTrack && !audioTrack.isMuted && audioTrack?.mediaStreamTrack?.enabled;
} }
/** /**

View File

@ -20,7 +20,7 @@ export class RecordingService {
private recordingStatus = <BehaviorSubject<RecordingStatusInfo>>new BehaviorSubject({ private recordingStatus = <BehaviorSubject<RecordingStatusInfo>>new BehaviorSubject({
status: RecordingStatus.STOPPED, status: RecordingStatus.STOPPED,
recordingList: [] as RecordingInfo[], recordingList: [] as RecordingInfo[],
recordingElapsedTime: new Date(0, 0, 0, 0, 0, 0) startedAt: new Date(0, 0, 0, 0, 0, 0)
}); });
private log: ILogger; private log: ILogger;
@ -41,12 +41,9 @@ export class RecordingService {
* @internal * @internal
*/ */
setRecordingStarted(recordingInfo?: RecordingInfo, startTimestamp?: number) { setRecordingStarted(recordingInfo?: RecordingInfo, startTimestamp?: number) {
// Register the start timestamp of the recording // Determine the actual start timestamp of the recording
// to calculate the elapsed time // Priority: startTimestamp parameter > recordingInfo.startedAt > current time
this.recordingStartTimestamp = recordingInfo?.startedAt || Date.now(); this.recordingStartTimestamp = startTimestamp || recordingInfo?.startedAt || Date.now();
// Initialize the recording elapsed time
this.startRecordingTimer();
const { recordingList } = this.recordingStatus.getValue(); const { recordingList } = this.recordingStatus.getValue();
let updatedRecordingList = [...recordingList]; let updatedRecordingList = [...recordingList];
@ -61,17 +58,22 @@ export class RecordingService {
updatedRecordingList = [recordingInfo, ...updatedRecordingList]; updatedRecordingList = [recordingInfo, ...updatedRecordingList];
} }
} }
// Calculate the elapsed time based on the actual start timestamp
const recordingElapsedTime = new Date(0, 0, 0, 0, 0, 0); const recordingElapsedTime = new Date(0, 0, 0, 0, 0, 0);
if (startTimestamp) { if (this.recordingStartTimestamp) {
const elapsedSeconds = Math.floor((Date.now() - startTimestamp) / 1000); const elapsedSeconds = Math.floor((Date.now() - this.recordingStartTimestamp) / 1000);
recordingElapsedTime.setSeconds(elapsedSeconds); recordingElapsedTime.setSeconds(Math.max(0, elapsedSeconds)); // Ensure non-negative
} }
this.updateStatus({ this.updateStatus({
status: RecordingStatus.STARTED, status: RecordingStatus.STARTED,
recordingList: updatedRecordingList, recordingList: updatedRecordingList,
recordingElapsedTime startedAt: recordingElapsedTime
}); });
// Start the timer after updating the initial state
this.startRecordingTimer();
} }
/** /**
@ -96,7 +98,7 @@ export class RecordingService {
this.updateStatus({ this.updateStatus({
status: RecordingStatus.STOPPED, status: RecordingStatus.STOPPED,
recordingList: updatedRecordingList, recordingList: updatedRecordingList,
recordingElapsedTime: new Date(0, 0, 0, 0, 0, 0) startedAt: new Date(0, 0, 0, 0, 0, 0)
}); });
this.recordingStartTimestamp = null; this.recordingStartTimestamp = null;
@ -107,11 +109,11 @@ export class RecordingService {
* The `started` stastus will be updated automatically when the recording is actually started. * The `started` stastus will be updated automatically when the recording is actually started.
*/ */
setRecordingStarting() { setRecordingStarting() {
const { recordingList, recordingElapsedTime } = this.recordingStatus.getValue(); const { recordingList, startedAt } = this.recordingStatus.getValue();
this.updateStatus({ this.updateStatus({
status: RecordingStatus.STARTING, status: RecordingStatus.STARTING,
recordingList, recordingList,
recordingElapsedTime startedAt
}); });
} }
@ -121,11 +123,11 @@ export class RecordingService {
*/ */
setRecordingFailed(error: string) { setRecordingFailed(error: string) {
this.stopRecordingTimer(); this.stopRecordingTimer();
const { recordingElapsedTime, recordingList } = this.recordingStatus.getValue(); const { startedAt, recordingList } = this.recordingStatus.getValue();
const statusInfo: RecordingStatusInfo = { const statusInfo: RecordingStatusInfo = {
status: RecordingStatus.FAILED, status: RecordingStatus.FAILED,
recordingList, recordingList,
recordingElapsedTime, startedAt,
error error
}; };
this.updateStatus(statusInfo); this.updateStatus(statusInfo);
@ -136,12 +138,12 @@ export class RecordingService {
* The `stopped` stastus will be updated automatically when the recording is actually stopped. * The `stopped` stastus will be updated automatically when the recording is actually stopped.
*/ */
setRecordingStopping() { setRecordingStopping() {
const { recordingElapsedTime, recordingList } = this.recordingStatus.getValue(); const { startedAt, recordingList } = this.recordingStatus.getValue();
this.updateStatus({ this.updateStatus({
status: RecordingStatus.STOPPING, status: RecordingStatus.STOPPING,
recordingList, recordingList,
recordingElapsedTime startedAt
}); });
} }
@ -199,14 +201,14 @@ export class RecordingService {
* @internal * @internal
*/ */
deleteRecording(recording: RecordingInfo) { 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); const updatedList = recordingList.filter((item) => item.id !== recording.id);
if (updatedList.length !== recordingList.length) { if (updatedList.length !== recordingList.length) {
this.updateStatus({ this.updateStatus({
status, status,
recordingList: updatedList, recordingList: updatedList,
recordingElapsedTime startedAt
}); });
return true; return true;
} }
@ -219,11 +221,11 @@ export class RecordingService {
* @internal * @internal
*/ */
setRecordingList(recordings: RecordingInfo[]) { setRecordingList(recordings: RecordingInfo[]) {
const { status, recordingElapsedTime, error } = this.recordingStatus.getValue(); const { status, startedAt, error } = this.recordingStatus.getValue();
this.updateStatus({ this.updateStatus({
status, status,
recordingList: recordings, recordingList: recordings,
recordingElapsedTime, startedAt,
error error
}); });
} }
@ -233,19 +235,21 @@ export class RecordingService {
* @param status {@link RecordingStatus} * @param status {@link RecordingStatus}
*/ */
private updateStatus(statusInfo: RecordingStatusInfo) { private updateStatus(statusInfo: RecordingStatusInfo) {
const { status, recordingList, error, recordingElapsedTime } = statusInfo; const { status, recordingList, error, startedAt } = statusInfo;
this.recordingStatus.next({ this.recordingStatus.next({
status, status,
recordingList, recordingList,
recordingElapsedTime, startedAt,
error error
}); });
} }
private startRecordingTimer() { private startRecordingTimer() {
// Don't override the timestamp if it's already set correctly
if (this.recordingStartTimestamp === null) { if (this.recordingStartTimestamp === null) {
this.recordingStartTimestamp = Date.now(); this.recordingStartTimestamp = Date.now();
} }
if (this.recordingTimeInterval) { if (this.recordingTimeInterval) {
clearInterval(this.recordingTimeInterval); clearInterval(this.recordingTimeInterval);
} }
@ -253,29 +257,29 @@ export class RecordingService {
this.recordingTimeInterval = setInterval(() => { this.recordingTimeInterval = setInterval(() => {
if (!this.recordingStartTimestamp) return; if (!this.recordingStartTimestamp) return;
let { recordingElapsedTime } = this.recordingStatus.getValue(); // Calculate elapsed time based on the actual recording start timestamp
if (recordingElapsedTime) { const elapsedSeconds = Math.floor((Date.now() - this.recordingStartTimestamp) / 1000);
// Calculamos con precisión el tiempo transcurrido const startedAt = new Date(0, 0, 0, 0, 0, 0);
const elapsedSeconds = Math.floor((Date.now() - this.recordingStartTimestamp) / 1000); startedAt.setSeconds(Math.max(0, elapsedSeconds)); // Ensure non-negative
const updatedElapsedTime = new Date(0, 0, 0, 0, 0, 0);
updatedElapsedTime.setSeconds(elapsedSeconds);
const { recordingList, status } = this.recordingStatus.getValue(); const { recordingList, status } = this.recordingStatus.getValue();
this.updateStatus({ this.updateStatus({
status, status,
recordingList, recordingList,
recordingElapsedTime: updatedElapsedTime startedAt
}); });
}
}, 1000); }, 1000);
} }
private stopRecordingTimer() { private stopRecordingTimer() {
clearInterval(this.recordingTimeInterval); if (this.recordingTimeInterval) {
clearInterval(this.recordingTimeInterval);
}
const { recordingList, status, error } = this.recordingStatus.getValue(); const { recordingList, status, error } = this.recordingStatus.getValue();
const statusInfo: RecordingStatusInfo = { const statusInfo: RecordingStatusInfo = {
status, status,
recordingList, recordingList,
startedAt: new Date(0, 0, 0, 0, 0, 0), // Reset elapsed time when stopped
error error
}; };
this.updateStatus(statusInfo); 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 { 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 { LoggerService } from '../logger/logger.service';
import { CustomDevice } from '../../models/device.model'; import { CustomDevice } from '../../models/device.model';
@ -10,13 +10,125 @@ import { CustomDevice } from '../../models/device.model';
@Injectable({ @Injectable({
providedIn: 'root' providedIn: 'root'
}) })
export class StorageService { export class StorageService implements OnDestroy {
public storage = window.localStorage; public localStorage = window.localStorage;
public sessionStorage = window.sessionStorage;
public log: ILogger; public log: ILogger;
protected PREFIX_KEY = STORAGE_PREFIX; protected PREFIX_KEY = STORAGE_PREFIX;
private tabId: string;
private readonly TAB_CLEANUP_INTERVAL = 30000; // 30 seconds
private cleanupInterval: any;
constructor(protected loggerSrv: LoggerService) { constructor(protected loggerSrv: LoggerService) {
this.log = this.loggerSrv.get('StorageService'); 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 { getParticipantName(): string | null {
@ -106,24 +218,164 @@ export class StorageService {
} }
protected set(key: string, item: any) { protected set(key: string, item: any) {
const value = JSON.stringify({ item: item }); if (SESSION_KEYS.includes(key as StorageKeys)) {
this.storage.setItem(this.PREFIX_KEY + key, value); this.setSessionValue(key, item);
} else {
this.setLocalValue(key, item);
}
} }
protected get(key: string): any { 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) { if (!!str) {
return JSON.parse(str).item; return JSON.parse(str).item;
} }
return null; 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() { public clear() {
this.log.d('Clearing localStorage'); this.log.d('Clearing localStorage and sessionStorage');
this.storage.clear();
// 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 { BehaviorSubject, Observable } from 'rxjs';
import { BackgroundEffect, EffectType } from '../../models/background-effect.model'; import { BackgroundEffect, EffectType } from '../../models/background-effect.model';
import { ParticipantService } from '../participant/participant.service'; import { ParticipantService } from '../participant/participant.service';
import { OpenViduService } from '../openvidu/openvidu.service';
import { StorageService } from '../storage/storage.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 { BackgroundBlur, BackgroundOptions, ProcessorWrapper, VirtualBackground } from '@livekit/track-processors';
import { LoggerService } from '../logger/logger.service'; import { LoggerService } from '../logger/logger.service';
import { ILogger } from '../../models/logger.model'; import { ILogger } from '../../models/logger.model';
import { ParticipantTrackPublication } from '../../models/participant.model';
/** /**
* @internal * @internal
@ -49,6 +49,7 @@ export class VirtualBackgroundService {
private log: ILogger; private log: ILogger;
constructor( constructor(
private participantService: ParticipantService, private participantService: ParticipantService,
private openviduService: OpenViduService,
private storageService: StorageService, private storageService: StorageService,
private loggerSrv: LoggerService private loggerSrv: LoggerService
) { ) {
@ -79,11 +80,12 @@ export class VirtualBackgroundService {
// If the background is already applied, do nothing // If the background is already applied, do nothing
if (this.backgroundIsAlreadyApplied(bg.id)) return; if (this.backgroundIsAlreadyApplied(bg.id)) return;
const cameraTracks = this.getCameraTracks(); const cameraTrack = this.getCameraTrack();
if (!cameraTracks) { if (!cameraTrack) {
this.log.e('No camera tracks found. Cannot apply background.'); this.log.e('No camera track found. Cannot apply background.');
return; return;
} }
try { try {
// If no effect is selected, remove the background // If no effect is selected, remove the background
if (bg.type === EffectType.NONE) { if (bg.type === EffectType.NONE) {
@ -91,8 +93,7 @@ export class VirtualBackgroundService {
return; return;
} }
const localTrack = cameraTracks[0].track as LocalTrack; const currentProcessor = cameraTrack.getProcessor() as ProcessorWrapper<BackgroundOptions>;
const currentProcessor = localTrack.getProcessor() as ProcessorWrapper<BackgroundOptions>;
// Check if the background is the same type as the previous one // Check if the background is the same type as the previous one
if (this.hasSameTypeAsPreviousOne(bg.type) && currentProcessor) { if (this.hasSameTypeAsPreviousOne(bg.type) && currentProcessor) {
@ -104,7 +105,7 @@ export class VirtualBackgroundService {
this.log.e('No processor found for the background effect.'); this.log.e('No processor found for the background effect.');
return; return;
} }
await this.applyProcessorToCameraTracks(cameraTracks, newProcessor); await this.applyProcessorToCameraTrack(cameraTrack, newProcessor);
} }
this.storageService.setBackground(bg.id); this.storageService.setBackground(bg.id);
@ -128,15 +129,14 @@ export class VirtualBackgroundService {
async removeBackground() { async removeBackground() {
if (this.isBackgroundApplied()) { if (this.isBackgroundApplied()) {
this.backgroundIdSelected.next('no_effect'); this.backgroundIdSelected.next('no_effect');
const tracks = this.participantService.getLocalParticipant()?.tracks; const cameraTrack = this.getCameraTrack();
const promises = tracks?.map(async (t) => { if (cameraTrack) {
try { try {
await (t.track as LocalTrack).stopProcessor(); await cameraTrack.stopProcessor();
} catch (e) { } catch (e) {
this.log.w('Error stopping processor:', e); this.log.w('Error stopping processor:', e);
} }
}); }
await Promise.all(promises || []);
this.storageService.removeBackground(); this.storageService.removeBackground();
} }
} }
@ -160,26 +160,41 @@ export class VirtualBackgroundService {
return undefined; return undefined;
} }
private async applyProcessorToCameraTracks( /**
cameraTracks: ParticipantTrackPublication[], * Gets the camera track from either the published tracks (if in room) or local tracks (if in prejoin)
processor: ProcessorWrapper<BackgroundOptions> * @returns The camera LocalTrack or undefined if not found
) { * @private
const promises = cameraTracks.map((track) => { */
return (track.track as LocalTrack).setProcessor(processor); 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 { private backgroundIsAlreadyApplied(backgroundId: string): boolean {
return backgroundId === this.backgroundIdSelected.getValue(); 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. * 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/components/videoconference/videoconference.component';
export * from './lib/config/openvidu-components-angular.config'; export * from './lib/config/openvidu-components-angular.config';
// Directives // Directives
export * from './lib/directives/template/internals.directive';
export * from './lib/directives/api/activities-panel.directive'; export * from './lib/directives/api/activities-panel.directive';
export * from './lib/directives/api/admin.directive'; export * from './lib/directives/api/admin.directive';
export * from './lib/directives/api/api.directive.module'; export * from './lib/directives/api/api.directive.module';

View File

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

View File

@ -34,7 +34,6 @@ export class CallComponent implements OnInit {
{ name: 'custom', lang: 'cus' } { name: 'custom', lang: 'cus' }
]; ];
prejoin: boolean = true; prejoin: boolean = true;
prejoinDisplayParticipantName: boolean = true;
participantName: string = `Participant${Math.floor(Math.random() * 1000)}`; participantName: string = `Participant${Math.floor(Math.random() * 1000)}`;
videoEnabled: boolean = true; videoEnabled: boolean = true;
audioEnabled: boolean = true; audioEnabled: boolean = true;
@ -59,6 +58,13 @@ export class CallComponent implements OnInit {
activitiesPanelBroadcastingActivity: boolean = true; activitiesPanelBroadcastingActivity: boolean = true;
toolbarSettingsButton: boolean = true; toolbarSettingsButton: boolean = true;
fakeDevices: boolean = false; 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 redirectToHomeOnLeaves: boolean = true;
private staticVideos = [ private staticVideos = [
@ -104,8 +110,6 @@ export class CallComponent implements OnInit {
} catch {} } catch {}
} }
if (params['prejoin'] !== undefined) this.prejoin = params['prejoin'] === 'true'; 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['participantName']) this.participantName = params['participantName'];
if (params['videoEnabled'] !== undefined) this.videoEnabled = params['videoEnabled'] === 'true'; if (params['videoEnabled'] !== undefined) this.videoEnabled = params['videoEnabled'] === 'true';
if (params['audioEnabled'] !== undefined) this.audioEnabled = params['audioEnabled'] === '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'; 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) { if (params['redirectToHome'] === undefined) {
this.redirectToHomeOnLeaves = true; this.redirectToHomeOnLeaves = true;
} else { } else {
@ -198,7 +211,9 @@ export class CallComponent implements OnInit {
if (publication.videoTrack?.attachedElements) { if (publication.videoTrack?.attachedElements) {
this.replaceWithStaticVideos(publication.videoTrack?.attachedElements); this.replaceWithStaticVideos(publication.videoTrack?.attachedElements);
const firstVideo = this.staticVideos.shift(); const firstVideo = this.staticVideos.shift();
this.staticVideos.push(firstVideo); if (firstVideo) {
this.staticVideos.push(firstVideo);
}
} }
}, 2000); }, 2000);
} }

View File

@ -271,10 +271,9 @@ Resources:
"GRAFANA_ADMIN_PASSWORD": "none", "GRAFANA_ADMIN_PASSWORD": "none",
"LIVEKIT_API_KEY": "none", "LIVEKIT_API_KEY": "none",
"LIVEKIT_API_SECRET": "none", "LIVEKIT_API_SECRET": "none",
"DEFAULT_APP_USERNAME": "none", "MEET_ADMIN_USER": "none",
"DEFAULT_APP_PASSWORD": "none", "MEET_ADMIN_SECRET": "none",
"DEFAULT_APP_ADMIN_USERNAME": "none", "MEET_API_KEY": "none",
"DEFAULT_APP_ADMIN_PASSWORD": "none",
"ENABLED_MODULES": "none" "ENABLED_MODULES": "none"
} }
@ -367,7 +366,7 @@ Resources:
'/usr/local/bin/install.sh': '/usr/local/bin/install.sh':
content: !Sub | content: !Sub |
#!/bin/bash -x #!/bin/bash -x
OPENVIDU_VERSION=3.3.0 OPENVIDU_VERSION=main
DOMAIN= DOMAIN=
YQ_VERSION=v4.44.5 YQ_VERSION=v4.44.5
@ -410,11 +409,10 @@ Resources:
DASHBOARD_ADMIN_PASSWORD="$(/usr/local/bin/store_secret.sh generate DASHBOARD_ADMIN_PASSWORD)" 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_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)" 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")" MEET_ADMIN_USER="$(/usr/local/bin/store_secret.sh save MEET_ADMIN_USER "meetadmin")"
DEFAULT_APP_PASSWORD="$(/usr/local/bin/store_secret.sh generate DEFAULT_APP_PASSWORD)" MEET_ADMIN_SECRET="$(/usr/local/bin/store_secret.sh generate MEET_ADMIN_SECRET)"
DEFAULT_APP_ADMIN_USERNAME="$(/usr/local/bin/store_secret.sh save DEFAULT_APP_ADMIN_USERNAME "calladmin")" MEET_API_KEY="$(/usr/local/bin/store_secret.sh generate MEET_API_KEY)"
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,openviduMeet")"
ENABLED_MODULES="$(/usr/local/bin/store_secret.sh save ENABLED_MODULES "observability,app")"
LIVEKIT_API_KEY="$(/usr/local/bin/store_secret.sh generate LIVEKIT_API_KEY "API" 12)" 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)" LIVEKIT_API_SECRET="$(/usr/local/bin/store_secret.sh generate LIVEKIT_API_SECRET)"
@ -439,10 +437,9 @@ Resources:
"--dashboard-admin-password=$DASHBOARD_ADMIN_PASSWORD" "--dashboard-admin-password=$DASHBOARD_ADMIN_PASSWORD"
"--grafana-admin-user=$GRAFANA_ADMIN_USERNAME" "--grafana-admin-user=$GRAFANA_ADMIN_USERNAME"
"--grafana-admin-password=$GRAFANA_ADMIN_PASSWORD" "--grafana-admin-password=$GRAFANA_ADMIN_PASSWORD"
"--default-app-user=$DEFAULT_APP_USERNAME" "--meet-admin-user=$MEET_ADMIN_USER"
"--default-app-password=$DEFAULT_APP_PASSWORD" "--meet-admin-password=$MEET_ADMIN_SECRET"
"--default-app-admin-user=$DEFAULT_APP_ADMIN_USERNAME" "--meet-api-key=$MEET_API_KEY"
"--default-app-admin-password=$DEFAULT_APP_ADMIN_PASSWORD"
"--livekit-api-key=$LIVEKIT_API_KEY" "--livekit-api-key=$LIVEKIT_API_KEY"
"--livekit-api-secret=$LIVEKIT_API_SECRET" "--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/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_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/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/MEET_ADMIN_USER=.*/MEET_ADMIN_USER=$(echo $SHARED_SECRET | jq -r .MEET_ADMIN_USER)/" "${!CONFIG_DIR}/meet.env"
sed -i "s/CALL_SECRET=.*/CALL_SECRET=$(echo $SHARED_SECRET | jq -r .DEFAULT_APP_PASSWORD)/" "${!CONFIG_DIR}/app.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/CALL_ADMIN_USER=.*/CALL_ADMIN_USER=$(echo $SHARED_SECRET | jq -r .DEFAULT_APP_ADMIN_USERNAME)/" "${!CONFIG_DIR}/app.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/CALL_ADMIN_SECRET=.*/CALL_ADMIN_SECRET=$(echo $SHARED_SECRET | jq -r .DEFAULT_APP_ADMIN_PASSWORD)/" "${!CONFIG_DIR}/app.env"
sed -i "s/ENABLED_MODULES=.*/ENABLED_MODULES=$(echo $SHARED_SECRET | jq -r .ENABLED_MODULES)/" "${!CONFIG_DIR}/openvidu.env" sed -i "s/ENABLED_MODULES=.*/ENABLED_MODULES=$(echo $SHARED_SECRET | jq -r .ENABLED_MODULES)/" "${!CONFIG_DIR}/openvidu.env"
# Update URLs in secret # 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 '. + {"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_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 '. + {"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 '. + {"MEET_ADMIN_USER": "'"$(/usr/local/bin/get_value_from_config.sh MEET_ADMIN_USER "${!CONFIG_DIR}/meet.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 '. + {"MEET_ADMIN_SECRET": "'"$(/usr/local/bin/get_value_from_config.sh MEET_ADMIN_SECRET "${!CONFIG_DIR}/meet.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 '. + {"MEET_API_KEY": "'"$(/usr/local/bin/get_value_from_config.sh MEET_API_KEY "${!CONFIG_DIR}/meet.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 '. + {"ENABLED_MODULES": "'"$(/usr/local/bin/get_value_from_config.sh ENABLED_MODULES "${!CONFIG_DIR}/openvidu.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 # Update shared secret
@ -936,6 +931,14 @@ Resources:
FromPort: 1935 FromPort: 1935
ToPort: 1935 ToPort: 1935
CidrIpv6: ::/0 CidrIpv6: ::/0
- IpProtocol: tcp
FromPort: 7881
ToPort: 7881
CidrIp: 0.0.0.0/0
- IpProtocol: tcp
FromPort: 7881
ToPort: 7881
CidrIpv6: ::/0
- IpProtocol: udp - IpProtocol: udp
FromPort: 7885 FromPort: 7885
ToPort: 7885 ToPort: 7885
@ -952,14 +955,6 @@ Resources:
FromPort: 50000 FromPort: 50000
ToPort: 60000 ToPort: 60000
CidrIpv6: ::/0 CidrIpv6: ::/0
- IpProtocol: tcp
FromPort: 50000
ToPort: 60000
CidrIp: 0.0.0.0/0
- IpProtocol: tcp
FromPort: 50000
ToPort: 60000
CidrIpv6: ::/0
Outputs: Outputs:
ServicesAndCredentials: ServicesAndCredentials:

View File

@ -270,7 +270,7 @@ var stringInterpolationParams = {
var installScriptTemplate = ''' var installScriptTemplate = '''
#!/bin/bash -x #!/bin/bash -x
OPENVIDU_VERSION=3.3.0 OPENVIDU_VERSION=main
DOMAIN= DOMAIN=
apt-get update && apt-get install -y \ 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)" 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_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)" 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")" MEET_ADMIN_USER="$(/usr/local/bin/store_secret.sh save MEET-ADMIN-USER "meetadmin")"
DEFAULT_APP_PASSWORD="$(/usr/local/bin/store_secret.sh generate DEFAULT-APP-PASSWORD)" MEET_ADMIN_SECRET="$(/usr/local/bin/store_secret.sh generate MEET-ADMIN-SECRET)"
DEFAULT_APP_ADMIN_USERNAME="$(/usr/local/bin/store_secret.sh save DEFAULT-APP-ADMIN-USERNAME "calladmin")" MEET_API_KEY="$(/usr/local/bin/store_secret.sh generate MEET-API-KEY)"
DEFAULT_APP_ADMIN_PASSWORD="$(/usr/local/bin/store_secret.sh generate DEFAULT-APP-ADMIN-PASSWORD)"
LIVEKIT_API_KEY="$(/usr/local/bin/store_secret.sh generate LIVEKIT-API-KEY "API" 12)" 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)" 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 # Base command
INSTALL_COMMAND="sh <(curl -fsSL http://get.openvidu.io/community/singlenode/$OPENVIDU_VERSION/install.sh)" 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" "--dashboard-admin-password=$DASHBOARD_ADMIN_PASSWORD"
"--grafana-admin-user=$GRAFANA_ADMIN_USERNAME" "--grafana-admin-user=$GRAFANA_ADMIN_USERNAME"
"--grafana-admin-password=$GRAFANA_ADMIN_PASSWORD" "--grafana-admin-password=$GRAFANA_ADMIN_PASSWORD"
"--default-app-user=$DEFAULT_APP_USERNAME" "--meet-admin-user=$MEET_ADMIN_USER"
"--default-app-password=$DEFAULT_APP_PASSWORD" "--meet-admin-password=$MEET_ADMIN_SECRET"
"--default-app-admin-user=$DEFAULT_APP_ADMIN_USERNAME" "--meet-api-key=$MEET_API_KEY"
"--default-app-admin-password=$DEFAULT_APP_ADMIN_PASSWORD"
"--livekit-api-key=$LIVEKIT_API_KEY" "--livekit-api-key=$LIVEKIT_API_KEY"
"--livekit-api-secret=$LIVEKIT_API_SECRET" "--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 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_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 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 MEET_ADMIN_USER=$(az keyvault secret show --vault-name ${keyVaultName} --name MEET-ADMIN-USER --query value -o tsv)
export DEFAULT_APP_PASSWORD=$(az keyvault secret show --vault-name ${keyVaultName} --name DEFAULT-APP-PASSWORD --query value -o tsv) export MEET_ADMIN_SECRET=$(az keyvault secret show --vault-name ${keyVaultName} --name MEET-ADMIN-SECRET --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 MEET_API_KEY=$(az keyvault secret show --vault-name ${keyVaultName} --name MEET-API-KEY --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 ENABLED_MODULES=$(az keyvault secret show --vault-name ${keyVaultName} --name ENABLED-MODULES --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/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_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/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/MEET_ADMIN_USER=.*/MEET_ADMIN_USER=$MEET_ADMIN_USER/" "${CONFIG_DIR}/meet.env"
sed -i "s/CALL_SECRET=.*/CALL_SECRET=$DEFAULT_APP_PASSWORD/" "${CONFIG_DIR}/app.env" sed -i "s/MEET_ADMIN_SECRET=.*/MEET_ADMIN_SECRET=$MEET_ADMIN_SECRET/" "${CONFIG_DIR}/meet.env"
sed -i "s/CALL_ADMIN_USER=.*/CALL_ADMIN_USER=$DEFAULT_APP_ADMIN_USERNAME/" "${CONFIG_DIR}/app.env" sed -i "s/MEET_API_KEY=.*/MEET_API_KEY=$MEET_API_KEY/" "${CONFIG_DIR}/meet.env"
sed -i "s/CALL_ADMIN_SECRET=.*/CALL_ADMIN_SECRET=$DEFAULT_APP_ADMIN_PASSWORD/" "${CONFIG_DIR}/app.env"
sed -i "s/ENABLED_MODULES=.*/ENABLED_MODULES=$ENABLED_MODULES/" "${CONFIG_DIR}/openvidu.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")" 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_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")" 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")" MEET_ADMIN_USER="$(/usr/local/bin/get_value_from_config.sh MEET_ADMIN_USER "${CONFIG_DIR}/meet.env")"
DEFAULT_APP_PASSWORD="$(/usr/local/bin/get_value_from_config.sh CALL_SECRET "${CONFIG_DIR}/app.env")" MEET_ADMIN_SECRET="$(/usr/local/bin/get_value_from_config.sh MEET_ADMIN_SECRET "${CONFIG_DIR}/meet.env")"
DEFAULT_APP_ADMIN_USERNAME="$(/usr/local/bin/get_value_from_config.sh CALL_ADMIN_USER "${CONFIG_DIR}/app.env")" MEET_API_KEY="$(/usr/local/bin/get_value_from_config.sh MEET_API_KEY "${CONFIG_DIR}/meet.env")"
DEFAULT_APP_ADMIN_PASSWORD="$(/usr/local/bin/get_value_from_config.sh CALL_ADMIN_SECRET "${CONFIG_DIR}/app.env")"
ENABLED_MODULES="$(/usr/local/bin/get_value_from_config.sh ENABLED_MODULES "${CONFIG_DIR}/openvidu.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 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-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 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 MEET-ADMIN-USER --value $MEET_ADMIN_USER
az keyvault secret set --vault-name ${keyVaultName} --name DEFAULT-APP-PASSWORD --value $DEFAULT_APP_PASSWORD az keyvault secret set --vault-name ${keyVaultName} --name MEET-ADMIN-SECRET --value $MEET_ADMIN_SECRET
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 MEET-API-KEY --value $MEET_API_KEY
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 ENABLED-MODULES --value $ENABLED_MODULES 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' 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 set -eu
export DOCKER_VERSION="${DOCKER_VERSION:-28.1.1}" export DOCKER_VERSION="${DOCKER_VERSION:-28.1.1}"
export DOCKER_COMPOSE_VERSION="${DOCKER_COMPOSE_VERSION:-v2.35.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 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 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 MONGO_SERVER_IMAGE="${MONGO_SERVER_IMAGE:-docker.io/mongo:8.0.9}"
export REDIS_SERVER_IMAGE="${REDIS_SERVER_IMAGE:-docker.io/redis:7.4.4-alpine}" export REDIS_SERVER_IMAGE="${REDIS_SERVER_IMAGE:-docker.io/redis:7.4.4-alpine}"
@ -15,7 +15,7 @@ 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_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_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_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_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_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_V2COMPATIBILITY_IMAGE="${OPENVIDU_V2COMPATIBILITY_IMAGE:-docker.io/openvidu/openvidu-v2compatibility:${OPENVIDU_VERSION}}"
@ -25,7 +25,7 @@ export LIVEKIT_EGRESS_SERVER_IMAGE="${LIVEKIT_EGRESS_SERVER_IMAGE:-docker.io/liv
export PROMETHEUS_IMAGE="${PROMETHEUS_IMAGE:-docker.io/prom/prometheus:v3.4.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 PROMTAIL_IMAGE="${PROMTAIL_IMAGE:-docker.io/grafana/promtail:3.5.1}"
export LOKI_IMAGE="${LOKI_IMAGE:-docker.io/grafana/loki: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}" export GRAFANA_IMAGE="${GRAFANA_IMAGE:-docker.io/grafana/grafana:11.6.2}"
# Function to compare two version strings # 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_OPERATOR_IMAGE=$OPENVIDU_OPERATOR_IMAGE \
-e OPENVIDU_SERVER_PRO_IMAGE=$OPENVIDU_SERVER_PRO_IMAGE \ -e OPENVIDU_SERVER_PRO_IMAGE=$OPENVIDU_SERVER_PRO_IMAGE \
-e OPENVIDU_SERVER_IMAGE=$OPENVIDU_SERVER_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_PRO_IMAGE=$OPENVIDU_DASHBOARD_PRO_IMAGE \
-e OPENVIDU_DASHBOARD_IMAGE=$OPENVIDU_DASHBOARD_IMAGE \ -e OPENVIDU_DASHBOARD_IMAGE=$OPENVIDU_DASHBOARD_IMAGE \
-e OPENVIDU_V2COMPATIBILITY_IMAGE=$OPENVIDU_V2COMPATIBILITY_IMAGE \ -e OPENVIDU_V2COMPATIBILITY_IMAGE=$OPENVIDU_V2COMPATIBILITY_IMAGE \

View File

@ -473,10 +473,9 @@ Resources:
"GRAFANA_ADMIN_PASSWORD": "none", "GRAFANA_ADMIN_PASSWORD": "none",
"LIVEKIT_API_KEY": "none", "LIVEKIT_API_KEY": "none",
"LIVEKIT_API_SECRET": "none", "LIVEKIT_API_SECRET": "none",
"DEFAULT_APP_USERNAME": "none", "MEET_ADMIN_USER": "none",
"DEFAULT_APP_PASSWORD": "none", "MEET_ADMIN_SECRET": "none",
"DEFAULT_APP_ADMIN_USERNAME": "none", "MEET_API_KEY": "none",
"DEFAULT_APP_ADMIN_PASSWORD": "none",
"OPENVIDU_VERSION": "none", "OPENVIDU_VERSION": "none",
"ENABLED_MODULES": "none" "ENABLED_MODULES": "none"
} }
@ -662,7 +661,7 @@ Resources:
content: !Sub | content: !Sub |
#!/bin/bash #!/bin/bash
set -e set -e
OPENVIDU_VERSION=3.3.0 OPENVIDU_VERSION=main
DOMAIN= DOMAIN=
YQ_VERSION=v4.44.5 YQ_VERSION=v4.44.5
@ -734,13 +733,12 @@ Resources:
DASHBOARD_ADMIN_PASSWORD="$(/usr/local/bin/store_secret.sh generate DASHBOARD_ADMIN_PASSWORD)" 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_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)" 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")" MEET_ADMIN_USER="$(/usr/local/bin/store_secret.sh save MEET_ADMIN_USER "meetadmin")"
DEFAULT_APP_PASSWORD="$(/usr/local/bin/store_secret.sh generate DEFAULT_APP_PASSWORD)" MEET_ADMIN_SECRET="$(/usr/local/bin/store_secret.sh generate MEET_ADMIN_SECRET)"
DEFAULT_APP_ADMIN_USERNAME="$(/usr/local/bin/store_secret.sh save DEFAULT_APP_ADMIN_USERNAME "calladmin")" MEET_API_KEY="$(/usr/local/bin/store_secret.sh generate MEET_API_KEY)"
DEFAULT_APP_ADMIN_PASSWORD="$(/usr/local/bin/store_secret.sh generate DEFAULT_APP_ADMIN_PASSWORD)"
LIVEKIT_API_KEY="$(/usr/local/bin/store_secret.sh generate LIVEKIT_API_KEY "API" 12)" 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)" 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")" ALL_SECRETS_GENERATED="$(/usr/local/bin/store_secret.sh save ALL_SECRETS_GENERATED "true")"
# Base command # Base command
@ -768,10 +766,9 @@ Resources:
"--dashboard-admin-password=$DASHBOARD_ADMIN_PASSWORD" "--dashboard-admin-password=$DASHBOARD_ADMIN_PASSWORD"
"--grafana-admin-user=$GRAFANA_ADMIN_USERNAME" "--grafana-admin-user=$GRAFANA_ADMIN_USERNAME"
"--grafana-admin-password=$GRAFANA_ADMIN_PASSWORD" "--grafana-admin-password=$GRAFANA_ADMIN_PASSWORD"
"--default-app-user=$DEFAULT_APP_USERNAME" "--meet-admin-user=$MEET_ADMIN_USER"
"--default-app-password=$DEFAULT_APP_PASSWORD" "--meet-admin-password=$MEET_ADMIN_SECRET"
"--default-app-admin-user=$DEFAULT_APP_ADMIN_USERNAME" "--meet-api-key=$MEET_API_KEY"
"--default-app-admin-password=$DEFAULT_APP_ADMIN_PASSWORD"
"--livekit-api-key=$LIVEKIT_API_KEY" "--livekit-api-key=$LIVEKIT_API_KEY"
"--livekit-api-secret=$LIVEKIT_API_SECRET" "--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/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_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/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/MEET_ADMIN_USER=.*/MEET_ADMIN_USER=$(echo $SHARED_SECRET | jq -r .MEET_ADMIN_USER)/" "${!CLUSTER_CONFIG_DIR}/master_node/meet.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/MEET_ADMIN_SECRET=.*/MEET_ADMIN_SECRET=$(echo $SHARED_SECRET | jq -r .MEET_ADMIN_SECRET)/" "${!CLUSTER_CONFIG_DIR}/master_node/meet.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/MEET_API_KEY=.*/MEET_API_KEY=$(echo $SHARED_SECRET | jq -r .MEET_API_KEY)/" "${!CLUSTER_CONFIG_DIR}/master_node/meet.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/ENABLED_MODULES=.*/ENABLED_MODULES=$(echo $SHARED_SECRET | jq -r .ENABLED_MODULES)/" "${!CLUSTER_CONFIG_DIR}/openvidu.env" sed -i "s/ENABLED_MODULES=.*/ENABLED_MODULES=$(echo $SHARED_SECRET | jq -r .ENABLED_MODULES)/" "${!CLUSTER_CONFIG_DIR}/openvidu.env"
# Update URLs in secret # 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 '. + {"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_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 '. + {"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 '. + {"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 '. + {"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 '. + {"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 '. + {"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 '. + {"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 '. + {"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 '. + {"ENABLED_MODULES": "'"$(/usr/local/bin/get_value_from_config.sh ENABLED_MODULES "${!CLUSTER_CONFIG_DIR}/openvidu.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 # Update shared secret
@ -1696,7 +1691,7 @@ Resources:
ToPort: 4443 ToPort: 4443
SourceSecurityGroupId: !GetAtt OpenViduMediaNodeSG.GroupId SourceSecurityGroupId: !GetAtt OpenViduMediaNodeSG.GroupId
OpenViduMediaNodeToMasterDefaultAppWebhookIngress: OpenViduMediaNodeToMasterMeetWebhookIngress:
Type: AWS::EC2::SecurityGroupIngress Type: AWS::EC2::SecurityGroupIngress
Properties: Properties:
GroupId: !GetAtt OpenViduMasterNodeSG.GroupId GroupId: !GetAtt OpenViduMasterNodeSG.GroupId
@ -1728,6 +1723,14 @@ Resources:
FromPort: 443 FromPort: 443
ToPort: 443 ToPort: 443
CidrIpv6: ::/0 CidrIpv6: ::/0
- IpProtocol: tcp
FromPort: 7881
ToPort: 7881
CidrIp: 0.0.0.0/0
- IpProtocol: tcp
FromPort: 7881
ToPort: 7881
CidrIpv6: ::/0
- IpProtocol: udp - IpProtocol: udp
FromPort: 7885 FromPort: 7885
ToPort: 7885 ToPort: 7885
@ -1744,6 +1747,14 @@ Resources:
FromPort: 50000 FromPort: 50000
ToPort: 60000 ToPort: 60000
CidrIpv6: ::/0 CidrIpv6: ::/0
- IpProtocol: tcp
FromPort: 50000
ToPort: 60000
CidrIp: 0.0.0.0/0
- IpProtocol: tcp
FromPort: 50000
ToPort: 60000
CidrIpv6: ::/0
OpenViduMasterNodeToMediaNodeRTMPIngress: OpenViduMasterNodeToMediaNodeRTMPIngress:
Type: AWS::EC2::SecurityGroupIngress Type: AWS::EC2::SecurityGroupIngress

View File

@ -427,7 +427,7 @@ var stringInterpolationParamsMaster = {
var installScriptTemplateMaster = ''' var installScriptTemplateMaster = '''
#!/bin/bash -x #!/bin/bash -x
OPENVIDU_VERSION=3.3.0 OPENVIDU_VERSION=main
DOMAIN= DOMAIN=
# Assume azure cli is installed # 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)" 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_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)" 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")" MEET_ADMIN_USER="$(/usr/local/bin/store_secret.sh save MEET-ADMIN-USER "meetadmin")"
DEFAULT_APP_PASSWORD="$(/usr/local/bin/store_secret.sh generate DEFAULT-APP-PASSWORD)" MEET_ADMIN_SECRET="$(/usr/local/bin/store_secret.sh generate MEET-ADMIN-SECRET)"
DEFAULT_APP_ADMIN_USERNAME="$(/usr/local/bin/store_secret.sh save DEFAULT-APP-ADMIN-USERNAME "calladmin")" MEET_API_KEY="$(/usr/local/bin/store_secret.sh generate MEET-API-KEY)"
DEFAULT_APP_ADMIN_PASSWORD="$(/usr/local/bin/store_secret.sh generate DEFAULT-APP-ADMIN-PASSWORD)"
LIVEKIT_API_KEY="$(/usr/local/bin/store_secret.sh generate LIVEKIT-API-KEY "API" 12)" 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)" 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}")" 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")" ALL_SECRETS_GENERATED="$(/usr/local/bin/store_secret.sh save ALL-SECRETS-GENERATED "true")"
# Base command # Base command
@ -525,10 +524,9 @@ COMMON_ARGS=(
"--dashboard-admin-password=$DASHBOARD_ADMIN_PASSWORD" "--dashboard-admin-password=$DASHBOARD_ADMIN_PASSWORD"
"--grafana-admin-user=$GRAFANA_ADMIN_USERNAME" "--grafana-admin-user=$GRAFANA_ADMIN_USERNAME"
"--grafana-admin-password=$GRAFANA_ADMIN_PASSWORD" "--grafana-admin-password=$GRAFANA_ADMIN_PASSWORD"
"--default-app-user=$DEFAULT_APP_USERNAME" "--meet-admin-user=$MEET_ADMIN_USER"
"--default-app-password=$DEFAULT_APP_PASSWORD" "--meet-admin-password=$MEET_ADMIN_SECRET"
"--default-app-admin-user=$DEFAULT_APP_ADMIN_USERNAME" "--meet-api-key=$MEET_API_KEY"
"--default-app-admin-password=$DEFAULT_APP_ADMIN_PASSWORD"
"--livekit-api-key=$LIVEKIT_API_KEY" "--livekit-api-key=$LIVEKIT_API_KEY"
"--livekit-api-secret=$LIVEKIT_API_SECRET" "--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 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_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 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 MEET_ADMIN_USER=$(az keyvault secret show --vault-name ${keyVaultName} --name MEET-ADMIN-USER --query value -o tsv)
export DEFAULT_APP_PASSWORD=$(az keyvault secret show --vault-name ${keyVaultName} --name DEFAULT-APP-PASSWORD --query value -o tsv) export MEET_ADMIN_SECRET=$(az keyvault secret show --vault-name ${keyVaultName} --name MEET-ADMIN-SECRET --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 MEET_API_KEY=$(az keyvault secret show --vault-name ${keyVaultName} --name MEET-API-KEY --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 ENABLED_MODULES=$(az keyvault secret show --vault-name ${keyVaultName} --name ENABLED-MODULES --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 # 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/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_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/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/MEET_ADMIN_USER=.*/MEET_ADMIN_USER=$MEET_ADMIN_USER/" "${CLUSTER_CONFIG_DIR}/master_node/meet.env"
sed -i "s/CALL_SECRET=.*/CALL_SECRET=$DEFAULT_APP_PASSWORD/" "${CLUSTER_CONFIG_DIR}/master_node/app.env" sed -i "s/MEET_ADMIN_SECRET=.*/MEET_ADMIN_SECRET=$MEET_ADMIN_SECRET/" "${CLUSTER_CONFIG_DIR}/master_node/meet.env"
sed -i "s/CALL_ADMIN_USER=.*/CALL_ADMIN_USER=$DEFAULT_APP_ADMIN_USERNAME/" "${CLUSTER_CONFIG_DIR}/master_node/app.env" sed -i "s/MEET_API_KEY=.*/MEET_API_KEY=$MEET_API_KEY/" "${CLUSTER_CONFIG_DIR}/master_node/meet.env"
sed -i "s/CALL_ADMIN_SECRET=.*/CALL_ADMIN_SECRET=$DEFAULT_APP_ADMIN_PASSWORD/" "${CLUSTER_CONFIG_DIR}/master_node/app.env"
sed -i "s/ENABLED_MODULES=.*/ENABLED_MODULES=$ENABLED_MODULES/" "${CLUSTER_CONFIG_DIR}/openvidu.env" sed -i "s/ENABLED_MODULES=.*/ENABLED_MODULES=$ENABLED_MODULES/" "${CLUSTER_CONFIG_DIR}/openvidu.env"
# Update URLs in secret # 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")" 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_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")" 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")" MEET_ADMIN_USER="$(/usr/local/bin/get_value_from_config.sh MEET_ADMIN_USER "${CLUSTER_CONFIG_DIR}/master_node/meet.env")"
DEFAULT_APP_PASSWORD="$(/usr/local/bin/get_value_from_config.sh CALL_SECRET "${CLUSTER_CONFIG_DIR}/master_node/app.env")" MEET_ADMIN_SECRET="$(/usr/local/bin/get_value_from_config.sh MEET_ADMIN_SECRET "${CLUSTER_CONFIG_DIR}/master_node/meet.env")"
DEFAULT_APP_ADMIN_USERNAME="$(/usr/local/bin/get_value_from_config.sh CALL_ADMIN_USER "${CLUSTER_CONFIG_DIR}/master_node/app.env")" MEET_API_KEY="$(/usr/local/bin/get_value_from_config.sh MEET_API_KEY "${CLUSTER_CONFIG_DIR}/master_node/meet.env")"
DEFAULT_APP_ADMIN_PASSWORD="$(/usr/local/bin/get_value_from_config.sh CALL_ADMIN_SECRET "${CLUSTER_CONFIG_DIR}/master_node/app.env")"
ENABLED_MODULES="$(/usr/local/bin/get_value_from_config.sh ENABLED_MODULES "${CLUSTER_CONFIG_DIR}/openvidu.env")" ENABLED_MODULES="$(/usr/local/bin/get_value_from_config.sh ENABLED_MODULES "${CLUSTER_CONFIG_DIR}/openvidu.env")"
# Update shared secret # 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 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-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 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 MEET-ADMIN-USER --value $MEET_ADMIN_USER
az keyvault secret set --vault-name ${keyVaultName} --name DEFAULT-APP-PASSWORD --value $DEFAULT_APP_PASSWORD az keyvault secret set --vault-name ${keyVaultName} --name MEET-ADMIN-SECRET --value $MEET_ADMIN_SECRET
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 MEET-API-KEY --value $MEET_API_KEY
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 ENABLED-MODULES --value $ENABLED_MODULES 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 parent: openviduMasterNodeNSG
name: 'mediaNode_to_masterNode_DEFAULTAPP_WEBHOOK_INGRESS' name: 'mediaNode_to_masterNode_MEET_WEBHOOK_INGRESS'
properties: { properties: {
protocol: 'Tcp' protocol: 'Tcp'
sourceApplicationSecurityGroups: [ sourceApplicationSecurityGroups: [
@ -1959,6 +1953,22 @@ resource openviduMediaNodeNSG 'Microsoft.Network/networkSecurityGroups@2023-11-0
direction: 'Inbound' 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' destinationPortRange: '1935'
access: 'Allow' access: 'Allow'
priority: 150 priority: 160
direction: 'Inbound' direction: 'Inbound'
} }
} }
@ -2009,7 +2019,7 @@ resource masterToMediaTurnTlsIngress 'Microsoft.Network/networkSecurityGroups/se
] ]
destinationPortRange: '5349' destinationPortRange: '5349'
access: 'Allow' access: 'Allow'
priority: 160 priority: 170
direction: 'Inbound' direction: 'Inbound'
} }
} }
@ -2032,7 +2042,7 @@ resource masterToMediaServerIngress 'Microsoft.Network/networkSecurityGroups/sec
] ]
destinationPortRange: '7880' destinationPortRange: '7880'
access: 'Allow' access: 'Allow'
priority: 170 priority: 180
direction: 'Inbound' direction: 'Inbound'
} }
} }
@ -2055,7 +2065,7 @@ resource masterToMediaHttpWhipIngress 'Microsoft.Network/networkSecurityGroups/s
] ]
destinationPortRange: '8080' destinationPortRange: '8080'
access: 'Allow' access: 'Allow'
priority: 180 priority: 190
direction: 'Inbound' direction: 'Inbound'
} }
} }

File diff suppressed because one or more lines are too long

View File

@ -3,9 +3,9 @@
set -eu set -eu
export DOCKER_VERSION="${DOCKER_VERSION:-28.1.1}" export DOCKER_VERSION="${DOCKER_VERSION:-28.1.1}"
export DOCKER_COMPOSE_VERSION="${DOCKER_COMPOSE_VERSION:-v2.35.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 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 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 MONGO_SERVER_IMAGE="${MONGO_SERVER_IMAGE:-docker.io/mongo:8.0.9}"
export REDIS_SERVER_IMAGE="${REDIS_SERVER_IMAGE:-docker.io/redis:7.4.4-alpine}" export REDIS_SERVER_IMAGE="${REDIS_SERVER_IMAGE:-docker.io/redis:7.4.4-alpine}"
@ -15,7 +15,7 @@ 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_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_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_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_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_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_V2COMPATIBILITY_IMAGE="${OPENVIDU_V2COMPATIBILITY_IMAGE:-docker.io/openvidu/openvidu-v2compatibility:${OPENVIDU_VERSION}}"
@ -25,7 +25,7 @@ export LIVEKIT_EGRESS_SERVER_IMAGE="${LIVEKIT_EGRESS_SERVER_IMAGE:-docker.io/liv
export PROMETHEUS_IMAGE="${PROMETHEUS_IMAGE:-docker.io/prom/prometheus:v3.4.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 PROMTAIL_IMAGE="${PROMTAIL_IMAGE:-docker.io/grafana/promtail:3.5.1}"
export LOKI_IMAGE="${LOKI_IMAGE:-docker.io/grafana/loki: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}" export GRAFANA_IMAGE="${GRAFANA_IMAGE:-docker.io/grafana/grafana:11.6.2}"
# Function to compare two version strings # 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_OPERATOR_IMAGE=$OPENVIDU_OPERATOR_IMAGE \
-e OPENVIDU_SERVER_PRO_IMAGE=$OPENVIDU_SERVER_PRO_IMAGE \ -e OPENVIDU_SERVER_PRO_IMAGE=$OPENVIDU_SERVER_PRO_IMAGE \
-e OPENVIDU_SERVER_IMAGE=$OPENVIDU_SERVER_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_PRO_IMAGE=$OPENVIDU_DASHBOARD_PRO_IMAGE \
-e OPENVIDU_DASHBOARD_IMAGE=$OPENVIDU_DASHBOARD_IMAGE \ -e OPENVIDU_DASHBOARD_IMAGE=$OPENVIDU_DASHBOARD_IMAGE \
-e OPENVIDU_V2COMPATIBILITY_IMAGE=$OPENVIDU_V2COMPATIBILITY_IMAGE \ -e OPENVIDU_V2COMPATIBILITY_IMAGE=$OPENVIDU_V2COMPATIBILITY_IMAGE \

View File

@ -3,9 +3,9 @@
set -eu set -eu
export DOCKER_VERSION="${DOCKER_VERSION:-28.1.1}" export DOCKER_VERSION="${DOCKER_VERSION:-28.1.1}"
export DOCKER_COMPOSE_VERSION="${DOCKER_COMPOSE_VERSION:-v2.35.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 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 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 MONGO_SERVER_IMAGE="${MONGO_SERVER_IMAGE:-docker.io/mongo:8.0.9}"
export REDIS_SERVER_IMAGE="${REDIS_SERVER_IMAGE:-docker.io/redis:7.4.4-alpine}" export REDIS_SERVER_IMAGE="${REDIS_SERVER_IMAGE:-docker.io/redis:7.4.4-alpine}"
@ -15,7 +15,7 @@ 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_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_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_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_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_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_V2COMPATIBILITY_IMAGE="${OPENVIDU_V2COMPATIBILITY_IMAGE:-docker.io/openvidu/openvidu-v2compatibility:${OPENVIDU_VERSION}}"
@ -25,7 +25,7 @@ export LIVEKIT_EGRESS_SERVER_IMAGE="${LIVEKIT_EGRESS_SERVER_IMAGE:-docker.io/liv
export PROMETHEUS_IMAGE="${PROMETHEUS_IMAGE:-docker.io/prom/prometheus:v3.4.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 PROMTAIL_IMAGE="${PROMTAIL_IMAGE:-docker.io/grafana/promtail:3.5.1}"
export LOKI_IMAGE="${LOKI_IMAGE:-docker.io/grafana/loki: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}" export GRAFANA_IMAGE="${GRAFANA_IMAGE:-docker.io/grafana/grafana:11.6.2}"
# Function to compare two version strings # 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_OPERATOR_IMAGE=$OPENVIDU_OPERATOR_IMAGE \
-e OPENVIDU_SERVER_PRO_IMAGE=$OPENVIDU_SERVER_PRO_IMAGE \ -e OPENVIDU_SERVER_PRO_IMAGE=$OPENVIDU_SERVER_PRO_IMAGE \
-e OPENVIDU_SERVER_IMAGE=$OPENVIDU_SERVER_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_PRO_IMAGE=$OPENVIDU_DASHBOARD_PRO_IMAGE \
-e OPENVIDU_DASHBOARD_IMAGE=$OPENVIDU_DASHBOARD_IMAGE \ -e OPENVIDU_DASHBOARD_IMAGE=$OPENVIDU_DASHBOARD_IMAGE \
-e OPENVIDU_V2COMPATIBILITY_IMAGE=$OPENVIDU_V2COMPATIBILITY_IMAGE \ -e OPENVIDU_V2COMPATIBILITY_IMAGE=$OPENVIDU_V2COMPATIBILITY_IMAGE \

View File

@ -465,10 +465,9 @@ Resources:
"GRAFANA_URL": "none", "GRAFANA_URL": "none",
"GRAFANA_ADMIN_USERNAME": "none", "GRAFANA_ADMIN_USERNAME": "none",
"GRAFANA_ADMIN_PASSWORD": "none", "GRAFANA_ADMIN_PASSWORD": "none",
"DEFAULT_APP_USERNAME": "none", "MEET_ADMIN_USER": "none",
"DEFAULT_APP_PASSWORD": "none", "MEET_ADMIN_SECRET": "none",
"DEFAULT_APP_ADMIN_USERNAME": "none", "MEET_API_KEY": "none",
"DEFAULT_APP_ADMIN_PASSWORD": "none",
"LIVEKIT_API_KEY": "none", "LIVEKIT_API_KEY": "none",
"LIVEKIT_API_SECRET": "none", "LIVEKIT_API_SECRET": "none",
"ENABLED_MODULES": "none", "ENABLED_MODULES": "none",
@ -773,7 +772,7 @@ Resources:
content: !Sub | content: !Sub |
#!/bin/bash -x #!/bin/bash -x
set -e set -e
OPENVIDU_VERSION=3.3.0 OPENVIDU_VERSION=main
DOMAIN= DOMAIN=
YQ_VERSION=v4.44.5 YQ_VERSION=v4.44.5
@ -867,13 +866,12 @@ Resources:
DASHBOARD_ADMIN_PASSWORD="$(/usr/local/bin/store_secret.sh generate DASHBOARD_ADMIN_PASSWORD)" 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_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)" 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")" MEET_ADMIN_USER="$(/usr/local/bin/store_secret.sh save MEET_ADMIN_USER "meetadmin")"
DEFAULT_APP_PASSWORD="$(/usr/local/bin/store_secret.sh generate DEFAULT_APP_PASSWORD)" MEET_ADMIN_SECRET="$(/usr/local/bin/store_secret.sh generate MEET_ADMIN_SECRET)"
DEFAULT_APP_ADMIN_USERNAME="$(/usr/local/bin/store_secret.sh save DEFAULT_APP_ADMIN_USERNAME "calladmin")" MEET_API_KEY="$(/usr/local/bin/store_secret.sh generate MEET_API_KEY)"
DEFAULT_APP_ADMIN_PASSWORD="$(/usr/local/bin/store_secret.sh generate DEFAULT_APP_ADMIN_PASSWORD)"
LIVEKIT_API_KEY="$(/usr/local/bin/store_secret.sh generate LIVEKIT_API_KEY "API" 12)" 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)" 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")" ALL_SECRETS_GENERATED="$(/usr/local/bin/store_secret.sh save ALL_SECRETS_GENERATED "true")"
fi fi
@ -939,10 +937,9 @@ Resources:
DASHBOARD_ADMIN_PASSWORD=$(echo "$SHARED_SECRET" | jq -r '.DASHBOARD_ADMIN_PASSWORD') DASHBOARD_ADMIN_PASSWORD=$(echo "$SHARED_SECRET" | jq -r '.DASHBOARD_ADMIN_PASSWORD')
GRAFANA_ADMIN_USERNAME=$(echo "$SHARED_SECRET" | jq -r '.GRAFANA_ADMIN_USERNAME') GRAFANA_ADMIN_USERNAME=$(echo "$SHARED_SECRET" | jq -r '.GRAFANA_ADMIN_USERNAME')
GRAFANA_ADMIN_PASSWORD=$(echo "$SHARED_SECRET" | jq -r '.GRAFANA_ADMIN_PASSWORD') GRAFANA_ADMIN_PASSWORD=$(echo "$SHARED_SECRET" | jq -r '.GRAFANA_ADMIN_PASSWORD')
DEFAULT_APP_USERNAME=$(echo "$SHARED_SECRET" | jq -r '.DEFAULT_APP_USERNAME') MEET_ADMIN_USER=$(echo "$SHARED_SECRET" | jq -r '.MEET_ADMIN_USER')
DEFAULT_APP_PASSWORD=$(echo "$SHARED_SECRET" | jq -r '.DEFAULT_APP_PASSWORD') MEET_ADMIN_SECRET=$(echo "$SHARED_SECRET" | jq -r '.MEET_ADMIN_SECRET')
DEFAULT_APP_ADMIN_USERNAME=$(echo "$SHARED_SECRET" | jq -r '.DEFAULT_APP_ADMIN_USERNAME') MEET_API_KEY=$(echo "$SHARED_SECRET" | jq -r '.MEET_API_KEY')
DEFAULT_APP_ADMIN_PASSWORD=$(echo "$SHARED_SECRET" | jq -r '.DEFAULT_APP_ADMIN_PASSWORD')
LIVEKIT_API_KEY=$(echo "$SHARED_SECRET" | jq -r '.LIVEKIT_API_KEY') LIVEKIT_API_KEY=$(echo "$SHARED_SECRET" | jq -r '.LIVEKIT_API_KEY')
LIVEKIT_API_SECRET=$(echo "$SHARED_SECRET" | jq -r '.LIVEKIT_API_SECRET') LIVEKIT_API_SECRET=$(echo "$SHARED_SECRET" | jq -r '.LIVEKIT_API_SECRET')
ENABLED_MODULES=$(echo "$SHARED_SECRET" | jq -r '.ENABLED_MODULES') ENABLED_MODULES=$(echo "$SHARED_SECRET" | jq -r '.ENABLED_MODULES')
@ -973,10 +970,9 @@ Resources:
"--dashboard-admin-password=$DASHBOARD_ADMIN_PASSWORD" "--dashboard-admin-password=$DASHBOARD_ADMIN_PASSWORD"
"--grafana-admin-user=$GRAFANA_ADMIN_USERNAME" "--grafana-admin-user=$GRAFANA_ADMIN_USERNAME"
"--grafana-admin-password=$GRAFANA_ADMIN_PASSWORD" "--grafana-admin-password=$GRAFANA_ADMIN_PASSWORD"
"--default-app-user=$DEFAULT_APP_USERNAME" "--meet-admin-user=$MEET_ADMIN_USER"
"--default-app-password=$DEFAULT_APP_PASSWORD" "--meet-admin-password=$MEET_ADMIN_SECRET"
"--default-app-admin-user=$DEFAULT_APP_ADMIN_USERNAME" "--meet-api-key=$MEET_API_KEY"
"--default-app-admin-password=$DEFAULT_APP_ADMIN_PASSWORD"
"--livekit-api-key=$LIVEKIT_API_KEY" "--livekit-api-key=$LIVEKIT_API_KEY"
"--livekit-api-secret=$LIVEKIT_API_SECRET" "--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/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_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/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/MEET_ADMIN_USER=.*/MEET_ADMIN_USER=$(echo $SHARED_SECRET | jq -r .MEET_ADMIN_USER)/" "${!CLUSTER_CONFIG_DIR}/master_node/meet.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/MEET_ADMIN_SECRET=.*/MEET_ADMIN_SECRET=$(echo $SHARED_SECRET | jq -r .MEET_ADMIN_SECRET)/" "${!CLUSTER_CONFIG_DIR}/master_node/meet.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/MEET_API_KEY=.*/MEET_API_KEY=$(echo $SHARED_SECRET | jq -r .MEET_API_KEY)/" "${!CLUSTER_CONFIG_DIR}/master_node/meet.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/ENABLED_MODULES=.*/ENABLED_MODULES=$(echo $SHARED_SECRET | jq -r .ENABLED_MODULES)/" "${!CLUSTER_CONFIG_DIR}/openvidu.env" sed -i "s/ENABLED_MODULES=.*/ENABLED_MODULES=$(echo $SHARED_SECRET | jq -r .ENABLED_MODULES)/" "${!CLUSTER_CONFIG_DIR}/openvidu.env"
# Update URLs in secret # 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 '. + {"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_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 '. + {"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 '. + {"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 '. + {"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 '. + {"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 '. + {"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 '. + {"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 '. + {"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 '. + {"ENABLED_MODULES": "'"$(/usr/local/bin/get_value_from_config.sh ENABLED_MODULES "${!CLUSTER_CONFIG_DIR}/openvidu.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 # Update shared secret
@ -2046,7 +2040,7 @@ Resources:
ToPort: 4443 ToPort: 4443
SourceSecurityGroupId: !GetAtt OpenViduMediaNodeSG.GroupId SourceSecurityGroupId: !GetAtt OpenViduMediaNodeSG.GroupId
OpenViduMasterToMasterDefaultAppIngress: OpenViduMasterToMasterMeetIngress:
Type: AWS::EC2::SecurityGroupIngress Type: AWS::EC2::SecurityGroupIngress
Properties: Properties:
GroupId: !Ref OpenViduMasterNodeSG GroupId: !Ref OpenViduMasterNodeSG
@ -2055,7 +2049,7 @@ Resources:
ToPort: 6080 ToPort: 6080
SourceSecurityGroupId: !Ref OpenViduMasterNodeSG SourceSecurityGroupId: !Ref OpenViduMasterNodeSG
OpenViduMediaNodeToMasterDefaultAppWebhookIngress: OpenViduMediaNodeToMasterMeetWebhookIngress:
Type: AWS::EC2::SecurityGroupIngress Type: AWS::EC2::SecurityGroupIngress
Properties: Properties:
GroupId: !GetAtt OpenViduMasterNodeSG.GroupId GroupId: !GetAtt OpenViduMasterNodeSG.GroupId
@ -2087,6 +2081,14 @@ Resources:
FromPort: 443 FromPort: 443
ToPort: 443 ToPort: 443
CidrIpv6: ::/0 CidrIpv6: ::/0
- IpProtocol: tcp
FromPort: 7881
ToPort: 7881
CidrIp: 0.0.0.0/0
- IpProtocol: tcp
FromPort: 7881
ToPort: 7881
CidrIpv6: ::/0
- IpProtocol: udp - IpProtocol: udp
FromPort: 7885 FromPort: 7885
ToPort: 7885 ToPort: 7885
@ -2103,6 +2105,14 @@ Resources:
FromPort: 50000 FromPort: 50000
ToPort: 60000 ToPort: 60000
CidrIpv6: ::/0 CidrIpv6: ::/0
- IpProtocol: tcp
FromPort: 50000
ToPort: 60000
CidrIp: 0.0.0.0/0
- IpProtocol: tcp
FromPort: 50000
ToPort: 60000
CidrIpv6: ::/0
OpenViduLoadBalancerToMediaNodeRTMPIngressSG: OpenViduLoadBalancerToMediaNodeRTMPIngressSG:
Type: AWS::EC2::SecurityGroupIngress Type: AWS::EC2::SecurityGroupIngress

View File

@ -495,7 +495,7 @@ var stringInterpolationParamsMaster4 = {
var installScriptTemplateMaster = ''' var installScriptTemplateMaster = '''
#!/bin/bash -x #!/bin/bash -x
set -e set -e
OPENVIDU_VERSION=3.3.0 OPENVIDU_VERSION=main
DOMAIN= DOMAIN=
# Assume azure cli is installed # 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)" 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_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)" 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")" MEET_ADMIN_USER="$(/usr/local/bin/store_secret.sh save MEET-ADMIN-USER "meetadmin")"
DEFAULT_APP_PASSWORD="$(/usr/local/bin/store_secret.sh generate DEFAULT-APP-PASSWORD)" MEET_ADMIN_SECRET="$(/usr/local/bin/store_secret.sh generate MEET-ADMIN-SECRET)"
DEFAULT_APP_ADMIN_USERNAME="$(/usr/local/bin/store_secret.sh save DEFAULT-APP-ADMIN-USERNAME "calladmin")" MEET_API_KEY="$(/usr/local/bin/store_secret.sh generate MEET-API-KEY)"
DEFAULT_APP_ADMIN_PASSWORD="$(/usr/local/bin/store_secret.sh generate DEFAULT-APP-ADMIN-PASSWORD)"
LIVEKIT_API_KEY="$(/usr/local/bin/store_secret.sh generate LIVEKIT-API-KEY "API" 12)" 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)" 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}")" 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")" ALL_SECRETS_GENERATED="$(/usr/local/bin/store_secret.sh save ALL-SECRETS-GENERATED "true")"
fi 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) 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_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) 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) MEET_ADMIN_USER=$(az keyvault secret show --vault-name ${keyVaultName} --name MEET-ADMIN-USER --query value -o tsv)
DEFAULT_APP_PASSWORD=$(az keyvault secret show --vault-name ${keyVaultName} --name DEFAULT-APP-PASSWORD --query value -o tsv) MEET_ADMIN_SECRET=$(az keyvault secret show --vault-name ${keyVaultName} --name MEET-ADMIN-SECRET --query value -o tsv)
DEFAULT_APP_ADMIN_USERNAME=$(az keyvault secret show --vault-name ${keyVaultName} --name DEFAULT-APP-ADMIN-USERNAME --query value -o tsv) MEET_API_KEY=$(az keyvault secret show --vault-name ${keyVaultName} --name MEET-API-KEY --query value -o tsv)
DEFAULT_APP_ADMIN_PASSWORD=$(az keyvault secret show --vault-name ${keyVaultName} --name DEFAULT-APP-ADMIN-PASSWORD --query value -o tsv)
ENABLED_MODULES=$(az keyvault secret show --vault-name ${keyVaultName} --name ENABLED-MODULES --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" "--dashboard-admin-password=$DASHBOARD_ADMIN_PASSWORD"
"--grafana-admin-user=$GRAFANA_ADMIN_USERNAME" "--grafana-admin-user=$GRAFANA_ADMIN_USERNAME"
"--grafana-admin-password=$GRAFANA_ADMIN_PASSWORD" "--grafana-admin-password=$GRAFANA_ADMIN_PASSWORD"
"--default-app-user=$DEFAULT_APP_USERNAME" "--meet-admin-user=$MEET_ADMIN_USER"
"--default-app-password=$DEFAULT_APP_PASSWORD" "--meet-admin-password=$MEET_ADMIN_SECRET"
"--default-app-admin-user=$DEFAULT_APP_ADMIN_USERNAME" "--meet-api-key=$MEET_API_KEY"
"--default-app-admin-password=$DEFAULT_APP_ADMIN_PASSWORD"
"--livekit-api-key=$LIVEKIT_API_KEY" "--livekit-api-key=$LIVEKIT_API_KEY"
"--livekit-api-secret=$LIVEKIT_API_SECRET" "--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 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_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 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 MEET_ADMIN_USER=$(az keyvault secret show --vault-name ${keyVaultName} --name MEET-ADMIN-USER --query value -o tsv)
export DEFAULT_APP_PASSWORD=$(az keyvault secret show --vault-name ${keyVaultName} --name DEFAULT-APP-PASSWORD --query value -o tsv) export MEET_ADMIN_SECRET=$(az keyvault secret show --vault-name ${keyVaultName} --name MEET-ADMIN-SECRET --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 MEET_API_KEY=$(az keyvault secret show --vault-name ${keyVaultName} --name MEET-API-KEY --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 ENABLED_MODULES=$(az keyvault secret show --vault-name ${keyVaultName} --name ENABLED-MODULES --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 # 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/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_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/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/MEET_ADMIN_USER=.*/MEET_ADMIN_USER=$MEET_ADMIN_USER/" "${CLUSTER_CONFIG_DIR}/master_node/meet.env"
sed -i "s/CALL_SECRET=.*/CALL_SECRET=$DEFAULT_APP_PASSWORD/" "${CLUSTER_CONFIG_DIR}/master_node/app.env" sed -i "s/MEET_ADMIN_SECRET=.*/MEET_ADMIN_SECRET=$MEET_ADMIN_SECRET/" "${CLUSTER_CONFIG_DIR}/master_node/meet.env"
sed -i "s/CALL_ADMIN_USER=.*/CALL_ADMIN_USER=$DEFAULT_APP_ADMIN_USERNAME/" "${CLUSTER_CONFIG_DIR}/master_node/app.env" sed -i "s/MEET_API_KEY=.*/MEET_API_KEY=$MEET_API_KEY/" "${CLUSTER_CONFIG_DIR}/master_node/meet.env"
sed -i "s/CALL_ADMIN_SECRET=.*/CALL_ADMIN_SECRET=$DEFAULT_APP_ADMIN_PASSWORD/" "${CLUSTER_CONFIG_DIR}/master_node/app.env"
sed -i "s/ENABLED_MODULES=.*/ENABLED_MODULES=$ENABLED_MODULES/" "${CLUSTER_CONFIG_DIR}/openvidu.env" sed -i "s/ENABLED_MODULES=.*/ENABLED_MODULES=$ENABLED_MODULES/" "${CLUSTER_CONFIG_DIR}/openvidu.env"
# Update URLs in secret # 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")" 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_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")" 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")" MEET_ADMIN_USER="$(/usr/local/bin/get_value_from_config.sh MEET_ADMIN_USER "${CLUSTER_CONFIG_DIR}/master_node/meet.env")"
DEFAULT_APP_PASSWORD="$(/usr/local/bin/get_value_from_config.sh CALL_SECRET "${CLUSTER_CONFIG_DIR}/master_node/app.env")" MEET_ADMIN_SECRET="$(/usr/local/bin/get_value_from_config.sh MEET_ADMIN_SECRET "${CLUSTER_CONFIG_DIR}/master_node/meet.env")"
DEFAULT_APP_ADMIN_USERNAME="$(/usr/local/bin/get_value_from_config.sh CALL_ADMIN_USER "${CLUSTER_CONFIG_DIR}/master_node/app.env")" MEET_API_KEY="$(/usr/local/bin/get_value_from_config.sh MEET_API_KEY "${CLUSTER_CONFIG_DIR}/master_node/meet.env")"
DEFAULT_APP_ADMIN_PASSWORD="$(/usr/local/bin/get_value_from_config.sh CALL_ADMIN_SECRET "${CLUSTER_CONFIG_DIR}/master_node/app.env")"
ENABLED_MODULES="$(/usr/local/bin/get_value_from_config.sh ENABLED_MODULES "${CLUSTER_CONFIG_DIR}/openvidu.env")" ENABLED_MODULES="$(/usr/local/bin/get_value_from_config.sh ENABLED_MODULES "${CLUSTER_CONFIG_DIR}/openvidu.env")"
# Update shared secret # 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 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-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 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 MEET-ADMIN-USER --value $MEET_ADMIN_USER
az keyvault secret set --vault-name ${keyVaultName} --name DEFAULT-APP-PASSWORD --value $DEFAULT_APP_PASSWORD az keyvault secret set --vault-name ${keyVaultName} --name MEET-ADMIN-SECRET --value $MEET_ADMIN_SECRET
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 MEET-API-KEY --value $MEET_API_KEY
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 ENABLED-MODULES --value $ENABLED_MODULES 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 parent: openviduMasterNodeNSG
name: 'masterNode_to_masterNode_DEFAULTAPP_INGRESS' name: 'masterNode_to_masterNode_MEET_INGRESS'
properties: { properties: {
protocol: 'Tcp' protocol: 'Tcp'
sourceApplicationSecurityGroups: [ 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 parent: openviduMasterNodeNSG
name: 'mediaNode_to_masterNode_DEFAULTAPP_WEBHOOK_INGRESS' name: 'mediaNode_to_masterNode_MEET_WEBHOOK_INGRESS'
properties: { properties: {
protocol: 'Tcp' protocol: 'Tcp'
sourceApplicationSecurityGroups: [ sourceApplicationSecurityGroups: [
@ -2847,6 +2840,22 @@ resource openviduMediaNodeNSG 'Microsoft.Network/networkSecurityGroups@2023-11-0
direction: 'Inbound' 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' destinationPortRange: '1945'
access: 'Allow' access: 'Allow'
priority: 150 priority: 160
direction: 'Inbound' direction: 'Inbound'
} }
} }
@ -2889,7 +2898,7 @@ resource loadBalancerToMediaHealthcheckIngress 'Microsoft.Network/networkSecurit
] ]
destinationPortRange: '9092' destinationPortRange: '9092'
access: 'Allow' access: 'Allow'
priority: 160 priority: 170
direction: 'Inbound' direction: 'Inbound'
} }
} }
@ -2908,7 +2917,7 @@ resource loadBalancerToMediaTurnTlsIngress 'Microsoft.Network/networkSecurityGro
] ]
destinationPortRange: '5349' destinationPortRange: '5349'
access: 'Allow' access: 'Allow'
priority: 170 priority: 180
direction: 'Inbound' direction: 'Inbound'
} }
} }
@ -2927,7 +2936,7 @@ resource loadBalancerToMediaTurnTlsHealthCheckIngress 'Microsoft.Network/network
] ]
destinationPortRange: '7880' destinationPortRange: '7880'
access: 'Allow' access: 'Allow'
priority: 180 priority: 190
direction: 'Inbound' direction: 'Inbound'
} }
} }
@ -2950,7 +2959,7 @@ resource masterToMediaServerIngress 'Microsoft.Network/networkSecurityGroups/sec
] ]
destinationPortRange: '7880' destinationPortRange: '7880'
access: 'Allow' access: 'Allow'
priority: 190 priority: 200
direction: 'Inbound' direction: 'Inbound'
} }
} }
@ -2973,7 +2982,7 @@ resource masterToMediaClientIngress 'Microsoft.Network/networkSecurityGroups/sec
] ]
destinationPortRange: '8080' destinationPortRange: '8080'
access: 'Allow' access: 'Allow'
priority: 200 priority: 210
direction: 'Inbound' direction: 'Inbound'
} }
} }

File diff suppressed because one or more lines are too long

View File

@ -3,9 +3,9 @@
set -eu set -eu
export DOCKER_VERSION="${DOCKER_VERSION:-28.1.1}" export DOCKER_VERSION="${DOCKER_VERSION:-28.1.1}"
export DOCKER_COMPOSE_VERSION="${DOCKER_COMPOSE_VERSION:-v2.35.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 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 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 MONGO_SERVER_IMAGE="${MONGO_SERVER_IMAGE:-docker.io/mongo:8.0.9}"
export REDIS_SERVER_IMAGE="${REDIS_SERVER_IMAGE:-docker.io/redis:7.4.4-alpine}" export REDIS_SERVER_IMAGE="${REDIS_SERVER_IMAGE:-docker.io/redis:7.4.4-alpine}"
@ -15,7 +15,7 @@ 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_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_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_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_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_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_V2COMPATIBILITY_IMAGE="${OPENVIDU_V2COMPATIBILITY_IMAGE:-docker.io/openvidu/openvidu-v2compatibility:${OPENVIDU_VERSION}}"
@ -25,7 +25,7 @@ export LIVEKIT_EGRESS_SERVER_IMAGE="${LIVEKIT_EGRESS_SERVER_IMAGE:-docker.io/liv
export PROMETHEUS_IMAGE="${PROMETHEUS_IMAGE:-docker.io/prom/prometheus:v3.4.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 PROMTAIL_IMAGE="${PROMTAIL_IMAGE:-docker.io/grafana/promtail:3.5.1}"
export LOKI_IMAGE="${LOKI_IMAGE:-docker.io/grafana/loki: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}" export GRAFANA_IMAGE="${GRAFANA_IMAGE:-docker.io/grafana/grafana:11.6.2}"
# Function to compare two version strings # 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_OPERATOR_IMAGE=$OPENVIDU_OPERATOR_IMAGE \
-e OPENVIDU_SERVER_PRO_IMAGE=$OPENVIDU_SERVER_PRO_IMAGE \ -e OPENVIDU_SERVER_PRO_IMAGE=$OPENVIDU_SERVER_PRO_IMAGE \
-e OPENVIDU_SERVER_IMAGE=$OPENVIDU_SERVER_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_PRO_IMAGE=$OPENVIDU_DASHBOARD_PRO_IMAGE \
-e OPENVIDU_DASHBOARD_IMAGE=$OPENVIDU_DASHBOARD_IMAGE \ -e OPENVIDU_DASHBOARD_IMAGE=$OPENVIDU_DASHBOARD_IMAGE \
-e OPENVIDU_V2COMPATIBILITY_IMAGE=$OPENVIDU_V2COMPATIBILITY_IMAGE \ -e OPENVIDU_V2COMPATIBILITY_IMAGE=$OPENVIDU_V2COMPATIBILITY_IMAGE \

View File

@ -3,9 +3,9 @@
set -eu set -eu
export DOCKER_VERSION="${DOCKER_VERSION:-28.1.1}" export DOCKER_VERSION="${DOCKER_VERSION:-28.1.1}"
export DOCKER_COMPOSE_VERSION="${DOCKER_COMPOSE_VERSION:-v2.35.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 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 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 MONGO_SERVER_IMAGE="${MONGO_SERVER_IMAGE:-docker.io/mongo:8.0.9}"
export REDIS_SERVER_IMAGE="${REDIS_SERVER_IMAGE:-docker.io/redis:7.4.4-alpine}" export REDIS_SERVER_IMAGE="${REDIS_SERVER_IMAGE:-docker.io/redis:7.4.4-alpine}"
@ -15,7 +15,7 @@ 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_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_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_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_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_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_V2COMPATIBILITY_IMAGE="${OPENVIDU_V2COMPATIBILITY_IMAGE:-docker.io/openvidu/openvidu-v2compatibility:${OPENVIDU_VERSION}}"
@ -25,7 +25,7 @@ export LIVEKIT_EGRESS_SERVER_IMAGE="${LIVEKIT_EGRESS_SERVER_IMAGE:-docker.io/liv
export PROMETHEUS_IMAGE="${PROMETHEUS_IMAGE:-docker.io/prom/prometheus:v3.4.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 PROMTAIL_IMAGE="${PROMTAIL_IMAGE:-docker.io/grafana/promtail:3.5.1}"
export LOKI_IMAGE="${LOKI_IMAGE:-docker.io/grafana/loki: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}" export GRAFANA_IMAGE="${GRAFANA_IMAGE:-docker.io/grafana/grafana:11.6.2}"
# Function to compare two version strings # 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_OPERATOR_IMAGE=$OPENVIDU_OPERATOR_IMAGE \
-e OPENVIDU_SERVER_PRO_IMAGE=$OPENVIDU_SERVER_PRO_IMAGE \ -e OPENVIDU_SERVER_PRO_IMAGE=$OPENVIDU_SERVER_PRO_IMAGE \
-e OPENVIDU_SERVER_IMAGE=$OPENVIDU_SERVER_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_PRO_IMAGE=$OPENVIDU_DASHBOARD_PRO_IMAGE \
-e OPENVIDU_DASHBOARD_IMAGE=$OPENVIDU_DASHBOARD_IMAGE \ -e OPENVIDU_DASHBOARD_IMAGE=$OPENVIDU_DASHBOARD_IMAGE \
-e OPENVIDU_V2COMPATIBILITY_IMAGE=$OPENVIDU_V2COMPATIBILITY_IMAGE \ -e OPENVIDU_V2COMPATIBILITY_IMAGE=$OPENVIDU_V2COMPATIBILITY_IMAGE \

View File

@ -293,10 +293,9 @@ Resources:
"GRAFANA_ADMIN_PASSWORD": "none", "GRAFANA_ADMIN_PASSWORD": "none",
"LIVEKIT_API_KEY": "none", "LIVEKIT_API_KEY": "none",
"LIVEKIT_API_SECRET": "none", "LIVEKIT_API_SECRET": "none",
"DEFAULT_APP_USERNAME": "none", "MEET_ADMIN_USER": "none",
"DEFAULT_APP_PASSWORD": "none", "MEET_ADMIN_SECRET": "none",
"DEFAULT_APP_ADMIN_USERNAME": "none", "MEET_API_KEY": "none",
"DEFAULT_APP_ADMIN_PASSWORD": "none",
"ENABLED_MODULES": "none" "ENABLED_MODULES": "none"
} }
@ -389,7 +388,7 @@ Resources:
'/usr/local/bin/install.sh': '/usr/local/bin/install.sh':
content: !Sub | content: !Sub |
#!/bin/bash -x #!/bin/bash -x
OPENVIDU_VERSION=3.3.0 OPENVIDU_VERSION=main
DOMAIN= DOMAIN=
YQ_VERSION=v4.44.5 YQ_VERSION=v4.44.5
@ -434,11 +433,10 @@ Resources:
DASHBOARD_ADMIN_PASSWORD="$(/usr/local/bin/store_secret.sh generate DASHBOARD_ADMIN_PASSWORD)" 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_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)" 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")" MEET_ADMIN_USER="$(/usr/local/bin/store_secret.sh save MEET_ADMIN_USER "meetadmin")"
DEFAULT_APP_PASSWORD="$(/usr/local/bin/store_secret.sh generate DEFAULT_APP_PASSWORD)" MEET_ADMIN_SECRET="$(/usr/local/bin/store_secret.sh generate MEET_ADMIN_SECRET)"
DEFAULT_APP_ADMIN_USERNAME="$(/usr/local/bin/store_secret.sh save DEFAULT_APP_ADMIN_USERNAME "calladmin")" MEET_API_KEY="$(/usr/local/bin/store_secret.sh generate MEET_API_KEY)"
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,openviduMeet,v2compatibility")"
ENABLED_MODULES="$(/usr/local/bin/store_secret.sh save ENABLED_MODULES "observability,app,v2compatibility")"
LIVEKIT_API_KEY="$(/usr/local/bin/store_secret.sh generate LIVEKIT_API_KEY "API" 12)" 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)" LIVEKIT_API_SECRET="$(/usr/local/bin/store_secret.sh generate LIVEKIT_API_SECRET)"
@ -465,10 +463,9 @@ Resources:
"--dashboard-admin-password=$DASHBOARD_ADMIN_PASSWORD" "--dashboard-admin-password=$DASHBOARD_ADMIN_PASSWORD"
"--grafana-admin-user=$GRAFANA_ADMIN_USERNAME" "--grafana-admin-user=$GRAFANA_ADMIN_USERNAME"
"--grafana-admin-password=$GRAFANA_ADMIN_PASSWORD" "--grafana-admin-password=$GRAFANA_ADMIN_PASSWORD"
"--default-app-user=$DEFAULT_APP_USERNAME" "--meet-admin-user=$MEET_ADMIN_USER"
"--default-app-password=$DEFAULT_APP_PASSWORD" "--meet-admin-password=$MEET_ADMIN_SECRET"
"--default-app-admin-user=$DEFAULT_APP_ADMIN_USERNAME" "--meet-api-key=$MEET_API_KEY"
"--default-app-admin-password=$DEFAULT_APP_ADMIN_PASSWORD"
"--livekit-api-key=$LIVEKIT_API_KEY" "--livekit-api-key=$LIVEKIT_API_KEY"
"--livekit-api-secret=$LIVEKIT_API_SECRET" "--livekit-api-secret=$LIVEKIT_API_SECRET"
) )
@ -666,10 +663,9 @@ Resources:
sed -i "s/GRAFANA_ADMIN_PASSWORD=.*/GRAFANA_ADMIN_PASSWORD=$(echo $SHARED_SECRET | jq -r .GRAFANA_ADMIN_PASSWORD)/" "${!CONFIG_DIR}/openvidu.env" sed -i "s/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_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/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/MEET_ADMIN_USER=.*/MEET_ADMIN_USER=$(echo $SHARED_SECRET | jq -r .MEET_ADMIN_USER)/" "${!CONFIG_DIR}/meet.env"
sed -i "s/CALL_SECRET=.*/CALL_SECRET=$(echo $SHARED_SECRET | jq -r .DEFAULT_APP_PASSWORD)/" "${!CONFIG_DIR}/app.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/CALL_ADMIN_USER=.*/CALL_ADMIN_USER=$(echo $SHARED_SECRET | jq -r .DEFAULT_APP_ADMIN_USERNAME)/" "${!CONFIG_DIR}/app.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/CALL_ADMIN_SECRET=.*/CALL_ADMIN_SECRET=$(echo $SHARED_SECRET | jq -r .DEFAULT_APP_ADMIN_PASSWORD)/" "${!CONFIG_DIR}/app.env"
sed -i "s/ENABLED_MODULES=.*/ENABLED_MODULES=$(echo $SHARED_SECRET | jq -r .ENABLED_MODULES)/" "${!CONFIG_DIR}/openvidu.env" sed -i "s/ENABLED_MODULES=.*/ENABLED_MODULES=$(echo $SHARED_SECRET | jq -r .ENABLED_MODULES)/" "${!CONFIG_DIR}/openvidu.env"
# Update URLs in secret # Update URLs in secret
@ -722,10 +718,9 @@ Resources:
SHARED_SECRET="$(echo "$SHARED_SECRET" | jq '. + {"GRAFANA_ADMIN_PASSWORD": "'"$(/usr/local/bin/get_value_from_config.sh GRAFANA_ADMIN_PASSWORD "${!CONFIG_DIR}/openvidu.env")"'"}')" SHARED_SECRET="$(echo "$SHARED_SECRET" | jq '. + {"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_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 '. + {"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 '. + {"MEET_ADMIN_USER": "'"$(/usr/local/bin/get_value_from_config.sh MEET_ADMIN_USER "${!CONFIG_DIR}/meet.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 '. + {"MEET_ADMIN_SECRET": "'"$(/usr/local/bin/get_value_from_config.sh MEET_ADMIN_SECRET "${!CONFIG_DIR}/meet.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 '. + {"MEET_API_KEY": "'"$(/usr/local/bin/get_value_from_config.sh MEET_API_KEY "${!CONFIG_DIR}/meet.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 '. + {"ENABLED_MODULES": "'"$(/usr/local/bin/get_value_from_config.sh ENABLED_MODULES "${!CONFIG_DIR}/openvidu.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 # Update shared secret
@ -974,6 +969,14 @@ Resources:
FromPort: 7885 FromPort: 7885
ToPort: 7885 ToPort: 7885
CidrIpv6: ::/0 CidrIpv6: ::/0
- IpProtocol: tcp
FromPort: 7881
ToPort: 7881
CidrIp: 0.0.0.0/0
- IpProtocol: tcp
FromPort: 7881
ToPort: 7881
CidrIpv6: ::/0
- IpProtocol: udp - IpProtocol: udp
FromPort: 50000 FromPort: 50000
ToPort: 60000 ToPort: 60000

View File

@ -283,7 +283,7 @@ var stringInterpolationParams = {
var installScriptTemplate = ''' var installScriptTemplate = '''
#!/bin/bash -x #!/bin/bash -x
OPENVIDU_VERSION=3.3.0 OPENVIDU_VERSION=main
DOMAIN= DOMAIN=
apt-get update && apt-get install -y \ apt-get update && apt-get install -y \
@ -314,13 +314,12 @@ DASHBOARD_ADMIN_USERNAME="$(/usr/local/bin/store_secret.sh save DASHBOARD-ADMIN-
DASHBOARD_ADMIN_PASSWORD="$(/usr/local/bin/store_secret.sh generate DASHBOARD-ADMIN-PASSWORD)" 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_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)" 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")" MEET_ADMIN_USER="$(/usr/local/bin/store_secret.sh save MEET-ADMIN-USER "meetadmin")"
DEFAULT_APP_PASSWORD="$(/usr/local/bin/store_secret.sh generate DEFAULT-APP-PASSWORD)" MEET_ADMIN_SECRET="$(/usr/local/bin/store_secret.sh generate MEET-ADMIN-SECRET)"
DEFAULT_APP_ADMIN_USERNAME="$(/usr/local/bin/store_secret.sh save DEFAULT-APP-ADMIN-USERNAME "calladmin")" MEET_API_KEY="$(/usr/local/bin/store_secret.sh generate MEET-API-KEY)"
DEFAULT_APP_ADMIN_PASSWORD="$(/usr/local/bin/store_secret.sh generate DEFAULT-APP-ADMIN-PASSWORD)"
LIVEKIT_API_KEY="$(/usr/local/bin/store_secret.sh generate LIVEKIT-API-KEY "API" 12)" 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)" LIVEKIT_API_SECRET="$(/usr/local/bin/store_secret.sh generate LIVEKIT-API-SECRET)"
ENABLED_MODULES="$(/usr/local/bin/store_secret.sh save ENABLED-MODULES "observability,app,v2compatibility")" ENABLED_MODULES="$(/usr/local/bin/store_secret.sh save ENABLED-MODULES "observability,openviduMeet,v2compatibility")"
# Base command # Base command
INSTALL_COMMAND="sh <(curl -fsSL http://get.openvidu.io/community/singlenode/$OPENVIDU_VERSION/install.sh)" INSTALL_COMMAND="sh <(curl -fsSL http://get.openvidu.io/community/singlenode/$OPENVIDU_VERSION/install.sh)"
@ -345,10 +344,9 @@ COMMON_ARGS=(
"--dashboard-admin-password=$DASHBOARD_ADMIN_PASSWORD" "--dashboard-admin-password=$DASHBOARD_ADMIN_PASSWORD"
"--grafana-admin-user=$GRAFANA_ADMIN_USERNAME" "--grafana-admin-user=$GRAFANA_ADMIN_USERNAME"
"--grafana-admin-password=$GRAFANA_ADMIN_PASSWORD" "--grafana-admin-password=$GRAFANA_ADMIN_PASSWORD"
"--default-app-user=$DEFAULT_APP_USERNAME" "--meet-admin-user=$MEET_ADMIN_USER"
"--default-app-password=$DEFAULT_APP_PASSWORD" "--meet-admin-password=$MEET_ADMIN_SECRET"
"--default-app-admin-user=$DEFAULT_APP_ADMIN_USERNAME" "--meet-api-key=$MEET_API_KEY"
"--default-app-admin-password=$DEFAULT_APP_ADMIN_PASSWORD"
"--livekit-api-key=$LIVEKIT_API_KEY" "--livekit-api-key=$LIVEKIT_API_KEY"
"--livekit-api-secret=$LIVEKIT_API_SECRET" "--livekit-api-secret=$LIVEKIT_API_SECRET"
) )
@ -495,17 +493,16 @@ export GRAFANA_ADMIN_USERNAME=$(az keyvault secret show --vault-name ${keyVaultN
export GRAFANA_ADMIN_PASSWORD=$(az keyvault secret show --vault-name ${keyVaultName} --name GRAFANA-ADMIN-PASSWORD --query value -o tsv) export 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_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 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 MEET_ADMIN_USER=$(az keyvault secret show --vault-name ${keyVaultName} --name MEET-ADMIN-USER --query value -o tsv)
export DEFAULT_APP_PASSWORD=$(az keyvault secret show --vault-name ${keyVaultName} --name DEFAULT-APP-PASSWORD --query value -o tsv) export MEET_ADMIN_SECRET=$(az keyvault secret show --vault-name ${keyVaultName} --name MEET-ADMIN-SECRET --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 MEET_API_KEY=$(az keyvault secret show --vault-name ${keyVaultName} --name MEET-API-KEY --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 ENABLED_MODULES=$(az keyvault secret show --vault-name ${keyVaultName} --name ENABLED-MODULES --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 # Replace rest of the values
sed -i "s/REDIS_PASSWORD=.*/REDIS_PASSWORD=$REDIS_PASSWORD/" "${CONFIG_DIR}/openvidu.env" sed -i "s/REDIS_PASSWORD=.*/REDIS_PASSWORD=$REDIS_PASSWORD/" "${CONFIG_DIR}/openvidu.env"
sed -i "s/OPENVIDU_RTC_ENGINE=.*/OPENVIDU_RTC_ENGINE=$OPENVIDU_RTC_ENGINE/" "${CLUSTER_CONFIG_DIR}/openvidu.env" sed -i "s/OPENVIDU_RTC_ENGINE=.*/OPENVIDU_RTC_ENGINE=$OPENVIDU_RTC_ENGINE/" "${CONFIG_DIR}/openvidu.env"
sed -i "s/OPENVIDU_PRO_LICENSE=.*/OPENVIDU_PRO_LICENSE=$OPENVIDU_PRO_LICENSE/" "${CLUSTER_CONFIG_DIR}/openvidu.env" sed -i "s/OPENVIDU_PRO_LICENSE=.*/OPENVIDU_PRO_LICENSE=$OPENVIDU_PRO_LICENSE/" "${CONFIG_DIR}/openvidu.env"
sed -i "s/MONGO_ADMIN_USERNAME=.*/MONGO_ADMIN_USERNAME=$MONGO_ADMIN_USERNAME/" "${CONFIG_DIR}/openvidu.env" sed -i "s/MONGO_ADMIN_USERNAME=.*/MONGO_ADMIN_USERNAME=$MONGO_ADMIN_USERNAME/" "${CONFIG_DIR}/openvidu.env"
sed -i "s/MONGO_ADMIN_PASSWORD=.*/MONGO_ADMIN_PASSWORD=$MONGO_ADMIN_PASSWORD/" "${CONFIG_DIR}/openvidu.env" sed -i "s/MONGO_ADMIN_PASSWORD=.*/MONGO_ADMIN_PASSWORD=$MONGO_ADMIN_PASSWORD/" "${CONFIG_DIR}/openvidu.env"
sed -i "s/MONGO_REPLICA_SET_KEY=.*/MONGO_REPLICA_SET_KEY=$MONGO_REPLICA_SET_KEY/" "${CONFIG_DIR}/openvidu.env" sed -i "s/MONGO_REPLICA_SET_KEY=.*/MONGO_REPLICA_SET_KEY=$MONGO_REPLICA_SET_KEY/" "${CONFIG_DIR}/openvidu.env"
@ -517,10 +514,9 @@ sed -i "s/GRAFANA_ADMIN_USERNAME=.*/GRAFANA_ADMIN_USERNAME=$GRAFANA_ADMIN_USERNA
sed -i "s/GRAFANA_ADMIN_PASSWORD=.*/GRAFANA_ADMIN_PASSWORD=$GRAFANA_ADMIN_PASSWORD/" "${CONFIG_DIR}/openvidu.env" sed -i "s/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_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/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/MEET_ADMIN_USER=.*/MEET_ADMIN_USER=$MEET_ADMIN_USER/" "${CONFIG_DIR}/meet.env"
sed -i "s/CALL_SECRET=.*/CALL_SECRET=$DEFAULT_APP_PASSWORD/" "${CONFIG_DIR}/app.env" sed -i "s/MEET_ADMIN_SECRET=.*/MEET_ADMIN_SECRET=$MEET_ADMIN_SECRET/" "${CONFIG_DIR}/meet.env"
sed -i "s/CALL_ADMIN_USER=.*/CALL_ADMIN_USER=$DEFAULT_APP_ADMIN_USERNAME/" "${CONFIG_DIR}/app.env" sed -i "s/MEET_API_KEY=.*/MEET_API_KEY=$MEET_API_KEY/" "${CONFIG_DIR}/meet.env"
sed -i "s/CALL_ADMIN_SECRET=.*/CALL_ADMIN_SECRET=$DEFAULT_APP_ADMIN_PASSWORD/" "${CONFIG_DIR}/app.env"
sed -i "s/ENABLED_MODULES=.*/ENABLED_MODULES=$ENABLED_MODULES/" "${CONFIG_DIR}/openvidu.env" sed -i "s/ENABLED_MODULES=.*/ENABLED_MODULES=$ENABLED_MODULES/" "${CONFIG_DIR}/openvidu.env"
@ -556,8 +552,8 @@ fi
REDIS_PASSWORD="$(/usr/local/bin/get_value_from_config.sh REDIS_PASSWORD "${CONFIG_DIR}/openvidu.env")" REDIS_PASSWORD="$(/usr/local/bin/get_value_from_config.sh REDIS_PASSWORD "${CONFIG_DIR}/openvidu.env")"
DOMAIN_NAME="$(/usr/local/bin/get_value_from_config.sh DOMAIN_NAME "${CONFIG_DIR}/openvidu.env")" DOMAIN_NAME="$(/usr/local/bin/get_value_from_config.sh DOMAIN_NAME "${CONFIG_DIR}/openvidu.env")"
LIVEKIT_TURN_DOMAIN_NAME="$(/usr/local/bin/get_value_from_config.sh LIVEKIT_TURN_DOMAIN_NAME "${CONFIG_DIR}/openvidu.env")" LIVEKIT_TURN_DOMAIN_NAME="$(/usr/local/bin/get_value_from_config.sh LIVEKIT_TURN_DOMAIN_NAME "${CONFIG_DIR}/openvidu.env")"
OPENVIDU_RTC_ENGINE="$(/usr/local/bin/get_value_from_config.sh OPENVIDU_RTC_ENGINE "${CLUSTER_CONFIG_DIR}/openvidu.env")" OPENVIDU_RTC_ENGINE="$(/usr/local/bin/get_value_from_config.sh OPENVIDU_RTC_ENGINE "${CONFIG_DIR}/openvidu.env")"
OPENVIDU_PRO_LICENSE="$(/usr/local/bin/get_value_from_config.sh OPENVIDU_PRO_LICENSE "${CLUSTER_CONFIG_DIR}/openvidu.env")" OPENVIDU_PRO_LICENSE="$(/usr/local/bin/get_value_from_config.sh OPENVIDU_PRO_LICENSE "${CONFIG_DIR}/openvidu.env")"
MONGO_ADMIN_USERNAME="$(/usr/local/bin/get_value_from_config.sh MONGO_ADMIN_USERNAME "${CONFIG_DIR}/openvidu.env")" MONGO_ADMIN_USERNAME="$(/usr/local/bin/get_value_from_config.sh MONGO_ADMIN_USERNAME "${CONFIG_DIR}/openvidu.env")"
MONGO_ADMIN_PASSWORD="$(/usr/local/bin/get_value_from_config.sh MONGO_ADMIN_PASSWORD "${CONFIG_DIR}/openvidu.env")" MONGO_ADMIN_PASSWORD="$(/usr/local/bin/get_value_from_config.sh MONGO_ADMIN_PASSWORD "${CONFIG_DIR}/openvidu.env")"
MONGO_REPLICA_SET_KEY="$(/usr/local/bin/get_value_from_config.sh MONGO_REPLICA_SET_KEY "${CONFIG_DIR}/openvidu.env")" MONGO_REPLICA_SET_KEY="$(/usr/local/bin/get_value_from_config.sh MONGO_REPLICA_SET_KEY "${CONFIG_DIR}/openvidu.env")"
@ -569,10 +565,9 @@ GRAFANA_ADMIN_USERNAME="$(/usr/local/bin/get_value_from_config.sh GRAFANA_ADMIN_
GRAFANA_ADMIN_PASSWORD="$(/usr/local/bin/get_value_from_config.sh GRAFANA_ADMIN_PASSWORD "${CONFIG_DIR}/openvidu.env")" 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_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")" 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")" MEET_ADMIN_USER="$(/usr/local/bin/get_value_from_config.sh MEET_ADMIN_USER "${CONFIG_DIR}/meet.env")"
DEFAULT_APP_PASSWORD="$(/usr/local/bin/get_value_from_config.sh CALL_SECRET "${CONFIG_DIR}/app.env")" MEET_ADMIN_SECRET="$(/usr/local/bin/get_value_from_config.sh MEET_ADMIN_SECRET "${CONFIG_DIR}/meet.env")"
DEFAULT_APP_ADMIN_USERNAME="$(/usr/local/bin/get_value_from_config.sh CALL_ADMIN_USER "${CONFIG_DIR}/app.env")" MEET_API_KEY="$(/usr/local/bin/get_value_from_config.sh MEET_API_KEY "${CONFIG_DIR}/meet.env")"
DEFAULT_APP_ADMIN_PASSWORD="$(/usr/local/bin/get_value_from_config.sh CALL_ADMIN_SECRET "${CONFIG_DIR}/app.env")"
ENABLED_MODULES="$(/usr/local/bin/get_value_from_config.sh ENABLED_MODULES "${CONFIG_DIR}/openvidu.env")" ENABLED_MODULES="$(/usr/local/bin/get_value_from_config.sh ENABLED_MODULES "${CONFIG_DIR}/openvidu.env")"
@ -593,10 +588,9 @@ az keyvault secret set --vault-name ${keyVaultName} --name GRAFANA-ADMIN-USERNAM
az keyvault secret set --vault-name ${keyVaultName} --name GRAFANA-ADMIN-PASSWORD --value $GRAFANA_ADMIN_PASSWORD az keyvault secret set --vault-name ${keyVaultName} --name 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-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 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 MEET-ADMIN-USER --value $MEET_ADMIN_USER
az keyvault secret set --vault-name ${keyVaultName} --name DEFAULT-APP-PASSWORD --value $DEFAULT_APP_PASSWORD az keyvault secret set --vault-name ${keyVaultName} --name MEET-ADMIN-SECRET --value $MEET_ADMIN_SECRET
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 MEET-API-KEY --value $MEET_API_KEY
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 ENABLED-MODULES --value $ENABLED_MODULES az keyvault secret set --vault-name ${keyVaultName} --name ENABLED-MODULES --value $ENABLED_MODULES
''' '''

File diff suppressed because one or more lines are too long

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