Compare commits

...

270 Commits

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

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

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

ov-components: Refactor configuration management in OpenVidu components

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

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

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

Adds visual cues for active recordings and improves overall
responsiveness of the recording list.
2025-07-21 14:12:28 +02:00
Carlos Santos e486665efd ov-components: Updated recording activity component 2025-07-21 14:11:40 +02:00
Carlos Santos b659400c88 ov-components: implement read-only mode and customizable controls for recording activity 2025-07-21 14:11:40 +02:00
cruizba 98c7e3f751 openvidu-deployment: Build azure bicep of Single Node - PRO 2025-07-20 22:56:50 +02:00
cruizba 5e1df8b511 openvidu-deployment: Fix wrong environment variable in Azure Single Node - PRO 2025-07-20 22:42:23 +02:00
cruizba 16ec1f3920 openvidu-deployment: Replace Default App (OpenVidu Call) with OpenVidu Meet in Single Node Pro 2025-07-20 22:23:19 +02:00
cruizba b01e8f4d23 openvidu-deployment: update enabled modules to include openviduMeet in deployment script 2025-07-20 20:59:56 +02:00
cruizba 2413a0bb6d openvidu-deployment: Generate json from bicep Single Node - Community 2025-07-20 20:12:10 +02:00
cruizba bf6091e997 openvidu-deployment: Replace Default App (OpenVidu Call) with OpenVidu Meet in Single Node - Community 2025-07-20 20:10:25 +02:00
cruizba 9e0034dfac openvidu-deployment: Replace Default App (OpenVidu Call) with OpenVidu Meet 2025-07-18 21:53:32 +02:00
Carlos Santos 637142cec6 ov-components: add room initialization checks and error handling in SessionComponent 2025-07-17 17:03:18 +02:00
Carlos Santos c304c9c761 ov-components: add PreJoin directive to support custom pre-join templates in VideoconferenceComponent 2025-07-17 16:53:17 +02:00
Carlos Santos 4dd007395f ov-components: Refactor components to use takeUntil for unsubscribing from observables
- Replaced manual subscription management with takeUntil pattern
- Introduced a destroy$ Subject in each component to handle unsubscriptions on component destruction.
- Improved memory management and code readability by eliminating multiple subscription variables.
2025-07-17 15:44:37 +02:00
Carlos Santos 7573656060 ov-components: refactor VideoconferenceComponent to improve template setup and icon management 2025-07-17 14:00:51 +02:00
Carlos Santos 8407363aaf ov-components: add track subscription and manage room tracks published state 2025-07-17 13:29:17 +02:00
Carlos Santos 55fd64c254 ov-components: enhance recording functionality with track checks and UI updates 2025-07-17 13:29:17 +02:00
cruizba d151834048 openvidu-deployment: Replace OPENVIDU_CALL_SERVER_IMAGE with OPENVIDU_MEET_SERVER_IMAGE in deployment scripts 2025-07-16 12:36:15 +02:00
pabloFuente ce47224400 openvidu-testapp: make update interval for dialog optional 2025-07-14 22:18:12 +02:00
cruizba 61fbf9850b Add TCP port rules for WebRTC traffic on port 7881 and 50000-60000 across multiple deployment configurations 2025-07-11 21:33:05 +02:00
Piwccle ba1df4660c openvidu-testapp: RTCIceCandidate stats for publisher and subscriber peer connections fixed in firefox 2025-07-10 16:12:08 +02:00
pabloFuente d44e24592d openvidu-testapp: RTCIceCandidate stats for publisher and subscriber peer connections 2025-07-09 18:24:00 +02:00
Carlos Santos 91aa127dad ov-components: replace and improve recordingElapsedTime logic 2025-07-04 17:51:30 +02:00
Carlos Santos 1be876678c ov-components: add subscription for virtual background effects management 2025-07-04 15:55:22 +02:00
Carlos Santos 388981be31 ov-components: reorder imports and add ShowDisconnectionDialogDirective to ApiDirectiveModule 2025-07-04 12:51:57 +02:00
Carlos Santos 6497751375 ov-component: add showDisconnectionDialog directive and update service for disconnection dialog management 2025-07-04 12:45:34 +02:00
pabloFuente 5f0639c157 openvidu-test-e2e: adapted egress test to tolerate mediasoup limitation 2025-07-03 14:23:10 +02:00
pabloFuente e435a1a937 openvidu-test-e2e: fix signalTest 2025-07-02 21:53:45 +02:00
pabloFuente 932eda8115 openvidu-test-e2e: include event RoomEvent.ParticipantActive 2025-07-02 18:35:44 +02:00
pabloFuente 5d91f4d343 openvidu-testapp: add RoomEvent.ParticipantActive 2025-07-02 18:14:45 +02:00
pabloFuente 703698b25f openvidu-components-angular: fix comment links 2025-07-02 18:14:07 +02:00
pabloFuente b884e924b6 openvidu-test-e2e: added Egress tests 2025-06-30 14:34:37 +02:00
pabloFuente ee8847c5cb openvidu-testapp: add checkbox to force relay from browser 2025-06-30 12:11:09 +02:00
GitHub Actions 99c787c4f5 Revert "Bump version to 3.3.0"
This reverts commit 2083c078fd.
2025-06-26 20:02:58 +00:00
GitHub Actions 2083c078fd Bump version to 3.3.0 2025-06-26 20:02:55 +00:00
github-actions bc42a72836 openvidu-components-angular: Bumped version to 3.3.0 2025-06-26 18:43:30 +00:00
cruizba c3b7c6f4bb openvidu-deployment: Shutdown gracefully agents in aws and azure. 2025-06-25 19:03:10 +02:00
pabloFuente b913dbb3b8 openvidu-testapp: updated dependencies 2025-06-25 13:26:42 +02:00
pabloFuente f16eefa9df openvidu-testapp: differ between final and non-final transcription events 2025-06-25 13:26:28 +02:00
pabloFuente 96132553ae openvidu-testapp: add lk.transcription handler 2025-06-24 17:26:38 +02:00
Carlos Santos 709779b7fd ov-components: Remove debugger statement from setRecordingStarted method in RecordingService 2025-06-24 16:18:18 +02:00
cruizba 11ac3d32eb openvidu-deployment: Fix wrong image 2025-06-24 11:34:54 +02:00
cruizba c8bbcfed56 openvidu-deployment: Add OPENVIDU_AGENT_SPEECH_PROCESSING_IMAGE environment variable to installation scripts 2025-06-24 11:33:21 +02:00
Piwccle 76a6f6c301 openvidu-deployment: azure - Add support for additional install flags in all the deployments 2025-06-18 16:26:50 +02:00
Carlos Santos 2de2920acf ov-components: Add check for existing Material Icons link in VideoconferenceComponent 2025-06-18 10:12:39 +02:00
cruizba fd8be9f23f openvidu-deployment: - AWS HA - Add experimental TURN TLS 2025-06-14 02:02:29 +02:00
cruizba e66e5a23e1 openvidu-deployment: - HA - Open port 5349 in media nodes for master nodes if Turn Domain is not configured 2025-06-13 22:02:13 +02:00
cruizba 335fd8e3c3 Remove unnecessary condition for SecurityGroupIngress in CloudFormation templates 2025-06-13 21:37:39 +02:00
cruizba 237ebe1d59 openvidu-deployment: Add "Additional Installer Flag" 2025-06-13 19:20:25 +02:00
cruizba 5e5e404d7d openvidu-deployment: Remove non official Cloudformation 2025-06-13 19:20:25 +02:00
Carlos Santos 16e869c5da ov-components: update API directive documentation 2025-06-11 12:50:08 +02:00
Carlos Santos c628b2ab68 ov-components: enhance directive table generation logic and improve error handling 2025-06-11 12:49:41 +02:00
Carlos Santos b7d9f822de ov-components: apply background from storage based on background effects button status 2025-06-10 17:28:00 +02:00
Piwccle fe606cbf15 openvidu-deployment: azure - added cluster data container for caddy and observability info 2025-06-10 16:30:41 +02:00
Piwccle 7c71f41d95 openvidu-deployment: azure - removed one '}' that was making TURN HA fail 2025-06-10 12:48:12 +02:00
Piwccle 9728d966ad openvidu-deployment: azure - bugfix: added dependsOn to remove possible race condition 2025-06-10 12:17:10 +02:00
Piwccle ce610cab03 openvidu-deployment: azure - removed parameter from HA CUID 2025-06-10 11:46:42 +02:00
Piwccle d8f14c6905 openvidu-deployment: azure - changes to let TURN work in HA deployment 2025-06-10 11:34:57 +02:00
Piwccle bdf4f07a28 openvidu-deployment: azure - removed one line of the install script that was there to try the new delete_media_node 2025-06-10 11:08:55 +02:00
Piwccle 206a51baf7 openvidu-deployment: azure - added scripts to delete media node if fails 2025-06-10 10:56:50 +02:00
Carlos Santos 94b51b9971 ov-components: Update language files to replace "session" with "room" for consistency across translations 2025-06-09 11:21:35 +02:00
github-actions 8984cfdcad openvidu-components-angular: Bumped version to 3.2.1 2025-06-05 11:01:42 +00:00
Carlos Santos 022d18578e ov-components: Remove unnecessary mat-line directive from settings panel options 2025-06-05 12:49:53 +02:00
GitHub Actions 9beb8b435a Revert "Bump version to 3.2.0"
This reverts commit 26b790bfd8.
2025-06-04 15:01:06 +00:00
GitHub Actions 26b790bfd8 Bump version to 3.2.0 2025-06-04 15:01:02 +00:00
github-actions 308315dee4 openvidu-components-angular: Bumped version to 3.2.0 2025-06-04 14:50:58 +00:00
cruizba 5fac396487 Update submodule openvidu-livekit for 3.2.0 2025-06-04 16:34:41 +02:00
Piwccle 99e36178a5 openvidu-deployment: builded .bicep 2025-06-04 13:29:32 +02:00
Piwccle 8999076291 openvidu-deployment: azure - HA fixed turn 2025-06-04 13:29:32 +02:00
cruizba 220c6d7d4a openvidu-deployment: Bump Redis server image version to 7.4.4. 2025-06-02 20:41:24 +02:00
cruizba f989dc26d2 openvidu-deployment: Force minimum docker and docker-compose version in update script. 2025-06-02 17:54:33 +02:00
cruizba 3107d504ce openvidu-deployment: Force minimum docker and docker-compose version. 2025-06-02 17:47:46 +02:00
cruizba b8c0bb3283 openvidu-deployment: azure - update signal for stopping media node services to SIGQUIT 2025-06-02 17:00:49 +02:00
Piwccle 4aa8739ee8 openvidu-deployment: compiled all .bicep and fixed roles 2025-05-29 17:43:37 +02:00
Piwccle bcfcfdd704 openvidu-deployment: added roles in HA to configure blob storage 2025-05-29 17:40:53 +02:00
Piwccle a59872c9e9 openvidu-deployment: fixed common arguments for openvidu single node pro 2025-05-29 17:33:37 +02:00
Piwccle cbf4bafa36 openvidu-deployment: added more validation in CUID 2025-05-29 17:21:47 +02:00
Piwccle 95584d7437 openvidu-deployment: CUID for azure deployments modified to work better 2025-05-29 17:17:59 +02:00
cruizba 20e348ed60 Revert redis to 7.4.3-alpine 2025-05-29 17:11:18 +02:00
Piwccle fbcc460f10 openvidu-deployment: fixed ha config_blobstorage in all master nodes 2025-05-29 16:51:59 +02:00
Piwccle e7e1a39e05 openvidu-deployment: added Openvidu Single Node PRO and added v2compatibility modules in HA and Elastic deployments 2025-05-29 15:58:41 +02:00
pabloFuente 18fd65350c openvidu-integration-tests: update start-openvidu-local-deployment action 2025-05-29 14:43:41 +02:00
cruizba 1551bf8244 openvidu-deployment: Bump docker image versions for 3.2.0 2025-05-28 23:39:14 +02:00
pabloFuente 882f9405db openvidu-test-e2e: put RTSP container in network mode host 2025-05-28 12:51:37 +02:00
Piwccle af3608c81c openvidu-deployment: azure - added optional automationAccountName 2025-05-27 11:18:05 +02:00
Carlos Santos 395d0c3ade ov-components: fix name getter in ParticipantModel to return participant's name 2025-05-26 19:09:19 +02:00
Carlos Santos f0bcb3ac03 ov-components:remove unnecessary screenshot logging in API directive tests 2025-05-26 18:38:25 +02:00
Carlos Santos 3579b9dc6a ov-components: update URL handling in API directive tests and improve error logging in leaveRoom method 2025-05-26 18:31:51 +02:00
Carlos Santos dbb45eaffd ov-components: improve leaveRoom method by adding fallback navigation on error 2025-05-26 18:24:14 +02:00
Carlos Santos fc7d7fef5a ov-components: replace fit with it for consistency in API directive tests 2025-05-26 17:52:55 +02:00
Carlos Santos 3f234276ee ov-components: enhance leaveRoom method with error handling and verification steps 2025-05-26 17:46:54 +02:00
GitHub Actions b10bc5f8c6 Revert "Bump version to 3.2.0-dev1"
This reverts commit a9e727fd51.
2025-05-26 14:34:37 +00:00
GitHub Actions a9e727fd51 Bump version to 3.2.0-dev1 2025-05-26 14:34:34 +00:00
cruizba c9b350eddc openvidu-deployment: Add single node PRO. Update,sh changes 2025-05-26 13:39:18 +02:00
Carlos Santos 21c297c73b ov-components: ensure room is left after each test in attribute and event directives 2025-05-26 10:33:17 +02:00
Carlos Santos 7df7b31d97 ov-components: add screenshot logging and delay in API directives tests 2025-05-23 18:13:35 +02:00
Carlos Santos d8d4870eb7 ov-components: add screenshot logging for video presence check 2025-05-23 17:18:02 +02:00
Carlos Santos aa15e8b713 ov-components: add delay before checking session presence in audio disabled test 2025-05-23 15:48:15 +02:00
Carlos Santos 621df40a7f ci: update cleanup action references in workflows to use official OpenVidu action 2025-05-23 15:37:13 +02:00
Carlos Santos 147f7078a0 ci: update action for building and serving openvidu-components-angular Testapp to use the latest version 2025-05-23 15:19:48 +02:00
Carlos Santos 55a67f191a ci: replace custom action for building and serving openvidu-components-angular Testapp with official OpenVidu action 2025-05-23 15:17:50 +02:00
Piwccle 31587d7181 openvidu-deployment: azure - added 2 'none' values missing in HA .bicep 2025-05-23 14:32:27 +02:00
Carlos Santos d0f714a5e2 ci: replace setup action for OpenVidu Call Backend with official action 2025-05-23 13:58:20 +02:00
Carlos Santos e4ed447009 ci: remove setup action for OpenVidu local deployment 2025-05-23 13:58:20 +02:00
Piwccle 57ea002953 openvidu-deployment: azure - compiled all new .bicep 2025-05-23 13:53:24 +02:00
Carlos Santos 1b5a230132 ci: uses OpenVidu local deployment action in integration tests 2025-05-23 13:29:06 +02:00
Carlos Santos 7348402364 ci: update openvidu-local-deployment action to use the official OpenVidu action 2025-05-23 13:27:22 +02:00
Piwccle 915a4598b1 openvidu-deployment: azure - CUID for elastic and ha deployments and adjustments to those .bicep to support the new CUID 2025-05-23 13:19:59 +02:00
Carlos Santos 0b017b2abb ci: update workflow to use OpenVidu action for local deployment setup 2025-05-23 12:55:36 +02:00
Carlos Santos c12dbf247c ov-components: refactor participant left and room disconnected handling to use a common disconnect method 2025-05-23 12:02:29 +02:00
pabloFuente f087c172fc openvidu-testapp: remove bold font weight from buttons 2025-05-23 11:49:25 +02:00
pabloFuente fe3e99e4e1 Find out RTSP container IP instead of using host.docker.internal 2025-05-23 11:49:25 +02:00
Carlos Santos 442b99771a ov-components: enhance error handling and add screenshot capture during room leave process 2025-05-23 11:41:56 +02:00
Carlos Santos 5f9fb06c2a ov-components: add error logging and screenshot capture on room leave failure 2025-05-23 11:35:21 +02:00
Carlos Santos dfae82d4cf ov-components: add event handling for participant left and update config state 2025-05-23 11:24:45 +02:00
Carlos Santos 549a822491 ov-components: rename redirectOnLeaves to redirectToHomeOnLeaves for clarity and update related logic 2025-05-23 11:09:07 +02:00
Carlos Santos b6f242107e ov-components: adjust max-width and padding for prejoin card styling 2025-05-22 13:29:16 +02:00
Carlos Santos 9023f03cee ov-components: center progress spinner in loading container 2025-05-22 13:26:11 +02:00
Carlos Santos 500b19d1b9 Revert "ov-components: change runner for API Directives and Events E2E tests to ov-actions-runner"
This reverts commit c275381cfd.
2025-05-22 13:06:59 +02:00
Carlos Santos 421c5ea29c ov-components: add participant left event handling and navigation in testapp 2025-05-22 12:47:13 +02:00
Carlos Santos cea8ce0def ov-components: update test descriptions for structural directives to improve clarity and consistency 2025-05-22 12:46:45 +02:00
Carlos Santos daf1349e75 ov-components: fixed panel toggle logic for external panels. Fixes #854
refactor togglePanel method for improved state management and clarity
2025-05-22 12:44:06 +02:00
Carlos Santos ffbc1f5e5d ov-components: update toolbar additional panel button count in tests and template 2025-05-22 12:42:26 +02:00
pabloFuente a02e23763a openvidu-testapp: updated all dependencies 2025-05-22 12:11:38 +02:00
Carlos Santos c275381cfd ov-components: change runner for API Directives and Events E2E tests to ov-actions-runner 2025-05-22 10:54:56 +02:00
Carlos Santos 34eb27dfe3 ov-components: update onRoomDisconnected event to reference onParticipantLeft for deprecation notice 2025-05-21 16:02:52 +02:00
Carlos Santos d3702ab6ac ov-components: update audio path for CI mode in Selenium configuration 2025-05-21 14:23:05 +02:00
Carlos Santos dafba8cdee ov-components: update audio capture path to use dynamic variable based on launch mode 2025-05-21 13:46:12 +02:00
Carlos Santos 995ce8c00b ov-components: add autoplay policy and file access arguments for CI chrome configuration 2025-05-21 13:45:04 +02:00
Carlos Santos 1e15fea5e4 ov-components: enable Toolbar E2E tests by removing conditional check 2025-05-21 13:33:12 +02:00
Carlos Santos 3139437bfe ov-components: remove OpenviduWebComponent and related files 2025-05-21 13:31:26 +02:00
Carlos Santos 59112b79fa ov-components: add initial E2E tests for captions functionality 2025-05-21 13:31:18 +02:00
Carlos Santos fd486270b9 ov-components: update audio capture file in E2E tests and remove unnecessary entry from .gitignore 2025-05-21 13:24:08 +02:00
Carlos Santos 042f9b5a89 ov-components: add E2E tests for toolbar 2025-05-21 13:22:29 +02:00
Carlos Santos 28e96647fa ci: Added stream tests 2025-05-21 13:15:30 +02:00
Carlos Santos 35eacb45a1 ov-components: update E2E tests for stream functionality and adjust audio capture file 2025-05-21 13:13:56 +02:00
Carlos Santos c42108bd22 ov-components: enable Panels E2E test by removing conditional check 2025-05-21 12:45:48 +02:00
Carlos Santos 63588148a7 ci: update screen sharing test command 2025-05-21 12:43:13 +02:00
Carlos Santos 52d253a1db ov-components: add E2E tests for screensharing features 2025-05-21 12:41:34 +02:00
Carlos Santos bf7d83134a ov-components: add panels E2E test suite 2025-05-21 12:11:01 +02:00
Carlos Santos 77bba7e587 ov-components: uncomment screen track replacement test 2025-05-21 12:06:36 +02:00
Carlos Santos fb999df526 ov-components: comment out headless mode in chrome arguments for local testing 2025-05-21 11:55:44 +02:00
Carlos Santos 314a3d1898 ov-components: change fit to it for video disabled prejoin page test 2025-05-21 11:55:31 +02:00
Carlos Santos 35f1085870 ov-components: remove obsolete events E2E tests 2025-05-21 11:55:10 +02:00
Carlos Santos cfdaabfc0b ov-components: remove unit tests and update media devices E2E tests 2025-05-21 11:54:36 +02:00
Carlos Santos 146aeed893 ov-components: assign participant name to observable on ready to join 2025-05-21 11:07:57 +02:00
Carlos Santos 02b594e405 ov-components: Added events e2e tests 2025-05-20 18:55:03 +02:00
Piwccle bd3504b818 openvidu-deployment: azure - fixed one thing left to be able to try the new .bicep 2025-05-20 18:49:39 +02:00
Piwccle 743b05911c openvidu-deployment: azure - fixing .bicep file to work with the new CUID 2025-05-20 18:46:31 +02:00
Piwccle 1b2af77196 openvidu-deployment: azure - CUID for Single Node Deployment ready 2025-05-20 18:29:22 +02:00
Carlos Santos 1f13b11f88 ov-components: enable Chat E2E tests in workflow 2025-05-20 17:38:44 +02:00
Carlos Santos 1cba6761bb ov-components: update E2E test names and add chat feature tests 2025-05-20 17:38:06 +02:00
Carlos Santos 920bba1bf3 ov-components: update test to focus on VIDEO DISABLED scenario in prejoin page 2025-05-20 17:33:09 +02:00
Piwccle 1d3cba9517 openvidu-deployment: azure - fixing SSH key parameter in Single Node CUID 2025-05-20 17:15:47 +02:00
Carlos Santos 331a102a19 ov-components: add end-to-end tests for structural directives in OpenVidu Components 2025-05-20 16:56:19 +02:00
Carlos Santos 6fe461dc0c ov-components: refactor workflow jobs for clarity and organization 2025-05-20 16:51:23 +02:00
Carlos Santos baf3da51c2 ov-components: add end-to-end tests for attribute and structural directives 2025-05-20 16:51:23 +02:00
Carlos Santos 48084544ba ov-components: remove debug console logs for query parameters and participant name in testapp 2025-05-20 16:51:23 +02:00
Piwccle 6a02f96af2 openvidu-deployment: azure - added UI definition to Single Node 2025-05-20 13:58:37 +02:00
Carlos Santos 3c72bc6c4d ov-components: update disconnect logic to handle client-initiated events in OpenViduService 2025-05-20 13:17:00 +02:00
Carlos Santos 3027ab6c5b ov-components: update parameter name for audio detection display in testapp 2025-05-20 12:51:19 +02:00
Carlos Santos ffc6bb4c41 ov-components: streamline E2E test setup by removing redundant steps and renaming jobs 2025-05-20 12:44:02 +02:00
Carlos Santos 5df42d5344 ov-components: refactor participant name retrieval logic in VideoconferenceComponent 2025-05-20 12:40:22 +02:00
Carlos Santos 4ad8c52c38 ov-components: enhance testapp with dynamic configuration options 2025-05-20 12:40:04 +02:00
Carlos Santos 741954ac2b OV-COMPONENTS: upgrade chromedriver to 136.0.2 and selenium-webdriver to 4.32.0 2025-05-20 12:39:19 +02:00
Carlos Santos 75d6576603 ov-components: add end-to-end tests for API directives and implement leaveRoom utility function 2025-05-20 12:38:20 +02:00
Carlos Santos 8a9e9e0d27 ov-components: fixed race condition with onTokenRequested event and participantName 2025-05-19 19:10:40 +02:00
Carlos Santos 1065f32a3a ov-components: add action for building and serving openvidu-components-angular Testapp 2025-05-19 18:08:49 +02:00
Carlos Santos 223c7473e5 ov-components: add setup action for OpenVidu Call Backend in workflows 2025-05-19 18:03:26 +02:00
Carlos Santos f951eddfe5 ov-components: add cleanup action to workflows for better resource management 2025-05-19 17:55:09 +02:00
Carlos Santos 8dfb513bb0 ov-components: add setup action for local deployment and streamline workflow steps 2025-05-19 17:52:29 +02:00
Carlos Santos ab7a453507 ov-components: remove unused active button styles and clean up SCSS variables 2025-05-19 17:08:50 +02:00
Carlos Santos 5b112286d9 ov-components: add material symbols support and improve toolbar symbols handling 2025-05-19 16:59:24 +02:00
Carlos Santos c4c5fb9866 ov-components: improve logging for participant disconnection event 2025-05-14 17:50:49 +02:00
Carlos Santos bd711b57f3 ov-components: simplify disconnect logic by removing callback. The participantLeft event will be fired by LK event handler 2025-05-14 13:48:53 +02:00
Piwccle 94f2e3e573 openvidu-deployment: azure - changed one description of a param to clarify the use of the param 2025-05-13 14:36:12 +02:00
Piwccle b7a94f4b03 openvidu-deployment: azure - compiled .bicep into .json 2025-05-13 11:59:15 +02:00
Piwccle 21b94bc4fd Merge branch 'master' of https://github.com/OpenVidu/openvidu 2025-05-13 11:55:40 +02:00
Carlos Santos 745e04baa6 ov-components: enhance recording URL handling for backward compatibility 2025-05-13 11:35:04 +02:00
Carlos Santos 4b611893e9 ov-components: enhance recording URL handling for backward compatibility 2025-05-13 11:14:40 +02:00
Piwccle b8fc003a4c openvidu-deployment: azure - added existing storage account support in CE, Elastic and HA deployments 2025-05-13 11:01:22 +02:00
Carlos Santos ed579b4e72 ov-components: update recording stream URL to use '/media' segment instead of '/stream' 2025-05-12 16:00:42 +02:00
Carlos Santos d8ce736cc0 ov-components: improve disconnect handling and manage client-initiated disconnect events 2025-05-12 13:39:45 +02:00
Carlos Santos cb8f11449f ov-components: add UNKNOWN_DISCONNECT message for various languages and handle participant disconnection reason 2025-05-06 15:43:56 +02:00
Piwccle efb691f481 openvidu-deployment: added config script of azure blob storage in elastic deployment and added a rol assignment in CE needed 2025-05-06 09:03:55 +02:00
Carlos Santos a8c2459d5f ov-components: implement OnPush change detection strategy and optimize change detection calls in prejoin component 2025-05-05 14:35:51 +02:00
Carlos Santos 0074b28d3a ov-components: Fix available langs for allowing custom ones 2025-05-05 13:52:10 +02:00
Carlos Santos 760cf604eb ov-components: update package dependencies and TypeScript configurations
- Added @types/dom-mediacapture-transform to package.json for type definitions.
- Changed hoveringTimeout type in StreamComponent to ReturnType<typeof setTimeout> for better type safety.
- Updated TypeScript lib version from ES2020 to ES2021 in tsconfig files for improved features.
- Included dom-mediacapture-transform in types for TypeScript configurations across various tsconfig files.
- Removed empty types array in tsconfig.app.json to ensure proper type checking.
- Adjusted skipLibCheck settings in tsconfig files to improve compatibility with Livekit track processors.
2025-05-05 12:15:16 +02:00
Carlos Santos 8ce155df6a ov-components: enhance recording status management and elapsed time calculation 2025-04-30 19:18:29 +02:00
Carlos Santos cff617b0c3 ov-components: disable webcomponent E2E tests 2025-04-30 18:12:36 +02:00
Carlos Santos 127fbbd4e1 ov-components: update build process and module definition for improved structure 2025-04-30 17:58:11 +02:00
Carlos Santos 3adfa91c54 fix(tsconfig): add skipLibCheck comment for Livekit track processors 2025-04-30 15:22:18 +02:00
Carlos Santos 7f00318cbb ov-components: update dependencies and TypeScript configuration
- Updated @livekit/track-processors from 0.3.2 to ^0.5.6
- Updated livekit-client from 2.5.2 to 2.11.4
- Changed TypeScript lib option from "dom" to "DOM" in tsconfig.lib.json
- Added skipLibCheck option in tsconfig.lib.json and tsconfig.base.json
- Updated TypeScript lib option from "dom" to "dom" in tsconfig.base.json
2025-04-30 14:57:47 +02:00
Carlos Santos 5433f516a9 ov-components: remove redundant background processor removal logic in virtual background service 2025-04-30 14:54:26 +02:00
Carlos Santos 81fcae2d4e ov-components: enhance virtual background service updating the switching logic for improving performance 2025-04-30 14:38:43 +02:00
Carlos Santos 0c1e1a7134 ov-components: Updated to Angular 19 2025-04-30 14:37:10 +02:00
Carlos Santos 518e1511d2 ov-components: add participant left handling and update room disconnection logic 2025-04-30 12:13:03 +02:00
Carlos Santos b92630ecd8 ov-components: enhance participant disconnection handling with reasons and refactor disconnect logic 2025-04-30 12:13:03 +02:00
Carlos Santos 11137a2a8f ov-components: add disconnect and error messages for various scenarios in multiple language files 2025-04-30 12:13:03 +02:00
Piwccle 9310ff3ea1 openvidu-deployment: azure - added blob storage resources and modified CE to support config blobStorage 2025-04-23 13:26:33 +02:00
Piwccle 37428c91b6 openvidu-deployment: azure - changed links that references de runbook 2025-04-22 12:06:04 +02:00
cruizba 9e04d59b61 Add openvidu-deployment scripts 2025-04-22 11:46:24 +02:00
Micael Gallego c1f2971881
Fix NewGenVidu project logo in README.md 2025-03-24 19:49:32 +01:00
Carlos Santos 8c28228357 ov-components: update default recording stream URL in directives and config service 2025-03-23 14:25:35 +01:00
Carlos Santos 90fd0ef44e ov-components: add recordingStreamBaseUrl directive and integrate with config service for dynamic stream URL construction 2025-03-14 19:08:34 +01:00
Carlos Santos 6137bdbbbc ov-components: add null check for participant name updates in pre-join component and participant name directive 2025-03-12 19:03:14 +01:00
Carlos Santos 2acf636842 ov-components: reduce max-width of pre-join component to improve layout 2025-03-12 18:41:42 +01:00
Carlos Santos 0ad51d6c58 ov-components: remove max-height constraint from pre-join component styles 2025-03-12 18:28:53 +01:00
Carlos Santos 6f97b9d8c2 ov-components: move loading state update to ngAfterContentChecked lifecycle hook 2025-03-12 18:28:47 +01:00
Carlos Santos a64cf1d577 ov-components: enhance pre-join component layout and add participant name visibility check 2025-03-12 18:05:37 +01:00
Carlos Santos e373d23cc9 ov-components: add participant name visibility control to pre-join component 2025-03-12 17:19:55 +01:00
Carlos Santos 72888e4ea9 ov-components: add camera and microphone button visibility controls in the E2E tests 2025-03-10 10:27:44 +01:00
Carlos Santos 2bf212ccfe ov-components: add camera and microphone button visibility controls in toolbar and settings panel 2025-03-10 10:11:38 +01:00
Carlos Santos 5fba250b1d ov-components: remove ServiceConfigService and update components to use LayoutService directly 2025-03-08 17:42:43 +01:00
Carlos Santos 7315360fbc ov-components: add onParticipantLeft event and disconnect callback to notify when local participant leaves 2025-03-07 19:25:27 +01:00
Carlos Santos d965e72822 ov-components: add OpenViduComponentsUiModule with component and pipe declarations 2025-03-07 18:52:12 +01:00
Carlos Santos 288a585809 ov-components: rename participantId to participantName in ParticipantLeftEvent and update disconnect method 2025-03-07 13:52:43 +01:00
Carlos Santos 272eb9002c ov-components: rename participant created event to participant connected and update documentation 2025-03-07 13:42:50 +01:00
Carlos Santos dee470609c ov-components: clarify event documentation for local participant actions 2025-03-07 13:36:10 +01:00
Carlos Santos ad8f368a91 ov-components: enhance documentation for room and participant events 2025-03-07 13:30:26 +01:00
Carlos Santos 5d855a1338 ov-components: log room name when a room is created 2025-03-07 13:04:22 +01:00
Carlos Santos 3defad20cc ov-components: emit room created event after participant connection 2025-03-07 13:04:06 +01:00
Carlos Santos 9b4f330c4a ov-components: clarify method documentation for local participant connection 2025-03-06 13:45:02 +01:00
pabloFuente 7d10ec6097 openvidu-testapp: fix webpack builder with custom configuration 2025-02-19 12:38:57 +01:00
pabloFuente 8d443ac506 openvidu-testapp: update rest of dependencies 2025-02-19 12:13:37 +01:00
pabloFuente 1521a766e5 openvidu-testapp: update minor upgrade change 2025-02-19 12:09:43 +01:00
pabloFuente a1edded5df openvidu-testapp: update to Angular Material 19 2025-02-19 12:04:21 +01:00
pabloFuente fa507d492f openvidu-testapp: update to Angular 19 2025-02-19 12:03:43 +01:00
pabloFuente 0ae67e8a87 openvidu-testapp: update Angular Material 18 2025-02-19 12:01:54 +01:00
pabloFuente 051b14c5b2 openvidu-testapp: update to Angular 18 2025-02-19 11:48:56 +01:00
pabloFuente f610f37ac3 openvidu-testapp: update pollyfils.ts 2025-02-19 11:47:11 +01:00
pabloFuente 501881c602 openvidu-testapp: update to Angular Material 17 2025-02-19 11:42:14 +01:00
pabloFuente ea8f0e2101 Upgrade to Angular 17 2025-02-19 11:41:31 +01:00
207 changed files with 43526 additions and 15740 deletions

View File

@ -45,7 +45,7 @@ jobs:
https://api.github.com/repos/OpenVidu/openvidu-call/dispatches \ https://api.github.com/repos/OpenVidu/openvidu-call/dispatches \
-d '{"event_type":"openvidu-components-angular","client_payload":{"commit-message":"'"$COMMIT_MESSAGE"'","commit-ref":"'"$COMMIT_URL"'", "branch-name":"'"$BRANCH_NAME"'"}}' -d '{"event_type":"openvidu-components-angular","client_payload":{"commit-message":"'"$COMMIT_MESSAGE"'","commit-ref":"'"$COMMIT_URL"'", "branch-name":"'"$BRANCH_NAME"'"}}'
nested_components_e2e_events: nested_events:
needs: test_setup needs: test_setup
name: Nested events name: Nested events
runs-on: ubuntu-latest runs-on: ubuntu-latest
@ -58,74 +58,33 @@ jobs:
uses: actions/setup-node@v4 uses: actions/setup-node@v4
with: with:
node-version: '20' node-version: '20'
- name: Install wait-on package
run: npm install -g wait-on
# - 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
run: | uses: OpenVidu/actions/start-openvidu-local-deployment@main
git clone --depth 1 https://github.com/OpenVidu/openvidu-local-deployment - name: Start OpenVidu Call backend
cd openvidu-local-deployment/community uses: OpenVidu/actions/start-openvidu-call@main
./configure_lan_private_ip_linux.sh
docker compose up -d
- name: Run OpenVidu Call Backend
run: |
git clone --depth 1 https://github.com/OpenVidu/openvidu-call
cd openvidu-call/backend
npm install
npm run dev:start &
- name: Install dependencies - name: Install dependencies
run: | run: |
cd openvidu-components-angular cd openvidu-components-angular
npm install npm install
- name: Build openvidu-components-angular - name: Build and Serve openvidu-components-angular Testapp
run: npm run lib:build --prefix openvidu-components-angular uses: OpenVidu/actions/start-openvidu-components-testapp@main
- name: Build openvidu-components-angular Testapp
run: npm run build --prefix openvidu-components-angular
- name: Serve openvidu-components-angular Testapp
run: npm run start --prefix openvidu-components-angular &
- name: Wait for openvidu-local-deployment
run: |
until curl -s -f -o /dev/null http://localhost:7880; do
echo "Waiting for openvidu-local-deployment to be ready..."
sleep 5
done
- name: Wait for openvidu-components-angular Testapp
run: |
until curl -s -f -o /dev/null http://localhost:4200; do
echo "Waiting for openvidu-components-angular Testapp to be ready..."
sleep 5
done
- name: Run nested components E2E event tests - name: Run nested components E2E event tests
env: env:
LAUNCH_MODE: CI LAUNCH_MODE: CI
run: npm run e2e:nested-events --prefix openvidu-components-angular run: npm run e2e:nested-events --prefix openvidu-components-angular
- name: Cleanup
if: always()
uses: OpenVidu/actions/cleanup@main
unit_tests: nested_structural_directives:
name: Unit Tests
runs-on: ubuntu-latest
needs: test_setup needs: test_setup
steps: name: Nested Structural Directives
- 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 Dependencies
run: |
cd openvidu-components-angular
npm install
- name: Run Unit Tests
run: npm run lib:test --prefix openvidu-components-angular
nested_components_e2e_directives:
needs: test_setup
name: Nested directives
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout Repository - name: Checkout Repository
@ -136,53 +95,29 @@ jobs:
uses: actions/setup-node@v4 uses: actions/setup-node@v4
with: with:
node-version: '20' node-version: '20'
- name: Install wait-on package
run: npm install -g wait-on
# - 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
run: | uses: OpenVidu/actions/start-openvidu-local-deployment@main
git clone --depth 1 https://github.com/OpenVidu/openvidu-local-deployment - name: Start OpenVidu Call backend
cd openvidu-local-deployment/community uses: OpenVidu/actions/start-openvidu-call@main
./configure_lan_private_ip_linux.sh - name: Build and Serve openvidu-components-angular Testapp
docker compose up -d uses: OpenVidu/actions/start-openvidu-components-testapp@main
- name: Run OpenVidu Call Backend - name: Run nested structural directives tests
run: |
git clone --depth 1 https://github.com/OpenVidu/openvidu-call
cd openvidu-call/backend
npm install
npm run dev:start &
- name: Install dependencies
run: |
cd openvidu-components-angular
npm install
- name: Build openvidu-components-angular
run: npm run lib:build --prefix openvidu-components-angular
- name: Build openvidu-components-angular Testapp
run: npm run build --prefix openvidu-components-angular
- name: Serve openvidu-components-angular Testapp
run: npm run start --prefix openvidu-components-angular &
- name: Wait for openvidu-local-deployment
run: |
until curl -s -f -o /dev/null http://localhost:7880; do
echo "Waiting for openvidu-local-deployment to be ready..."
sleep 5
done
- name: Wait for openvidu-components-angular Testapp
run: |
until curl -s -f -o /dev/null http://localhost:4200; do
echo "Waiting for openvidu-components-angular Testapp to be ready..."
sleep 5
done
- name: Run nested components E2E directives tests
env: env:
LAUNCH_MODE: CI LAUNCH_MODE: CI
run: npm run e2e:nested-directives --prefix openvidu-components-angular run: npm run e2e:nested-structural-directives --prefix openvidu-components-angular
- name: Cleanup
if: always()
uses: OpenVidu/actions/cleanup@main
webcomponent_e2e_directives: nested_attribute_directives:
needs: test_setup needs: test_setup
name: Webcomponent directives name: Nested Attribute Directives
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout Repository - name: Checkout Repository
@ -193,439 +128,318 @@ jobs:
uses: actions/setup-node@v4 uses: actions/setup-node@v4
with: with:
node-version: '20' node-version: '20'
- name: Install wait-on package
run: npm install -g wait-on
# - 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
run: | uses: OpenVidu/actions/start-openvidu-local-deployment@main
git clone --depth 1 https://github.com/OpenVidu/openvidu-local-deployment - name: Start OpenVidu Call backend
cd openvidu-local-deployment/community uses: OpenVidu/actions/start-openvidu-call@main
./configure_lan_private_ip_linux.sh - name: Build and Serve openvidu-components-angular Testapp
docker compose up -d uses: OpenVidu/actions/start-openvidu-components-testapp@main
- name: Run OpenVidu Call Backend - name: Run nested attribute directives tests
run: | env:
git clone --depth 1 https://github.com/OpenVidu/openvidu-call LAUNCH_MODE: CI
cd openvidu-call/backend run: npm run e2e:nested-attribute-directives --prefix openvidu-components-angular
npm install - name: Cleanup
npm run dev:start & if: always()
- name: Install dependencies uses: OpenVidu/actions/cleanup@main
run: |
cd openvidu-components-angular e2e_directives:
npm install needs: test_setup
- name: Build openvidu-angular name: API Directives Tests
run: npm run lib:build --prefix openvidu-components-angular runs-on: ubuntu-latest
- name: Build openvidu-webcomponent steps:
run: npm run webcomponent:testing-build --prefix openvidu-components-angular - name: Checkout Repository
- name: Serve Webcomponent Testapp uses: actions/checkout@v4
run: npm run webcomponent:serve-testapp --prefix openvidu-components-angular & with:
- name: Wait for openvidu-local-deployment ref: ${{ inputs.commit_sha || github.sha }}
run: | - name: Setup Node.js
until curl -s -f -o /dev/null http://localhost:7880; do uses: actions/setup-node@v4
echo "Waiting for openvidu-local-deployment to be ready..." with:
sleep 5 node-version: '20'
done - name: Install wait-on package
- name: Wait for openvidu-components-angular Testapp run: npm install -g wait-on
run: | # - name: Run Browserless Chrome
until curl -s -f -o /dev/null http://localhost:8080; do # run: docker run -d -p 3000:3000 --network host browserless/chrome:1.57-chrome-stable
echo "Waiting for openvidu-components-angular Testapp to be ready..." - name: Run Chrome
sleep 5 run: docker run --network=host -d -p 4444:4444 selenium/standalone-chrome:138.0
done - 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-directives --prefix openvidu-components-angular
- name: Cleanup
if: always()
uses: OpenVidu/actions/cleanup@main
e2e_internal_directives:
needs: test_setup
name: Internal Directives Tests
runs-on: ubuntu-latest
steps:
- name: Checkout Repository
uses: actions/checkout@v4
with:
ref: ${{ inputs.commit_sha || github.sha }}
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
- name: Install wait-on package
run: npm install -g wait-on
# - name: Run Browserless Chrome
# run: docker run -d -p 3000:3000 --network host browserless/chrome:1.57-chrome-stable
- name: Run Chrome
run: docker run --network=host -d -p 4444:4444 selenium/standalone-chrome:138.0
- name: Run openvidu-local-deployment
uses: OpenVidu/actions/start-openvidu-local-deployment@main
- name: Start OpenVidu Call backend
uses: OpenVidu/actions/start-openvidu-call@main
- name: Build and Serve openvidu-components-angular Testapp
uses: OpenVidu/actions/start-openvidu-components-testapp@main
- name: Run Tests
env:
LAUNCH_MODE: CI
run: npm run e2e:lib-internal-directives --prefix openvidu-components-angular
- name: Cleanup
if: always()
uses: OpenVidu/actions/cleanup@main
e2e_chat:
needs: test_setup
name: Chat E2E
runs-on: ubuntu-latest
steps:
- name: Checkout Repository
uses: actions/checkout@v4
with:
ref: ${{ inputs.commit_sha || github.sha }}
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
- name: Install wait-on package
run: npm install -g wait-on
# - name: Run Browserless Chrome
# run: docker run -d -p 3000:3000 --network host browserless/chrome:1.57-chrome-stable
- name: Run Chrome
run: docker run --network=host -d -p 4444:4444 selenium/standalone-chrome:138.0
- name: Run openvidu-local-deployment
uses: OpenVidu/actions/start-openvidu-local-deployment@main
- name: Start OpenVidu Call backend
uses: OpenVidu/actions/start-openvidu-call@main
- name: Build and Serve openvidu-components-angular Testapp
uses: OpenVidu/actions/start-openvidu-components-testapp@main
- name: Run Tests
env:
LAUNCH_MODE: CI
run: npm run e2e:lib-chat --prefix openvidu-components-angular
- name: Cleanup
if: always()
uses: OpenVidu/actions/cleanup@main
e2e_events:
needs: test_setup
name: Events E2E
runs-on: ubuntu-latest
steps:
- name: Checkout Repository
uses: actions/checkout@v4
with:
ref: ${{ inputs.commit_sha || github.sha }}
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
- name: Install wait-on package
run: npm install -g wait-on
# - name: Run Browserless Chrome
# run: docker run -d -p 3000:3000 --network host browserless/chrome:1.57-chrome-stable
- name: Run Chrome
run: docker run --network=host -d -p 4444:4444 selenium/standalone-chrome:138.0
- name: Run openvidu-local-deployment
uses: OpenVidu/actions/start-openvidu-local-deployment@main
- name: Start OpenVidu Call backend
uses: OpenVidu/actions/start-openvidu-call@main
- name: Build and Serve openvidu-components-angular Testapp
uses: OpenVidu/actions/start-openvidu-components-testapp@main
- name: Run Tests
env:
LAUNCH_MODE: CI
run: npm run e2e:lib-events --prefix openvidu-components-angular
- name: Cleanup
if: always()
uses: OpenVidu/actions/cleanup@main
e2e_media_devices:
needs: test_setup
name: Media devices E2E
runs-on: ubuntu-latest
steps:
- name: Checkout Repository
uses: actions/checkout@v4
with:
ref: ${{ inputs.commit_sha || github.sha }}
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
- name: Install wait-on package
run: npm install -g wait-on
# - name: Run Browserless Chrome
# run: docker run -d -p 3000:3000 --network host browserless/chrome:1.57-chrome-stable
- name: Run Chrome
run: docker run --network=host -d -p 4444:4444 selenium/standalone-chrome:138.0
- name: Run openvidu-local-deployment
uses: OpenVidu/actions/start-openvidu-local-deployment@main
- name: Start OpenVidu Call backend
uses: OpenVidu/actions/start-openvidu-call@main
- name: Build and Serve openvidu-components-angular Testapp
uses: OpenVidu/actions/start-openvidu-components-testapp@main
- name: Run Tests
env:
LAUNCH_MODE: CI
run: npm run e2e:lib-media-devices --prefix openvidu-components-angular
- name: Cleanup
if: always()
uses: OpenVidu/actions/cleanup@main
e2e_panels:
needs: test_setup
name: Panels E2E
runs-on: ubuntu-latest
steps:
- name: Checkout Repository
uses: actions/checkout@v4
with:
ref: ${{ inputs.commit_sha || github.sha }}
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
- name: Install wait-on package
run: npm install -g wait-on
# - name: Run Browserless Chrome
# run: docker run -d -p 3000:3000 --network host browserless/chrome:1.57-chrome-stable
- name: Run Chrome
run: docker run --network=host -d -p 4444:4444 selenium/standalone-chrome:138.0
- name: Run openvidu-local-deployment
uses: OpenVidu/actions/start-openvidu-local-deployment@main
- name: Start OpenVidu Call backend
uses: OpenVidu/actions/start-openvidu-call@main
- name: Build and Serve openvidu-components-angular Testapp
uses: OpenVidu/actions/start-openvidu-components-testapp@main
- name: Run Tests
env:
LAUNCH_MODE: CI
run: npm run e2e:lib-panels --prefix openvidu-components-angular
- name: Cleanup
if: always()
uses: OpenVidu/actions/cleanup@main
e2e_screen_sharing:
needs: test_setup
name: Screen sharing E2E
runs-on: ubuntu-latest
steps:
- name: Checkout Repository
uses: actions/checkout@v4
with:
ref: ${{ inputs.commit_sha || github.sha }}
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
- name: Install wait-on package
run: npm install -g wait-on
# - name: Run Browserless Chrome
# run: docker run -d -p 3000:3000 --network host browserless/chrome:1.57-chrome-stable
- name: Run Chrome
run: docker run --network=host -d -p 4444:4444 selenium/standalone-chrome:138.0
- name: Run openvidu-local-deployment
uses: OpenVidu/actions/start-openvidu-local-deployment@main
- name: Start OpenVidu Call backend
uses: OpenVidu/actions/start-openvidu-call@main
- name: Build and Serve openvidu-components-angular Testapp
uses: OpenVidu/actions/start-openvidu-components-testapp@main
- name: Run Tests
env:
LAUNCH_MODE: CI
run: npm run e2e:lib-screensharing --prefix openvidu-components-angular
- name: Cleanup
if: always()
uses: OpenVidu/actions/cleanup@main
e2e_stream:
needs: test_setup
name: Stream E2E
runs-on: ubuntu-latest
steps:
- name: Checkout Repository
uses: actions/checkout@v4
with:
ref: ${{ inputs.commit_sha || github.sha }}
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
- name: Install wait-on package
run: npm install -g wait-on
# - name: Run Browserless Chrome
# run: docker run -d -p 3000:3000 --network host browserless/chrome:1.57-chrome-stable
- name: Run Chrome
run: docker run --network=host -d -v $(pwd)/openvidu-components-angular/e2e/assets:/e2e-assets selenium/standalone-chrome:138.0
- name: Run openvidu-local-deployment
uses: OpenVidu/actions/start-openvidu-local-deployment@main
- name: Start OpenVidu Call backend
uses: OpenVidu/actions/start-openvidu-call@main
- name: Build and Serve openvidu-components-angular Testapp
uses: OpenVidu/actions/start-openvidu-components-testapp@main
- name: Run Tests
env:
LAUNCH_MODE: CI
run: npm run e2e:lib-stream --prefix openvidu-components-angular
- name: Cleanup
if: always()
uses: OpenVidu/actions/cleanup@main
e2e_toolbar:
needs: test_setup
name: Toolbar E2E
runs-on: ubuntu-latest
steps:
- name: Checkout Repository
uses: actions/checkout@v4
with:
ref: ${{ inputs.commit_sha || github.sha }}
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
- name: Install wait-on package
run: npm install -g wait-on
# - name: Run Browserless Chrome
# run: docker run -d -p 3000:3000 --network host browserless/chrome:1.57-chrome-stable
- name: Run Chrome
run: docker run --network=host -d -p 4444:4444 selenium/standalone-chrome:138.0
- name: Run openvidu-local-deployment
uses: OpenVidu/actions/start-openvidu-local-deployment@main
- name: Start OpenVidu Call backend
uses: OpenVidu/actions/start-openvidu-call@main
- name: Build and Serve openvidu-components-angular Testapp
uses: OpenVidu/actions/start-openvidu-components-testapp@main
- name: Run Webcomponent E2E - name: Run Webcomponent E2E
env: env:
LAUNCH_MODE: CI LAUNCH_MODE: CI
run: npm run e2e:webcomponent-directives --prefix openvidu-components-angular run: npm run e2e:lib-toolbar --prefix openvidu-components-angular
- name: Cleanup
webcomponent_e2e_chat: if: always()
needs: test_setup uses: OpenVidu/actions/cleanup@main
name: Webcomponent chat
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: Run Browserless Chrome
# run: docker run -d -p 3000:3000 --network host browserless/chrome:1.57-chrome-stable
- name: Run Chrome
run: docker run --network=host -d -p 4444:4444 selenium/standalone-chrome:127.0
- name: Run openvidu-local-deployment
run: |
git clone --depth 1 https://github.com/OpenVidu/openvidu-local-deployment
cd openvidu-local-deployment/community
./configure_lan_private_ip_linux.sh
docker compose up -d
- name: Run OpenVidu Call Backend
run: |
git clone --depth 1 https://github.com/OpenVidu/openvidu-call
cd openvidu-call/backend
npm install
npm run dev:start &
- name: Install dependencies
run: |
cd openvidu-components-angular
npm install
- name: Build openvidu-angular
run: npm run lib:build --prefix openvidu-components-angular
- name: Build openvidu-webcomponent
run: npm run webcomponent:testing-build --prefix openvidu-components-angular
- name: Serve Webcomponent Testapp
run: npm run webcomponent:serve-testapp --prefix openvidu-components-angular &
- name: Wait for openvidu-local-deployment
run: |
until curl -s -f -o /dev/null http://localhost:7880; do
echo "Waiting for openvidu-local-deployment to be ready..."
sleep 5
done
- name: Wait for openvidu-components-angular Testapp
run: |
until curl -s -f -o /dev/null http://localhost:8080; do
echo "Waiting for openvidu-components-angular Testapp to be ready..."
sleep 5
done
- name: Run Webcomponent E2E
env:
LAUNCH_MODE: CI
run: npm run e2e:webcomponent-chat --prefix openvidu-components-angular
webcomponent_e2e_events:
needs: test_setup
name: Webcomponent events
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: Run Browserless Chrome
# run: docker run -d -p 3000:3000 --network host browserless/chrome:1.57-chrome-stable
- name: Run Chrome
run: docker run --network=host -d -p 4444:4444 selenium/standalone-chrome:127.0
- name: Run openvidu-local-deployment
run: |
git clone --depth 1 https://github.com/OpenVidu/openvidu-local-deployment
cd openvidu-local-deployment/community
./configure_lan_private_ip_linux.sh
docker compose up -d
- name: Run OpenVidu Call Backend
run: |
git clone --depth 1 https://github.com/OpenVidu/openvidu-call
cd openvidu-call/backend
npm install
npm run dev:start &
- name: Install dependencies
run: |
cd openvidu-components-angular
npm install
- name: Build openvidu-angular
run: npm run lib:build --prefix openvidu-components-angular
- name: Build openvidu-webcomponent
run: npm run webcomponent:testing-build --prefix openvidu-components-angular
- name: Serve Webcomponent Testapp
run: npm run webcomponent:serve-testapp --prefix openvidu-components-angular &
- name: Wait for openvidu-local-deployment
run: |
until curl -s -f -o /dev/null http://localhost:7880; do
echo "Waiting for openvidu-local-deployment to be ready..."
sleep 5
done
- name: Wait for openvidu-components-angular Testapp
run: |
until curl -s -f -o /dev/null http://localhost:8080; do
echo "Waiting for openvidu-components-angular Testapp to be ready..."
sleep 5
done
- name: Run Webcomponent E2E
env:
LAUNCH_MODE: CI
run: npm run e2e:webcomponent-events --prefix openvidu-components-angular
webcomponent_e2e_media_devices:
needs: test_setup
name: Webcomponent media devices
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: Run Browserless Chrome
# run: docker run -d -p 3000:3000 --network host browserless/chrome:1.57-chrome-stable
- name: Run Chrome
run: docker run --network=host -d -p 4444:4444 selenium/standalone-chrome:127.0
- name: Run openvidu-local-deployment
run: |
git clone --depth 1 https://github.com/OpenVidu/openvidu-local-deployment
cd openvidu-local-deployment/community
./configure_lan_private_ip_linux.sh
docker compose up -d
- name: Run OpenVidu Call Backend
run: |
git clone --depth 1 https://github.com/OpenVidu/openvidu-call
cd openvidu-call/backend
npm install
npm run dev:start &
- name: Install dependencies
run: |
cd openvidu-components-angular
npm install
- name: Build openvidu-angular
run: npm run lib:build --prefix openvidu-components-angular
- name: Build openvidu-webcomponent
run: npm run webcomponent:testing-build --prefix openvidu-components-angular
- name: Serve Webcomponent Testapp
run: npm run webcomponent:serve-testapp --prefix openvidu-components-angular &
- name: Wait for openvidu-local-deployment
run: |
until curl -s -f -o /dev/null http://localhost:7880; do
echo "Waiting for openvidu-local-deployment to be ready..."
sleep 5
done
- name: Wait for openvidu-components-angular Testapp
run: |
until curl -s -f -o /dev/null http://localhost:8080; do
echo "Waiting for openvidu-components-angular Testapp to be ready..."
sleep 5
done
- name: Run Webcomponent E2E
env:
LAUNCH_MODE: CI
run: npm run e2e:webcomponent-media-devices --prefix openvidu-components-angular
webcomponent_e2e_panels:
needs: test_setup
name: Webcomponent panels
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: Run Browserless Chrome
# run: docker run -d -p 3000:3000 --network host browserless/chrome:1.57-chrome-stable
- name: Run Chrome
run: docker run --network=host -d -p 4444:4444 selenium/standalone-chrome:127.0
- name: Run openvidu-local-deployment
run: |
git clone --depth 1 https://github.com/OpenVidu/openvidu-local-deployment
cd openvidu-local-deployment/community
./configure_lan_private_ip_linux.sh
docker compose up -d
- name: Run OpenVidu Call Backend
run: |
git clone --depth 1 https://github.com/OpenVidu/openvidu-call
cd openvidu-call/backend
npm install
npm run dev:start &
- name: Install dependencies
run: |
cd openvidu-components-angular
npm install
- name: Build openvidu-angular
run: npm run lib:build --prefix openvidu-components-angular
- name: Build openvidu-webcomponent
run: npm run webcomponent:testing-build --prefix openvidu-components-angular
- name: Serve Webcomponent Testapp
run: npm run webcomponent:serve-testapp --prefix openvidu-components-angular &
- name: Wait for openvidu-local-deployment
run: |
until curl -s -f -o /dev/null http://localhost:7880; do
echo "Waiting for openvidu-local-deployment to be ready..."
sleep 5
done
- name: Wait for openvidu-components-angular Testapp
run: |
until curl -s -f -o /dev/null http://localhost:8080; do
echo "Waiting for openvidu-components-angular Testapp to be ready..."
sleep 5
done
- name: Run Webcomponent E2E
env:
LAUNCH_MODE: CI
run: npm run e2e:webcomponent-panels --prefix openvidu-components-angular
webcomponent_e2e_screen_sharing:
needs: test_setup
name: Webcomponent screen sharing
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: Run Browserless Chrome
# run: docker run -d -p 3000:3000 --network host browserless/chrome:1.57-chrome-stable
- name: Run Chrome
run: docker run --network=host -d -p 4444:4444 selenium/standalone-chrome:127.0
- name: Run openvidu-local-deployment
run: |
git clone --depth 1 https://github.com/OpenVidu/openvidu-local-deployment
cd openvidu-local-deployment/community
./configure_lan_private_ip_linux.sh
docker compose up -d
- name: Run OpenVidu Call Backend
run: |
git clone --depth 1 https://github.com/OpenVidu/openvidu-call
cd openvidu-call/backend
npm install
npm run dev:start &
- name: Install dependencies
run: |
cd openvidu-components-angular
npm install
- name: Build openvidu-angular
run: npm run lib:build --prefix openvidu-components-angular
- name: Build openvidu-webcomponent
run: npm run webcomponent:testing-build --prefix openvidu-components-angular
- name: Serve Webcomponent Testapp
run: npm run webcomponent:serve-testapp --prefix openvidu-components-angular &
- name: Wait for openvidu-local-deployment
run: |
until curl -s -f -o /dev/null http://localhost:7880; do
echo "Waiting for openvidu-local-deployment to be ready..."
sleep 5
done
- name: Wait for openvidu-components-angular Testapp
run: |
until curl -s -f -o /dev/null http://localhost:8080; do
echo "Waiting for openvidu-components-angular Testapp to be ready..."
sleep 5
done
- name: Run Webcomponent E2E
env:
LAUNCH_MODE: CI
run: npm run e2e:webcomponent-screensharing --prefix openvidu-components-angular
webcomponent_e2e_stream:
needs: test_setup
name: Webcomponent stream
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: Run Browserless Chrome
# run: docker run -d -p 3000:3000 --network host browserless/chrome:1.57-chrome-stable
- name: Run Chrome
run: docker run --network=host -d -p 4444:4444 selenium/standalone-chrome:127.0
- name: Run openvidu-local-deployment
run: |
git clone --depth 1 https://github.com/OpenVidu/openvidu-local-deployment
cd openvidu-local-deployment/community
./configure_lan_private_ip_linux.sh
docker compose up -d
- name: Run OpenVidu Call Backend
run: |
git clone --depth 1 https://github.com/OpenVidu/openvidu-call
cd openvidu-call/backend
npm install
npm run dev:start &
- name: Install dependencies
run: |
cd openvidu-components-angular
npm install
- name: Build openvidu-angular
run: npm run lib:build --prefix openvidu-components-angular
- name: Build openvidu-webcomponent
run: npm run webcomponent:testing-build --prefix openvidu-components-angular
- name: Serve Webcomponent Testapp
run: npm run webcomponent:serve-testapp --prefix openvidu-components-angular &
- name: Wait for openvidu-local-deployment
run: |
until curl -s -f -o /dev/null http://localhost:7880; do
echo "Waiting for openvidu-local-deployment to be ready..."
sleep 5
done
- name: Wait for openvidu-components-angular Testapp
run: |
until curl -s -f -o /dev/null http://localhost:8080; do
echo "Waiting for openvidu-components-angular Testapp to be ready..."
sleep 5
done
- name: Run Webcomponent E2E
env:
LAUNCH_MODE: CI
run: npm run e2e:webcomponent-stream --prefix openvidu-components-angular
webcomponent_e2e_toolbar:
needs: test_setup
name: Webcomponent toolbar
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: Run Browserless Chrome
# run: docker run -d -p 3000:3000 --network host browserless/chrome:1.57-chrome-stable
- name: Run Chrome
run: docker run --network=host -d -p 4444:4444 selenium/standalone-chrome:127.0
- name: Run openvidu-local-deployment
run: |
git clone --depth 1 https://github.com/OpenVidu/openvidu-local-deployment
cd openvidu-local-deployment/community
./configure_lan_private_ip_linux.sh
docker compose up -d
- name: Run OpenVidu Call Backend
run: |
git clone --depth 1 https://github.com/OpenVidu/openvidu-call
cd openvidu-call/backend
npm install
npm run dev:start &
- name: Install dependencies
run: |
cd openvidu-components-angular
npm install
- name: Build openvidu-angular
run: npm run lib:build --prefix openvidu-components-angular
- name: Build openvidu-webcomponent
run: npm run webcomponent:testing-build --prefix openvidu-components-angular
- name: Serve Webcomponent Testapp
run: npm run webcomponent:serve-testapp --prefix openvidu-components-angular &
- name: Wait for openvidu-local-deployment
run: |
until curl -s -f -o /dev/null http://localhost:7880; do
echo "Waiting for openvidu-local-deployment to be ready..."
sleep 5
done
- name: Wait for openvidu-components-angular Testapp
run: |
until curl -s -f -o /dev/null http://localhost:8080; do
echo "Waiting for openvidu-components-angular Testapp to be ready..."
sleep 5
done
- name: Run Webcomponent E2E
env:
LAUNCH_MODE: CI
run: npm run e2e:webcomponent-toolbar --prefix openvidu-components-angular

View File

@ -14,21 +14,13 @@ jobs:
timeout-minutes: 30 timeout-minutes: 30
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout OpenVidu Local Deployment
uses: actions/checkout@v4
with:
repository: OpenVidu/openvidu-local-deployment
ref: development
path: openvidu-local-deployment
- name: Configure OpenVidu Local Deployment - name: Configure OpenVidu Local Deployment
working-directory: ./openvidu-local-deployment/community uses: OpenVidu/actions/start-openvidu-local-deployment@main
run: | with:
./configure_lan_private_ip_linux.sh ref-openvidu-local-deployment: development
sed -i 's/interval: 10s/interval: 1s/' livekit.yaml pre_startup_commands: |
sed -i '/interval: 1s/a \ fixer_interval: 10s' livekit.yaml sed -i 's/interval: 10s/interval: 1s/' livekit.yaml
docker compose pull sed -i '/interval: 1s/a \ fixer_interval: 10s' livekit.yaml
- name: Install LiveKit CLI - name: Install LiveKit CLI
run: | run: |
curl -sSL https://get.livekit.io/cli | bash curl -sSL https://get.livekit.io/cli | bash
@ -58,3 +50,7 @@ jobs:
name: openvidu-integration-tests-report name: openvidu-integration-tests-report
path: ./openvidu/openvidu-test-integration/test-results.json path: ./openvidu/openvidu-test-integration/test-results.json
retention-days: 7 retention-days: 7
- name: Cleanup
if: always()
uses: OpenVidu/actions/cleanup@main

View File

@ -39,7 +39,7 @@ Thank you to all our backers! 🙏 [[Become a backer](https://opencollective.com
OpenVidu has been supported under project "CPP2021-008720 NewGenVidu: An elastic, user-friendly and privacy-friendly videoconferencing platform", funded by MCIN/AEI/10.13039/501100011033 and by the European Union-NextGenerationEU/PRTR. OpenVidu has been supported under project "CPP2021-008720 NewGenVidu: An elastic, user-friendly and privacy-friendly videoconferencing platform", funded by MCIN/AEI/10.13039/501100011033 and by the European Union-NextGenerationEU/PRTR.
<img height="75px" src="https://openvidu.io/img/logos/support.jpg"> <img height="75px" src="https://docs.openvidu.io/en/stable/img/logos/support.jpg">
## Sponsors ## Sponsors

View File

@ -10,9 +10,4 @@
node_modules node_modules
dist/ dist/
docs/ docs/
openvidu-webcomponent/
coverage/** coverage/**
e2e/webcomponent-app/openvidu-webcomponent-*.css
e2e/webcomponent-app/openvidu-webcomponent-*.js
e2e/assets/*

View File

@ -18,7 +18,8 @@
"builder": "@angular-devkit/build-angular:application", "builder": "@angular-devkit/build-angular:application",
"options": { "options": {
"outputPath": { "outputPath": {
"base": "dist/openvidu-components-testapp" "base": "dist/openvidu-components-testapp",
"browser": ""
}, },
"index": "src/index.html", "index": "src/index.html",
"polyfills": ["zone.js"], "polyfills": ["zone.js"],
@ -148,81 +149,6 @@
} }
} }
} }
},
"openvidu-webcomponent": {
"projectType": "application",
"root": "",
"sourceRoot": "src",
"architect": {
"build": {
"builder": "@angular-devkit/build-angular:browser",
"options": {
"outputPath": "dist/openvidu-webcomponent-rc",
"index": "src/index.html",
"main": "src/app/openvidu-webcomponent/openvidu-webcomponent.main.ts",
"polyfills": ["zone.js"],
"tsConfig": "src/app/openvidu-webcomponent/tsconfig.openvidu-webcomponent.json",
"aot": true,
"assets": ["src/favicon.ico"],
"styles": ["src/app/openvidu-webcomponent/openvidu-webcomponent.component.scss"],
"scripts": []
},
"configurations": {
"production": {
"fileReplacements": [
{
"replace": "src/environments/environment.ts",
"with": "src/environments/environment.prod.ts"
}
],
"optimization": true,
"outputHashing": "none",
"sourceMap": false,
"namedChunks": false,
"extractLicenses": true,
"vendorChunk": false,
"buildOptimizer": true,
"budgets": [
{
"type": "initial",
"maximumWarning": "1mb",
"maximumError": "2mb"
},
{
"type": "anyComponentStyle",
"maximumWarning": "2kb",
"maximumError": "4kb"
}
]
},
"testing": {
"fileReplacements": [
{
"replace": "src/environments/environment.ts",
"with": "src/environments/environment.testing.ts"
}
],
"optimization": true,
"outputHashing": "none",
"sourceMap": false,
"namedChunks": false,
"extractLicenses": true,
"budgets": [
{
"type": "initial",
"maximumWarning": "1mb",
"maximumError": "2mb"
},
{
"type": "anyComponentStyle",
"maximumWarning": "2kb",
"maximumError": "4kb"
}
]
}
}
}
}
} }
}, },
"cli": { "cli": {

View File

@ -1,29 +1,35 @@
import { Builder, WebDriver } from 'selenium-webdriver'; import { Builder, WebDriver } from 'selenium-webdriver';
import { OPENVIDU_CALL_SERVER } from '../config'; import { TestAppConfig } from './selenium.conf';
import { WebComponentConfig } from '../selenium.conf'; import { OpenViduComponentsPO } from './utils.po.test';
import { OpenViduComponentsPO } from '../utils.po.test';
const url = `${WebComponentConfig.appUrl}?OV_URL=${OPENVIDU_CALL_SERVER}`; let url = '';
describe('Testing API Directives', () => { describe('Testing API Directives', () => {
let browser: WebDriver; let browser: WebDriver;
let utils: OpenViduComponentsPO; let utils: OpenViduComponentsPO;
async function createChromeBrowser(): Promise<WebDriver> { async function createChromeBrowser(): Promise<WebDriver> {
return await new Builder() return await new Builder()
.forBrowser(WebComponentConfig.browserName) .forBrowser(TestAppConfig.browserName)
.withCapabilities(WebComponentConfig.browserCapabilities) .withCapabilities(TestAppConfig.browserCapabilities)
.setChromeOptions(WebComponentConfig.browserOptions) .setChromeOptions(TestAppConfig.browserOptions)
.usingServer(WebComponentConfig.seleniumAddress) .usingServer(TestAppConfig.seleniumAddress)
.build(); .build();
} }
beforeEach(async () => { beforeEach(async () => {
browser = await createChromeBrowser(); browser = await createChromeBrowser();
utils = new OpenViduComponentsPO(browser); utils = new OpenViduComponentsPO(browser);
url = `${TestAppConfig.appUrl}&roomName=API_DIRECTIVES_${Math.floor(Math.random() * 1000)}`;
}); });
afterEach(async () => { afterEach(async () => {
// console.log('data:image/png;base64,' + await browser.takeScreenshot()); // console.log('data:image/png;base64,' + await browser.takeScreenshot());
try {
if (await utils.isPresent('#session-container')) {
await utils.leaveRoom();
}
} catch (error) {}
await browser.sleep(500);
await browser.quit(); await browser.quit();
}); });
@ -168,7 +174,7 @@ describe('Testing API Directives', () => {
}); });
it('should show the token error WITH prejoin page', async () => { it('should show the token error WITH prejoin page', async () => {
const fixedUrl = `${url}&roomName=TEST_TOKEN&participantName=PNAME`; const fixedUrl = `${TestAppConfig.appUrl}&roomName=TEST_TOKEN&participantName=PNAME`;
await browser.get(`${fixedUrl}`); await browser.get(`${fixedUrl}`);
// Checking if prejoin page exist // Checking if prejoin page exist
@ -198,7 +204,7 @@ describe('Testing API Directives', () => {
}); });
it('should show the token error WITHOUT prejoin page', async () => { it('should show the token error WITHOUT prejoin page', async () => {
const fixedUrl = `${url}&roomName=TOKEN_ERROR&prejoin=false&participantName=PNAME`; const fixedUrl = `${TestAppConfig.appUrl}&roomName=TOKEN_ERROR&prejoin=false&participantName=PNAME`;
await browser.get(`${fixedUrl}`); await browser.get(`${fixedUrl}`);
// Checking if session container is present // Checking if session container is present
@ -232,11 +238,11 @@ describe('Testing API Directives', () => {
await utils.checkSessionIsPresent(); await utils.checkSessionIsPresent();
await utils.waitForElement('#video-poster');
expect(await utils.getNumberOfElements('video')).toEqual(0);
await utils.waitForElement('#videocam_off'); await utils.waitForElement('#videocam_off');
expect(await utils.isPresent('#videocam_off')).toBeTrue(); expect(await utils.isPresent('#videocam_off')).toBeTrue();
await utils.waitForElement('#video-poster');
expect(await utils.getNumberOfElements('video')).toEqual(0);
}); });
it('should run the app with VIDEO DISABLED and WITHOUT PREJOIN page', async () => { it('should run the app with VIDEO DISABLED and WITHOUT PREJOIN page', async () => {
@ -281,6 +287,7 @@ describe('Testing API Directives', () => {
it('should run the app with AUDIO DISABLED and WITHOUT PREJOIN page', async () => { it('should run the app with AUDIO DISABLED and WITHOUT PREJOIN page', async () => {
await browser.get(`${url}&prejoin=false&audioEnabled=false`); await browser.get(`${url}&prejoin=false&audioEnabled=false`);
await browser.sleep(1000);
await utils.checkSessionIsPresent(); await utils.checkSessionIsPresent();
// Checking if video is displayed // Checking if video is displayed
@ -292,6 +299,30 @@ describe('Testing API Directives', () => {
expect(await utils.isPresent('#mic_off')).toBeTrue(); expect(await utils.isPresent('#mic_off')).toBeTrue();
}); });
it('should run the app without camera button', async () => {
await browser.get(`${url}&prejoin=false&cameraBtn=false`);
await utils.checkSessionIsPresent();
// Checking if toolbar is present
await utils.checkToolbarIsPresent();
// Checking if camera button is not present
expect(await utils.isPresent('#camera-btn')).toBeFalse();
});
it('should run the app without microphone button', async () => {
await browser.get(`${url}&prejoin=false&microphoneBtn=false`);
await utils.checkSessionIsPresent();
// Checking if toolbar is present
await utils.checkToolbarIsPresent();
// Checking if microphone button is not present
expect(await utils.isPresent('#microphone-btn')).toBeFalse();
});
it('should HIDE the SCREENSHARE button', async () => { it('should HIDE the SCREENSHARE button', async () => {
await browser.get(`${url}&prejoin=false&screenshareBtn=false`); await browser.get(`${url}&prejoin=false&screenshareBtn=false`);
@ -508,7 +539,7 @@ describe('Testing API Directives', () => {
it('should HIDE the MUTE button in participants panel', async () => { it('should HIDE the MUTE button in participants panel', async () => {
const roomName = 'e2etest'; const roomName = 'e2etest';
const fixedUrl = `${url}&prejoin=false&participantMuteBtn=false&roomName=${roomName}`; const fixedUrl = `${TestAppConfig.appUrl}&prejoin=false&participantMuteBtn=false&roomName=${roomName}`;
await browser.get(fixedUrl); await browser.get(fixedUrl);
await utils.checkSessionIsPresent(); await utils.checkSessionIsPresent();
@ -527,8 +558,9 @@ describe('Testing API Directives', () => {
expect(await utils.isPresent('#remote-participant-item')).toBeFalse(); expect(await utils.isPresent('#remote-participant-item')).toBeFalse();
// Starting new browser for adding a new participant // Starting new browser for adding a new participant
const newTabScript = `window.open("${fixedUrl}")`; const newTabScript = `window.open("${fixedUrl}&participantName=SecondParticipant")`;
await browser.executeScript(newTabScript); await browser.executeScript(newTabScript);
await browser.sleep(10000);
// Go to first tab // Go to first tab
const tabs = await browser.getAllWindowHandles(); const tabs = await browser.getAllWindowHandles();

Binary file not shown.

View File

@ -1,9 +1,8 @@
import { Builder, Key, WebDriver } from 'selenium-webdriver'; import { Builder, Key, WebDriver } from 'selenium-webdriver';
import { OPENVIDU_CALL_SERVER } from '../config'; import { TestAppConfig } from './selenium.conf';
import { WebComponentConfig } from '../selenium.conf'; import { OpenViduComponentsPO } from './utils.po.test';
import { OpenViduComponentsPO } from '../utils.po.test';
const url = `${WebComponentConfig.appUrl}?OV_URL=${OPENVIDU_CALL_SERVER}`; const url = TestAppConfig.appUrl;
//TODO: Uncomment when captions are implemented //TODO: Uncomment when captions are implemented
// describe('Testing captions features', () => { // describe('Testing captions features', () => {
@ -11,10 +10,10 @@ const url = `${WebComponentConfig.appUrl}?OV_URL=${OPENVIDU_CALL_SERVER}`;
// let utils: OpenViduComponentsPO; // let utils: OpenViduComponentsPO;
// async function createChromeBrowser(): Promise<WebDriver> { // async function createChromeBrowser(): Promise<WebDriver> {
// return await new Builder() // return await new Builder()
// .forBrowser(WebComponentConfig.browserName) // .forBrowser(TestAppConfig.browserName)
// .withCapabilities(WebComponentConfig.browserCapabilities) // .withCapabilities(TestAppConfig.browserCapabilities)
// .setChromeOptions(WebComponentConfig.browserOptions) // .setChromeOptions(TestAppConfig.browserOptions)
// .usingServer(WebComponentConfig.seleniumAddress) // .usingServer(TestAppConfig.seleniumAddress)
// .build(); // .build();
// } // }
@ -177,4 +176,4 @@ const url = `${WebComponentConfig.appUrl}?OV_URL=${OPENVIDU_CALL_SERVER}`;
// expect(await button.getText()).toEqual('settingsEspañol'); // expect(await button.getText()).toEqual('settingsEspañol');
// }); // });
// }); // });

View File

@ -1,9 +1,8 @@
import { Builder, WebDriver } from 'selenium-webdriver'; import { Builder, WebDriver } from 'selenium-webdriver';
import { OPENVIDU_CALL_SERVER } from '../config'; import { TestAppConfig } from './selenium.conf';
import { WebComponentConfig } from '../selenium.conf'; import { OpenViduComponentsPO } from './utils.po.test';
import { OpenViduComponentsPO } from '../utils.po.test';
const url = `${WebComponentConfig.appUrl}?OV_URL=${OPENVIDU_CALL_SERVER}`; const url = TestAppConfig.appUrl;
describe('Testing CHAT features', () => { describe('Testing CHAT features', () => {
let browser: WebDriver; let browser: WebDriver;
@ -11,10 +10,10 @@ describe('Testing CHAT features', () => {
async function createChromeBrowser(): Promise<WebDriver> { async function createChromeBrowser(): Promise<WebDriver> {
return await new Builder() return await new Builder()
.forBrowser(WebComponentConfig.browserName) .forBrowser(TestAppConfig.browserName)
.withCapabilities(WebComponentConfig.browserCapabilities) .withCapabilities(TestAppConfig.browserCapabilities)
.setChromeOptions(WebComponentConfig.browserOptions) .setChromeOptions(TestAppConfig.browserOptions)
.usingServer(WebComponentConfig.seleniumAddress) .usingServer(TestAppConfig.seleniumAddress)
.build(); .build();
} }
@ -24,6 +23,10 @@ describe('Testing CHAT features', () => {
}); });
afterEach(async () => { afterEach(async () => {
try {
// leaving room if connected
await utils.leaveRoom();
} catch (error) {}
await browser.quit(); await browser.quit();
}); });

View File

@ -1,4 +1,3 @@
export const LAUNCH_MODE = process.env.LAUNCH_MODE || 'DEV'; export const LAUNCH_MODE = process.env.LAUNCH_MODE || 'DEV';
export const OPENVIDU_CALL_SERVER = process.env.OPENVIDU_CALL_SERVER || 'http://localhost:6080';
jasmine.DEFAULT_TIMEOUT_INTERVAL = 30000; jasmine.DEFAULT_TIMEOUT_INTERVAL = 30000;

View File

@ -1,20 +1,19 @@
import { Builder, Key, WebDriver } from 'selenium-webdriver'; import { Builder, Key, WebDriver } from 'selenium-webdriver';
import { OPENVIDU_CALL_SERVER } from '../config'; import { TestAppConfig } from './selenium.conf';
import { WebComponentConfig } from '../selenium.conf'; import { OpenViduComponentsPO } from './utils.po.test';
import { OpenViduComponentsPO } from '../utils.po.test';
const url = `${WebComponentConfig.appUrl}?OV_URL=${OPENVIDU_CALL_SERVER}`; const url = TestAppConfig.appUrl;
describe('Testing videoconference EVENTS', () => { describe('Testing videoconference EVENTS', () => {
let browser: WebDriver; let browser: WebDriver;
let utils: OpenViduComponentsPO; let utils: OpenViduComponentsPO;
const isHeadless: boolean = (WebComponentConfig.browserOptions as any).options_.args.includes('--headless'); const isHeadless: boolean = (TestAppConfig.browserOptions as any).options_.args.includes('--headless');
async function createChromeBrowser(): Promise<WebDriver> { async function createChromeBrowser(): Promise<WebDriver> {
return await new Builder() return await new Builder()
.forBrowser(WebComponentConfig.browserName) .forBrowser(TestAppConfig.browserName)
.withCapabilities(WebComponentConfig.browserCapabilities) .withCapabilities(TestAppConfig.browserCapabilities)
.setChromeOptions(WebComponentConfig.browserOptions) .setChromeOptions(TestAppConfig.browserOptions)
.usingServer(WebComponentConfig.seleniumAddress) .usingServer(TestAppConfig.seleniumAddress)
.build(); .build();
} }
@ -24,6 +23,10 @@ describe('Testing videoconference EVENTS', () => {
}); });
afterEach(async () => { afterEach(async () => {
try {
// leaving room if connected
await utils.leaveRoom();
} catch (error) {}
await browser.quit(); await browser.quit();
}); });
@ -596,7 +599,7 @@ describe('Testing videoconference EVENTS', () => {
expect(await utils.isPresent('#onReadyToJoin')).toBeFalse(); expect(await utils.isPresent('#onReadyToJoin')).toBeFalse();
}); });
// * PUBLISHER EVENTS // PARTICIPANT EVENTS
it('should receive onParticipantCreated event from LOCAL participant', async () => { it('should receive onParticipantCreated event from LOCAL participant', async () => {
const participantName = 'TEST_USER'; const participantName = 'TEST_USER';
@ -606,7 +609,7 @@ describe('Testing videoconference EVENTS', () => {
}); });
it('should receive the onParticipantLeft event', async () => { it('should receive the onParticipantLeft event', async () => {
await browser.get(`${url}&prejoin=false`); await browser.get(`${url}&prejoin=false&redirectToHome=false`);
await utils.checkSessionIsPresent(); await utils.checkSessionIsPresent();
@ -617,6 +620,7 @@ describe('Testing videoconference EVENTS', () => {
expect(await utils.isPresent('#leave-btn')).toBeTrue(); expect(await utils.isPresent('#leave-btn')).toBeTrue();
await leaveButton.click(); await leaveButton.click();
await utils.waitForElement('#events');
// Checking if onParticipantLeft has been received // Checking if onParticipantLeft has been received
await utils.waitForElement('#onParticipantLeft'); await utils.waitForElement('#onParticipantLeft');
expect(await utils.isPresent('#onParticipantLeft')).toBeTrue(); expect(await utils.isPresent('#onParticipantLeft')).toBeTrue();

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

@ -1,19 +1,18 @@
import { Builder, WebDriver } from 'selenium-webdriver'; import { Builder, WebDriver } from 'selenium-webdriver';
import { OPENVIDU_CALL_SERVER } from '../config'; import { getBrowserOptionsWithoutDevices, TestAppConfig } from './selenium.conf';
import { getBrowserOptionsWithoutDevices, WebComponentConfig } from '../selenium.conf'; import { OpenViduComponentsPO } from './utils.po.test';
import { OpenViduComponentsPO } from '../utils.po.test';
const url = `${WebComponentConfig.appUrl}?OV_URL=${OPENVIDU_CALL_SERVER}`; const url = TestAppConfig.appUrl;
describe('Testing replace track with emulated devices', () => { describe('Media Devices: Virtual Device Replacement and Permissions Handling', () => {
let browser: WebDriver; let browser: WebDriver;
let utils: OpenViduComponentsPO; let utils: OpenViduComponentsPO;
async function createChromeBrowser(): Promise<WebDriver> { async function createChromeBrowser(): Promise<WebDriver> {
return await new Builder() return await new Builder()
.forBrowser(WebComponentConfig.browserName) .forBrowser(TestAppConfig.browserName)
.withCapabilities(WebComponentConfig.browserCapabilities) .withCapabilities(TestAppConfig.browserCapabilities)
.setChromeOptions(WebComponentConfig.browserOptions) .setChromeOptions(TestAppConfig.browserOptions)
.usingServer(WebComponentConfig.seleniumAddress) .usingServer(TestAppConfig.seleniumAddress)
.build(); .build();
} }
@ -23,118 +22,99 @@ describe('Testing replace track with emulated devices', () => {
}); });
afterEach(async () => { afterEach(async () => {
// console.log('data:image/png;base64,' + await browser.takeScreenshot()); try {
await utils.leaveRoom();
} catch (error) {}
await browser.quit(); await browser.quit();
}); });
it('should replace the video track in prejoin page', async () => { it('should allow selecting and replacing the video track with a custom virtual device in the prejoin page', async () => {
const script = 'return document.getElementsByTagName("video")[0].srcObject.getVideoTracks()[0].label;'; const script = 'return document.getElementsByTagName("video")[0].srcObject.getVideoTracks()[0].label;';
await browser.get(`${url}&fakeDevices=true`); await browser.get(`${url}&fakeDevices=true`);
let videoDevices = await utils.waitForElement('#video-devices-form'); let videoDevices = await utils.waitForElement('#video-devices-form');
await videoDevices.click(); await videoDevices.click();
let element = await utils.waitForElement('#option-custom_fake_video_1'); let element = await utils.waitForElement('#option-custom_fake_video_1');
await element.click(); await element.click();
let videoLabel; let videoLabel;
await browser.sleep(1000); await browser.sleep(1000);
videoLabel = await browser.executeScript<string>(script); videoLabel = await browser.executeScript<string>(script);
expect(videoLabel).toEqual('custom_fake_video_1'); expect(videoLabel).toEqual('custom_fake_video_1');
await videoDevices.click(); await videoDevices.click();
element = await utils.waitForElement('#option-fake_device_0'); element = await utils.waitForElement('#option-fake_device_0');
await element.click(); await element.click();
await browser.sleep(1000); await browser.sleep(1000);
videoLabel = await browser.executeScript<string>(script); videoLabel = await browser.executeScript<string>(script);
expect(videoLabel).toEqual('fake_device_0'); expect(videoLabel).toEqual('fake_device_0');
}); });
it('should replace the video track in videoconference page', async () => { it('should allow selecting and replacing the video track with a custom virtual device in the videoconference page', async () => {
const script = 'return document.getElementsByTagName("video")[0].srcObject.getVideoTracks()[0].label;'; const script = 'return document.getElementsByTagName("video")[0].srcObject.getVideoTracks()[0].label;';
await browser.get(`${url}&prejoin=false&fakeDevices=true`); await browser.get(`${url}&prejoin=false&fakeDevices=true`);
await utils.checkSessionIsPresent(); await utils.checkSessionIsPresent();
// Checking if toolbar is present
await utils.checkToolbarIsPresent(); await utils.checkToolbarIsPresent();
await utils.togglePanel('settings'); await utils.togglePanel('settings');
await utils.waitForElement('.settings-container'); await utils.waitForElement('.settings-container');
expect(await utils.isPresent('.settings-container')).toBeTrue(); expect(await utils.isPresent('.settings-container')).toBeTrue();
await browser.sleep(500);
await utils.clickOn('#video-opt'); await utils.clickOn('#video-opt');
expect(await utils.isPresent('ov-video-devices-select')).toBeTrue(); expect(await utils.isPresent('ov-video-devices-select')).toBeTrue();
let videoDevices = await utils.waitForElement('#video-devices-form'); let videoDevices = await utils.waitForElement('#video-devices-form');
await videoDevices.click(); await videoDevices.click();
let element = await utils.waitForElement('#option-custom_fake_video_1'); let element = await utils.waitForElement('#option-custom_fake_video_1');
await element.click(); await element.click();
let videoLabel; let videoLabel;
await browser.sleep(1000); await browser.sleep(1000);
videoLabel = await browser.executeScript<string>(script); videoLabel = await browser.executeScript<string>(script);
expect(videoLabel).toEqual('custom_fake_video_1'); expect(videoLabel).toEqual('custom_fake_video_1');
await videoDevices.click(); await videoDevices.click();
element = await utils.waitForElement('#option-fake_device_0'); element = await utils.waitForElement('#option-fake_device_0');
await element.click(); await element.click();
await browser.sleep(1000); await browser.sleep(1000);
videoLabel = await browser.executeScript<string>(script); videoLabel = await browser.executeScript<string>(script);
expect(videoLabel).toEqual('fake_device_0'); expect(videoLabel).toEqual('fake_device_0');
}); });
// TODO: Uncommented when Livekit allows to replace the screen track it('should replace the screen track with a custom virtual device', async () => {
// it('should replace the screen track', async () => { const script = 'return document.getElementsByClassName("OV_video-element screen-type")[0].srcObject.getVideoTracks()[0].label;';
// const script = 'return document.getElementsByClassName("OV_video-element screen-type")[0].srcObject.getVideoTracks()[0].label;';
// await browser.get(`${url}&prejoin=false&fakeDevices=true`); await browser.get(`${url}&prejoin=false&fakeDevices=true`);
// await utils.checkLayoutPresent(); await utils.checkLayoutPresent();
// await utils.checkToolbarIsPresent(); await utils.checkToolbarIsPresent();
// await utils.clickOn('#screenshare-btn'); await utils.clickOn('#screenshare-btn');
// await browser.sleep(500); await browser.sleep(500);
// let screenLabel = await browser.executeScript<string>(script); let screenLabel = await browser.executeScript<string>(script);
// expect(screenLabel).not.toEqual('custom_fake_screen'); expect(screenLabel).not.toEqual('custom_fake_screen');
// await utils.clickOn('#video-settings-btn-SCREEN'); await utils.clickOn('#screenshare-btn');
// await browser.sleep(500); await browser.sleep(500);
// await utils.waitForElement('.video-settings-menu'); await utils.waitForElement('#replace-screen-button');
// const replaceBtn = await utils.waitForElement('#replace-screen-button'); await utils.clickOn('#replace-screen-button');
// await replaceBtn.sendKeys(Key.ENTER); await browser.sleep(1000);
// await browser.sleep(1000); screenLabel = await browser.executeScript<string>(script);
// screenLabel = await browser.executeScript<string>(script); expect(screenLabel).toEqual('custom_fake_screen');
// expect(screenLabel).to.be.toEqual('custom_fake_screen'); });
// });
}); });
describe('Testing WITHOUT MEDIA DEVICES permissions', () => { describe('Media Devices: UI Behavior Without Media Device Permissions', () => {
let browser: WebDriver; let browser: WebDriver;
let utils: OpenViduComponentsPO; let utils: OpenViduComponentsPO;
async function createChromeBrowser(): Promise<WebDriver> { async function createChromeBrowser(): Promise<WebDriver> {
return await new Builder() return await new Builder()
.forBrowser(WebComponentConfig.browserName) .forBrowser(TestAppConfig.browserName)
.withCapabilities(WebComponentConfig.browserCapabilities) .withCapabilities(TestAppConfig.browserCapabilities)
.setChromeOptions(getBrowserOptionsWithoutDevices()) .setChromeOptions(getBrowserOptionsWithoutDevices())
.usingServer(WebComponentConfig.seleniumAddress) .usingServer(TestAppConfig.seleniumAddress)
.build(); .build();
} }
@ -144,74 +124,56 @@ describe('Testing WITHOUT MEDIA DEVICES permissions', () => {
}); });
afterEach(async () => { afterEach(async () => {
try {
await utils.leaveRoom();
} catch (error) {}
await browser.quit(); await browser.quit();
}); });
it('should be able to ACCESS to PREJOIN page', async () => { it('should disable camera and microphone buttons in the prejoin page when permissions are denied', async () => {
await browser.get(`${url}`); await browser.get(`${url}`);
await utils.checkPrejoinIsPresent(); await utils.checkPrejoinIsPresent();
let button = await utils.waitForElement('#camera-button'); let button = await utils.waitForElement('#camera-button');
expect(await button.isEnabled()).toBeFalse(); expect(await button.isEnabled()).toBeFalse();
button = await utils.waitForElement('#microphone-button'); button = await utils.waitForElement('#microphone-button');
expect(await button.isEnabled()).toBeFalse(); expect(await button.isEnabled()).toBeFalse();
}); });
it('should be able to ACCESS to ROOM page', async () => { it('should disable camera and microphone buttons in the room page when permissions are denied', async () => {
await browser.get(`${url}`); await browser.get(`${url}`);
await utils.checkPrejoinIsPresent(); await utils.checkPrejoinIsPresent();
await utils.clickOn('#join-button'); await utils.clickOn('#join-button');
await utils.checkSessionIsPresent(); await utils.checkSessionIsPresent();
await utils.checkToolbarIsPresent(); await utils.checkToolbarIsPresent();
let button = await utils.waitForElement('#camera-btn'); let button = await utils.waitForElement('#camera-btn');
expect(await button.isEnabled()).toBeFalse(); expect(await button.isEnabled()).toBeFalse();
button = await utils.waitForElement('#mic-btn'); button = await utils.waitForElement('#mic-btn');
expect(await button.isEnabled()).toBeFalse(); expect(await button.isEnabled()).toBeFalse();
}); });
it('should be able to ACCESS to ROOM page without prejoin', async () => { it('should disable camera and microphone buttons in the room page without prejoin when permissions are denied', async () => {
await browser.get(`${url}&prejoin=false`); await browser.get(`${url}&prejoin=false`);
await utils.checkSessionIsPresent(); await utils.checkSessionIsPresent();
await utils.checkToolbarIsPresent(); await utils.checkToolbarIsPresent();
let button = await utils.waitForElement('#camera-btn'); let button = await utils.waitForElement('#camera-btn');
expect(await button.isEnabled()).toBeFalse(); expect(await button.isEnabled()).toBeFalse();
button = await utils.waitForElement('#mic-btn'); button = await utils.waitForElement('#mic-btn');
expect(await button.isEnabled()).toBeFalse(); expect(await button.isEnabled()).toBeFalse();
}); });
it('should the settings buttons be disabled', async () => { it('should disable camera and microphone device selection buttons in settings when permissions are denied', async () => {
await browser.get(`${url}&prejoin=false`); await browser.get(`${url}&prejoin=false`);
await utils.checkToolbarIsPresent(); await utils.checkToolbarIsPresent();
// Open more options menu
await utils.togglePanel('settings'); await utils.togglePanel('settings');
await browser.sleep(500); await browser.sleep(500);
await utils.waitForElement('.settings-container'); await utils.waitForElement('.settings-container');
expect(await utils.isPresent('.settings-container')).toBeTrue(); expect(await utils.isPresent('.settings-container')).toBeTrue();
await utils.clickOn('#video-opt'); await utils.clickOn('#video-opt');
expect(await utils.isPresent('ov-video-devices-select')).toBeTrue(); expect(await utils.isPresent('ov-video-devices-select')).toBeTrue();
let button = await utils.waitForElement('#camera-button'); let button = await utils.waitForElement('#camera-button');
expect(await button.isEnabled()).toBeFalse(); expect(await button.isEnabled()).toBeFalse();
await utils.clickOn('#audio-opt'); await utils.clickOn('#audio-opt');
expect(await utils.isPresent('ov-audio-devices-select')).toBeTrue(); expect(await utils.isPresent('ov-audio-devices-select')).toBeTrue();
button = await utils.waitForElement('#microphone-button'); button = await utils.waitForElement('#microphone-button');
expect(await button.isEnabled()).toBeFalse(); expect(await button.isEnabled()).toBeFalse();
}); });

View File

@ -0,0 +1,380 @@
import { Builder, By, WebDriver } from 'selenium-webdriver';
import { NestedConfig } from '../selenium.conf';
import { OpenViduComponentsPO } from '../utils.po.test';
const url = NestedConfig.appUrl;
describe('OpenVidu Components ATTRIBUTE toolbar directives', () => {
let browser: WebDriver;
let utils: OpenViduComponentsPO;
async function createChromeBrowser(): Promise<WebDriver> {
return await new Builder()
.forBrowser(NestedConfig.browserName)
.withCapabilities(NestedConfig.browserCapabilities)
.setChromeOptions(NestedConfig.browserOptions)
.usingServer(NestedConfig.seleniumAddress)
.build();
}
beforeEach(async () => {
browser = await createChromeBrowser();
utils = new OpenViduComponentsPO(browser);
});
afterEach(async () => {
// console.log('data:image/png;base64,' + await browser.takeScreenshot());
try {
await utils.leaveRoom();
} catch (error) {}
await browser.quit();
});
it('should HIDE the CHAT PANEL BUTTON', async () => {
await browser.get(`${url}`);
await utils.clickOn('#ovToolbar-checkbox');
await utils.clickOn('#chatPanelButton-checkbox');
await utils.clickOn('#apply-btn');
await utils.checkToolbarIsPresent();
// Check if chat button does not exist
expect(await utils.isPresent('chat-panel-btn')).toBeFalse();
});
it('should HIDE the PARTICIPANTS PANEL BUTTON', async () => {
await browser.get(`${url}`);
await utils.clickOn('#ovToolbar-checkbox');
await utils.clickOn('#participantsPanelButton-checkbox');
await utils.clickOn('#apply-btn');
await utils.checkToolbarIsPresent();
// Check if participants button does not exist
expect(await utils.isPresent('participants-panel-btn')).toBeFalse();
});
it('should HIDE the ACTIVITIES PANEL BUTTON', async () => {
await browser.get(`${url}`);
await utils.clickOn('#ovToolbar-checkbox');
await utils.clickOn('#activitiesPanelButton-checkbox');
await utils.clickOn('#apply-btn');
await utils.checkToolbarIsPresent();
// Check if participants button does not exist
expect(await utils.isPresent('activities-panel-btn')).toBeFalse();
});
it('should HIDE the DISPLAY LOGO', async () => {
await browser.get(`${url}`);
await utils.clickOn('#ovToolbar-checkbox');
await utils.clickOn('#displayLogo-checkbox');
await utils.clickOn('#apply-btn');
await utils.checkToolbarIsPresent();
expect(await utils.isPresent('branding-logo')).toBeFalse();
});
it('should HIDE the DISPLAY ROOM name', async () => {
await browser.get(`${url}`);
await utils.clickOn('#ovToolbar-checkbox');
await utils.clickOn('#displayRoomName-checkbox');
await utils.clickOn('#apply-btn');
await utils.checkToolbarIsPresent();
expect(await utils.isPresent('session-name')).toBeFalse();
});
it('should HIDE the FULLSCREEN button', async () => {
await browser.get(`${url}`);
await utils.clickOn('#ovToolbar-checkbox');
await utils.clickOn('#fullscreenButton-checkbox');
await utils.clickOn('#apply-btn');
await utils.checkToolbarIsPresent();
// Open more options menu
await utils.clickOn('#more-options-btn');
await browser.sleep(500);
await utils.waitForElement('#more-options-menu');
// Checking if fullscreen button is not present
expect(await utils.isPresent('#fullscreen-btn')).toBeFalse();
});
it('should HIDE the STREAMING button', async () => {
await browser.get(`${url}`);
await utils.clickOn('#ovToolbar-checkbox');
await utils.clickOn('#broadcastingButton-checkbox');
await utils.clickOn('#apply-btn');
await utils.checkToolbarIsPresent();
// Open more options menu
await utils.clickOn('#more-options-btn');
await browser.sleep(500);
await utils.waitForElement('#more-options-menu');
// Checking if fullscreen button is not present
expect(await utils.isPresent('#broadcasting-btn')).toBeFalse();
});
it('should HIDE the LEAVE button', async () => {
await browser.get(`${url}`);
await utils.clickOn('#ovToolbar-checkbox');
await utils.clickOn('#leaveButton-checkbox');
await utils.clickOn('#apply-btn');
await utils.checkToolbarIsPresent();
expect(await utils.isPresent('leave-btn')).toBeFalse();
});
it('should HIDE the SCREENSHARE button', async () => {
await browser.get(`${url}`);
await utils.clickOn('#ovToolbar-checkbox');
await utils.clickOn('#screenshareButton-checkbox');
await utils.clickOn('#apply-btn');
await utils.checkToolbarIsPresent();
expect(await utils.isPresent('screenshare-btn')).toBeFalse();
});
});
describe('OpenVidu Components ATTRIBUTE stream directives', () => {
let browser: WebDriver;
let utils: OpenViduComponentsPO;
async function createChromeBrowser(): Promise<WebDriver> {
return await new Builder()
.forBrowser(NestedConfig.browserName)
.withCapabilities(NestedConfig.browserCapabilities)
.setChromeOptions(NestedConfig.browserOptions)
.usingServer(NestedConfig.seleniumAddress)
.build();
}
beforeEach(async () => {
browser = await createChromeBrowser();
utils = new OpenViduComponentsPO(browser);
});
afterEach(async () => {
// console.log('data:image/png;base64,' + await browser.takeScreenshot());
try {
await utils.leaveRoom();
} catch (error) {}
await browser.quit();
});
it('should HIDE the AUDIO detector', async () => {
await browser.get(`${url}`);
await utils.clickOn('#ovStream-checkbox');
await utils.clickOn('#displayAudioDetection-checkbox');
await utils.clickOn('#apply-btn');
await utils.waitForElement('#session-container');
await utils.waitForElement('#custom-stream');
expect(await utils.isPresent('audio-wave-container')).toBeFalse();
});
it('should HIDE the PARTICIPANT NAME', async () => {
await browser.get(`${url}`);
await utils.clickOn('#ovStream-checkbox');
await utils.clickOn('#displayParticipantName-checkbox');
await utils.clickOn('#apply-btn');
await utils.waitForElement('#session-container');
await utils.waitForElement('#custom-stream');
expect(await utils.isPresent('participant-name-container')).toBeFalse();
});
it('should HIDE the SETTINGS button', async () => {
await browser.get(`${url}`);
await utils.clickOn('#ovStream-checkbox');
await utils.clickOn('#settingsButton-checkbox');
await utils.clickOn('#apply-btn');
await utils.waitForElement('#custom-stream');
expect(await utils.isPresent('settings-container')).toBeFalse();
});
});
describe('OpenVidu Components ATTRIBUTE participant panels directives', () => {
let browser: WebDriver;
let utils: OpenViduComponentsPO;
async function createChromeBrowser(): Promise<WebDriver> {
return await new Builder()
.forBrowser(NestedConfig.browserName)
.withCapabilities(NestedConfig.browserCapabilities)
.setChromeOptions(NestedConfig.browserOptions)
.usingServer(NestedConfig.seleniumAddress)
.build();
}
beforeEach(async () => {
browser = await createChromeBrowser();
utils = new OpenViduComponentsPO(browser);
});
afterEach(async () => {
// console.log('data:image/png;base64,' + await browser.takeScreenshot());
try {
await utils.leaveRoom();
} catch (error) {}
await browser.quit();
});
it('should HIDE the participant MUTE button', async () => {
const fixedSession = `${url}?sessionId=fixedNameTesting`;
await browser.get(`${fixedSession}`);
await utils.clickOn('#ovParticipantPanelItem-checkbox');
await utils.clickOn('#muteButton-checkbox');
await utils.clickOn('#apply-btn');
await utils.checkSessionIsPresent();
await utils.checkToolbarIsPresent();
await utils.clickOn('#participants-panel-btn');
await utils.waitForElement('#participants-container');
// Starting new browser for adding a new participant
const newTabScript = `window.open("${fixedSession}")`;
await browser.executeScript(newTabScript);
// Get tabs opened
const tabs = await browser.getAllWindowHandles();
// Focus on the last tab
browser.switchTo().window(tabs[1]);
await utils.clickOn('#apply-btn');
// Switch to first tab
await browser.switchTo().window(tabs[0]);
await utils.waitForElement('#remote-participant-item');
expect(await utils.isPresent('mute-btn')).toBeFalse();
});
});
describe('OpenVidu Components ATTRIBUTE activity panel directives', () => {
let browser: WebDriver;
let utils: OpenViduComponentsPO;
async function createChromeBrowser(): Promise<WebDriver> {
return await new Builder()
.forBrowser(NestedConfig.browserName)
.withCapabilities(NestedConfig.browserCapabilities)
.setChromeOptions(NestedConfig.browserOptions)
.usingServer(NestedConfig.seleniumAddress)
.build();
}
beforeEach(async () => {
browser = await createChromeBrowser();
utils = new OpenViduComponentsPO(browser);
});
afterEach(async () => {
// console.log('data:image/png;base64,' + await browser.takeScreenshot());
try {
await utils.leaveRoom();
} catch (error) {}
await browser.quit();
});
it('should HIDE the RECORDING activity', async () => {
await browser.get(`${url}`);
await utils.clickOn('#ovActivitiesPanel-checkbox');
await utils.clickOn('#recordingActivity-checkbox');
await utils.clickOn('#apply-btn');
await utils.checkToolbarIsPresent();
await utils.clickOn('#activities-panel-btn');
await browser.sleep(500);
await utils.waitForElement('#custom-activities-panel');
expect(await utils.isPresent('ov-recording-activity')).toBeFalse();
});
it('should HIDE the STREAMING activity', async () => {
await browser.get(`${url}`);
await utils.clickOn('#ovActivitiesPanel-checkbox');
await utils.clickOn('#broadcastingActivity-checkbox');
await utils.clickOn('#apply-btn');
await utils.checkToolbarIsPresent();
await utils.clickOn('#activities-panel-btn');
await browser.sleep(500);
await utils.waitForElement('#custom-activities-panel');
await utils.waitForElement('ov-recording-activity');
expect(await utils.isPresent('ov-broadcasting-activity')).toBeFalse();
});
});

View File

@ -5,7 +5,7 @@ import { OpenViduComponentsPO } from '../utils.po.test';
const url = NestedConfig.appUrl; const url = NestedConfig.appUrl;
describe('Testing EVENTS', () => { describe('OpenVidu Components EVENTS', () => {
let browser: WebDriver; let browser: WebDriver;
let utils: OpenViduComponentsPO; let utils: OpenViduComponentsPO;
@ -24,6 +24,9 @@ describe('Testing EVENTS', () => {
}); });
afterEach(async () => { afterEach(async () => {
try {
await utils.leaveRoom();
} catch (error) {}
await browser.quit(); await browser.quit();
}); });

View File

@ -5,7 +5,7 @@ import { OpenViduComponentsPO } from '../utils.po.test';
const url = NestedConfig.appUrl; const url = NestedConfig.appUrl;
describe('Testing TOOLBAR STRUCTURAL DIRECTIVES', () => { describe('E2E: Toolbar structural directive scenarios', () => {
let browser: WebDriver; let browser: WebDriver;
let utils: OpenViduComponentsPO; let utils: OpenViduComponentsPO;
async function createChromeBrowser(): Promise<WebDriver> { async function createChromeBrowser(): Promise<WebDriver> {
@ -24,10 +24,13 @@ describe('Testing TOOLBAR STRUCTURAL DIRECTIVES', () => {
afterEach(async () => { afterEach(async () => {
// console.log('data:image/png;base64,' + await browser.takeScreenshot()); // console.log('data:image/png;base64,' + await browser.takeScreenshot());
try {
await utils.leaveRoom();
} catch (error) {}
await browser.quit(); await browser.quit();
}); });
it('should inject the custom TOOLBAR without additional buttons', async () => { it('should render only the custom toolbar (no additional buttons, no default toolbar)', async () => {
await browser.get(`${url}`); await browser.get(`${url}`);
await utils.clickOn('#ovToolbar-checkbox'); await utils.clickOn('#ovToolbar-checkbox');
@ -45,7 +48,7 @@ describe('Testing TOOLBAR STRUCTURAL DIRECTIVES', () => {
expect(await utils.isPresent('#default-toolbar')).toBeFalse(); expect(await utils.isPresent('#default-toolbar')).toBeFalse();
}); });
it('should inject the custom TOOLBAR with additional buttons', async () => { it('should render the custom toolbar with additional custom buttons and hide the default toolbar', async () => {
await browser.get(`${url}`); await browser.get(`${url}`);
await utils.clickOn('#ovToolbar-checkbox'); await utils.clickOn('#ovToolbar-checkbox');
@ -69,7 +72,7 @@ describe('Testing TOOLBAR STRUCTURAL DIRECTIVES', () => {
expect(await utils.isPresent('#default-toolbar')).toBeFalse(); expect(await utils.isPresent('#default-toolbar')).toBeFalse();
}); });
it('should inject the custom TOOLBAR with additional PANEL buttons', async () => { it('should render the custom toolbar with additional custom panel buttons and hide the default toolbar', async () => {
await browser.get(`${url}`); await browser.get(`${url}`);
await utils.clickOn('#ovToolbar-checkbox'); await utils.clickOn('#ovToolbar-checkbox');
@ -93,7 +96,7 @@ describe('Testing TOOLBAR STRUCTURAL DIRECTIVES', () => {
expect(await utils.isPresent('#default-toolbar')).toBeFalse(); expect(await utils.isPresent('#default-toolbar')).toBeFalse();
}); });
it('should inject the TOOLBAR ADDITIONAL BUTTONS only', async () => { it('should render only additional toolbar buttons (default toolbar visible, no custom toolbar)', async () => {
let element; let element;
await browser.get(`${url}`); await browser.get(`${url}`);
@ -116,7 +119,7 @@ describe('Testing TOOLBAR STRUCTURAL DIRECTIVES', () => {
expect(await utils.isPresent('#custom-toolbar')).toBeFalse(); expect(await utils.isPresent('#custom-toolbar')).toBeFalse();
}); });
it('should inject the TOOLBAR ADDITIONAL PANEL BUTTONS only', async () => { it('should render only additional toolbar panel buttons (default toolbar visible, no custom toolbar)', async () => {
let element; let element;
await browser.get(`${url}`); await browser.get(`${url}`);
@ -133,14 +136,14 @@ describe('Testing TOOLBAR STRUCTURAL DIRECTIVES', () => {
expect(await utils.isPresent('#custom-toolbar-additional-panel-buttons')).toBeTrue(); expect(await utils.isPresent('#custom-toolbar-additional-panel-buttons')).toBeTrue();
element = await browser.findElements(By.id('toolbar-additional-panel-btn')); element = await browser.findElements(By.id('toolbar-additional-panel-btn'));
expect(element.length).toEqual(2); expect(element.length).toEqual(1);
// Check if custom toolbar not is present // Check if custom toolbar not is present
expect(await utils.isPresent('#custom-toolbar')).toBeFalse(); expect(await utils.isPresent('#custom-toolbar')).toBeFalse();
}); });
}); });
describe('Testing PANEL STRUCTURAL DIRECTIVES', () => { describe('E2E: Panel structural directive scenarios', () => {
let browser: WebDriver; let browser: WebDriver;
let utils: OpenViduComponentsPO; let utils: OpenViduComponentsPO;
@ -159,10 +162,49 @@ describe('Testing PANEL STRUCTURAL DIRECTIVES', () => {
}); });
afterEach(async () => { afterEach(async () => {
try {
await utils.leaveRoom();
} catch (error) {}
await browser.quit(); await browser.quit();
}); });
it('should inject the CUSTOM PANEL without children', async () => { it('should render an additional custom panel with default panels', async () => {
let element;
await browser.get(`${url}`);
await utils.clickOn('#ovAdditionalPanels-checkbox');
await utils.clickOn('#apply-btn');
// Check if toolbar panel buttons are present
await utils.checkToolbarIsPresent();
// Open additional panel
await utils.clickOn('#toolbar-additional-panel-btn');
await browser.sleep(500);
// Check if custom panel is present
expect(await utils.isPresent('#custom-additional-panel')).toBeTrue();
element = await utils.waitForElement('#additional-panel-title');
expect(await element.getAttribute('innerText')).toEqual('NEW PANEL');
// Open the participants panel
await utils.clickOn('#participants-panel-btn');
await browser.sleep(500);
// Check if default panel is present
expect(await utils.isPresent('#default-participants-panel')).toBeTrue();
// Open additional panel again
await utils.clickOn('#toolbar-additional-panel-btn');
expect(await utils.isPresent('#custom-additional-panel')).toBeTrue();
// Close the additional panel
await utils.clickOn('#toolbar-additional-panel-btn');
expect(await utils.isPresent('#custom-additional-panel')).toBeFalse();
});
it('should render only the custom panel container (no children, no default panels)', async () => {
await browser.get(`${url}`); await browser.get(`${url}`);
await utils.clickOn('#ovPanel-checkbox'); await utils.clickOn('#ovPanel-checkbox');
@ -198,7 +240,7 @@ describe('Testing PANEL STRUCTURAL DIRECTIVES', () => {
expect(await utils.isPresent('#custom-chat-panel')).toBeFalse(); expect(await utils.isPresent('#custom-chat-panel')).toBeFalse();
}); });
it('should inject the CUSTOM PANEL with ADDITIONAL PANEL only', async () => { it('should render the custom panel container with an additional panel only', async () => {
let element; let element;
await browser.get(`${url}`); await browser.get(`${url}`);
@ -227,7 +269,7 @@ describe('Testing PANEL STRUCTURAL DIRECTIVES', () => {
expect(await utils.isPresent('#custom-additional-panel')).toBeFalse(); expect(await utils.isPresent('#custom-additional-panel')).toBeFalse();
}); });
it('should inject the CUSTOM PANEL with CHAT PANEL only', async () => { it('should render the custom panel container with a custom chat panel only', async () => {
await browser.get(`${url}`); await browser.get(`${url}`);
await utils.clickOn('#ovPanel-checkbox'); await utils.clickOn('#ovPanel-checkbox');
@ -266,7 +308,7 @@ describe('Testing PANEL STRUCTURAL DIRECTIVES', () => {
expect(await utils.isPresent('#custom-chat-panel')).toBeTrue(); expect(await utils.isPresent('#custom-chat-panel')).toBeTrue();
}); });
it('should inject the CUSTOM PANEL with ACTIVITIES PANEL only', async () => { it('should render the custom panel container with a custom activities panel only', async () => {
let element; let element;
await browser.get(`${url}`); await browser.get(`${url}`);
@ -293,7 +335,7 @@ describe('Testing PANEL STRUCTURAL DIRECTIVES', () => {
expect(await element.getAttribute('innerText')).toEqual('CUSTOM ACTIVITIES PANEL'); expect(await element.getAttribute('innerText')).toEqual('CUSTOM ACTIVITIES PANEL');
}); });
it('should inject the CUSTOM PANEL with PARTICIPANTS PANEL only and without children', async () => { it('should render the custom panel container with a custom participants panel only (no children)', async () => {
await browser.get(`${url}`); await browser.get(`${url}`);
await utils.clickOn('#ovPanel-checkbox'); await utils.clickOn('#ovPanel-checkbox');
@ -331,7 +373,7 @@ describe('Testing PANEL STRUCTURAL DIRECTIVES', () => {
expect(await utils.isPresent('#custom-chat-panel')).toBeFalse(); expect(await utils.isPresent('#custom-chat-panel')).toBeFalse();
}); });
it('should inject the CUSTOM PANEL with PARTICIPANTS PANEL and P ITEM only', async () => { it('should render the custom panel container with a custom participants panel and a custom participant item', async () => {
await browser.get(`${url}`); await browser.get(`${url}`);
await utils.clickOn('#ovPanel-checkbox'); await utils.clickOn('#ovPanel-checkbox');
@ -370,7 +412,7 @@ describe('Testing PANEL STRUCTURAL DIRECTIVES', () => {
expect(await utils.isPresent('#default-participant-panel-item')).toBeFalse(); expect(await utils.isPresent('#default-participant-panel-item')).toBeFalse();
}); });
it('should inject the CUSTOM PANEL with PARTICIPANTS PANEL and P ITEM and P ITEM ELEMENT', async () => { it('should render the custom panel container with a custom participants panel, custom participant item, and custom item element', async () => {
await browser.get(`${url}`); await browser.get(`${url}`);
await utils.clickOn('#ovPanel-checkbox'); await utils.clickOn('#ovPanel-checkbox');
@ -415,7 +457,7 @@ describe('Testing PANEL STRUCTURAL DIRECTIVES', () => {
expect(await utils.isPresent('#default-participant-panel-item')).toBeFalse(); expect(await utils.isPresent('#default-participant-panel-item')).toBeFalse();
}); });
it('should inject an ACTIVITIES PANEL only', async () => { it('should render only a custom activities panel (no default panel)', async () => {
await browser.get(`${url}`); await browser.get(`${url}`);
await utils.clickOn('#ovActivitiesPanel-checkbox'); await utils.clickOn('#ovActivitiesPanel-checkbox');
@ -440,7 +482,7 @@ describe('Testing PANEL STRUCTURAL DIRECTIVES', () => {
expect(await utils.isPresent('#activities-container')).toBeTrue(); expect(await utils.isPresent('#activities-container')).toBeTrue();
}); });
it('should inject an ADDITIONAL PANEL only', async () => { it('should render only a custom additional panel (no default panel)', async () => {
let element; let element;
await browser.get(`${url}`); await browser.get(`${url}`);
@ -466,7 +508,7 @@ describe('Testing PANEL STRUCTURAL DIRECTIVES', () => {
expect(await utils.isPresent('#custom-additional-panel')).toBeFalse(); expect(await utils.isPresent('#custom-additional-panel')).toBeFalse();
}); });
it('should inject the CHAT PANEL only', async () => { it('should render only a custom chat panel (no custom panel container)', async () => {
await browser.get(`${url}`); await browser.get(`${url}`);
await utils.clickOn('#ovChatPanel-checkbox'); await utils.clickOn('#ovChatPanel-checkbox');
@ -504,7 +546,7 @@ describe('Testing PANEL STRUCTURAL DIRECTIVES', () => {
expect(await utils.isPresent('#custom-chat-panel')).toBeTrue(); expect(await utils.isPresent('#custom-chat-panel')).toBeTrue();
}); });
it('should inject the PARTICIPANTS PANEL only', async () => { it('should render only a custom participants panel (no custom panel container)', async () => {
await browser.get(`${url}`); await browser.get(`${url}`);
await utils.clickOn('#ovParticipantsPanel-checkbox'); await utils.clickOn('#ovParticipantsPanel-checkbox');
@ -542,7 +584,7 @@ describe('Testing PANEL STRUCTURAL DIRECTIVES', () => {
expect(await utils.isPresent('#custom-chat-panel')).toBeFalse(); expect(await utils.isPresent('#custom-chat-panel')).toBeFalse();
}); });
it('should inject the PARTICIPANTS PANEL ITEM only', async () => { it('should render only a custom participant panel item (no custom panel container)', async () => {
await browser.get(`${url}`); await browser.get(`${url}`);
await utils.clickOn('#ovParticipantPanelItem-checkbox'); await utils.clickOn('#ovParticipantPanelItem-checkbox');
@ -583,7 +625,7 @@ describe('Testing PANEL STRUCTURAL DIRECTIVES', () => {
expect(await utils.isPresent('#custom-chat-panel')).toBeFalse(); expect(await utils.isPresent('#custom-chat-panel')).toBeFalse();
}); });
it('should inject the PARTICIPANTS PANEL ITEM ELEMENT only', async () => { it('should render only a custom participant panel item element (no custom panel container)', async () => {
await browser.get(`${url}`); await browser.get(`${url}`);
await utils.clickOn('#ovParticipantPanelItemElements-checkbox'); await utils.clickOn('#ovParticipantPanelItemElements-checkbox');
@ -621,7 +663,7 @@ describe('Testing PANEL STRUCTURAL DIRECTIVES', () => {
expect(await utils.isPresent('#custom-chat-panel')).toBeFalse(); expect(await utils.isPresent('#custom-chat-panel')).toBeFalse();
}); });
it('should inject the CUSTOM PANEL with CHAT and PARTICIPANTS PANELS', async () => { it('should render the custom panel container with both custom chat and participants panels', async () => {
await browser.get(`${url}`); await browser.get(`${url}`);
await utils.clickOn('#ovPanel-checkbox'); await utils.clickOn('#ovPanel-checkbox');
@ -662,7 +704,7 @@ describe('Testing PANEL STRUCTURAL DIRECTIVES', () => {
}); });
}); });
describe('Testing LAYOUT STRUCTURAL DIRECTIVES', () => { describe('E2E: Layout and stream structural directive scenarios', () => {
let browser: WebDriver; let browser: WebDriver;
let utils: OpenViduComponentsPO; let utils: OpenViduComponentsPO;
@ -681,10 +723,13 @@ describe('Testing LAYOUT STRUCTURAL DIRECTIVES', () => {
}); });
afterEach(async () => { afterEach(async () => {
try {
await utils.leaveRoom();
} catch (error) {}
await browser.quit(); await browser.quit();
}); });
it('should inject the custom LAYOUT WITHOUT STREAM', async () => { it('should render only the custom layout (no stream, no default layout)', async () => {
await browser.get(`${url}`); await browser.get(`${url}`);
await utils.clickOn('#ovLayout-checkbox'); await utils.clickOn('#ovLayout-checkbox');
@ -705,7 +750,7 @@ describe('Testing LAYOUT STRUCTURAL DIRECTIVES', () => {
expect(await utils.isPresent('video')).toBeFalse(); expect(await utils.isPresent('video')).toBeFalse();
}); });
it('should inject the custom LAYOUT WITH STREAM', async () => { it('should render the custom layout with a custom stream (no default layout/stream)', async () => {
await browser.get(`${url}`); await browser.get(`${url}`);
await utils.clickOn('#ovLayout-checkbox'); await utils.clickOn('#ovLayout-checkbox');
@ -733,7 +778,7 @@ describe('Testing LAYOUT STRUCTURAL DIRECTIVES', () => {
expect(await utils.isPresent('video')).toBeTrue(); expect(await utils.isPresent('video')).toBeTrue();
}); });
it('should inject the CUSTOM STREAM only', async () => { it('should render only a custom stream (no custom layout, no default stream)', async () => {
await browser.get(`${url}`); await browser.get(`${url}`);
await utils.clickOn('#ovStream-checkbox'); await utils.clickOn('#ovStream-checkbox');
@ -759,293 +804,3 @@ describe('Testing LAYOUT STRUCTURAL DIRECTIVES', () => {
expect(await utils.isPresent('video')).toBeTrue(); expect(await utils.isPresent('video')).toBeTrue();
}); });
}); });
describe('Testing ATTRIBUTE DIRECTIVES', () => {
let browser: WebDriver;
let utils: OpenViduComponentsPO;
async function createChromeBrowser(): Promise<WebDriver> {
return await new Builder()
.forBrowser(NestedConfig.browserName)
.withCapabilities(NestedConfig.browserCapabilities)
.setChromeOptions(NestedConfig.browserOptions)
.usingServer(NestedConfig.seleniumAddress)
.build();
}
beforeEach(async () => {
browser = await createChromeBrowser();
utils = new OpenViduComponentsPO(browser);
});
afterEach(async () => {
// console.log('data:image/png;base64,' + await browser.takeScreenshot());
await browser.quit();
});
it('should HIDE the CHAT PANEL BUTTON', async () => {
await browser.get(`${url}`);
await utils.clickOn('#ovToolbar-checkbox');
await utils.clickOn('#chatPanelButton-checkbox');
await utils.clickOn('#apply-btn');
await utils.checkToolbarIsPresent();
// Check if chat button does not exist
expect(await utils.isPresent('chat-panel-btn')).toBeFalse();
});
it('should HIDE the PARTICIPANTS PANEL BUTTON', async () => {
await browser.get(`${url}`);
await utils.clickOn('#ovToolbar-checkbox');
await utils.clickOn('#participantsPanelButton-checkbox');
await utils.clickOn('#apply-btn');
await utils.checkToolbarIsPresent();
// Check if participants button does not exist
expect(await utils.isPresent('participants-panel-btn')).toBeFalse();
});
it('should HIDE the ACTIVITIES PANEL BUTTON', async () => {
await browser.get(`${url}`);
await utils.clickOn('#ovToolbar-checkbox');
await utils.clickOn('#activitiesPanelButton-checkbox');
await utils.clickOn('#apply-btn');
await utils.checkToolbarIsPresent();
// Check if participants button does not exist
expect(await utils.isPresent('activities-panel-btn')).toBeFalse();
});
it('should HIDE the DISPLAY LOGO', async () => {
await browser.get(`${url}`);
await utils.clickOn('#ovToolbar-checkbox');
await utils.clickOn('#displayLogo-checkbox');
await utils.clickOn('#apply-btn');
await utils.checkToolbarIsPresent();
expect(await utils.isPresent('branding-logo')).toBeFalse();
});
it('should HIDE the DISPLAY ROOM name', async () => {
await browser.get(`${url}`);
await utils.clickOn('#ovToolbar-checkbox');
await utils.clickOn('#displayRoomName-checkbox');
await utils.clickOn('#apply-btn');
await utils.checkToolbarIsPresent();
expect(await utils.isPresent('session-name')).toBeFalse();
});
it('should HIDE the FULLSCREEN button', async () => {
await browser.get(`${url}`);
await utils.clickOn('#ovToolbar-checkbox');
await utils.clickOn('#fullscreenButton-checkbox');
await utils.clickOn('#apply-btn');
await utils.checkToolbarIsPresent();
// Open more options menu
await utils.clickOn('#more-options-btn');
await browser.sleep(500);
await utils.waitForElement('#more-options-menu');
// Checking if fullscreen button is not present
expect(await utils.isPresent('#fullscreen-btn')).toBeFalse();
});
it('should HIDE the STREAMING button', async () => {
await browser.get(`${url}`);
await utils.clickOn('#ovToolbar-checkbox');
await utils.clickOn('#broadcastingButton-checkbox');
await utils.clickOn('#apply-btn');
await utils.checkToolbarIsPresent();
// Open more options menu
await utils.clickOn('#more-options-btn');
await browser.sleep(500);
await utils.waitForElement('#more-options-menu');
// Checking if fullscreen button is not present
expect(await utils.isPresent('#broadcasting-btn')).toBeFalse();
});
it('should HIDE the LEAVE button', async () => {
await browser.get(`${url}`);
await utils.clickOn('#ovToolbar-checkbox');
await utils.clickOn('#leaveButton-checkbox');
await utils.clickOn('#apply-btn');
await utils.checkToolbarIsPresent();
expect(await utils.isPresent('leave-btn')).toBeFalse();
});
it('should HIDE the SCREENSHARE button', async () => {
await browser.get(`${url}`);
await utils.clickOn('#ovToolbar-checkbox');
await utils.clickOn('#screenshareButton-checkbox');
await utils.clickOn('#apply-btn');
await utils.checkToolbarIsPresent();
expect(await utils.isPresent('screenshare-btn')).toBeFalse();
});
it('should HIDE the AUDIO detector', async () => {
await browser.get(`${url}`);
await utils.clickOn('#ovStream-checkbox');
await utils.clickOn('#displayAudioDetection-checkbox');
await utils.clickOn('#apply-btn');
await utils.waitForElement('#session-container');
await utils.waitForElement('#custom-stream');
expect(await utils.isPresent('audio-wave-container')).toBeFalse();
});
it('should HIDE the PARTICIPANT NAME', async () => {
await browser.get(`${url}`);
await utils.clickOn('#ovStream-checkbox');
await utils.clickOn('#displayParticipantName-checkbox');
await utils.clickOn('#apply-btn');
await utils.waitForElement('#session-container');
await utils.waitForElement('#custom-stream');
expect(await utils.isPresent('participant-name-container')).toBeFalse();
});
it('should HIDE the SETTINGS button', async () => {
await browser.get(`${url}`);
await utils.clickOn('#ovStream-checkbox');
await utils.clickOn('#settingsButton-checkbox');
await utils.clickOn('#apply-btn');
await utils.waitForElement('#custom-stream');
expect(await utils.isPresent('settings-container')).toBeFalse();
});
it('should HIDE the participant MUTE button', async () => {
const fixedSession = `${url}?sessionId=fixedNameTesting`;
await browser.get(`${fixedSession}`);
await utils.clickOn('#ovParticipantPanelItem-checkbox');
await utils.clickOn('#muteButton-checkbox');
await utils.clickOn('#apply-btn');
await utils.checkSessionIsPresent();
await utils.checkToolbarIsPresent();
await utils.clickOn('#participants-panel-btn');
await utils.waitForElement('#participants-container');
// Starting new browser for adding a new participant
const newTabScript = `window.open("${fixedSession}")`;
await browser.executeScript(newTabScript);
// Get tabs opened
const tabs = await browser.getAllWindowHandles();
// Focus on the last tab
browser.switchTo().window(tabs[1]);
await utils.clickOn('#apply-btn');
// Switch to first tab
await browser.switchTo().window(tabs[0]);
await utils.waitForElement('#remote-participant-item');
expect(await utils.isPresent('mute-btn')).toBeFalse();
});
it('should HIDE the RECORDING activity', async () => {
await browser.get(`${url}`);
await utils.clickOn('#ovActivitiesPanel-checkbox');
await utils.clickOn('#recordingActivity-checkbox');
await utils.clickOn('#apply-btn');
await utils.checkToolbarIsPresent();
await utils.clickOn('#activities-panel-btn');
await browser.sleep(500);
await utils.waitForElement('#custom-activities-panel');
expect(await utils.isPresent('ov-recording-activity')).toBeFalse();
});
it('should HIDE the STREAMING activity', async () => {
await browser.get(`${url}`);
await utils.clickOn('#ovActivitiesPanel-checkbox');
await utils.clickOn('#broadcastingActivity-checkbox');
await utils.clickOn('#apply-btn');
await utils.checkToolbarIsPresent();
await utils.clickOn('#activities-panel-btn');
await browser.sleep(500);
await utils.waitForElement('#custom-activities-panel');
await utils.waitForElement('ov-recording-activity');
expect(await utils.isPresent('ov-broadcasting-activity')).toBeFalse();
});
});

View File

@ -1,20 +1,19 @@
import { Builder, Key, WebDriver } from 'selenium-webdriver'; import { Builder, WebDriver } from 'selenium-webdriver';
import { OPENVIDU_CALL_SERVER } from '../config'; import { TestAppConfig } from './selenium.conf';
import { getBrowserOptionsWithoutDevices, WebComponentConfig } from '../selenium.conf'; import { OpenViduComponentsPO } from './utils.po.test';
import { OpenViduComponentsPO } from '../utils.po.test';
const url = `${WebComponentConfig.appUrl}?OV_URL=${OPENVIDU_CALL_SERVER}`; const url = TestAppConfig.appUrl;
describe('Testing panels', () => { describe('Panels: UI Navigation and Section Switching', () => {
let browser: WebDriver; let browser: WebDriver;
let utils: OpenViduComponentsPO; let utils: OpenViduComponentsPO;
async function createChromeBrowser(): Promise<WebDriver> { async function createChromeBrowser(): Promise<WebDriver> {
return await new Builder() return await new Builder()
.forBrowser(WebComponentConfig.browserName) .forBrowser(TestAppConfig.browserName)
.withCapabilities(WebComponentConfig.browserCapabilities) .withCapabilities(TestAppConfig.browserCapabilities)
.setChromeOptions(WebComponentConfig.browserOptions) .setChromeOptions(TestAppConfig.browserOptions)
.usingServer(WebComponentConfig.seleniumAddress) .usingServer(TestAppConfig.seleniumAddress)
.build(); .build();
} }
@ -24,6 +23,9 @@ describe('Testing panels', () => {
}); });
afterEach(async () => { afterEach(async () => {
try {
await utils.leaveRoom();
} catch (error) {}
await browser.quit(); await browser.quit();
}); });
@ -61,164 +63,104 @@ describe('Testing panels', () => {
// expect(await utils.isPresent('#background-effects-container')).toBeFalse(); // expect(await utils.isPresent('#background-effects-container')).toBeFalse();
// }); // });
it('should toggle CHAT panel', async () => { it('should open and close the CHAT panel and verify its content', async () => {
await browser.get(`${url}&prejoin=false`); await browser.get(`${url}&prejoin=false`);
await utils.checkLayoutPresent(); await utils.checkLayoutPresent();
const chatButton = await utils.waitForElement('#chat-panel-btn'); const chatButton = await utils.waitForElement('#chat-panel-btn');
await chatButton.click(); await chatButton.click();
await utils.waitForElement('.sidenav-menu'); await utils.waitForElement('.sidenav-menu');
await utils.waitForElement('.input-container'); await utils.waitForElement('.input-container');
expect(await utils.isPresent('.input-container')).toBeTrue(); expect(await utils.isPresent('.input-container')).toBeTrue();
await utils.waitForElement('.messages-container'); await utils.waitForElement('.messages-container');
expect(await utils.isPresent('.messages-container')).toBeTrue(); expect(await utils.isPresent('.messages-container')).toBeTrue();
await chatButton.click(); await chatButton.click();
expect(await utils.isPresent('.input-container')).toBeFalse(); expect(await utils.isPresent('.input-container')).toBeFalse();
expect(await utils.isPresent('.messages-container')).toBeFalse(); expect(await utils.isPresent('.messages-container')).toBeFalse();
}); });
it('should toggle PARTICIPANTS panel', async () => { it('should open and close the PARTICIPANTS panel and verify its content', async () => {
await browser.get(`${url}&prejoin=false`); await browser.get(`${url}&prejoin=false`);
await utils.checkLayoutPresent(); await utils.checkLayoutPresent();
const participantBtn = await utils.waitForElement('#participants-panel-btn'); const participantBtn = await utils.waitForElement('#participants-panel-btn');
await participantBtn.click(); await participantBtn.click();
await utils.waitForElement('.sidenav-menu'); await utils.waitForElement('.sidenav-menu');
await utils.waitForElement('.local-participant-container'); await utils.waitForElement('.local-participant-container');
expect(await utils.isPresent('.local-participant-container')).toBeTrue(); expect(await utils.isPresent('.local-participant-container')).toBeTrue();
await utils.waitForElement('ov-participant-panel-item'); await utils.waitForElement('ov-participant-panel-item');
expect(await utils.isPresent('ov-participant-panel-item')).toBeTrue(); expect(await utils.isPresent('ov-participant-panel-item')).toBeTrue();
await participantBtn.click(); await participantBtn.click();
expect(await utils.isPresent('.local-participant-container')).toBeFalse(); expect(await utils.isPresent('.local-participant-container')).toBeFalse();
expect(await utils.isPresent('ov-participant-panel-item')).toBeFalse(); expect(await utils.isPresent('ov-participant-panel-item')).toBeFalse();
}); });
it('should toggle ACTIVITIES panel', async () => { it('should open and close the ACTIVITIES panel and verify its content', async () => {
await browser.get(`${url}&prejoin=false`); await browser.get(`${url}&prejoin=false`);
await utils.checkLayoutPresent(); await utils.checkLayoutPresent();
// Get activities button and click into it
const activitiesBtn = await utils.waitForElement('#activities-panel-btn'); const activitiesBtn = await utils.waitForElement('#activities-panel-btn');
await activitiesBtn.click(); await activitiesBtn.click();
await utils.waitForElement('.sidenav-menu'); await utils.waitForElement('.sidenav-menu');
await utils.waitForElement('#activities-container'); await utils.waitForElement('#activities-container');
expect(await utils.isPresent('#activities-container')).toBeTrue(); expect(await utils.isPresent('#activities-container')).toBeTrue();
await utils.waitForElement('#recording-activity'); await utils.waitForElement('#recording-activity');
expect(await utils.isPresent('#recording-activity')).toBeTrue(); expect(await utils.isPresent('#recording-activity')).toBeTrue();
await activitiesBtn.click(); await activitiesBtn.click();
expect(await utils.isPresent('#activities-container')).toBeFalse(); expect(await utils.isPresent('#activities-container')).toBeFalse();
expect(await utils.isPresent('#recording-activity')).toBeFalse(); expect(await utils.isPresent('#recording-activity')).toBeFalse();
}); });
it('should toggle SETTINGS panel', async () => { it('should open the SETTINGS panel and verify its content', async () => {
let element; let element;
await browser.get(`${url}&prejoin=false`); await browser.get(`${url}&prejoin=false`);
await utils.checkLayoutPresent(); await utils.checkLayoutPresent();
// Checking if toolbar is present
await utils.checkToolbarIsPresent(); await utils.checkToolbarIsPresent();
await utils.togglePanel('settings'); await utils.togglePanel('settings');
element = await utils.waitForElement('.sidenav-menu'); element = await utils.waitForElement('.sidenav-menu');
expect(await utils.isPresent('#default-settings-panel')).toBeTrue(); expect(await utils.isPresent('#default-settings-panel')).toBeTrue();
}); });
it('should switching between PARTICIPANTS and CHAT panels', async () => { it('should switch between PARTICIPANTS and CHAT panels and verify correct content is shown', async () => {
await browser.get(`${url}&prejoin=false`); await browser.get(`${url}&prejoin=false`);
await utils.checkSessionIsPresent(); await utils.checkSessionIsPresent();
await utils.checkToolbarIsPresent(); await utils.checkToolbarIsPresent();
// Open chat panel
const chatButton = await utils.waitForElement('#chat-panel-btn'); const chatButton = await utils.waitForElement('#chat-panel-btn');
await chatButton.click(); await chatButton.click();
await utils.waitForElement('.sidenav-menu'); await utils.waitForElement('.sidenav-menu');
expect(await utils.isPresent('.sidenav-menu')).toBeTrue(); expect(await utils.isPresent('.sidenav-menu')).toBeTrue();
await utils.waitForElement('.input-container'); await utils.waitForElement('.input-container');
expect(await utils.isPresent('.input-container')).toBeTrue(); expect(await utils.isPresent('.input-container')).toBeTrue();
expect(await utils.isPresent('.messages-container')).toBeTrue(); expect(await utils.isPresent('.messages-container')).toBeTrue();
// Open participants panel
const participantBtn = await utils.waitForElement('#participants-panel-btn'); const participantBtn = await utils.waitForElement('#participants-panel-btn');
await participantBtn.click(); await participantBtn.click();
await utils.waitForElement('.sidenav-menu'); await utils.waitForElement('.sidenav-menu');
expect(await utils.isPresent('.local-participant-container')).toBeTrue(); expect(await utils.isPresent('.local-participant-container')).toBeTrue();
expect(await utils.isPresent('ov-participant-panel-item')).toBeTrue(); expect(await utils.isPresent('ov-participant-panel-item')).toBeTrue();
// Switch to chat panel
await chatButton.click(); await chatButton.click();
await utils.waitForElement('.sidenav-menu'); await utils.waitForElement('.sidenav-menu');
expect(await utils.isPresent('.input-container')).toBeTrue(); expect(await utils.isPresent('.input-container')).toBeTrue();
expect(await utils.isPresent('.messages-container')).toBeTrue(); expect(await utils.isPresent('.messages-container')).toBeTrue();
expect(await utils.isPresent('.local-participant-container')).toBeFalse(); expect(await utils.isPresent('.local-participant-container')).toBeFalse();
expect(await utils.isPresent('ov-participant-panel-item')).toBeFalse(); expect(await utils.isPresent('ov-participant-panel-item')).toBeFalse();
// Close chat panel
await chatButton.click(); await chatButton.click();
expect(await utils.getNumberOfElements('.input-container')).toEqual(0); expect(await utils.getNumberOfElements('.input-container')).toEqual(0);
expect(await utils.isPresent('messages-container')).toBeFalse(); expect(await utils.isPresent('messages-container')).toBeFalse();
}); });
it('should switching between sections in SETTINGS PANEL', async () => { it('should switch between sections in the SETTINGS panel and verify correct content is shown', async () => {
let element; let element;
await browser.get(`${url}&prejoin=false`); await browser.get(`${url}&prejoin=false`);
await utils.checkToolbarIsPresent(); await utils.checkToolbarIsPresent();
// Checking if toolbar is present
await utils.checkToolbarIsPresent();
// Open more options menu
await utils.togglePanel('settings'); await utils.togglePanel('settings');
await utils.waitForElement('.sidenav-menu'); await utils.waitForElement('.sidenav-menu');
expect(await utils.isPresent('.sidenav-menu')).toBeTrue(); expect(await utils.isPresent('.sidenav-menu')).toBeTrue();
await browser.sleep(500);
// Check if general section is shown
element = await utils.waitForElement('#general-opt'); element = await utils.waitForElement('#general-opt');
await element.click(); await element.click();
expect(await utils.isPresent('ov-participant-name-input')).toBeTrue(); expect(await utils.isPresent('ov-participant-name-input')).toBeTrue();
// Check if video section is shown
element = await utils.waitForElement('#video-opt'); element = await utils.waitForElement('#video-opt');
await element.click(); await element.click();
expect(await utils.isPresent('ov-video-devices-select')).toBeTrue(); expect(await utils.isPresent('ov-video-devices-select')).toBeTrue();
// Check if audio section is shown
element = await utils.waitForElement('#audio-opt'); element = await utils.waitForElement('#audio-opt');
await element.click(); await element.click();
expect(await utils.isPresent('ov-audio-devices-select')).toBeTrue(); expect(await utils.isPresent('ov-audio-devices-select')).toBeTrue();
}); });
}); });

View File

@ -1,19 +1,19 @@
import { Builder, WebDriver } from 'selenium-webdriver'; import { Builder, WebDriver } from 'selenium-webdriver';
import { OPENVIDU_CALL_SERVER } from '../config'; import { TestAppConfig } from './selenium.conf';
import { WebComponentConfig } from '../selenium.conf'; import { OpenViduComponentsPO } from './utils.po.test';
import { OpenViduComponentsPO } from '../utils.po.test';
const url = `${WebComponentConfig.appUrl}?OV_URL=${OPENVIDU_CALL_SERVER}`; const url = TestAppConfig.appUrl;
describe('Testing screenshare features', () => { describe('E2E: Screensharing features', () => {
let browser: WebDriver; let browser: WebDriver;
let utils: OpenViduComponentsPO; let utils: OpenViduComponentsPO;
async function createChromeBrowser(): Promise<WebDriver> { async function createChromeBrowser(): Promise<WebDriver> {
return await new Builder() return await new Builder()
.forBrowser(WebComponentConfig.browserName) .forBrowser(TestAppConfig.browserName)
.withCapabilities(WebComponentConfig.browserCapabilities) .withCapabilities(TestAppConfig.browserCapabilities)
.setChromeOptions(WebComponentConfig.browserOptions) .setChromeOptions(TestAppConfig.browserOptions)
.usingServer(WebComponentConfig.seleniumAddress) .usingServer(TestAppConfig.seleniumAddress)
.build(); .build();
} }
@ -23,95 +23,89 @@ describe('Testing screenshare features', () => {
}); });
afterEach(async () => { afterEach(async () => {
try {
await utils.leaveRoom();
} catch (error) {}
await browser.quit(); await browser.quit();
}); });
it('should toggle screensharing twice', async () => { it('should toggle screensharing on and off twice, updating video count', async () => {
await browser.get(`${url}&prejoin=false`); await browser.get(`${url}&prejoin=false`);
await utils.checkLayoutPresent(); await utils.checkLayoutPresent();
// Clicking to screensharing button // Enable screensharing
await utils.waitForElement('#screenshare-btn'); await utils.waitForElement('#screenshare-btn');
await utils.clickOn('#screenshare-btn'); await utils.clickOn('#screenshare-btn');
await browser.sleep(500); await browser.sleep(500);
await utils.waitForElement('.OV_big'); await utils.waitForElement('.OV_big');
expect(await utils.getNumberOfElements('video')).toEqual(2); expect(await utils.getNumberOfElements('video')).toEqual(2);
// expect(await utils.getNumberOfElements('.OV_stream.speaking')).toEqual(1); // Disable screensharing
await utils.disableScreenShare(); await utils.disableScreenShare();
expect(await utils.getNumberOfElements('video')).toEqual(1); expect(await utils.getNumberOfElements('video')).toEqual(1);
// toggle screenshare again // Enable again
await utils.clickOn('#screenshare-btn'); await utils.clickOn('#screenshare-btn');
await browser.sleep(500); await browser.sleep(500);
await utils.waitForElement('.OV_big'); await utils.waitForElement('.OV_big');
expect(await utils.getNumberOfElements('video')).toEqual(2); expect(await utils.getNumberOfElements('video')).toEqual(2);
// Disable again
await utils.disableScreenShare(); await utils.disableScreenShare();
expect(await utils.getNumberOfElements('video')).toEqual(1); expect(await utils.getNumberOfElements('video')).toEqual(1);
}); });
it('should show screen and muted camera', async () => { it('should show screenshare and muted camera (camera off, screenshare on)', async () => {
await browser.get(`${url}&prejoin=false`); await browser.get(`${url}&prejoin=false`);
await utils.checkLayoutPresent(); await utils.checkLayoutPresent();
// Mute camera
await utils.waitForElement('#camera-btn'); await utils.waitForElement('#camera-btn');
await utils.clickOn('#camera-btn'); await utils.clickOn('#camera-btn');
// Clicking to screensharing button // Enable screensharing
const screenshareButton = await utils.waitForElement('#screenshare-btn'); const screenshareButton = await utils.waitForElement('#screenshare-btn');
expect(await screenshareButton.isDisplayed()).toBeTrue(); expect(await screenshareButton.isDisplayed()).toBeTrue();
await screenshareButton.click(); await screenshareButton.click();
await browser.sleep(500); await browser.sleep(500);
await utils.waitForElement('.OV_big'); await utils.waitForElement('.OV_big');
expect(await utils.getNumberOfElements('video')).toEqual(2); expect(await utils.getNumberOfElements('video')).toEqual(2);
// Disable screensharing
await utils.disableScreenShare(); await utils.disableScreenShare();
expect(await utils.getNumberOfElements('video')).toEqual(1); expect(await utils.getNumberOfElements('video')).toEqual(1);
}); });
it('should screensharing with PINNED video', async () => { it('should display screensharing with a single pinned video', async () => {
await browser.get(`${url}&prejoin=false`); await browser.get(`${url}&prejoin=false`);
await utils.checkLayoutPresent(); await utils.checkLayoutPresent();
// Clicking to screensharing button // Enable screensharing
const screenshareButton = await utils.waitForElement('#screenshare-btn'); const screenshareButton = await utils.waitForElement('#screenshare-btn');
expect(await screenshareButton.isDisplayed()).toBeTrue(); expect(await screenshareButton.isDisplayed()).toBeTrue();
await screenshareButton.click(); await screenshareButton.click();
await utils.waitForElement('.OV_big'); await utils.waitForElement('.OV_big');
expect(await utils.getNumberOfElements('.OV_big')).toEqual(1); expect(await utils.getNumberOfElements('.OV_big')).toEqual(1);
}); });
it('should screensharing with PINNED video and replace the existing one', async () => { it('should replace pinned video when a second participant starts screensharing', async () => {
const roomName = 'screensharingE2E'; const roomName = 'screensharingE2E';
const fixedUrl = `${url}&roomName=${roomName}&prejoin=false`; const fixedUrl = `${url}&roomName=${roomName}&prejoin=false`;
await browser.get(fixedUrl); await browser.get(fixedUrl);
await utils.checkLayoutPresent(); await utils.checkLayoutPresent();
// Clicking to screensharing button // First participant screenshares
await utils.waitForElement('#screenshare-btn'); await utils.waitForElement('#screenshare-btn');
await utils.clickOn('#screenshare-btn'); await utils.clickOn('#screenshare-btn');
await utils.waitForElement('.OV_big'); await utils.waitForElement('.OV_big');
expect(await utils.getNumberOfElements('.OV_big')).toEqual(1); expect(await utils.getNumberOfElements('.OV_big')).toEqual(1);
// Starting new browser for adding the second participant // Second participant joins and screenshares
const newTabScript = `window.open("${fixedUrl}")`; const newTabScript = `window.open("${fixedUrl}")`;
await browser.executeScript(newTabScript); await browser.executeScript(newTabScript);
const tabs = await browser.getAllWindowHandles(); const tabs = await browser.getAllWindowHandles();
await browser.switchTo().window(tabs[1]); await browser.switchTo().window(tabs[1]);
await utils.checkLayoutPresent(); await utils.checkLayoutPresent();
// Clicking to screensharing button
await utils.waitForElement('#screenshare-btn'); await utils.waitForElement('#screenshare-btn');
await utils.clickOn('#screenshare-btn'); await utils.clickOn('#screenshare-btn');
await browser.sleep(500); await browser.sleep(500);
@ -119,7 +113,7 @@ describe('Testing screenshare features', () => {
await utils.waitForElement('.OV_big'); await utils.waitForElement('.OV_big');
expect(await utils.getNumberOfElements('.OV_big')).toEqual(1); expect(await utils.getNumberOfElements('.OV_big')).toEqual(1);
// Go to first tab // Switch back to first tab and check
await browser.switchTo().window(tabs[0]); await browser.switchTo().window(tabs[0]);
await browser.sleep(500); await browser.sleep(500);
expect(await utils.getNumberOfElements('video')).toEqual(4); expect(await utils.getNumberOfElements('video')).toEqual(4);
@ -127,39 +121,37 @@ describe('Testing screenshare features', () => {
expect(await utils.getNumberOfElements('.OV_big')).toEqual(1); expect(await utils.getNumberOfElements('.OV_big')).toEqual(1);
}); });
it('should disabled a screensharing and pinned the previous one', async () => { it('should unpin screensharing and restore previous pinned video when disabled', async () => {
const roomName = 'screensharingtwoE2E'; const roomName = 'screensharingtwoE2E';
const fixedUrl = `${url}&roomName=${roomName}&prejoin=false`; const fixedUrl = `${url}&roomName=${roomName}&prejoin=false`;
await browser.get(fixedUrl); await browser.get(fixedUrl);
await utils.checkLayoutPresent(); await utils.checkLayoutPresent();
// Clicking to screensharing button // First participant screenshares
await utils.waitForElement('#screenshare-btn'); await utils.waitForElement('#screenshare-btn');
await utils.clickOn('#screenshare-btn'); await utils.clickOn('#screenshare-btn');
await browser.sleep(500); await browser.sleep(500);
await utils.waitForElement('.OV_big'); await utils.waitForElement('.OV_big');
expect(await utils.getNumberOfElements('.OV_big')).toEqual(1); expect(await utils.getNumberOfElements('.OV_big')).toEqual(1);
// Starting new browser for adding the second participant // Second participant joins and screenshares
const tabs = await utils.openTab(fixedUrl); const tabs = await utils.openTab(fixedUrl);
await browser.switchTo().window(tabs[1]); await browser.switchTo().window(tabs[1]);
await utils.checkLayoutPresent(); await utils.checkLayoutPresent();
// Clicking to screensharing button
await utils.waitForElement('#screenshare-btn'); await utils.waitForElement('#screenshare-btn');
await utils.clickOn('#screenshare-btn'); await utils.clickOn('#screenshare-btn');
await browser.sleep(500); await browser.sleep(500);
expect(await utils.getNumberOfElements('video')).toEqual(4); expect(await utils.getNumberOfElements('video')).toEqual(4);
await utils.waitForElement('.OV_big'); await utils.waitForElement('.OV_big');
expect(await utils.getNumberOfElements('.OV_big')).toEqual(1); expect(await utils.getNumberOfElements('.OV_big')).toEqual(1);
// Disable screensharing
// Disable screensharing for second participant
await utils.disableScreenShare(); await utils.disableScreenShare();
expect(await utils.getNumberOfElements('video')).toEqual(3); expect(await utils.getNumberOfElements('video')).toEqual(3);
await utils.waitForElement('.OV_big'); await utils.waitForElement('.OV_big');
expect(await utils.getNumberOfElements('.OV_big')).toEqual(1); expect(await utils.getNumberOfElements('.OV_big')).toEqual(1);
// Go to first tab // Switch back to first tab and check
await browser.switchTo().window(tabs[0]); await browser.switchTo().window(tabs[0]);
await browser.sleep(500); await browser.sleep(500);
expect(await utils.getNumberOfElements('video')).toEqual(3); expect(await utils.getNumberOfElements('video')).toEqual(3);
@ -167,38 +159,52 @@ describe('Testing screenshare features', () => {
expect(await utils.getNumberOfElements('.OV_big')).toEqual(1); expect(await utils.getNumberOfElements('.OV_big')).toEqual(1);
}); });
// it('should screensharing with audio muted', async () => { it('should correctly share screen with microphone muted and maintain proper track state', async () => {
// let isAudioEnabled; // Helper for inspecting stream tracks
// const getAudioScript = (className: string) => { const getMediaTracks = (className: string) => {
// return `return document.getElementsByClassName('${className}')[0].srcObject.getAudioTracks()[0].enabled;`; return `
// }; const tracks = document.getElementsByClassName('${className}')[0].srcObject.getTracks();
// await browser.get(`${url}&prejoin=false`); return tracks.map(track => ({
kind: track.kind,
enabled: track.enabled,
id: track.id,
label: track.label
}));`;
};
// await utils.checkLayoutPresent(); // Setup: Navigate to room and skip prejoin
await browser.get(`${url}&prejoin=false`);
await utils.checkLayoutPresent();
// const micButton = await utils.waitForElement('#mic-btn'); // Step 1: First mute the microphone
// await micButton.click(); const micButton = await utils.waitForElement('#mic-btn');
await micButton.click();
// // Clicking to screensharing button // Step 2: Start screen sharing
// const screenshareButton = await utils.waitForElement('#screenshare-btn'); await utils.clickOn('#screenshare-btn');
// expect(await utils.isPresent('#screenshare-btn')).toBeTrue();
// await screenshareButton.click();
// await utils.waitForElement('.screen-type'); // Step 3: Verify both streams are present
// expect(await utils.getNumberOfElements('video')).toEqual(2); await utils.waitForElement('.screen-type');
expect(await utils.getNumberOfElements('video')).toEqual(2);
// isAudioEnabled = await browser.executeScript(getAudioScript('screen-type')); // Step 4: Verify screen share track properties
// expect(isAudioEnabled).toBeFalse(); const screenTracks: any[] = await browser.executeScript(getMediaTracks('screen-type'));
expect(screenTracks.length).toEqual(1);
expect(screenTracks[0].kind).toEqual('video');
expect(screenTracks[0].enabled).toBeTrue();
// await utils.waitForElement('#status-mic'); // Step 5: Verify microphone status indicators for both streams
// expect(await utils.getNumberOfElements('#status-mic')).toEqual(2); // await utils.waitForElement('#status-mic');
// const micStatusCount = await utils.getNumberOfElements('#status-mic');
// // Clicking to screensharing button // expect(micStatusCount).toEqual(2);
// await screenshareButton.click();
// expect(await utils.getNumberOfElements('video')).toEqual(1);
// });
// Step 6: Stop screen sharing and verify stream count
await utils.clickOn('#screenshare-btn');
await browser.sleep(500);
await utils.clickOn('#disable-screen-button');
await browser.sleep(500);
expect(await utils.getNumberOfElements('video')).toEqual(1);
});
// it('should show and hide CAMERA stream when muting video with screensharing', async () => { // it('should show and hide CAMERA stream when muting video with screensharing', async () => {
// await browser.get(`${url}&prejoin=false`); // await browser.get(`${url}&prejoin=false`);

View File

@ -10,12 +10,14 @@ interface BrowserConfig {
browserName: string; browserName: string;
} }
const audioPath = LAUNCH_MODE === 'CI' ? `e2e-assets/audio_test.wav` : 'e2e/assets/audio_test.wav';
const chromeArguments = [ const chromeArguments = [
'--window-size=1300,1000', '--window-size=1300,1000',
'--headless', // '--headless',
'--use-fake-ui-for-media-stream', '--use-fake-ui-for-media-stream',
'--use-fake-device-for-media-stream', '--use-fake-device-for-media-stream',
'--use-file-for-fake-audio-capture=e2e/assets/audio.wav' `--use-file-for-fake-audio-capture=${audioPath}`
]; ];
const chromeArgumentsCI = [ const chromeArgumentsCI = [
'--window-size=1300,1000', '--window-size=1300,1000',
@ -29,7 +31,10 @@ const chromeArgumentsCI = [
'--disable-background-networking', '--disable-background-networking',
'--disable-default-apps', '--disable-default-apps',
'--use-fake-ui-for-media-stream', '--use-fake-ui-for-media-stream',
'--use-fake-device-for-media-stream' '--use-fake-device-for-media-stream',
`--use-file-for-fake-audio-capture=${audioPath}`,
'--autoplay-policy=no-user-gesture-required',
'--allow-file-access-from-files'
]; ];
const chromeArgumentsWithoutMediaDevices = ['--headless', '--window-size=1300,900', '--deny-permission-prompts']; const chromeArgumentsWithoutMediaDevices = ['--headless', '--window-size=1300,900', '--deny-permission-prompts'];
const chromeArgumentsWithoutMediaDevicesCI = [ const chromeArgumentsWithoutMediaDevicesCI = [
@ -46,8 +51,8 @@ const chromeArgumentsWithoutMediaDevicesCI = [
'--deny-permission-prompts' '--deny-permission-prompts'
]; ];
export const WebComponentConfig: BrowserConfig = { export const TestAppConfig: BrowserConfig = {
appUrl: 'http://localhost:8080/', appUrl: 'http://localhost:4200/#/call?staticVideos=false',
seleniumAddress: LAUNCH_MODE === 'CI' ? 'http://localhost:4444/wd/hub' : '', seleniumAddress: LAUNCH_MODE === 'CI' ? 'http://localhost:4444/wd/hub' : '',
browserName: 'chrome', browserName: 'chrome',
browserCapabilities: Capabilities.chrome().set('acceptInsecureCerts', true), browserCapabilities: Capabilities.chrome().set('acceptInsecureCerts', true),

View File

@ -1,19 +1,18 @@
import { Builder, ILocation, IRectangle, ISize, WebDriver } from 'selenium-webdriver'; import { Builder, ILocation, IRectangle, ISize, WebDriver } from 'selenium-webdriver';
import { OPENVIDU_CALL_SERVER } from '../config'; import { TestAppConfig } from './selenium.conf';
import { WebComponentConfig } from '../selenium.conf'; import { OpenViduComponentsPO } from './utils.po.test';
import { OpenViduComponentsPO } from '../utils.po.test';
const url = `${WebComponentConfig.appUrl}?OV_URL=${OPENVIDU_CALL_SERVER}`; const url = TestAppConfig.appUrl;
describe('Checking stream elements by disabling/enabling the media', () => { describe('Stream rendering and media toggling scenarios', () => {
let browser: WebDriver; let browser: WebDriver;
let utils: OpenViduComponentsPO; let utils: OpenViduComponentsPO;
async function createChromeBrowser(): Promise<WebDriver> { async function createChromeBrowser(): Promise<WebDriver> {
return await new Builder() return await new Builder()
.forBrowser(WebComponentConfig.browserName) .forBrowser(TestAppConfig.browserName)
.withCapabilities(WebComponentConfig.browserCapabilities) .withCapabilities(TestAppConfig.browserCapabilities)
.setChromeOptions(WebComponentConfig.browserOptions) .setChromeOptions(TestAppConfig.browserOptions)
.usingServer(WebComponentConfig.seleniumAddress) .usingServer(TestAppConfig.seleniumAddress)
.build(); .build();
} }
@ -23,10 +22,13 @@ describe('Checking stream elements by disabling/enabling the media', () => {
}); });
afterEach(async () => { afterEach(async () => {
try {
await utils.leaveRoom();
} catch (error) {}
await browser.quit(); await browser.quit();
}); });
it('should show 0 video element when a participant joins with video disabled', async () => { it('should not render any video element when joining with video disabled', async () => {
await browser.get(`${url}&prejoin=true&videoEnabled=false`); await browser.get(`${url}&prejoin=true&videoEnabled=false`);
await utils.checkPrejoinIsPresent(); await utils.checkPrejoinIsPresent();
@ -39,7 +41,7 @@ describe('Checking stream elements by disabling/enabling the media', () => {
expect(await utils.getNumberOfElements('audio')).toEqual(1); expect(await utils.getNumberOfElements('audio')).toEqual(1);
}); });
it('should show a video element when a participant joins with audio muted', async () => { it('should render a video element but no audio when joining with audio muted', async () => {
await browser.get(`${url}&prejoin=true&audioEnabled=false`); await browser.get(`${url}&prejoin=true&audioEnabled=false`);
await utils.checkPrejoinIsPresent(); await utils.checkPrejoinIsPresent();
@ -52,7 +54,7 @@ describe('Checking stream elements by disabling/enabling the media', () => {
expect(await utils.getNumberOfElements('audio')).toEqual(0); expect(await utils.getNumberOfElements('audio')).toEqual(0);
}); });
it('should show a video element when a participant joins', async () => { it('should render both video and audio elements when joining with both enabled', async () => {
await browser.get(`${url}&prejoin=true&videoEnabled=true&audioEnabled=true`); await browser.get(`${url}&prejoin=true&videoEnabled=true&audioEnabled=true`);
await utils.checkPrejoinIsPresent(); await utils.checkPrejoinIsPresent();
@ -65,7 +67,7 @@ describe('Checking stream elements by disabling/enabling the media', () => {
expect(await utils.getNumberOfElements('audio')).toEqual(1); expect(await utils.getNumberOfElements('audio')).toEqual(1);
}); });
it('should show a video element when a participant shares its screen with VIDEO and AUDIO MUTED', async () => { it('should add a screen share video/audio when sharing screen with both camera and mic muted', async () => {
await browser.get(`${url}&prejoin=true&videoEnabled=false&audioEnabled=false`); await browser.get(`${url}&prejoin=true&videoEnabled=false&audioEnabled=false`);
await utils.checkPrejoinIsPresent(); await utils.checkPrejoinIsPresent();
@ -90,7 +92,7 @@ describe('Checking stream elements by disabling/enabling the media', () => {
expect(await utils.getNumberOfElements('audio')).toEqual(0); expect(await utils.getNumberOfElements('audio')).toEqual(0);
}); });
it('should show a video element when a LOCAL participant shares its screen', async () => { it('should add a screen share video/audio when sharing screen with both camera and mic enabled', async () => {
await browser.get(`${url}&prejoin=true&videoEnabled=true&audioEnabled=true`); await browser.get(`${url}&prejoin=true&videoEnabled=true&audioEnabled=true`);
await utils.checkPrejoinIsPresent(); await utils.checkPrejoinIsPresent();
@ -115,9 +117,9 @@ describe('Checking stream elements by disabling/enabling the media', () => {
expect(await utils.getNumberOfElements('audio')).toEqual(1); expect(await utils.getNumberOfElements('audio')).toEqual(1);
}); });
/* ------------ Checking video elements with two participants ------------ */ /* ------------ Checking video/audio elements with two participants ------------ */
it('should show zero video elements when two participants join with VIDEO and AUDIO MUTED', async () => { it('should not render any video/audio elements when two participants join with both video and audio muted', async () => {
const roomName = `streams-${Date.now()}`; const roomName = `streams-${Date.now()}`;
const fixedUrl = `${url}&roomName=${roomName}&prejoin=false&videoEnabled=false&audioEnabled=false`; const fixedUrl = `${url}&roomName=${roomName}&prejoin=false&videoEnabled=false&audioEnabled=false`;
await browser.get(fixedUrl); await browser.get(fixedUrl);
@ -145,7 +147,7 @@ describe('Checking stream elements by disabling/enabling the media', () => {
expect(await utils.getNumberOfElements('audio')).toEqual(0); expect(await utils.getNumberOfElements('audio')).toEqual(0);
}); });
it('should show two video elements when a two participants join with audio muted', async () => { it('should render two video elements and no audio when two participants join with audio muted', async () => {
const roomName = `streams-${Date.now()}`; const roomName = `streams-${Date.now()}`;
const fixedUrl = `${url}&roomName=${roomName}&prejoin=false&videoEnabled=true&audioEnabled=false`; const fixedUrl = `${url}&roomName=${roomName}&prejoin=false&videoEnabled=true&audioEnabled=false`;
await browser.get(fixedUrl); await browser.get(fixedUrl);
@ -158,6 +160,8 @@ describe('Checking stream elements by disabling/enabling the media', () => {
expect(await utils.getNumberOfElements('audio')).toEqual(0); expect(await utils.getNumberOfElements('audio')).toEqual(0);
const tabs = await utils.openTab(fixedUrl); const tabs = await utils.openTab(fixedUrl);
await browser.sleep(1000);
await browser.switchTo().window(tabs[0]); await browser.switchTo().window(tabs[0]);
await utils.waitForElement('.OV_stream.remote'); await utils.waitForElement('.OV_stream.remote');
@ -173,7 +177,7 @@ describe('Checking stream elements by disabling/enabling the media', () => {
expect(await utils.getNumberOfElements('audio')).toEqual(0); expect(await utils.getNumberOfElements('audio')).toEqual(0);
}); });
it('should show zero video elements when two participants join with video disabled', async () => { it('should not render any video elements but should render two audio elements when two participants join with video disabled', async () => {
const roomName = `streams-${Date.now()}`; const roomName = `streams-${Date.now()}`;
const fixedUrl = `${url}&roomName=${roomName}&prejoin=false&videoEnabled=false`; const fixedUrl = `${url}&roomName=${roomName}&prejoin=false&videoEnabled=false`;
await browser.get(fixedUrl); await browser.get(fixedUrl);
@ -186,6 +190,7 @@ describe('Checking stream elements by disabling/enabling the media', () => {
expect(await utils.getNumberOfElements('audio')).toEqual(1); expect(await utils.getNumberOfElements('audio')).toEqual(1);
const tabs = await utils.openTab(fixedUrl); const tabs = await utils.openTab(fixedUrl);
await browser.sleep(1000);
await browser.switchTo().window(tabs[0]); await browser.switchTo().window(tabs[0]);
await utils.waitForElement('.OV_stream.remote'); await utils.waitForElement('.OV_stream.remote');
@ -201,7 +206,7 @@ describe('Checking stream elements by disabling/enabling the media', () => {
expect(await utils.getNumberOfElements('audio')).toEqual(2); expect(await utils.getNumberOfElements('audio')).toEqual(2);
}); });
it('should show 3 video elements when a participant shares its screen with AUDIO and VIDEO MUTED', async () => { it('should add a screen share video/audio when a participant with both video and audio muted shares their screen (two participants)', async () => {
const roomName = `streams-${Date.now()}`; const roomName = `streams-${Date.now()}`;
const fixedUrl = `${url}&roomName=${roomName}&prejoin=false&videoEnabled=false&audioEnabled=false`; const fixedUrl = `${url}&roomName=${roomName}&prejoin=false&videoEnabled=false&audioEnabled=false`;
await browser.get(fixedUrl); await browser.get(fixedUrl);
@ -240,7 +245,7 @@ describe('Checking stream elements by disabling/enabling the media', () => {
expect(await utils.getNumberOfElements('audio')).toEqual(0); expect(await utils.getNumberOfElements('audio')).toEqual(0);
}); });
it('should show 3 video elements when a REMOTE participant shares its screen', async () => { it('should add a screen share video/audio when a remote participant with both video and audio enabled shares their screen', async () => {
const roomName = `streams-${Date.now()}`; const roomName = `streams-${Date.now()}`;
const fixedUrl = `${url}&roomName=${roomName}&prejoin=false&videoEnabled=true&audioEnabled=true`; const fixedUrl = `${url}&roomName=${roomName}&prejoin=false&videoEnabled=true&audioEnabled=true`;
await browser.get(fixedUrl); await browser.get(fixedUrl);
@ -279,7 +284,7 @@ describe('Checking stream elements by disabling/enabling the media', () => {
expect(await utils.getNumberOfElements('audio')).toEqual(2); expect(await utils.getNumberOfElements('audio')).toEqual(2);
}); });
it('should show 4 video elements when a two participants share theirs screen', async () => { it('should add a screen share video/audio for both participants when both share their screen with video/audio muted', async () => {
const roomName = `streams-${Date.now()}`; const roomName = `streams-${Date.now()}`;
const fixedUrl = `${url}&roomName=${roomName}&prejoin=false&videoEnabled=false&audioEnabled=false`; const fixedUrl = `${url}&roomName=${roomName}&prejoin=false&videoEnabled=false&audioEnabled=false`;
await browser.get(fixedUrl); await browser.get(fixedUrl);
@ -323,15 +328,15 @@ describe('Checking stream elements by disabling/enabling the media', () => {
}); });
}); });
describe('Testing stream features', () => { describe('Stream UI controls and interaction features', () => {
let browser: WebDriver; let browser: WebDriver;
let utils: OpenViduComponentsPO; let utils: OpenViduComponentsPO;
async function createChromeBrowser(): Promise<WebDriver> { async function createChromeBrowser(): Promise<WebDriver> {
return await new Builder() return await new Builder()
.forBrowser(WebComponentConfig.browserName) .forBrowser(TestAppConfig.browserName)
.withCapabilities(WebComponentConfig.browserCapabilities) .withCapabilities(TestAppConfig.browserCapabilities)
.setChromeOptions(WebComponentConfig.browserOptions) .setChromeOptions(TestAppConfig.browserOptions)
.usingServer(WebComponentConfig.seleniumAddress) .usingServer(TestAppConfig.seleniumAddress)
.build(); .build();
} }
@ -341,6 +346,9 @@ describe('Testing stream features', () => {
}); });
afterEach(async () => { afterEach(async () => {
try {
await utils.leaveRoom();
} catch (error) {}
await browser.quit(); await browser.quit();
}); });
@ -658,7 +666,7 @@ describe('Testing stream features', () => {
expect(streamProps.y).toEqual(0); expect(streamProps.y).toEqual(0);
}); });
xit('should show the audio detection elements when participant is speaking', async () => { it('should show the audio detection elements when participant is speaking', async () => {
const roomName = 'speakingE2E'; const roomName = 'speakingE2E';
const fixedUrl = `${url}&roomName=${roomName}&prejoin=false`; const fixedUrl = `${url}&roomName=${roomName}&prejoin=false`;
await browser.get(`${fixedUrl}&audioEnabled=false`); await browser.get(`${fixedUrl}&audioEnabled=false`);
@ -668,25 +676,30 @@ describe('Testing stream features', () => {
// Starting new browser for adding the second participant // Starting new browser for adding the second participant
const newTabScript = `window.open("${fixedUrl}")`; const newTabScript = `window.open("${fixedUrl}")`;
await browser.executeScript(newTabScript); await browser.executeScript(newTabScript);
await browser.sleep(1000);
const tabs = await browser.getAllWindowHandles(); const tabs = await browser.getAllWindowHandles();
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);
}); });
}); });
describe('Testing video is playing', () => { describe('Video playback reliability with different media states', () => {
let browser: WebDriver; let browser: WebDriver;
let utils: OpenViduComponentsPO; let utils: OpenViduComponentsPO;
async function createChromeBrowser(): Promise<WebDriver> { async function createChromeBrowser(): Promise<WebDriver> {
return await new Builder() return await new Builder()
.forBrowser(WebComponentConfig.browserName) .forBrowser(TestAppConfig.browserName)
.withCapabilities(WebComponentConfig.browserCapabilities) .withCapabilities(TestAppConfig.browserCapabilities)
.setChromeOptions(WebComponentConfig.browserOptions) .setChromeOptions(TestAppConfig.browserOptions)
.usingServer(WebComponentConfig.seleniumAddress) .usingServer(TestAppConfig.seleniumAddress)
.build(); .build();
} }
@ -696,6 +709,9 @@ describe('Testing video is playing', () => {
}); });
afterEach(async () => { afterEach(async () => {
try {
await utils.leaveRoom();
} catch (error) {}
await browser.quit(); await browser.quit();
}); });

View File

@ -1,19 +1,18 @@
import { Builder, WebDriver } from 'selenium-webdriver'; import { Builder, WebDriver } from 'selenium-webdriver';
import { OPENVIDU_CALL_SERVER } from '../config'; import { TestAppConfig } from './selenium.conf';
import { WebComponentConfig } from '../selenium.conf'; import { OpenViduComponentsPO } from './utils.po.test';
import { OpenViduComponentsPO } from '../utils.po.test';
const url = `${WebComponentConfig.appUrl}?OV_URL=${OPENVIDU_CALL_SERVER}`; const url = TestAppConfig.appUrl;
describe('Testing TOOLBAR features', () => { describe('Toolbar button functionality for local media control', () => {
let browser: WebDriver; let browser: WebDriver;
let utils: OpenViduComponentsPO; let utils: OpenViduComponentsPO;
async function createChromeBrowser(): Promise<WebDriver> { async function createChromeBrowser(): Promise<WebDriver> {
return await new Builder() return await new Builder()
.forBrowser(WebComponentConfig.browserName) .forBrowser(TestAppConfig.browserName)
.withCapabilities(WebComponentConfig.browserCapabilities) .withCapabilities(TestAppConfig.browserCapabilities)
.setChromeOptions(WebComponentConfig.browserOptions) .setChromeOptions(TestAppConfig.browserOptions)
.usingServer(WebComponentConfig.seleniumAddress) .usingServer(TestAppConfig.seleniumAddress)
.build(); .build();
} }
@ -23,10 +22,13 @@ describe('Testing TOOLBAR features', () => {
}); });
afterEach(async () => { afterEach(async () => {
try {
await utils.leaveRoom();
} catch (error) {}
await browser.quit(); await browser.quit();
}); });
it('should mute and unmute the local microphone', async () => { it('should toggle mute/unmute on the local microphone and update the icon accordingly', async () => {
await browser.get(`${url}&prejoin=false`); await browser.get(`${url}&prejoin=false`);
await utils.checkLayoutPresent(); await utils.checkLayoutPresent();
@ -43,7 +45,7 @@ describe('Testing TOOLBAR features', () => {
expect(await utils.isPresent('#mic-btn #mic')).toBeTrue(); expect(await utils.isPresent('#mic-btn #mic')).toBeTrue();
}); });
it('should mute and unmute the local camera', async () => { it('should toggle mute/unmute on the local camera and update the icon accordingly', async () => {
await browser.get(`${url}&prejoin=false`); await browser.get(`${url}&prejoin=false`);
await utils.checkLayoutPresent(); await utils.checkLayoutPresent();

View File

@ -136,6 +136,42 @@ export class OpenViduComponentsPO {
await this.clickOn('#fullscreen-btn'); await this.clickOn('#fullscreen-btn');
} }
async leaveRoom() {
try {
// Close any open panels or menus clicking on the body
await this.clickOn('body');
await this.browser.sleep(300);
// Verify that the leave button is present
await this.waitForElement('#leave-btn');
// Click on the leave button
await this.clickOn('#leave-btn');
// Verify that the session container is no longer present
await this.browser.wait(
async () => {
return !(await this.isPresent('#session-container'));
},
this.TIMEOUT,
'Session container should disappear after leaving room'
);
// Wait for the prejoin container to be present again
await this.browser.sleep(500);
// Verify that there are no video elements left in the DOM
const videoCount = await this.getNumberOfElements('video');
if (videoCount > 0) {
console.warn(`Warning: ${videoCount} video elements still present after leaving room`);
}
} catch (error) {
console.error('Error during leaveRoom:', error);
throw error;
}
}
async togglePanel(panelName: string) { async togglePanel(panelName: string) {
switch (panelName) { switch (panelName) {
case 'activities': case 'activities':
@ -159,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

@ -1,279 +0,0 @@
import monkeyPatchMediaDevices from './utils/media-devices.js';
var MINIMAL;
var LANG;
var CAPTIONS_LANG;
var CUSTOM_LANG_OPTIONS;
var CUSTOM_CAPTIONS_LANG_OPTIONS;
var PREJOIN;
var VIDEO_ENABLED;
var AUDIO_ENABLED;
var SCREENSHARE_BUTTON;
var FULLSCREEN_BUTTON;
var ACTIVITIES_PANEL_BUTTON;
var RECORDING_BUTTON;
var BROADCASTING_BUTTON;
var CHAT_PANEL_BUTTON;
var DISPLAY_LOGO;
var DISPLAY_ROOM_NAME;
var DISPLAY_PARTICIPANT_NAME;
var DISPLAY_AUDIO_DETECTION;
var VIDEO_CONTROLS;
var LEAVE_BUTTON;
var PARTICIPANT_MUTE_BUTTON;
var PARTICIPANTS_PANEL_BUTTON;
var ACTIVITIES_RECORDING_ACTIVITY;
var ACTIVITIES_BROADCASTING_ACTIVITY;
var TOOLBAR_SETTINGS_BUTTON;
var CAPTIONS_BUTTON;
var ROOM_NAME;
var FAKE_DEVICES;
var FAKE_RECORDINGS;
var PARTICIPANT_NAME;
var OPENVIDU_CALL_SERVER_URL;
document.addEventListener('DOMContentLoaded', () => {
var url = new URL(window.location.href);
OPENVIDU_CALL_SERVER_URL = url.searchParams.get('OPENVIDU_CALL_SERVER_URL') || 'http://localhost:6080';
FAKE_DEVICES = url.searchParams.get('fakeDevices') === null ? false : url.searchParams.get('fakeDevices') === 'true';
FAKE_RECORDINGS = url.searchParams.get('fakeRecordings') === null ? false : url.searchParams.get('fakeRecordings') === 'true';
// Directives
MINIMAL = url.searchParams.get('minimal') === null ? false : url.searchParams.get('minimal') === 'true';
LANG = url.searchParams.get('lang') || 'en';
CUSTOM_LANG_OPTIONS = url.searchParams.get('langOptions') === null ? false : url.searchParams.get('langOptions') === 'true';
// CAPTIONS_LANG = url.searchParams.get('captionsLang') || 'en-US';
// CUSTOM_CAPTIONS_LANG_OPTIONS = url.searchParams.get('captionsLangOptions') === null ? false : url.searchParams.get('captionsLangOptions') === 'true';
PARTICIPANT_NAME =
url.searchParams.get('participantName') === null
? 'TEST_USER' + Math.random().toString(36).substr(2, 9)
: url.searchParams.get('participantName');
PREJOIN = url.searchParams.get('prejoin') === null ? true : url.searchParams.get('prejoin') === 'true';
VIDEO_ENABLED = url.searchParams.get('videoEnabled') === null ? true : url.searchParams.get('videoEnabled') === 'true';
AUDIO_ENABLED = url.searchParams.get('audioEnabled') === null ? true : url.searchParams.get('audioEnabled') === 'true';
SCREENSHARE_BUTTON = url.searchParams.get('screenshareBtn') === null ? true : url.searchParams.get('screenshareBtn') === 'true';
RECORDING_BUTTON =
url.searchParams.get('toolbarRecordingButton') === null ? true : url.searchParams.get('toolbarRecordingButton') === 'true';
FULLSCREEN_BUTTON = url.searchParams.get('fullscreenBtn') === null ? true : url.searchParams.get('fullscreenBtn') === 'true';
BROADCASTING_BUTTON =
url.searchParams.get('toolbarBroadcastingButton') === null ? true : url.searchParams.get('toolbarBroadcastingButton') === 'true';
TOOLBAR_SETTINGS_BUTTON =
url.searchParams.get('toolbarSettingsBtn') === null ? true : url.searchParams.get('toolbarSettingsBtn') === 'true';
CAPTIONS_BUTTON = url.searchParams.get('toolbarCaptionsBtn') === null ? true : url.searchParams.get('toolbarCaptionsBtn') === 'true';
LEAVE_BUTTON = url.searchParams.get('leaveBtn') === null ? true : url.searchParams.get('leaveBtn') === 'true';
ACTIVITIES_PANEL_BUTTON =
url.searchParams.get('activitiesPanelBtn') === null ? true : url.searchParams.get('activitiesPanelBtn') === 'true';
CHAT_PANEL_BUTTON = url.searchParams.get('chatPanelBtn') === null ? true : url.searchParams.get('chatPanelBtn') === 'true';
PARTICIPANTS_PANEL_BUTTON =
url.searchParams.get('participantsPanelBtn') === null ? true : url.searchParams.get('participantsPanelBtn') === 'true';
ACTIVITIES_BROADCASTING_ACTIVITY =
url.searchParams.get('activitiesPanelBroadcastingActivity') === null
? true
: url.searchParams.get('activitiesPanelBroadcastingActivity') === 'true';
ACTIVITIES_RECORDING_ACTIVITY =
url.searchParams.get('activitiesPanelRecordingActivity') === null
? true
: url.searchParams.get('activitiesPanelRecordingActivity') === 'true';
DISPLAY_LOGO = url.searchParams.get('displayLogo') === null ? true : url.searchParams.get('displayLogo') === 'true';
DISPLAY_ROOM_NAME = url.searchParams.get('displayRoomName') === null ? true : url.searchParams.get('displayRoomName') === 'true';
DISPLAY_PARTICIPANT_NAME =
url.searchParams.get('displayParticipantName') === null ? true : url.searchParams.get('displayParticipantName') === 'true';
DISPLAY_AUDIO_DETECTION =
url.searchParams.get('displayAudioDetection') === null ? true : url.searchParams.get('displayAudioDetection') === 'true';
VIDEO_CONTROLS = url.searchParams.get('videoControls') === null ? true : url.searchParams.get('videoControls') === 'true';
PARTICIPANT_MUTE_BUTTON =
url.searchParams.get('participantMuteBtn') === null ? true : url.searchParams.get('participantMuteBtn') === 'true';
ROOM_NAME = url.searchParams.get('roomName') === null ? `E2ESession${Math.floor(Date.now())}` : url.searchParams.get('roomName');
var webComponent = document.querySelector('openvidu-webcomponent');
webComponent.addEventListener('onTokenRequested', (event) => {
appendElement('onTokenRequested');
console.log('Token ready', event.detail);
joinSession(ROOM_NAME, event.detail);
});
webComponent.addEventListener('onReadyToJoin', (event) => appendElement('onReadyToJoin'));
webComponent.addEventListener('onRoomDisconnected', (event) => appendElement('onRoomDisconnected'));
webComponent.addEventListener('onParticipantLeft', (event) => appendElement('onParticipantLeft'));
webComponent.addEventListener('onVideoEnabledChanged', (event) => appendElement('onVideoEnabledChanged-' + event.detail));
webComponent.addEventListener('onVideoDeviceChanged', (event) => appendElement('onVideoDeviceChanged'));
webComponent.addEventListener('onAudioEnabledChanged', (eSESSIONvent) => appendElement('onAudioEnabledChanged-' + event.detail));
webComponent.addEventListener('onAudioDeviceChanged', (event) => appendElement('onAudioDeviceChanged'));
webComponent.addEventListener('onScreenShareEnabledChanged', (event) => appendElement('onScreenShareEnabledChanged'));
webComponent.addEventListener('onParticipantsPanelStatusChanged', (event) =>
appendElement('onParticipantsPanelStatusChanged-' + event.detail.isOpened)
);
webComponent.addEventListener('onLangChanged', (event) => appendElement('onLangChanged-' + event.detail.lang));
webComponent.addEventListener('onChatPanelStatusChanged', (event) =>
appendElement('onChatPanelStatusChanged-' + event.detail.isOpened)
);
webComponent.addEventListener('onActivitiesPanelStatusChanged', (event) =>
appendElement('onActivitiesPanelStatusChanged-' + event.detail.isOpened)
);
webComponent.addEventListener('onSettingsPanelStatusChanged', (event) =>
appendElement('onSettingsPanelStatusChanged-' + event.detail.isOpened)
);
webComponent.addEventListener('onFullscreenEnabledChanged', (event) => appendElement('onFullscreenEnabledChanged-' + event.detail));
webComponent.addEventListener('onRecordingStartRequested', async (event) => {
appendElement('onRecordingStartRequested-' + event.detail.roomName);
// Can't test the recording
// RECORDING_ID = await startRecording(SESSION_NAME);
});
// Can't test the recording
// webComponent.addEventListener('onRecordingStopRequested', async (event) => {
// appendElement('onRecordingStopRequested-' + event.detail.roomName);
// await stopRecording(RECORDING_ID);
// });
webComponent.addEventListener('onRecordingStopRequested', async (event) => {
appendElement('onRecordingStopRequested-' + event.detail.roomName);
});
// Can't test the recording
// webComponent.addEventListener('onActivitiesPanelStopRecordingClicked', async (event) => {
// appendElement('onActivitiesPanelStopRecordingClicked');
// await stopRecording(RECORDING_ID);
// });
webComponent.addEventListener('onRecordingDeleteRequested', (event) => {
const { roomName, recordingId } = event.detail;
appendElement(`onRecordingDeleteRequested-${roomName}-${recordingId}`);
});
webComponent.addEventListener('onBroadcastingStartRequested', async (event) => {
const { roomName, broadcastUrl } = event.detail;
appendElement(`onBroadcastingStartRequested-${roomName}-${broadcastUrl}`);
});
webComponent.addEventListener('onActivitiesPanelStopBroadcastingClicked', async (event) => {
appendElement('onActivitiesPanelStopBroadcastingClicked');
});
webComponent.addEventListener('onRoomCreated', (event) => {
var room = event.detail;
appendElement('onRoomCreated');
room.on('disconnected', (e) => {
console.log('Room disconnected', e);
appendElement('roomDisconnected');
});
});
webComponent.addEventListener('onParticipantCreated', (event) => {
var participant = event.detail;
appendElement(`${participant.name}-onParticipantCreated`);
});
setWebcomponentAttributes();
});
function setWebcomponentAttributes() {
var webComponent = document.querySelector('openvidu-webcomponent');
webComponent.participantName = PARTICIPANT_NAME;
webComponent.minimal = MINIMAL;
webComponent.lang = LANG;
if (CUSTOM_LANG_OPTIONS) {
webComponent.langOptions = [
{ name: 'Esp', lang: 'es' },
{ name: 'Eng', lang: 'en' }
];
}
// TODO: Uncomment when the captions are implemented
// webComponent.captionsLang = CAPTIONS_LANG;
// if (CUSTOM_CAPTIONS_LANG_OPTIONS) {
// webComponent.captionsLangOptions = [
// { name: 'Esp', lang: 'es-ES' },
// { name: 'Eng', lang: 'en-US' }
// ];
// }
if (FAKE_DEVICES) {
console.warn('Using fake devices');
monkeyPatchMediaDevices();
}
if (FAKE_RECORDINGS) {
console.warn('Using fake recordings');
webComponent.recordingActivityRecordingsList = [{ status: 'ready', filename: 'fakeRecording' }];
}
webComponent.prejoin = PREJOIN;
webComponent.videoEnabled = VIDEO_ENABLED;
webComponent.audioEnabled = AUDIO_ENABLED;
webComponent.toolbarScreenshareButton = SCREENSHARE_BUTTON;
webComponent.toolbarFullscreenButton = FULLSCREEN_BUTTON;
webComponent.toolbarSettingsButton = TOOLBAR_SETTINGS_BUTTON;
// webComponent.toolbarCaptionsButton = CAPTIONS_BUTTON;
webComponent.toolbarLeaveButton = LEAVE_BUTTON;
webComponent.toolbarRecordingButton = RECORDING_BUTTON;
webComponent.toolbarBroadcastingButton = BROADCASTING_BUTTON;
webComponent.toolbarActivitiesPanelButton = ACTIVITIES_PANEL_BUTTON;
webComponent.toolbarChatPanelButton = CHAT_PANEL_BUTTON;
webComponent.toolbarParticipantsPanelButton = PARTICIPANTS_PANEL_BUTTON;
webComponent.toolbarDisplayLogo = DISPLAY_LOGO;
webComponent.toolbarDisplayRoomName = DISPLAY_ROOM_NAME;
webComponent.streamDisplayParticipantName = DISPLAY_PARTICIPANT_NAME;
webComponent.streamDisplayAudioDetection = DISPLAY_AUDIO_DETECTION;
webComponent.streamVideoControls = VIDEO_CONTROLS;
webComponent.participantPanelItemMuteButton = PARTICIPANT_MUTE_BUTTON;
webComponent.activitiesPanelRecordingActivity = ACTIVITIES_RECORDING_ACTIVITY;
webComponent.activitiesPanelBroadcastingActivity = ACTIVITIES_BROADCASTING_ACTIVITY;
}
function appendElement(id) {
var eventsDiv = document.getElementById('events');
eventsDiv.setAttribute('style', 'position: absolute;');
var element = document.createElement('div');
element.setAttribute('id', id);
element.setAttribute('style', 'height: 1px;');
eventsDiv.appendChild(element);
}
async function joinSession(roomName, participantName) {
var webComponent = document.querySelector('openvidu-webcomponent');
console.log('Joining session', roomName, participantName);
try {
webComponent.token = await getToken(roomName, participantName);
} catch (error) {
webComponent.tokenError = error;
}
}
async function getToken(roomName, participantName) {
try {
const response = await fetch(OPENVIDU_CALL_SERVER_URL + '/call/api/rooms', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
// 'Authorization': 'Basic ' + btoa('OPENVIDUAPP:' + OPENVIDU_SECRET),
},
body: JSON.stringify({
participantName,
roomName
})
});
if (!response.ok) {
throw new Error('Failed to fetch token');
}
const data = await response.json();
return data.token;
} catch (error) {
console.error(error);
throw error;
}
}

View File

@ -1,40 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<title>openvidu-web-component</title>
<script type="module" src="utils/filter-stream.js"></script>
<!-- <script type="module" src="utils/shader-renderer.js"></script> -->
<script type="module" src="utils/media-devices.js"></script>
<script type="module" src="app.js"></script>
<script src="openvidu-webcomponent-dev.js"></script>
<link rel="stylesheet" href="openvidu-webcomponent-dev.css" />
<style>
:root {
--ov-background-color: #303030;
--ov-secondary-action-color: #3e3f3f;
--ov-accent-action-color: #598eff;
--ov-error-color: #eb5144;
--ov-accent-action-color: #ffae35;
--ov-light-color: #e6e6e6;
--ov-secondary-action-color: #3a3d3d;
--ov-text-primary-color: #ffffff;
--ov-text-primary-color: #1d1d1d;
--ov-surface-color: #ffffff;
--ov-toolbar-buttons-radius: 50%;
--ov-leave-button-radius: 10px;
--ov-video-radius: 5px;
--ov-surface-radius: 5px;
}
</style>
</head>
<body>
<div id="events"></div>
<!-- OpenVidu Web Component -->
<openvidu-webcomponent></openvidu-webcomponent>
</body>
</html>

View File

@ -1,60 +0,0 @@
const fs = require('fs-extra');
const concat = require('concat');
const VERSION = require('./package.json').version;
const ovWebcomponentRCPath = './dist/openvidu-webcomponent-rc';
const ovWebcomponentProdPath = './dist/openvidu-webcomponent';
module.exports.buildWebcomponent = async () => {
console.log('Building OpenVidu Web Component (' + VERSION + ')');
const tutorialWcPath = '../../openvidu-tutorials/openvidu-webcomponent/web';
const e2eWcPath = './e2e/webcomponent-app';
try {
await buildElement();
await copyFiles(tutorialWcPath);
await copyFiles(e2eWcPath);
await renameWebComponentTestName(e2eWcPath);
console.log(`OpenVidu Web Component (${VERSION}) built`);
} catch (error) {
console.error(error);
}
};
async function buildElement() {
const files = [`${ovWebcomponentRCPath}/runtime.js`, `${ovWebcomponentRCPath}/main.js`, `${ovWebcomponentRCPath}/polyfills.js`];
try {
await fs.ensureDir('./dist/openvidu-webcomponent');
await concat(files, `${ovWebcomponentProdPath}/openvidu-webcomponent-${VERSION}.js`);
await fs.copy(`${ovWebcomponentRCPath}/styles.css`, `${ovWebcomponentProdPath}/openvidu-webcomponent-${VERSION}.css`);
// await fs.copy(
// "./dist/openvidu-webcomponent/assets",
// "./openvidu-webcomponent/assets"
// );
} catch (err) {
console.error('Error executing build function in webcomponent-builds.js');
throw err;
}
}
function renameWebComponentTestName(dir) {
fs.renameSync(`${dir}/openvidu-webcomponent-${VERSION}.js`, `${dir}/openvidu-webcomponent-dev.js`);
fs.renameSync(`${dir}/openvidu-webcomponent-${VERSION}.css`, `${dir}/openvidu-webcomponent-dev.css`);
}
async function copyFiles(destination) {
if (fs.existsSync(destination)) {
try {
console.log(`Copying openvidu-webcomponent files from: ${ovWebcomponentProdPath} to: ${destination}`);
await fs.ensureDir(ovWebcomponentProdPath);
await fs.copy(ovWebcomponentProdPath, destination);
} catch (err) {
console.error('Error executing copy function in webcomponent-builds.js');
throw err;
}
}
}
this.buildWebcomponent();

File diff suppressed because it is too large Load Diff

View File

@ -1,35 +1,33 @@
{ {
"dependencies": { "dependencies": {
"@angular/animations": "18.2.5", "@angular/animations": "19.2.8",
"@angular/cdk": "18.2.5", "@angular/cdk": "19.2.11",
"@angular/common": "18.2.5", "@angular/common": "19.2.8",
"@angular/core": "18.2.5", "@angular/core": "19.2.8",
"@angular/forms": "18.2.5", "@angular/forms": "19.2.8",
"@angular/material": "18.2.5", "@angular/material": "19.2.11",
"@angular/platform-browser": "18.2.5", "@angular/platform-browser": "19.2.8",
"@angular/platform-browser-dynamic": "18.2.5", "@angular/platform-browser-dynamic": "19.2.8",
"@angular/router": "18.2.5", "@angular/router": "19.2.8",
"@livekit/track-processors": "0.3.2", "@livekit/track-processors": "^0.5.6",
"@types/dom-mediacapture-transform": "^0.1.11",
"autolinker": "4.0.0", "autolinker": "4.0.0",
"livekit-client": "2.5.2", "livekit-client": "2.11.4",
"rxjs": "7.8.1", "rxjs": "7.8.1",
"tslib": "2.7.0", "tslib": "2.7.0",
"zone.js": "^0.14.6" "zone.js": "^0.15.0"
}, },
"devDependencies": { "devDependencies": {
"@angular-devkit/build-angular": "18.2.5", "@angular-devkit/build-angular": "19.2.9",
"@angular/cli": "18.2.5", "@angular/cli": "19.2.9",
"@angular/compiler": "18.2.5", "@angular/compiler": "19.2.8",
"@angular/compiler-cli": "18.2.5", "@angular/compiler-cli": "19.2.8",
"@angular/elements": "18.2.5",
"@compodoc/compodoc": "^1.1.25", "@compodoc/compodoc": "^1.1.25",
"@types/dom-mediacapture-transform": "0.1.9",
"@types/dom-webcodecs": "0.1.11",
"@types/jasmine": "^5.1.4", "@types/jasmine": "^5.1.4",
"@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": "132.0.0", "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",
@ -49,13 +47,13 @@
"karma-mocha-reporter": "2.2.5", "karma-mocha-reporter": "2.2.5",
"karma-notify-reporter": "1.3.0", "karma-notify-reporter": "1.3.0",
"lint-staged": "^15.2.10", "lint-staged": "^15.2.10",
"ng-packagr": "18.2.1", "ng-packagr": "19.2.2",
"npm-watch": "^0.13.0", "npm-watch": "^0.13.0",
"prettier": "3.3.3", "prettier": "3.3.3",
"selenium-webdriver": "4.25.0", "selenium-webdriver": "4.32.0",
"ts-node": "10.9.2", "ts-node": "10.9.2",
"tslint": "6.1.3", "tslint": "6.1.3",
"typescript": "5.4.5", "typescript": "5.8.3",
"webpack-bundle-analyzer": "^4.10.2" "webpack-bundle-analyzer": "^4.10.2"
}, },
"name": "openvidu-components-testapp", "name": "openvidu-components-testapp",
@ -75,7 +73,6 @@
"start-prod": "npx http-server ./dist/openvidu-components-testapp/browser --port 4200", "start-prod": "npx http-server ./dist/openvidu-components-testapp/browser --port 4200",
"start:ssl": "ng serve --ssl --configuration development --host 0.0.0.0 --port 5080", "start:ssl": "ng serve --ssl --configuration development --host 0.0.0.0 --port 5080",
"build": "ng build openvidu-components-testapp --configuration production", "build": "ng build openvidu-components-testapp --configuration production",
"bundle-report": "ng build openvidu-webcomponent --stats-json --configuration production && webpack-bundle-analyzer dist/openvidu-webcomponent/stats.json",
"doc:build": "npx compodoc -c ./projects/openvidu-components-angular/doc/.compodocrc.json", "doc:build": "npx compodoc -c ./projects/openvidu-components-angular/doc/.compodocrc.json",
"doc:generate-directives-tutorials": "node ./projects/openvidu-components-angular/doc/scripts/generate-directive-tutorials.js", "doc:generate-directives-tutorials": "node ./projects/openvidu-components-angular/doc/scripts/generate-directive-tutorials.js",
"doc:generate-directive-tables": "node ./projects/openvidu-components-angular/doc/scripts/generate-directive-tables.js", "doc:generate-directive-tables": "node ./projects/openvidu-components-angular/doc/scripts/generate-directive-tables.js",
@ -89,22 +86,19 @@
"lib:test": "ng test openvidu-components-angular --no-watch --code-coverage", "lib:test": "ng test openvidu-components-angular --no-watch --code-coverage",
"e2e:nested-all": "tsc --project ./e2e && npx jasmine --fail-fast ./e2e/dist/nested-components/*.test.js", "e2e:nested-all": "tsc --project ./e2e && npx jasmine --fail-fast ./e2e/dist/nested-components/*.test.js",
"e2e:nested-events": "tsc --project ./e2e && npx jasmine --fail-fast ./e2e/dist/nested-components/events.test.js", "e2e:nested-events": "tsc --project ./e2e && npx jasmine --fail-fast ./e2e/dist/nested-components/events.test.js",
"e2e:nested-directives": "tsc --project ./e2e && npx jasmine --fail-fast ./e2e/dist/nested-components/directives.test.js", "e2e:nested-structural-directives": "tsc --project ./e2e && npx jasmine --fail-fast ./e2e/dist/nested-components/structural-directives.test.js",
"e2e:webcomponent-all": "tsc --project ./e2e && npx jasmine --fail-fast ./e2e/dist/webcomponent-e2e/**/*.test.js", "e2e:nested-attribute-directives": "tsc --project ./e2e && npx jasmine --fail-fast ./e2e/dist/nested-components/attribute-directives.test.js",
"e2e:webcomponent-directives": "tsc --project ./e2e && npx jasmine --fail-fast ./e2e/dist/webcomponent-e2e/api-directives.test.js", "e2e:lib-directives": "tsc --project ./e2e && npx jasmine --fail-fast ./e2e/dist/api-directives.test.js",
"e2e:webcomponent-captions": "tsc --project ./e2e && npx jasmine --fail-fast ./e2e/dist/webcomponent-e2e/captions.test.js", "e2e:lib-internal-directives": "tsc --project ./e2e && npx jasmine --fail-fast ./e2e/dist/internal-directives.test.js",
"e2e:webcomponent-chat": "tsc --project ./e2e && npx jasmine --fail-fast ./e2e/dist/webcomponent-e2e/chat.test.js", "e2e:lib-chat": "tsc --project ./e2e && npx jasmine --fail-fast ./e2e/dist/chat.test.js",
"e2e:webcomponent-events": "tsc --project ./e2e && npx jasmine --fail-fast ./e2e/dist/webcomponent-e2e/events.test.js", "e2e:lib-events": "tsc --project ./e2e && npx jasmine ./e2e/dist/events.test.js",
"e2e:webcomponent-media-devices": "tsc --project ./e2e && npx jasmine --fail-fast ./e2e/dist/webcomponent-e2e/media-devices.test.js", "e2e:lib-media-devices": "tsc --project ./e2e && npx jasmine --fail-fast ./e2e/dist/media-devices.test.js",
"e2e:webcomponent-panels": "tsc --project ./e2e && npx jasmine --fail-fast ./e2e/dist/webcomponent-e2e/panels.test.js", "e2e:lib-panels": "tsc --project ./e2e && npx jasmine --fail-fast ./e2e/dist/panels.test.js",
"e2e:webcomponent-screensharing": "tsc --project ./e2e && npx jasmine --fail-fast ./e2e/dist/webcomponent-e2e/screensharing.test.js", "e2e:lib-screensharing": "tsc --project ./e2e && npx jasmine --fail-fast ./e2e/dist/screensharing.test.js",
"e2e:webcomponent-stream": "tsc --project ./e2e && npx jasmine --fail-fast ./e2e/dist/webcomponent-e2e/stream.test.js", "e2e:lib-stream": "tsc --project ./e2e && npx jasmine --fail-fast ./e2e/dist/stream.test.js",
"e2e:webcomponent-toolbar": "tsc --project ./e2e && npx jasmine --fail-fast ./e2e/dist/webcomponent-e2e/toolbar.test.js", "e2e:lib-toolbar": "tsc --project ./e2e && npx jasmine --fail-fast ./e2e/dist/toolbar.test.js",
"webcomponent:testing-build": "./node_modules/@angular/cli/bin/ng.js build openvidu-webcomponent --configuration testing && node ./openvidu-webcomponent-build.js",
"webcomponent:build": "./node_modules/@angular/cli/bin/ng.js build openvidu-webcomponent --configuration production && node ./openvidu-webcomponent-build.js",
"webcomponent:serve-testapp": "npx http-server ./e2e/webcomponent-app/",
"simulate:multiparty": "livekit-cli load-test --url ws://localhost:7880 --api-key devkey --api-secret secret --room daily-call --publishers 8 --audio-publishers 8 --identity-prefix Participant --identity publisher", "simulate:multiparty": "livekit-cli load-test --url ws://localhost:7880 --api-key devkey --api-secret secret --room daily-call --publishers 8 --audio-publishers 8 --identity-prefix Participant --identity publisher",
"husky": "cd .. && husky install" "husky": "cd .. && husky install"
}, },
"version": "3.1.0" "version": "3.3.0"
} }

View File

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

View File

@ -5,8 +5,7 @@
"../src/lib/directives/**/*.ts", "../src/lib/directives/**/*.ts",
"../src/lib/services/**/*.ts", "../src/lib/services/**/*.ts",
"../src/lib/models/**/*.ts", "../src/lib/models/**/*.ts",
"../src/lib/pipes/**/*.ts", "../src/lib/pipes/**/*.ts"
// "../../../src/app/openvidu-webcomponent/**/*.ts",
], ],
"exclude": [ "exclude": [
"src/test.ts", "src/test.ts",

View File

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

View File

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

View File

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

View File

@ -8,7 +8,8 @@ import { RecordingService } from '../../services/recording/recording.service';
@Component({ @Component({
selector: 'ov-admin-dashboard', selector: 'ov-admin-dashboard',
templateUrl: './admin-dashboard.component.html', templateUrl: './admin-dashboard.component.html',
styleUrls: ['./admin-dashboard.component.scss'] styleUrls: ['./admin-dashboard.component.scss'],
standalone: false
}) })
export class AdminDashboardComponent implements OnInit, OnDestroy { export class AdminDashboardComponent implements OnInit, OnDestroy {
/** /**

View File

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

View File

@ -7,7 +7,8 @@ import { OpenViduComponentsConfigService } from '../../services/config/directive
@Component({ @Component({
selector: 'ov-admin-login', selector: 'ov-admin-login',
templateUrl: './admin-login.component.html', templateUrl: './admin-login.component.html',
styleUrls: ['./admin-login.component.scss'] styleUrls: ['./admin-login.component.scss'],
standalone: false
}) })
export class AdminLoginComponent implements OnInit { export class AdminLoginComponent implements OnInit {
/** /**

View File

@ -10,6 +10,7 @@ import { Component } from '@angular/core';
<div class="stick loud play"></div> <div class="stick loud play"></div>
<div class="stick normal play"></div> <div class="stick normal play"></div>
</div>`, </div>`,
styleUrls: ['./audio-wave.component.scss'] styleUrls: ['./audio-wave.component.scss'],
standalone: false
}) })
export class AudioWaveComponent {} export class AudioWaveComponent {}

View File

@ -13,7 +13,8 @@ import { Component, Input } from '@angular/core';
</div> </div>
</div> </div>
`, `,
styleUrls: ['./avatar-profile.component.scss'] styleUrls: ['./avatar-profile.component.scss'],
standalone: false
}) })
export class AvatarProfileComponent { export class AvatarProfileComponent {
letter: string; letter: string;

View File

@ -5,8 +5,8 @@ import { MatDialogRef } from '@angular/material/dialog';
* @internal * @internal
*/ */
@Component({ @Component({
selector: 'app-delete-dialog', selector: 'app-delete-dialog',
template: ` template: `
<div mat-dialog-content>{{ 'PANEL.RECORDING.DELETE_QUESTION' | translate }}</div> <div mat-dialog-content>{{ 'PANEL.RECORDING.DELETE_QUESTION' | translate }}</div>
<div mat-dialog-actions> <div mat-dialog-actions>
<button mat-button [disableRipple]="true" (click)="close()">{{ 'PANEL.RECORDING.CANCEL' | translate }}</button> <button mat-button [disableRipple]="true" (click)="close()">{{ 'PANEL.RECORDING.CANCEL' | translate }}</button>
@ -15,8 +15,8 @@ import { MatDialogRef } from '@angular/material/dialog';
</button> </button>
</div> </div>
`, `,
styles: [ styles: [
` `
::ng-deep .mat-mdc-dialog-content { ::ng-deep .mat-mdc-dialog-content {
color: var(--ov-text-surface-color) !important; color: var(--ov-text-surface-color) !important;
} }
@ -36,7 +36,8 @@ import { MatDialogRef } from '@angular/material/dialog';
border-radius: var(--ov-surface-radius); border-radius: var(--ov-surface-radius);
} }
` `
] ],
standalone: false
}) })
export class DeleteDialogComponent { export class DeleteDialogComponent {
constructor(public dialogRef: MatDialogRef<DeleteDialogComponent>) {} constructor(public dialogRef: MatDialogRef<DeleteDialogComponent>) {}

View File

@ -7,16 +7,16 @@ import { DialogData } from '../../models/dialog.model';
*/ */
@Component({ @Component({
selector: 'ov-dialog-template', selector: 'ov-dialog-template',
template: ` template: `
<h1 mat-dialog-title>{{ data.title }}</h1> <h1 mat-dialog-title>{{ data.title }}</h1>
<div mat-dialog-content id="openvidu-dialog">{{ data.description }}</div> <div mat-dialog-content id="openvidu-dialog">{{ data.description }}</div>
<div mat-dialog-actions *ngIf="data.showActionButtons"> <div mat-dialog-actions *ngIf="data.showActionButtons">
<button mat-button [disableRipple]="true" (click)="close()">{{ 'PANEL.CLOSE' | translate }}</button> <button mat-button [disableRipple]="true" (click)="close()">{{ 'PANEL.CLOSE' | translate }}</button>
</div> </div>
`, `,
styles: [ styles: [
` `
::ng-deep .mat-mdc-dialog-content { ::ng-deep .mat-mdc-dialog-content {
color: var(--ov-text-surface-color) !important; color: var(--ov-text-surface-color) !important;
} }
@ -33,7 +33,8 @@ import { DialogData } from '../../models/dialog.model';
border-radius: var(--ov-surface-radius); border-radius: var(--ov-surface-radius);
} }
` `
] ],
standalone: false
}) })
export class DialogTemplateComponent { export class DialogTemplateComponent {
constructor( constructor(

View File

@ -8,8 +8,8 @@ import { DialogData } from '../../models/dialog.model';
*/ */
@Component({ @Component({
selector: 'ov-pro-feature-template', selector: 'ov-pro-feature-template',
template: ` template: `
<h1 mat-dialog-title>{{ data.title }}</h1> <h1 mat-dialog-title>{{ data.title }}</h1>
<div mat-dialog-content>{{ data.description }}</div> <div mat-dialog-content>{{ data.description }}</div>
<div mat-dialog-actions *ngIf="data.showActionButtons"> <div mat-dialog-actions *ngIf="data.showActionButtons">
@ -19,7 +19,8 @@ import { DialogData } from '../../models/dialog.model';
</button> </button>
<button mat-button (click)="close()">{{'PANEL.CLOSE' | translate}}</button> <button mat-button (click)="close()">{{'PANEL.CLOSE' | translate}}</button>
</div> </div>
` `,
standalone: false
}) })
export class ProFeatureDialogTemplateComponent { export class ProFeatureDialogTemplateComponent {
constructor(public dialogRef: MatDialogRef<ProFeatureDialogTemplateComponent>, @Inject(MAT_DIALOG_DATA) public data: DialogData) {} constructor(public dialogRef: MatDialogRef<ProFeatureDialogTemplateComponent>, @Inject(MAT_DIALOG_DATA) public data: DialogData) {}

View File

@ -6,8 +6,8 @@ import { RecordingDialogData } from '../../models/dialog.model';
* @internal * @internal
*/ */
@Component({ @Component({
selector: 'app-recording-dialog', selector: 'app-recording-dialog',
template: ` template: `
<div mat-dialog-content> <div mat-dialog-content>
<video #videoElement controls autoplay [src]="src" (error)="handleError()"></video> <video #videoElement controls autoplay [src]="src" (error)="handleError()"></video>
</div> </div>
@ -15,8 +15,8 @@ import { RecordingDialogData } from '../../models/dialog.model';
<button mat-button [disableRipple]="true" (click)="close()">{{ 'PANEL.CLOSE' | translate }}</button> <button mat-button [disableRipple]="true" (click)="close()">{{ 'PANEL.CLOSE' | translate }}</button>
</div> </div>
`, `,
styles: [ styles: [
` `
::ng-deep .mat-mdc-dialog-content { ::ng-deep .mat-mdc-dialog-content {
color: var(--ov-text-surface-color) !important; color: var(--ov-text-surface-color) !important;
} }
@ -38,7 +38,8 @@ import { RecordingDialogData } from '../../models/dialog.model';
border-radius: var(--ov-surface-radius); border-radius: var(--ov-surface-radius);
} }
` `
] ],
standalone: false
}) })
export class RecordingDialogComponent { export class RecordingDialogComponent {
@ViewChild('videoElement', { static: true }) videoElement: ElementRef<HTMLVideoElement>; @ViewChild('videoElement', { static: true }) videoElement: ElementRef<HTMLVideoElement>;

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

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

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';
@ -19,8 +21,8 @@ import { ParticipantService } from '../../services/participant/participant.servi
import { CdkDrag } from '@angular/cdk/drag-drop'; 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 { ServiceConfigService } from '../../services/config/service-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';
/** /**
* *
@ -31,7 +33,8 @@ import { OpenViduComponentsConfigService } from '../../services/config/directive
selector: 'ov-layout', selector: 'ov-layout',
templateUrl: './layout.component.html', templateUrl: './layout.component.html',
styleUrls: ['./layout.component.scss'], styleUrls: ['./layout.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush changeDetection: ChangeDetectionStrategy.OnPush,
standalone: false
}) })
export class LayoutComponent implements OnInit, OnDestroy, AfterViewInit { export class LayoutComponent implements OnInit, OnDestroy, AfterViewInit {
/** /**
@ -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,31 +98,31 @@ 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;
private layoutService: LayoutService;
/** /**
* @ignore * @ignore
*/ */
constructor( constructor(
private serviceConfig: ServiceConfigService, private layoutService: LayoutService,
private panelService: PanelService, private panelService: PanelService,
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
this.layoutService = this.serviceConfig.getLayoutService(); ) {}
}
ngOnInit(): void { ngOnInit(): void {
this.setupTemplates();
this.subscribeToParticipants(); this.subscribeToParticipants();
this.subscribeToCaptions(); this.subscribeToCaptions();
} }
@ -110,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();
} }
@ -129,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();
@ -138,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) {
@ -149,20 +201,18 @@ export class LayoutComponent implements OnInit, OnDestroy, AfterViewInit {
} }
}); });
this.remoteParticipantsSubs = combineLatest([ combineLatest([this.participantService.remoteParticipants$, this.directiveService.layoutRemoteParticipants$])
this.participantService.remoteParticipants$, .pipe(
this.directiveService.layoutRemoteParticipants$ map(([serviceParticipants, directiveParticipants]) =>
]) directiveParticipants !== undefined ? directiveParticipants : serviceParticipants
.pipe( ),
map(([serviceParticipants, directiveParticipants]) => takeUntil(this.destroy$)
directiveParticipants !== undefined ? directiveParticipants : serviceParticipants
) )
) .subscribe((participants) => {
.subscribe((participants) => { this.remoteParticipants = participants;
this.remoteParticipants = participants; this.layoutService.update();
this.layoutService.update(); this.cd.markForCheck();
this.cd.markForCheck(); });
});
} }
private listenToResizeLayout() { private listenToResizeLayout() {
@ -221,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

@ -18,7 +18,8 @@ import { Track } from 'livekit-client';
transition(':enter', [style({ opacity: 0 }), animate('100ms', style({ opacity: 1 }))]), transition(':enter', [style({ opacity: 0 }), animate('100ms', style({ opacity: 1 }))]),
transition(':leave', [style({ opacity: 1 }), animate('200ms', style({ opacity: 0 }))]) transition(':leave', [style({ opacity: 1 }), animate('200ms', style({ opacity: 0 }))])
]) ])
] ],
standalone: false
}) })
export class MediaElementComponent implements AfterViewInit { export class MediaElementComponent implements AfterViewInit {
_track: Track; _track: Track;

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

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

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';
@ -20,7 +20,8 @@ import { BroadcastingStartRequestedEvent, BroadcastingStopRequestedEvent } from
selector: 'ov-activities-panel', selector: 'ov-activities-panel',
templateUrl: './activities-panel.component.html', templateUrl: './activities-panel.component.html',
styleUrls: ['../panel.component.scss', './activities-panel.component.scss'], styleUrls: ['../panel.component.scss', './activities-panel.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush changeDetection: ChangeDetectionStrategy.OnPush,
standalone: false
}) })
export class ActivitiesPanelComponent implements OnInit { export class ActivitiesPanelComponent implements OnInit {
/** /**
@ -53,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.
@ -79,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
@ -104,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();
} }
/** /**
@ -117,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;
} }
@ -125,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,
@ -18,7 +18,8 @@ import { OpenViduService } from '../../../../services/openvidu/openvidu.service'
selector: 'ov-broadcasting-activity', selector: 'ov-broadcasting-activity',
templateUrl: './broadcasting-activity.component.html', templateUrl: './broadcasting-activity.component.html',
styleUrls: ['./broadcasting-activity.component.scss', '../activities-panel.component.scss'], styleUrls: ['./broadcasting-activity.component.scss', '../activities-panel.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush changeDetection: ChangeDetectionStrategy.OnPush,
standalone: false
}) })
// TODO: Allow to add more than one broadcast url // TODO: Allow to add more than one broadcast url
@ -75,7 +76,7 @@ export class BroadcastingActivityComponent implements OnInit {
*/ */
isPanelOpened: boolean = false; isPanelOpened: boolean = false;
private broadcastingSub: Subscription; private destroy$ = new Subject<void>();
/** /**
* @internal * @internal
@ -98,7 +99,8 @@ export class BroadcastingActivityComponent implements OnInit {
* @internal * @internal
*/ */
ngOnDestroy() { ngOnDestroy() {
if (this.broadcastingSub) this.broadcastingSub.unsubscribe(); this.destroy$.next();
this.destroy$.complete();
} }
/** /**
@ -146,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.
@ -24,13 +25,14 @@ import { LoggerService } from '../../../../services/logger/logger.service';
selector: 'ov-recording-activity', selector: 'ov-recording-activity',
templateUrl: './recording-activity.component.html', templateUrl: './recording-activity.component.html',
styleUrls: ['./recording-activity.component.scss', '../activities-panel.component.scss'], styleUrls: ['./recording-activity.component.scss', '../activities-panel.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush changeDetection: ChangeDetectionStrategy.OnPush,
standalone: false
}) })
// 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
*/ */
@ -66,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
*/ */
@ -98,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
@ -114,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');
} }
@ -124,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;
} }
/** /**
@ -225,11 +293,104 @@ 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 {
this.recordingList = recordingList.filter((rec) => rec.status === RecordingStatus.STARTED);
}
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) {
@ -238,4 +399,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

@ -12,7 +12,8 @@ import { VirtualBackgroundService } from '../../../services/virtual-background/v
selector: 'ov-background-effects-panel', selector: 'ov-background-effects-panel',
templateUrl: './background-effects-panel.component.html', templateUrl: './background-effects-panel.component.html',
styleUrls: ['../panel.component.scss', './background-effects-panel.component.scss'], styleUrls: ['../panel.component.scss', './background-effects-panel.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush changeDetection: ChangeDetectionStrategy.OnPush,
standalone: false
}) })
export class BackgroundEffectsPanelComponent implements OnInit { export class BackgroundEffectsPanelComponent implements OnInit {
backgroundSelectedId: string; backgroundSelectedId: string;
@ -56,10 +57,6 @@ export class BackgroundEffectsPanelComponent implements OnInit {
} }
async applyBackground(effect: BackgroundEffect) { async applyBackground(effect: BackgroundEffect) {
if (effect.type === EffectType.NONE) { await this.backgroundService.applyBackground(effect);
await this.backgroundService.removeBackground();
} else {
await this.backgroundService.applyBackground(effect);
}
} }
} }

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';
@ -13,7 +13,8 @@ import { PanelService } from '../../../services/panel/panel.service';
selector: 'ov-chat-panel', selector: 'ov-chat-panel',
templateUrl: './chat-panel.component.html', templateUrl: './chat-panel.component.html',
styleUrls: ['../panel.component.scss', './chat-panel.component.scss'], styleUrls: ['../panel.component.scss', './chat-panel.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush changeDetection: ChangeDetectionStrategy.OnPush,
standalone: false
}) })
export class ChatPanelComponent implements OnInit, AfterViewInit { export class ChatPanelComponent implements OnInit, AfterViewInit {
/** /**
@ -33,7 +34,7 @@ export class ChatPanelComponent implements OnInit, AfterViewInit {
*/ */
messageList: ChatMessage[] = []; messageList: ChatMessage[] = [];
private chatMessageSubscription: Subscription; private destroy$ = new Subject<void>();
/** /**
* @ignore * @ignore
@ -65,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();
} }
/** /**
@ -108,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';
/** /**
* *
@ -37,7 +38,8 @@ import { BackgroundEffect } from '../../models/background-effect.model';
selector: 'ov-panel', selector: 'ov-panel',
templateUrl: './panel.component.html', templateUrl: './panel.component.html',
styleUrls: ['./panel.component.scss'], styleUrls: ['./panel.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush changeDetection: ChangeDetectionStrategy.OnPush,
standalone: false
}) })
export class PanelComponent implements OnInit { export class PanelComponent implements OnInit {
/** /**
@ -74,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();
} }
} }
@ -118,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();
} }
} }
@ -130,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();
} }
} }
@ -194,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,
@ -206,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,21 +1,23 @@
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',
styleUrls: ['./participant-panel-item.component.scss'], styleUrls: ['./participant-panel-item.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush changeDetection: ChangeDetectionStrategy.OnPush,
standalone: false
}) })
export class ParticipantPanelItemComponent implements OnInit, OnDestroy { export class ParticipantPanelItemComponent implements OnInit, OnDestroy {
/** /**
@ -34,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();
} }
@ -79,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}.
@ -25,7 +27,8 @@ import { ParticipantModel } from '../../../../models/participant.model';
selector: 'ov-participants-panel', selector: 'ov-participants-panel',
templateUrl: './participants-panel.component.html', templateUrl: './participants-panel.component.html',
styleUrls: ['../../panel.component.scss', './participants-panel.component.scss'], styleUrls: ['../../panel.component.scss', './participants-panel.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush changeDetection: ChangeDetectionStrategy.OnPush,
standalone: false
}) })
export class ParticipantsPanelComponent implements OnInit, OnDestroy, AfterViewInit { export class ParticipantsPanelComponent implements OnInit, OnDestroy, AfterViewInit {
/** /**
@ -47,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
@ -68,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();
} }
/** /**
@ -108,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

@ -22,25 +22,27 @@
[value]="settingsOptions.GENERAL" [value]="settingsOptions.GENERAL"
> >
<mat-icon matListItemIcon>manage_accounts</mat-icon> <mat-icon matListItemIcon>manage_accounts</mat-icon>
<div mat-line *ngIf="!isMobile">{{ 'PANEL.SETTINGS.GENERAL' | translate }}</div> <div *ngIf="!isMobile">{{ 'PANEL.SETTINGS.GENERAL' | translate }}</div>
</mat-list-option> </mat-list-option>
<mat-list-option <mat-list-option
*ngIf="showCameraButton"
class="option" class="option"
id="video-opt" id="video-opt"
[selected]="selectedOption === settingsOptions.VIDEO" [selected]="selectedOption === settingsOptions.VIDEO"
[value]="settingsOptions.VIDEO" [value]="settingsOptions.VIDEO"
> >
<mat-icon matListItemIcon>videocam</mat-icon> <mat-icon matListItemIcon>videocam</mat-icon>
<div mat-line *ngIf="!isMobile">{{ 'PANEL.SETTINGS.VIDEO' | translate }}</div> <div *ngIf="!isMobile">{{ 'PANEL.SETTINGS.VIDEO' | translate }}</div>
</mat-list-option> </mat-list-option>
<mat-list-option <mat-list-option
*ngIf="showMicrophoneButton"
class="option" class="option"
id="audio-opt" id="audio-opt"
[selected]="selectedOption === settingsOptions.AUDIO" [selected]="selectedOption === settingsOptions.AUDIO"
[value]="settingsOptions.AUDIO" [value]="settingsOptions.AUDIO"
> >
<mat-icon matListItemIcon>mic</mat-icon> <mat-icon matListItemIcon>mic</mat-icon>
<div mat-line *ngIf="!isMobile">{{ 'PANEL.SETTINGS.AUDIO' | translate }}</div> <div *ngIf="!isMobile">{{ 'PANEL.SETTINGS.AUDIO' | translate }}</div>
</mat-list-option> </mat-list-option>
<!-- <mat-list-option <!-- <mat-list-option
*ngIf="showCaptions" *ngIf="showCaptions"
@ -68,12 +70,12 @@
</mat-list> </mat-list>
</div> </div>
<ov-video-devices-select <ov-video-devices-select
*ngIf="selectedOption === settingsOptions.VIDEO" *ngIf="showCameraButton && selectedOption === settingsOptions.VIDEO"
(onVideoDeviceChanged)="onVideoDeviceChanged.emit($event)" (onVideoDeviceChanged)="onVideoDeviceChanged.emit($event)"
(onVideoEnabledChanged)="onVideoEnabledChanged.emit($event)" (onVideoEnabledChanged)="onVideoEnabledChanged.emit($event)"
></ov-video-devices-select> ></ov-video-devices-select>
<ov-audio-devices-select <ov-audio-devices-select
*ngIf="selectedOption === settingsOptions.AUDIO" *ngIf="showMicrophoneButton && selectedOption === settingsOptions.AUDIO"
(onAudioDeviceChanged)="onAudioDeviceChanged.emit($event)" (onAudioDeviceChanged)="onAudioDeviceChanged.emit($event)"
(onAudioEnabledChanged)="onAudioEnabledChanged.emit($event)" (onAudioEnabledChanged)="onAudioEnabledChanged.emit($event)"
></ov-audio-devices-select> ></ov-audio-devices-select>

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';
@ -13,7 +13,8 @@ import { LangOption } from '../../../models/lang.model';
@Component({ @Component({
selector: 'ov-settings-panel', selector: 'ov-settings-panel',
templateUrl: './settings-panel.component.html', templateUrl: './settings-panel.component.html',
styleUrls: ['../panel.component.scss', './settings-panel.component.scss'] styleUrls: ['../panel.component.scss', './settings-panel.component.scss'],
standalone: false
}) })
export class SettingsPanelComponent implements OnInit { export class SettingsPanelComponent implements OnInit {
@Output() onVideoEnabledChanged = new EventEmitter<boolean>(); @Output() onVideoEnabledChanged = new EventEmitter<boolean>();
@ -23,10 +24,11 @@ export class SettingsPanelComponent implements OnInit {
@Output() onLangChanged = new EventEmitter<LangOption>(); @Output() onLangChanged = new EventEmitter<LangOption>();
settingsOptions: typeof PanelSettingsOptions = PanelSettingsOptions; settingsOptions: typeof PanelSettingsOptions = PanelSettingsOptions;
selectedOption: PanelSettingsOptions = PanelSettingsOptions.GENERAL; selectedOption: PanelSettingsOptions = PanelSettingsOptions.GENERAL;
showCameraButton: boolean = true;
showMicrophoneButton: boolean = true;
showCaptions: boolean = true; showCaptions: boolean = true;
panelSubscription: Subscription;
isMobile: boolean = false; isMobile: boolean = false;
private captionsSubs: Subscription; private destroy$ = new Subject<void>();
constructor( constructor(
private panelService: PanelService, private panelService: PanelService,
private platformService: PlatformService, private platformService: PlatformService,
@ -39,7 +41,8 @@ export class SettingsPanelComponent implements OnInit {
} }
ngOnDestroy() { ngOnDestroy() {
if (this.captionsSubs) this.captionsSubs.unsubscribe(); this.destroy$.next();
this.destroy$.complete();
} }
close() { close() {
@ -50,13 +53,13 @@ export class SettingsPanelComponent implements OnInit {
} }
private subscribeToDirectives() { private subscribeToDirectives() {
this.captionsSubs = this.libService.captionsButton$.subscribe((value: boolean) => { this.libService.cameraButton$.pipe(takeUntil(this.destroy$)).subscribe((value: boolean) => (this.showCameraButton = value));
this.showCaptions = value; this.libService.microphoneButton$.pipe(takeUntil(this.destroy$)).subscribe((value: boolean) => (this.showMicrophoneButton = value));
}); this.libService.captionsButton$.pipe(takeUntil(this.destroy$)).subscribe((value: boolean) => (this.showCaptions = value));
} }
private subscribeToPanelToggling() { 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

@ -24,7 +24,7 @@
<div class="media-controls-container"> <div class="media-controls-container">
<!-- Camera --> <!-- Camera -->
<div class="video-controls-container"> <div class="video-controls-container" *ngIf="showCameraButton">
<ov-video-devices-select <ov-video-devices-select
(onVideoDeviceChanged)="onVideoDeviceChanged.emit($event)" (onVideoDeviceChanged)="onVideoDeviceChanged.emit($event)"
(onVideoEnabledChanged)="videoEnabledChanged($event)" (onVideoEnabledChanged)="videoEnabledChanged($event)"
@ -32,7 +32,7 @@
</div> </div>
<!-- Microphone --> <!-- Microphone -->
<div class="audio-controls-container"> <div class="audio-controls-container" *ngIf="showMicrophoneButton">
<ov-audio-devices-select <ov-audio-devices-select
(onAudioDeviceChanged)="onAudioDeviceChanged.emit($event)" (onAudioDeviceChanged)="onAudioDeviceChanged.emit($event)"
(onAudioEnabledChanged)="audioEnabledChanged($event)" (onAudioEnabledChanged)="audioEnabledChanged($event)"
@ -40,7 +40,7 @@
></ov-audio-devices-select> ></ov-audio-devices-select>
</div> </div>
<div class="participant-name-container"> <div class="participant-name-container" *ngIf="showParticipantName">
<ov-participant-name-input <ov-participant-name-input
[isPrejoinPage]="true" [isPrejoinPage]="true"
[error]="!!_error" [error]="!!_error"
@ -54,7 +54,7 @@
</div> </div>
<div class="join-btn-container"> <div class="join-btn-container">
<button mat-flat-button (click)="joinSession()" id="join-button"> <button mat-flat-button (click)="join()" id="join-button">
{{ 'PREJOIN.JOIN' | translate }} {{ 'PREJOIN.JOIN' | translate }}
</button> </button>
</div> </div>

View File

@ -3,31 +3,36 @@
height: 100%; height: 100%;
background-color: var(--ov-background-color); background-color: var(--ov-background-color);
display: flex; display: flex;
justify-content: center;
align-items: center;
} }
#loading-container { #loading-container {
position: absolute; position: absolute;
top: 40%; top: 40%;
bottom: 0;
left: 0; left: 0;
right: 0; right: 0;
margin: auto; text-align: center;
text-align: -webkit-center;
text-align: -moz-center;
color: var(--ov-text-primary-color); color: var(--ov-text-primary-color);
.mat-mdc-progress-spinner {
margin: auto;
}
} }
#prejoin-card { #prejoin-card {
display: grid; display: flex;
align-content: center; flex-direction: column;
align-items: center;
justify-content: center;
margin: auto; margin: auto;
// margin-left: 0px;
border-radius: var(--ov-surface-radius); border-radius: var(--ov-surface-radius);
width: 70vh; width: 90%;
height: 85vh; max-width: 370px;
padding: 20px; // max-height: 650px;
height: min-content;
padding: 55px 30px;
background-color: var(--ov-surface-color); background-color: var(--ov-surface-color);
box-shadow: 6px 4px 20px 0px #0003; box-shadow: 6px 4px 20px rgba(0, 0, 0, 0.3);
position: relative; position: relative;
} }
@ -45,41 +50,46 @@
.video-container { .video-container {
margin: auto; margin: auto;
min-height: 45vh; height: 35vh;
max-height: 45vh; width: 100%;
height: 45vh; max-width: 100%;
max-width: 80%; display: flex;
justify-content: center;
align-items: center;
} }
#video-poster { #video-poster {
height: 100%; height: 100%;
width: 100%; width: 100%;
position: relative; position: relative;
border-radius: var(--ov-surface-radius);
overflow: hidden;
} }
.media-controls-container { .media-controls-container {
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
justify-content: space-between; justify-content: space-between;
max-width: 80%; width: 100%;
margin: auto; margin-top: 15px;
height: 25vh; height: auto;
} }
.participant-name-container { .participant-name-container {
display: block !important; display: block !important;
width: 100%; width: 100%;
margin-bottom: 2%; margin: 10px 0;
} }
.video-controls-container, .video-controls-container,
.audio-controls-container { .audio-controls-container {
width: calc(50% - 3px); width: calc(50% - 10px);
margin-top: 10px; margin: 5px 0;
margin-bottom: 10px;
} }
.join-btn-container { .join-btn-container {
width: 100%; width: 100%;
margin-top: 15px;
} }
#join-button { #join-button {
@ -89,67 +99,61 @@
border-radius: var(--ov-surface-radius); border-radius: var(--ov-surface-radius);
width: 100%; width: 100%;
height: 50px; height: 50px;
transition: background-color 0.3s;
} }
// #join-button:hover {
// background-color: lighten(var(--ov-primary-action-color), 10%);
// }
.error { .error {
font-size: 12px; font-size: 12px;
font-weight: bold; font-weight: bold;
font-style: italic; font-style: italic;
color: var(--ov-error-color); color: var(--ov-error-color);
margin-top: 5px;
} }
/* Styles for screens up to 768px wide */
@media (max-width: 768px) { @media (max-width: 768px) {
/* Specific styles for small screens */
.container {
padding: 0px;
}
#prejoin-card { #prejoin-card {
margin: auto; padding: 10px;
height: 100%;
padding: 0px;
} }
.video-container { .video-container {
height: 50vh; height: 40vh;
width: 90%;
max-width: 90%;
} }
.media-controls-container { .media-controls-container {
height: 30vh; flex-direction: column;
width: 90%; align-items: center;
max-width: 90%; height: auto;
}
.video-controls-container,
.audio-controls-container {
width: 100%;
} }
} }
/* Styles for screens with horizontal orientation */ @media (max-width: 800px) and (orientation: landscape) {
@media (max-width: 800) and (orientation: landscape) { .media-controls-container {
/* Specific styles for screens in landscape orientation */ flex-direction: row;
.container { justify-content: space-between;
height: 100%;
padding: 10px 60px;
}
.prejoin-toolbar {
display: none;
} }
.video-controls-container, .video-controls-container,
.audio-controls-container { .audio-controls-container {
width: 48%; width: 48%;
margin-bottom: 2%;
} }
} }
/* Styles for screens with maximum height of 630px */
@media (max-height: 630px) { @media (max-height: 630px) {
.video-container { .video-container {
max-width: 85%; height: 30vh;
height: 37vh;
min-height: 37vh;
} }
.media-controls-container { .media-controls-container {
height: 35vh; height: auto;
max-width: 85%;
} }
} }
} }

View File

@ -1,5 +1,15 @@
import { ChangeDetectorRef, Component, EventEmitter, HostListener, Input, OnDestroy, OnInit, Output } from '@angular/core'; import {
import { Subscription } from 'rxjs'; ChangeDetectionStrategy,
ChangeDetectorRef,
Component,
EventEmitter,
HostListener,
Input,
OnDestroy,
OnInit,
Output
} from '@angular/core';
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';
@ -9,7 +19,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
@ -17,7 +26,9 @@ import { StorageService } from '../../services/storage/storage.service';
@Component({ @Component({
selector: 'ov-pre-join', selector: 'ov-pre-join',
templateUrl: './pre-join.component.html', templateUrl: './pre-join.component.html',
styleUrls: ['./pre-join.component.scss'] styleUrls: ['./pre-join.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
standalone: false
}) })
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) {
@ -34,21 +45,22 @@ export class PreJoinComponent implements OnInit, OnDestroy {
windowSize: number; windowSize: number;
isLoading = true; isLoading = true;
participantName: string | undefined; participantName: string | undefined = '';
/** /**
* @ignore * @ignore
*/ */
isMinimal: boolean = false; isMinimal: boolean = false;
showCameraButton: boolean = true;
showMicrophoneButton: boolean = true;
showLogo: boolean = true; showLogo: boolean = true;
showParticipantName: boolean = true;
videoTrack: LocalTrack | undefined; videoTrack: LocalTrack | undefined;
audioTrack: LocalTrack | undefined; audioTrack: LocalTrack | undefined;
private tracks: LocalTrack[]; private tracks: LocalTrack[];
private log: ILogger; private log: ILogger;
private screenShareStateSubscription: Subscription; private destroy$ = new Subject<void>();
private minimalSub: Subscription;
private displayLogoSub: Subscription;
private shouldRemoveTracksWhenComponentIsDestroyed: boolean = true; private shouldRemoveTracksWhenComponentIsDestroyed: boolean = true;
@HostListener('window:resize') @HostListener('window:resize')
@ -61,7 +73,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
) { ) {
@ -73,20 +84,21 @@ export class PreJoinComponent implements OnInit, OnDestroy {
await this.initializeDevices(); await this.initializeDevices();
this.windowSize = window.innerWidth; this.windowSize = window.innerWidth;
this.isLoading = false; this.isLoading = false;
this.changeDetector.markForCheck();
} }
ngAfterContentChecked(): void { // ngAfterContentChecked(): void {
this.changeDetector.detectChanges(); // // this.changeDetector.detectChanges();
} // this.isLoading = false;
// }
async ngOnDestroy() { async ngOnDestroy() {
this.destroy$.next();
this.destroy$.complete();
this.cdkSrv.setSelector('body'); this.cdkSrv.setSelector('body');
if (this.screenShareStateSubscription) this.screenShareStateSubscription.unsubscribe();
if (this.minimalSub) this.minimalSub.unsubscribe();
if (this.displayLogoSub) this.displayLogoSub.unsubscribe();
if (this.shouldRemoveTracksWhenComponentIsDestroyed) { if (this.shouldRemoveTracksWhenComponentIsDestroyed) {
this.tracks.forEach((track) => { this.tracks?.forEach((track) => {
track.stop(); track.stop();
}); });
} }
@ -109,37 +121,73 @@ export class PreJoinComponent implements OnInit, OnDestroy {
this.cdkSrv.setSelector('#prejoin-container'); this.cdkSrv.setSelector('#prejoin-container');
} }
joinSession() { join() {
if (!this.participantName) { if (this.showParticipantName && !this.participantName) {
this._error = this.translateService.translate('PREJOIN.NICKNAME_REQUIRED'); this._error = this.translateService.translate('PREJOIN.NICKNAME_REQUIRED');
return; return;
} }
// 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;
this.onReadyToJoin.emit();
// Assign participant name to the observable if it is defined
if (this.participantName) {
this.libService.updateGeneralConfig({ participantName: this.participantName });
// Wait for the next tick to ensure the participant name propagates
// through the observable before emitting onReadyToJoin
this.libService.participantName$
.pipe(
takeUntil(this.destroy$),
filter((name) => name === this.participantName),
tap(() => this.onReadyToJoin.emit())
)
.subscribe();
} else {
// No participant name to set, emit immediately
this.onReadyToJoin.emit();
}
} }
onParticipantNameChanged(name: string) { onParticipantNameChanged(name: string) {
this.participantName = name; if (name) this.participantName = name;
} }
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.cd.markForCheck(); this.changeDetector.markForCheck();
}); });
this.displayLogoSub = this.libService.displayLogo$.subscribe((value: boolean) => {
this.libService.cameraButton$.pipe(takeUntil(this.destroy$)).subscribe((value: boolean) => {
this.showCameraButton = value;
this.changeDetector.markForCheck();
});
this.libService.microphoneButton$.pipe(takeUntil(this.destroy$)).subscribe((value: boolean) => {
this.showMicrophoneButton = value;
this.changeDetector.markForCheck();
});
this.libService.displayLogo$.pipe(takeUntil(this.destroy$)).subscribe((value: boolean) => {
this.showLogo = value; this.showLogo = value;
// this.cd.markForCheck(); this.changeDetector.markForCheck();
}); });
this.libService.participantName$.subscribe((value: string) => {
if (value) this.participantName = value; this.libService.participantName$.pipe(takeUntil(this.destroy$)).subscribe((value: string) => {
// this.cd.markForCheck(); if (value) {
this.participantName = value;
this.changeDetector.markForCheck();
}
});
this.libService.prejoinDisplayParticipantName$.pipe(takeUntil(this.destroy$)).subscribe((value: boolean) => {
this.showParticipantName = value;
this.changeDetector.markForCheck();
}); });
} }

View File

@ -16,7 +16,7 @@ import {
import { ILogger } from '../../models/logger.model'; import { 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';
@ -46,8 +46,9 @@ import {
RoomEvent, RoomEvent,
Track Track
} from 'livekit-client'; } from 'livekit-client';
import { ParticipantModel } from '../../models/participant.model'; import { ParticipantLeftEvent, ParticipantLeftReason, ParticipantModel } from '../../models/participant.model';
import { ServiceConfigService } from '../../services/config/service-config.service'; import { RecordingStatus } from '../../models/recording.model';
import { TemplateManagerService, SessionTemplateConfiguration } from '../../services/template/template-manager.service';
/** /**
* @internal * @internal
@ -58,37 +59,43 @@ import { ServiceConfigService } from '../../services/config/service-config.servi
templateUrl: './session.component.html', templateUrl: './session.component.html',
styleUrls: ['./session.component.scss'], styleUrls: ['./session.component.scss'],
animations: [trigger('sessionAnimation', [transition(':enter', [style({ opacity: 0 }), animate('50ms', style({ opacity: 1 }))])])], animations: [trigger('sessionAnimation', [transition(':enter', [style({ opacity: 0 }), animate('50ms', style({ opacity: 1 }))])])],
changeDetection: ChangeDetectionStrategy.OnPush changeDetection: ChangeDetectionStrategy.OnPush,
standalone: false
}) })
export class SessionComponent implements OnInit, OnDestroy { export class SessionComponent implements OnInit, OnDestroy {
@ContentChild('toolbar', { read: TemplateRef }) toolbarTemplate: TemplateRef<any>; @ContentChild('toolbar', { read: TemplateRef }) toolbarTemplate: TemplateRef<any>;
@ContentChild('panel', { read: TemplateRef }) panelTemplate: TemplateRef<any>; @ContentChild('panel', { read: TemplateRef }) panelTemplate: TemplateRef<any>;
@ContentChild('layout', { read: TemplateRef }) layoutTemplate: TemplateRef<any>; @ContentChild('layout', { read: TemplateRef }) layoutTemplate: TemplateRef<any>;
/** /**
* Provides event notifications that fire when OpenVidu Room is created. * Provides event notifications that fire when Room is created for the local participant.
*
*/ */
@Output() onRoomCreated: EventEmitter<Room> = new EventEmitter<Room>(); @Output() onRoomCreated: EventEmitter<Room> = new EventEmitter<Room>();
/** /**
* Provides event notifications that fire when OpenVidu Room is disconnected. * Provides event notifications that fire when Room is being reconnected for the local participant.
*/ */
@Output() onRoomReconnecting: EventEmitter<void> = new EventEmitter<void>(); @Output() onRoomReconnecting: EventEmitter<void> = new EventEmitter<void>();
/** /**
* Provides event notifications that fire when OpenVidu Room is reconnected. * Provides event notifications that fire when Room is reconnected for the local participant.
*/ */
@Output() onRoomReconnected: EventEmitter<void> = new EventEmitter<void>(); @Output() onRoomReconnected: EventEmitter<void> = new EventEmitter<void>();
/** /**
* Provides event notifications that fire when OpenVidu Room is disconnected. * Provides event notifications that fire when participant is disconnected from Room.
* @deprecated Use {@link SessionComponent.onParticipantLeft} instead.
*/ */
@Output() onRoomDisconnected: EventEmitter<void> = new EventEmitter<void>(); @Output() onRoomDisconnected: EventEmitter<void> = new EventEmitter<void>();
/** /**
* Provides event notifications that fire when local participant is created. * Provides event notifications that fire when local participant is connected to the Room.
*/ */
@Output() onParticipantCreated: EventEmitter<ParticipantModel> = new EventEmitter<ParticipantModel>(); @Output() onParticipantConnected: EventEmitter<ParticipantModel> = new EventEmitter<ParticipantModel>();
/**
* This event is emitted when the local participant leaves the room.
*/
@Output() onParticipantLeft: EventEmitter<ParticipantLeftEvent> = new EventEmitter<ParticipantLeftEvent>();
room: Room; room: Room;
sideMenu: MatSidenav; sideMenu: MatSidenav;
@ -97,17 +104,20 @@ 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;
private layoutService: LayoutService;
constructor( constructor(
private serviceConfig: ServiceConfigService, private layoutService: LayoutService,
private actionService: ActionService, private actionService: ActionService,
private openviduService: OpenViduService, private openviduService: OpenViduService,
private participantService: ParticipantService, private participantService: ParticipantService,
@ -120,15 +130,16 @@ 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.layoutService = this.serviceConfig.getLayoutService(); this.setupTemplates();
} }
@HostListener('window:beforeunload') @HostListener('window:beforeunload')
beforeunloadHandler() { beforeunloadHandler() {
this.disconnectRoom(); this.disconnectRoom(ParticipantLeftReason.BROWSER_UNLOAD);
} }
@HostListener('window:resize') @HostListener('window:resize')
@ -177,15 +188,39 @@ export class SessionComponent implements OnInit, OnDestroy {
set layoutContainer(container: ElementRef) { set layoutContainer(container: ElementRef) {
setTimeout(async () => { setTimeout(async () => {
if (container) { if (container) {
// Apply background from storage when layout container is in DOM if (this.libService.showBackgroundEffectsButton()) {
await this.backgroundService.applyBackgroundFromStorage(); // Apply background from storage when layout container is in DOM only when background effects button is enabled
await this.backgroundService.applyBackgroundFromStorage();
}
} }
}, 0); }, 0);
} }
async ngOnInit() { async ngOnInit() {
this.room = this.openviduService.getRoom(); this.shouldDisconnectRoomWhenComponentIsDestroyed = true;
this.onRoomCreated.emit(this.room);
// 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();
@ -198,19 +233,22 @@ 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
this.onRoomCreated.emit(this.room);
this.cd.markForCheck(); this.cd.markForCheck();
this.loading = false; this.loading = false;
this.onParticipantCreated.emit(this.participantService.getLocalParticipant()); this.onParticipantConnected.emit(this.participantService.getLocalParticipant());
} catch (error) { } catch (error) {
this.log.e('There was an error connecting to the room:', error.code, error.message); this.log.e('There was an error connecting to the room:', error.code, error.message);
this.actionService.openDialog(this.translateService.translate('ERRORS.SESSION'), error?.error || error?.message || error); this.actionService.openDialog(this.translateService.translate('ERRORS.SESSION'), error?.error || error?.message || error);
@ -222,22 +260,40 @@ 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(); await this.disconnectRoom(ParticipantLeftReason.LEAVE);
} }
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();
} }
async disconnectRoom() { async disconnectRoom(reason: ParticipantLeftReason) {
// Mark session as disconnected for avoiding to do it again in ngOnDestroy // Mark session as disconnected for avoiding to do it again in ngOnDestroy
this.shouldDisconnectRoomWhenComponentIsDestroyed = false; this.shouldDisconnectRoomWhenComponentIsDestroyed = false;
await this.openviduService.disconnectRoom(); await this.openviduService.disconnectRoom(() => {
this.onParticipantLeft.emit({
roomName: this.openviduService.getRoomName(),
participantName: this.participantService.getLocalParticipant()?.identity || '',
reason
});
}, false);
} }
private subscribeToTogglingMenu() { private subscribeToTogglingMenu() {
@ -254,7 +310,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;
@ -273,7 +329,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;
}); });
} }
@ -443,9 +499,12 @@ 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) {
this.recordingService.setRecordingStarted(); const recordingActive = recordingList.find((recording) => recording.status === RecordingStatus.STARTED);
this.recordingService.setRecordingStarted(recordingActive);
} }
if (isBroadcastingStarted) { if (isBroadcastingStarted) {
this.broadcastingService.setBroadcastingStarted(broadcastingId); this.broadcastingService.setBroadcastingStarted(broadcastingId);
@ -458,7 +517,7 @@ export class SessionComponent implements OnInit, OnDestroy {
); );
} }
subscribeToReconnection() { private subscribeToReconnection() {
this.room.on(RoomEvent.Reconnecting, () => { this.room.on(RoomEvent.Reconnecting, () => {
this.log.w('Connection lost: Reconnecting'); this.log.w('Connection lost: Reconnecting');
this.actionService.openConnectionDialog( this.actionService.openConnectionDialog(
@ -474,15 +533,67 @@ export class SessionComponent implements OnInit, OnDestroy {
}); });
this.room.on(RoomEvent.Disconnected, async (reason: DisconnectReason | undefined) => { this.room.on(RoomEvent.Disconnected, async (reason: DisconnectReason | undefined) => {
if (reason === DisconnectReason.SERVER_SHUTDOWN) { this.shouldDisconnectRoomWhenComponentIsDestroyed = false;
this.log.e('Room Disconnected', reason); this.actionService.closeConnectionDialog();
this.actionService.openConnectionDialog( const participantLeftEvent: ParticipantLeftEvent = {
this.translateService.translate('ERRORS.CONNECTION'), roomName: this.openviduService.getRoomName(),
this.translateService.translate('ERRORS.RECONNECT') participantName: this.participantService.getLocalParticipant()?.identity || '',
); reason: ParticipantLeftReason.NETWORK_DISCONNECT
this.onRoomDisconnected.emit(); };
const messageErrorKey = 'ERRORS.DISCONNECT';
let descriptionErrorKey = '';
switch (reason) {
case DisconnectReason.CLIENT_INITIATED:
// Skip disconnect reason if a default disconnect method has been called
if (!this.openviduService.shouldHandleClientInitiatedDisconnectEvent) return;
participantLeftEvent.reason = ParticipantLeftReason.LEAVE;
break;
case DisconnectReason.DUPLICATE_IDENTITY:
participantLeftEvent.reason = ParticipantLeftReason.DUPLICATE_IDENTITY;
descriptionErrorKey = 'ERRORS.DUPLICATE_IDENTITY';
break;
case DisconnectReason.SERVER_SHUTDOWN:
descriptionErrorKey = 'ERRORS.SERVER_SHUTDOWN';
participantLeftEvent.reason = ParticipantLeftReason.SERVER_SHUTDOWN;
break;
case DisconnectReason.PARTICIPANT_REMOVED:
participantLeftEvent.reason = ParticipantLeftReason.PARTICIPANT_REMOVED;
descriptionErrorKey = 'ERRORS.PARTICIPANT_REMOVED';
break;
case DisconnectReason.ROOM_DELETED:
participantLeftEvent.reason = ParticipantLeftReason.ROOM_DELETED;
descriptionErrorKey = 'ERRORS.ROOM_DELETED';
break;
case DisconnectReason.SIGNAL_CLOSE:
participantLeftEvent.reason = ParticipantLeftReason.SIGNAL_CLOSE;
descriptionErrorKey = 'ERRORS.SIGNAL_CLOSE';
break;
default:
participantLeftEvent.reason = ParticipantLeftReason.OTHER;
descriptionErrorKey = 'ERRORS.DISCONNECT';
break;
}
this.log.d('Participant disconnected', participantLeftEvent);
this.onParticipantLeft.emit(participantLeftEvent);
this.onRoomDisconnected.emit();
if (this.libService.getShowDisconnectionDialog() && descriptionErrorKey) {
this.actionService.openDialog(
this.translateService.translate(messageErrorKey),
this.translateService.translate(descriptionErrorKey)
);
}
});
}
private subscribeToVirtualBackground() {
this.libService.backgroundEffectsButton$.subscribe(async (enable) => {
if (!enable && this.backgroundService.isBackgroundApplied()) {
await this.backgroundService.removeBackground();
if (this.panelService.isBackgroundEffectsPanelOpened()) {
this.panelService.closePanel();
}
} }
// await this.disconnectRoom();
}); });
} }

View File

@ -12,7 +12,8 @@ import { ParticipantModel } from '../../../models/participant.model';
@Component({ @Component({
selector: 'ov-audio-devices-select', selector: 'ov-audio-devices-select',
templateUrl: './audio-devices.component.html', templateUrl: './audio-devices.component.html',
styleUrls: ['./audio-devices.component.scss'] styleUrls: ['./audio-devices.component.scss'],
standalone: false
}) })
export class AudioDevicesComponent implements OnInit, OnDestroy { export class AudioDevicesComponent implements OnInit, OnDestroy {
@Output() onAudioDeviceChanged = new EventEmitter<CustomDevice>(); @Output() onAudioDeviceChanged = new EventEmitter<CustomDevice>();

View File

@ -23,11 +23,11 @@ export class CaptionsSettingComponent implements OnInit, OnDestroy {
private captionsStatusSubs: Subscription; private captionsStatusSubs: Subscription;
private sttStatusSubs: Subscription; private sttStatusSubs: Subscription;
private layoutService: LayoutService; constructor(
private layoutService: LayoutService,
constructor(private serviceConfig: ServiceConfigService, private captionService: CaptionService, private openviduService: OpenViduService) { private captionService: CaptionService,
this.layoutService = this.serviceConfig.getLayoutService(); private openviduService: OpenViduService
} ) {}
ngOnInit(): void { ngOnInit(): void {
// this.isOpenViduPro = this.openviduService.isOpenViduPro(); // this.isOpenViduPro = this.openviduService.isOpenViduPro();

View File

@ -12,7 +12,8 @@ import { Subscription } from 'rxjs';
@Component({ @Component({
selector: 'ov-lang-selector', selector: 'ov-lang-selector',
templateUrl: './lang-selector.component.html', templateUrl: './lang-selector.component.html',
styleUrls: ['./lang-selector.component.scss'] styleUrls: ['./lang-selector.component.scss'],
standalone: false
}) })
export class LangSelectorComponent implements OnInit, OnDestroy { export class LangSelectorComponent implements OnInit, OnDestroy {
/** /**

View File

@ -9,7 +9,8 @@ import { StorageService } from '../../../services/storage/storage.service';
@Component({ @Component({
selector: 'ov-participant-name-input', selector: 'ov-participant-name-input',
templateUrl: './participant-name-input.component.html', templateUrl: './participant-name-input.component.html',
styleUrls: ['./participant-name-input.component.scss'] styleUrls: ['./participant-name-input.component.scss'],
standalone: false
}) })
export class ParticipantNameInputComponent implements OnInit { export class ParticipantNameInputComponent implements OnInit {
name: string; name: string;

View File

@ -12,7 +12,8 @@ import { ParticipantModel } from '../../../models/participant.model';
@Component({ @Component({
selector: 'ov-video-devices-select', selector: 'ov-video-devices-select',
templateUrl: './video-devices.component.html', templateUrl: './video-devices.component.html',
styleUrls: ['./video-devices.component.scss'] styleUrls: ['./video-devices.component.scss'],
standalone: false
}) })
export class VideoDevicesComponent implements OnInit, OnDestroy { export class VideoDevicesComponent implements OnInit, OnDestroy {
@Output() onVideoDeviceChanged = new EventEmitter<CustomDevice>(); @Output() onVideoDeviceChanged = new EventEmitter<CustomDevice>();

View File

@ -48,10 +48,10 @@
mat-icon-button mat-icon-button
id="pin-btn" id="pin-btn"
(click)="toggleVideoPinned()" (click)="toggleVideoPinned()"
[class.active-btn]="_track.isPinned"
[matTooltip]="_track.isPinned ? ('STREAM.UNPIN' | translate) : ('STREAM.PIN' | translate)" [matTooltip]="_track.isPinned ? ('STREAM.UNPIN' | translate) : ('STREAM.PIN' | translate)"
> >
<mat-icon>push_pin</mat-icon> <mat-icon *ngIf="_track.isPinned" fontSet="material-symbols-outlined" fontIcon="keep">keep_off</mat-icon>
<mat-icon *ngIf="!_track.isPinned" id="status-pinned" fontIcon="push_pin"></mat-icon>
</button> </button>
<button <button
*ngIf="!_track.participant.isLocal" *ngIf="!_track.participant.isLocal"

View File

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

View File

@ -1,4 +1,4 @@
$ov-video-elements-bg-color: var(--ov-primary-action-color);; $ov-video-elements-bg-color: var(--ov-primary-action-color);
:host { :host {
/* Fixes layout bug. The OV_root is created with the entire layout width and it has a weird UX behaviour */ /* Fixes layout bug. The OV_root is created with the entire layout width and it has a weird UX behaviour */
.no-size { .no-size {
@ -41,9 +41,6 @@ $ov-video-elements-bg-color: var(--ov-primary-action-color);;
} }
} }
.active-btn {
color: var(--ov-accent-action-color) !important;
}
.muted-btn { .muted-btn {
color: var(--ov-error-color) !important; color: var(--ov-error-color) !important;
} }
@ -80,11 +77,15 @@ $ov-video-elements-bg-color: var(--ov-primary-action-color);;
line-height: 0; line-height: 0;
#status-mic, #status-mic,
#status-muted-forcibly { #status-muted-forcibly,
color: var(--ov-error-color); #status-pinned {
font-size: 24px; font-size: 24px;
margin: 5px; margin: 5px;
} }
#status-mic,
#status-muted-forcibly {
color: var(--ov-error-color);
}
} }
/* Contains the video element, used to fix video letter-boxing */ /* Contains the video element, used to fix video letter-boxing */

View File

@ -1,13 +1,12 @@
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';
import { ParticipantService } from '../../services/participant/participant.service'; import { ParticipantService } from '../../services/participant/participant.service';
import { Track } from 'livekit-client'; import { Track } from 'livekit-client';
import { ParticipantTrackPublication } from '../../models/participant.model'; import { ParticipantTrackPublication } from '../../models/participant.model';
import { ServiceConfigService } from '../../services/config/service-config.service';
/** /**
* The **StreamComponent** is hosted inside of the {@link LayoutComponent}. * The **StreamComponent** is hosted inside of the {@link LayoutComponent}.
@ -16,7 +15,8 @@ import { ServiceConfigService } from '../../services/config/service-config.servi
@Component({ @Component({
selector: 'ov-stream', selector: 'ov-stream',
templateUrl: './stream.component.html', templateUrl: './stream.component.html',
styleUrls: ['./stream.component.scss'] styleUrls: ['./stream.component.scss'],
standalone: false
}) })
export class StreamComponent implements OnInit, OnDestroy { export class StreamComponent implements OnInit, OnDestroy {
/** /**
@ -68,7 +68,7 @@ export class StreamComponent implements OnInit, OnDestroy {
/** /**
* @ignore * @ignore
*/ */
hoveringTimeout: NodeJS.Timeout; hoveringTimeout: ReturnType<typeof setTimeout>;
/** /**
* @ignore * @ignore
@ -92,35 +92,27 @@ 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;
private layoutService: LayoutService;
/** /**
* @ignore * @ignore
*/ */
constructor( constructor(
private serviceConfig: ServiceConfigService, private layoutService: LayoutService,
private participantService: ParticipantService, private participantService: ParticipantService,
private cdkSrv: CdkOverlayService, private cdkSrv: CdkOverlayService,
private libService: OpenViduComponentsConfigService private libService: OpenViduComponentsConfigService
) { ) {}
this.layoutService = this.serviceConfig.getLayoutService();
}
ngOnInit() { ngOnInit() {
this.subscribeToStreamDirectives(); this.subscribeToStreamDirectives();
} }
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();
} }
/** /**
@ -186,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

@ -1,28 +1,31 @@
<head>
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@24,400,0,0" />
</head>
<mat-toolbar id="toolbar" class="toolbar-container"> <mat-toolbar id="toolbar" class="toolbar-container">
<div id="info-container" class="info-container"> <div id="info-container" class="info-container">
<div> <div>
<img *ngIf="!isMinimal && showLogo" id="branding-logo" [ovLogo]="brandingLogo" /> <img *ngIf="!isMinimal && showLogo" id="branding-logo" [ovLogo]="brandingLogo" />
<div <div
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>
@ -32,6 +35,7 @@
<button <button
id="camera-btn" id="camera-btn"
mat-icon-button mat-icon-button
*ngIf="showCameraButton"
(click)="toggleCamera()" (click)="toggleCamera()"
[disabled]="isConnectionLost || !hasVideoDevices || cameraMuteChanging" [disabled]="isConnectionLost || !hasVideoDevices || cameraMuteChanging"
[class.warn-btn]="!isCameraEnabled" [class.warn-btn]="!isCameraEnabled"
@ -46,6 +50,7 @@
<button <button
id="mic-btn" id="mic-btn"
mat-icon-button mat-icon-button
*ngIf="showMicrophoneButton"
(click)="toggleMicrophone()" (click)="toggleMicrophone()"
[disabled]="isConnectionLost || !hasAudioDevices || microphoneMuteChanging" [disabled]="isConnectionLost || !hasAudioDevices || microphoneMuteChanging"
[class.warn-btn]="!isMicrophoneEnabled" [class.warn-btn]="!isMicrophoneEnabled"
@ -121,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

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

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,12 +44,12 @@ 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, ParticipantModel } from '../../models/participant.model'; import { ParticipantLeftEvent, ParticipantLeftReason, ParticipantModel } from '../../models/participant.model';
import { Room, RoomEvent } from 'livekit-client'; import { Room, RoomEvent } from 'livekit-client';
import { ToolbarAdditionalButtonsPosition } from '../../models/toolbar.model'; import { ToolbarAdditionalButtonsPosition } from '../../models/toolbar.model';
import { ServiceConfigService } from '../../services/config/service-config.service';
/** /**
* The **ToolbarComponent** is hosted inside of the {@link VideoconferenceComponent}. * The **ToolbarComponent** is hosted inside of the {@link VideoconferenceComponent}.
@ -59,7 +59,8 @@ import { ServiceConfigService } from '../../services/config/service-config.servi
selector: 'ov-toolbar', selector: 'ov-toolbar',
templateUrl: './toolbar.component.html', templateUrl: './toolbar.component.html',
styleUrls: ['./toolbar.component.scss'], styleUrls: ['./toolbar.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush changeDetection: ChangeDetectionStrategy.OnPush,
standalone: false
}) })
export class ToolbarComponent implements OnInit, OnDestroy, AfterViewInit { export class ToolbarComponent implements OnInit, OnDestroy, AfterViewInit {
/** /**
@ -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,15 +89,20 @@ 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 a participant leaves the room. * This event is emitted when the room has been disconnected.
* @deprecated Use {@link ToolbarComponent.onParticipantLeft} instead.
*/
@Output() onRoomDisconnected: EventEmitter<void> = new EventEmitter<void>();
/**
* This event is emitted when the local participant leaves the room.
*/ */
@Output() onParticipantLeft: EventEmitter<ParticipantLeftEvent> = new EventEmitter<ParticipantLeftEvent>(); @Output() onParticipantLeft: EventEmitter<ParticipantLeftEvent> = new EventEmitter<ParticipantLeftEvent>();
@ -139,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
*/ */
@ -202,6 +213,14 @@ export class ToolbarComponent implements OnInit, OnDestroy, AfterViewInit {
* @ignore * @ignore
*/ */
isMinimal: boolean = false; isMinimal: boolean = false;
/**
* @ignore
*/
showCameraButton: boolean = true;
/**
* @ignore
*/
showMicrophoneButton: boolean = true;
/** /**
* @ignore * @ignore
*/ */
@ -226,6 +245,11 @@ export class ToolbarComponent implements OnInit, OnDestroy, AfterViewInit {
*/ */
showRecordingButton: boolean = true; showRecordingButton: boolean = true;
/**
* @ignore
*/
showViewRecordingsButton: boolean = false;
/** /**
* @ignore * @ignore
*/ */
@ -266,7 +290,12 @@ export class ToolbarComponent implements OnInit, OnDestroy, AfterViewInit {
/** /**
* @ignore * @ignore
*/ */
showSessionName: boolean = true; showRoomName: boolean = true;
/**
* @ignore
*/
roomName: string = '';
/** /**
* @ignore * @ignore
@ -298,6 +327,11 @@ export class ToolbarComponent implements OnInit, OnDestroy, AfterViewInit {
*/ */
recordingStatus: RecordingStatus = RecordingStatus.STOPPED; recordingStatus: RecordingStatus = RecordingStatus.STOPPED;
/**
* @ignore
*/
isRecordingReadOnlyMode: boolean = false;
/** /**
* @ignore * @ignore
*/ */
@ -325,37 +359,25 @@ 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 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;
private layoutService: LayoutService;
/** /**
* @ignore * @ignore
*/ */
constructor( constructor(
private serviceConfig: ServiceConfigService, private layoutService: LayoutService,
private documentService: DocumentService, private documentService: DocumentService,
private chatService: ChatService, private chatService: ChatService,
private panelService: PanelService, private panelService: PanelService,
@ -371,10 +393,10 @@ 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');
this.layoutService = this.serviceConfig.getLayoutService();
} }
/** /**
* @ignore * @ignore
@ -402,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();
@ -423,32 +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.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
*/ */
@ -503,26 +550,52 @@ export class ToolbarComponent implements OnInit, OnDestroy, AfterViewInit {
} }
/** /**
* The participant leaves the room voluntarily.
* @ignore * @ignore
*/ */
async disconnect() { async disconnect() {
const event: ParticipantLeftEvent = {
roomName: this.openviduService.getRoomName(),
participantId: this.participantService.getLocalParticipant()?.identity || ''
};
try { try {
await this.openviduService.disconnectRoom(); await this.openviduService.disconnectRoom(() => {
this.onParticipantLeft.emit(event); this.onParticipantLeft.emit({
roomName: this.openviduService.getRoomName(),
participantName: this.participantService.getLocalParticipant()?.identity || '',
reason: ParticipantLeftReason.LEAVE
});
this.onRoomDisconnected.emit();
}, false);
} catch (error) { } catch (error) {
this.log.e('There was an error disconnecting:', error.code, error.message); this.log.e('There was an error disconnecting:', error.code, error.message);
this.actionService.openDialog(this.translateService.translate('ERRORS.DISCONNECT'), error?.error || error?.message || error); this.actionService.openDialog(this.translateService.translate('ERRORS.DISCONNECT'), error?.error || error?.message || error);
} }
} }
/**
* @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()
}; };
@ -532,9 +605,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');
}
} }
} }
@ -551,9 +622,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');
}
} }
} }
@ -606,7 +675,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);
} }
@ -621,21 +694,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;
@ -647,7 +722,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++;
} }
@ -656,7 +731,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);
@ -680,8 +755,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);
@ -689,15 +769,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;
@ -707,78 +787,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.screenshareButtonSub = this.libService.screenshareButton$.subscribe((value: boolean) => { this.libService.toolbarViewRecordingsButton$.pipe(takeUntil(this.destroy$)).subscribe((value: boolean) => {
this.showViewRecordingsButton = value;
this.checkDisplayMoreOptions();
this.cd.markForCheck();
});
this.libService.cameraButton$.pipe(takeUntil(this.destroy$)).subscribe((value: boolean) => {
this.showCameraButton = value;
this.cd.markForCheck();
});
this.libService.microphoneButton$.pipe(takeUntil(this.destroy$)).subscribe((value: boolean) => {
this.showMicrophoneButton = value;
this.cd.markForCheck();
});
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,
@ -788,12 +887,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();
}); });
@ -807,4 +905,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,33 +1,44 @@
<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()"
(onRoomDisconnected)="onRoomDisconnected.emit()" (onRoomDisconnected)="onRoomDisconnected.emit()"
(onRoomReconnected)="onRoomReconnected.emit()" (onRoomReconnected)="onRoomReconnected.emit()"
(onParticipantCreated)="onParticipantCreated.emit($event)" (onParticipantConnected)="onParticipantCreated.emit($event)"
(onParticipantConnected)="onParticipantConnected.emit($event)"
(onParticipantLeft)="_onParticipantLeft($event)"
> >
<ng-template #toolbar> <ng-template #toolbar>
<ng-container *ngIf="openviduAngularToolbarTemplate"> <ng-container *ngIf="openviduAngularToolbarTemplate">
@ -54,6 +65,7 @@
<ov-toolbar <ov-toolbar
id="default-toolbar" id="default-toolbar"
(onParticipantLeft)="_onParticipantLeft($event)" (onParticipantLeft)="_onParticipantLeft($event)"
(onRoomDisconnected)="onRoomDisconnected.emit()"
(onVideoEnabledChanged)="onVideoEnabledChanged.emit($event)" (onVideoEnabledChanged)="onVideoEnabledChanged.emit($event)"
(onAudioEnabledChanged)="onAudioEnabledChanged.emit($event)" (onAudioEnabledChanged)="onAudioEnabledChanged.emit($event)"
(onScreenShareEnabledChanged)="onScreenShareEnabledChanged.emit($event)" (onScreenShareEnabledChanged)="onScreenShareEnabledChanged.emit($event)"
@ -61,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>
@ -125,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>
@ -137,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>
@ -155,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

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

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, skip } 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,67 +74,286 @@ 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
}) })
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&icon_names=background_replace,keep_off';
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
@ -181,6 +423,10 @@ export class VideoconferenceComponent implements OnDestroy, AfterViewInit {
* @internal * @internal
*/ */
openviduAngularAdditionalPanelsTemplate: TemplateRef<any>; openviduAngularAdditionalPanelsTemplate: TemplateRef<any>;
/**
* @internal
*/
openviduAngularParticipantPanelAfterLocalParticipantTemplate: TemplateRef<any>;
/** /**
* @internal * @internal
*/ */
@ -197,36 +443,51 @@ export class VideoconferenceComponent implements OnDestroy, AfterViewInit {
* @internal * @internal
*/ */
openviduAngularStreamTemplate: TemplateRef<any>; openviduAngularStreamTemplate: TemplateRef<any>;
/**
* @internal
*/
openviduAngularPreJoinTemplate: TemplateRef<any>;
/**
* @internal
*/
ovLayoutAdditionalElementsTemplate: TemplateRef<any>;
/** /**
* Provides event notifications that fire when videconference is ready to received the token. * @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.
* This event emits the participant name as data. * This event emits the participant name as data.
*/ */
@Output() onTokenRequested: EventEmitter<string> = new EventEmitter<string>(); @Output() onTokenRequested: EventEmitter<string> = new EventEmitter<string>();
/** /**
* Provides event notifications that fire when the participant is ready to join to the room. This event is only emitted when the prejoin page has been shown. * Provides event notifications that fire when the local participant is ready to join to the room.
* This event is only emitted when the prejoin page has been shown.
*/ */
@Output() onReadyToJoin: EventEmitter<void> = new EventEmitter<void>(); @Output() onReadyToJoin: EventEmitter<void> = new EventEmitter<void>();
/** /**
* This event is emitted when the room connection has been lost and the reconnection process has started. * Provides event notifications that fire when Room is disconnected for the local participant.
* @deprecated Use {@link VideoconferenceComponent.onParticipantLeft} instead
*/ */
@Output() onRoomDisconnected: EventEmitter<void> = new EventEmitter<void>(); @Output() onRoomDisconnected: EventEmitter<void> = new EventEmitter<void>();
/** /**
* Provides event notifications that fire when OpenVidu Room is disconnected. * Provides event notifications that fire when Room is being reconnected for the local participant.
*/ */
@Output() onRoomReconnecting: EventEmitter<void> = new EventEmitter<void>(); @Output() onRoomReconnecting: EventEmitter<void> = new EventEmitter<void>();
/** /**
* Provides event notifications that fire when OpenVidu Room is reconnected. * Provides event notifications that fire when Room is reconnected for the local participant.
*/ */
@Output() onRoomReconnected: EventEmitter<void> = new EventEmitter<void>(); @Output() onRoomReconnected: EventEmitter<void> = new EventEmitter<void>();
/** /**
* This event is emitted when a participant leaves the room. * This event is emitted when the local participant leaves the room.
*/ */
@Output() onParticipantLeft: EventEmitter<ParticipantLeftEvent> = new EventEmitter<ParticipantLeftEvent>(); @Output() onParticipantLeft: EventEmitter<ParticipantLeftEvent> = new EventEmitter<ParticipantLeftEvent>();
@ -313,6 +574,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.
@ -334,48 +602,79 @@ export class VideoconferenceComponent implements OnDestroy, AfterViewInit {
new EventEmitter<BroadcastingStopRequestedEvent>(); new EventEmitter<BroadcastingStopRequestedEvent>();
/** /**
* Provides event notifications that fire when OpenVidu Room is created. * @internal
* This event is fired when the user clicks on the view recordings button.
*/
@Output() onViewRecordingsClicked: EventEmitter<void> = new EventEmitter<void>();
/**
* Provides event notifications that fire when Room is created for the local participant.
* It provides the {@link https://openvidu.io/latest/docs/getting-started/#room Room} payload as event data. * It provides the {@link https://openvidu.io/latest/docs/getting-started/#room Room} payload as event data.
*/ */
@Output() onRoomCreated: EventEmitter<Room> = new EventEmitter<Room>(); @Output() onRoomCreated: EventEmitter<Room> = new EventEmitter<Room>();
/** /**
* Provides event notifications that fire when local participant is created. * Provides event notifications that fire when local participant is created and connected to the Room.
* It provides the {@link ParticipantModel} payload as event data. * @deprecated Use `onParticipantConnected` instead
*/ */
@Output() onParticipantCreated: EventEmitter<ParticipantModel> = new EventEmitter<ParticipantModel>(); @Output() onParticipantCreated: EventEmitter<ParticipantModel> = new EventEmitter<ParticipantModel>();
/** /**
* @internal * Provides event notifications that fire when local participant is connected to the Room.
* It provides the {@link ParticipantModel} payload as event data.
*/ */
error: boolean = false; @Output() onParticipantConnected: EventEmitter<ParticipantModel> = new EventEmitter<ParticipantModel>();
/**
* @internal
*/
errorMessage: string = '';
/**
* @internal
*/
showPrejoin: boolean = true;
/** /**
* @internal * @internal
* Centralized state management for the videoconference component
*/ */
isRoomReady: boolean = false; componentState: VideoconferenceStateInfo = {
state: VideoconferenceState.INITIALIZING,
showPrejoin: true,
isRoomReady: false,
isConnected: false,
hasAudioDevices: false,
hasVideoDevices: false,
hasUserInitiatedJoin: false,
wasPrejoinShown: false,
isLoading: true,
error: {
hasError: false,
message: '',
tokenError: null
}
};
/** private destroy$ = new Subject<void>();
* @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;
// 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
@ -386,17 +685,27 @@ 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');
// 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();
} }
@ -404,108 +713,204 @@ export class VideoconferenceComponent implements OnDestroy, AfterViewInit {
* @internal * @internal
*/ */
ngAfterViewInit() { ngAfterViewInit() {
if (this.externalToolbar) { this.addMaterialIconsIfNeeded();
this.log.d('Setting EXTERNAL TOOLBAR'); this.setupTemplates();
this.openviduAngularToolbarTemplate = this.externalToolbar.template; this.deviceSrv.initializeDevices().then(() => {
} else { this.updateComponentState({
this.log.d('Setting DEFAULT TOOLBAR'); isLoading: false
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.storageSrv.getParticipantName(); 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');
@ -515,35 +920,115 @@ 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
this._onReadyToJoin();
// Ensure we have a participant name before proceeding with the join
this.log.d('Prejoin page is hidden, checking participant name');
// Check if we have a participant name already
if (this.latestParticipantName) {
// We have a name, proceed immediately
this._onReadyToJoin();
} else {
// No name yet - set up a one-time subscription to wait for it
this.libService.participantName$
.pipe(
filter((name) => !!name),
take(1),
takeUntil(this.destroy$)
)
.subscribe(() => {
// Now we have the name in latestParticipantName
this._onReadyToJoin();
});
// Add safety timeout in case name never arrives
setTimeout(() => {
if (!this.latestParticipantName) {
this.log.w('No participant name received after timeout, proceeding anyway');
const storedName = this.storageSrv.getParticipantName();
if (storedName) {
this.latestParticipantName = storedName;
this.libService.updateGeneralConfig({ participantName: storedName });
}
this._onReadyToJoin();
}
}, VideoconferenceComponent.PARTICIPANT_NAME_TIMEOUT_MS);
}
} }
// this.cd.markForCheck();
}); });
this.participantNameSub = this.libService.participantName$.subscribe((name: string) => { this.libService.participantName$.pipe(takeUntil(this.destroy$)).subscribe((name: string) => {
if (name) this.storageSrv.setParticipantName(name); if (name) {
this.latestParticipantName = name;
this.storageSrv.setParticipantName(name);
// If we're waiting for a participant name to proceed with joining, do it now
if (
this.componentState.state === VideoconferenceState.JOINING &&
this.componentState.isRoomReady &&
!this.componentState.showPrejoin
) {
this.log.d('Participant name received, proceeding to join');
this.updateComponentState({
state: VideoconferenceState.READY_TO_CONNECT,
showPrejoin: false
});
}
}
}); });
} }
} }

View File

@ -3,7 +3,6 @@ import { ParticipantProperties } from '../models/participant.model';
export interface OpenViduComponentsConfig { export interface OpenViduComponentsConfig {
production?: boolean; production?: boolean;
participantFactory?: ParticipantFactoryFunction; participantFactory?: ParticipantFactoryFunction;
services?: any;
} }
export type ParticipantFactoryFunction = (props: ParticipantProperties) => any; export type ParticipantFactoryFunction = (props: ParticipantProperties) => any;

View File

@ -17,7 +17,8 @@ import { OpenViduComponentsConfigService } from '../../services/config/directive
* <ov-activities-panel *ovActivitiesPanel [recordingActivity]="false"></ov-activities-panel> * <ov-activities-panel *ovActivitiesPanel [recordingActivity]="false"></ov-activities-panel>
*/ */
@Directive({ @Directive({
selector: 'ov-videoconference[activitiesPanelRecordingActivity], ov-activities-panel[recordingActivity]' selector: 'ov-videoconference[activitiesPanelRecordingActivity], ov-activities-panel[recordingActivity]',
standalone: false
}) })
export class ActivitiesPanelRecordingActivityDirective implements AfterViewInit, OnDestroy { export class ActivitiesPanelRecordingActivityDirective implements AfterViewInit, OnDestroy {
@Input() set activitiesPanelRecordingActivity(value: boolean) { @Input() set activitiesPanelRecordingActivity(value: boolean) {
@ -31,7 +32,10 @@ export class ActivitiesPanelRecordingActivityDirective implements AfterViewInit,
recordingActivityValue: boolean = true; recordingActivityValue: boolean = true;
constructor(public elementRef: ElementRef, private libService: OpenViduComponentsConfigService) {} constructor(
public elementRef: ElementRef,
private libService: OpenViduComponentsConfigService
) {}
ngAfterViewInit() { ngAfterViewInit() {
this.update(this.recordingActivityValue); this.update(this.recordingActivityValue);
@ -45,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);
}
} }
} }
@ -66,8 +68,9 @@ export class ActivitiesPanelRecordingActivityDirective implements AfterViewInit,
* @example * @example
* <ov-activities-panel *ovActivitiesPanel [broadcastingActivity]="false"></ov-activities-panel> * <ov-activities-panel *ovActivitiesPanel [broadcastingActivity]="false"></ov-activities-panel>
*/ */
@Directive({ @Directive({
selector: 'ov-videoconference[activitiesPanelBroadcastingActivity], ov-activities-panel[broadcastingActivity]' selector: 'ov-videoconference[activitiesPanelBroadcastingActivity], ov-activities-panel[broadcastingActivity]',
standalone: false
}) })
export class ActivitiesPanelBroadcastingActivityDirective implements AfterViewInit, OnDestroy { export class ActivitiesPanelBroadcastingActivityDirective implements AfterViewInit, OnDestroy {
@Input() set activitiesPanelBroadcastingActivity(value: boolean) { @Input() set activitiesPanelBroadcastingActivity(value: boolean) {
@ -81,7 +84,10 @@ export class ActivitiesPanelBroadcastingActivityDirective implements AfterViewIn
broadcastingActivityValue: boolean = true; broadcastingActivityValue: boolean = true;
constructor(public elementRef: ElementRef, private libService: OpenViduComponentsConfigService) {} constructor(
public elementRef: ElementRef,
private libService: OpenViduComponentsConfigService
) {}
ngAfterViewInit() { ngAfterViewInit() {
this.update(this.broadcastingActivityValue); this.update(this.broadcastingActivityValue);
@ -95,9 +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

@ -12,18 +12,21 @@ import { OpenViduComponentsConfigService } from '../../services/config/directive
* *
*/ */
@Directive({ @Directive({
selector: 'ov-admin-dashboard[recordingsList]' selector: 'ov-admin-dashboard[recordingsList]',
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);
@ -37,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);
}
} }
} }
@ -53,18 +54,21 @@ export class AdminDashboardRecordingsListDirective implements AfterViewInit, OnD
* *
*/ */
@Directive({ @Directive({
selector: 'ov-admin-dashboard[navbarTitle]' selector: 'ov-admin-dashboard[navbarTitle]',
standalone: false
}) })
export class AdminDashboardTitleDirective implements AfterViewInit, OnDestroy { export class AdminDashboardTitleDirective implements AfterViewInit, OnDestroy {
@Input() set navbarTitle(value: string) {
@Input() set navbarTitle(value: any) {
this.navbarTitleValue = value; this.navbarTitleValue = value;
this.update(this.navbarTitleValue); this.update(this.navbarTitleValue);
} }
navbarTitleValue: any = null; navbarTitleValue: string = 'OpenVidu Dashboard';
constructor(public elementRef: ElementRef, private libService: OpenViduComponentsConfigService) {} constructor(
public elementRef: ElementRef,
private libService: OpenViduComponentsConfigService
) {}
ngAfterViewInit() { ngAfterViewInit() {
this.update(this.navbarTitleValue); this.update(this.navbarTitleValue);
@ -73,18 +77,15 @@ export class AdminDashboardTitleDirective implements AfterViewInit, OnDestroy {
this.clear(); this.clear();
} }
clear() { clear() {
this.navbarTitleValue = null; this.navbarTitleValue = 'OpenVidu Dashboard';
this.update(null); this.update(null);
} }
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}.
* *
@ -95,10 +96,10 @@ export class AdminDashboardTitleDirective implements AfterViewInit, OnDestroy {
* *
*/ */
@Directive({ @Directive({
selector: 'ov-admin-login[navbarTitle]' selector: 'ov-admin-login[navbarTitle]',
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);
@ -106,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);
@ -120,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}.
* *
@ -137,11 +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
}) })
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);
@ -149,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);
@ -163,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,7 +1,24 @@
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 { AdminLoginErrorDirective, AdminDashboardRecordingsListDirective, AdminLoginTitleDirective, AdminDashboardTitleDirective } from './admin.directive'; import {
import { LayoutRemoteParticipantsDirective, FallbackLogoDirective, ToolbarBrandingLogoDirective } from './internals.directive'; AdminDashboardRecordingsListDirective,
AdminDashboardTitleDirective,
AdminLoginErrorDirective,
AdminLoginTitleDirective
} from './admin.directive';
import {
FallbackLogoDirective,
LayoutRemoteParticipantsDirective,
PrejoinDisplayParticipantName,
ToolbarBrandingLogoDirective,
ToolbarViewRecordingsButtonDirective,
RecordingActivityReadOnlyDirective,
RecordingActivityShowControlsDirective,
StartStopRecordingButtonsDirective,
RecordingActivityViewRecordingsButtonDirective,
RecordingActivityShowRecordingsListDirective,
ToolbarRoomNameDirective
} from './internals.directive';
import { ParticipantPanelItemMuteButtonDirective } from './participant-panel-item.directive'; import { ParticipantPanelItemMuteButtonDirective } from './participant-panel-item.directive';
import { import {
StreamDisplayAudioDetectionDirective, StreamDisplayAudioDetectionDirective,
@ -10,19 +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
} from './toolbar.directive'; } from './toolbar.directive';
import { import {
AudioEnabledDirective, AudioEnabledDirective,
@ -34,94 +53,69 @@ import {
MinimalDirective, MinimalDirective,
ParticipantNameDirective, ParticipantNameDirective,
PrejoinDirective, PrejoinDirective,
RecordingStreamBaseUrlDirective,
ShowDisconnectionDialogDirective,
TokenDirective, TokenDirective,
TokenErrorDirective, TokenErrorDirective,
VideoEnabledDirective VideoEnabledDirective
} from './videoconference.directive'; } from './videoconference.directive';
const directives = [
LivekitUrlDirective,
TokenDirective,
TokenErrorDirective,
MinimalDirective,
LangDirective,
LangOptionsDirective,
// CaptionsLangOptionsDirective,
// CaptionsLangDirective,
PrejoinDirective,
PrejoinDisplayParticipantName,
VideoEnabledDirective,
RecordingActivityReadOnlyDirective,
RecordingActivityShowControlsDirective,
AudioEnabledDirective,
ShowDisconnectionDialogDirective,
RecordingStreamBaseUrlDirective,
ToolbarCameraButtonDirective,
ToolbarMicrophoneButtonDirective,
ToolbarScreenshareButtonDirective,
ToolbarFullscreenButtonDirective,
ToolbarBackgroundEffectsButtonDirective,
// ToolbarCaptionsButtonDirective,
ToolbarLeaveButtonDirective,
ToolbarRecordingButtonDirective,
ToolbarBroadcastingButtonDirective,
ToolbarParticipantsPanelButtonDirective,
ToolbarChatPanelButtonDirective,
ToolbarActivitiesPanelButtonDirective,
ToolbarDisplayRoomNameDirective,
ToolbarDisplayLogoDirective,
ToolbarSettingsButtonDirective,
ToolbarAdditionalButtonsPossitionDirective,
ToolbarViewRecordingsButtonDirective,
StreamDisplayParticipantNameDirective,
StreamDisplayAudioDetectionDirective,
StreamVideoControlsDirective,
FallbackLogoDirective,
ToolbarBrandingLogoDirective,
ParticipantPanelItemMuteButtonDirective,
ParticipantNameDirective,
ActivitiesPanelRecordingActivityDirective,
ActivitiesPanelBroadcastingActivityDirective,
AdminDashboardRecordingsListDirective,
AdminLoginTitleDirective,
AdminLoginErrorDirective,
AdminDashboardTitleDirective,
LayoutRemoteParticipantsDirective,
StartStopRecordingButtonsDirective,
RecordingActivityViewRecordingsButtonDirective,
RecordingActivityShowRecordingsListDirective,
ToolbarRoomNameDirective
];
@NgModule({ @NgModule({
declarations: [ declarations: [...directives],
LivekitUrlDirective, exports: [...directives]
TokenDirective,
TokenErrorDirective,
MinimalDirective,
LangDirective,
LangOptionsDirective,
// CaptionsLangOptionsDirective,
// CaptionsLangDirective,
PrejoinDirective,
VideoEnabledDirective,
AudioEnabledDirective,
ToolbarScreenshareButtonDirective,
ToolbarFullscreenButtonDirective,
ToolbarBackgroundEffectsButtonDirective,
// ToolbarCaptionsButtonDirective,
ToolbarLeaveButtonDirective,
ToolbarRecordingButtonDirective,
ToolbarBroadcastingButtonDirective,
ToolbarParticipantsPanelButtonDirective,
ToolbarChatPanelButtonDirective,
ToolbarActivitiesPanelButtonDirective,
ToolbarDisplayRoomNameDirective,
ToolbarDisplayLogoDirective,
ToolbarSettingsButtonDirective,
ToolbarAdditionalButtonsPossitionDirective,
StreamDisplayParticipantNameDirective,
StreamDisplayAudioDetectionDirective,
StreamVideoControlsDirective,
FallbackLogoDirective,
ToolbarBrandingLogoDirective,
ParticipantPanelItemMuteButtonDirective,
ParticipantNameDirective,
ActivitiesPanelRecordingActivityDirective,
ActivitiesPanelBroadcastingActivityDirective,
AdminDashboardRecordingsListDirective,
AdminLoginTitleDirective,
AdminLoginErrorDirective,
AdminDashboardTitleDirective,
LayoutRemoteParticipantsDirective
],
exports: [
LivekitUrlDirective,
TokenDirective,
TokenErrorDirective,
MinimalDirective,
LangDirective,
LangOptionsDirective,
// CaptionsLangOptionsDirective,
// CaptionsLangDirective,
PrejoinDirective,
VideoEnabledDirective,
AudioEnabledDirective,
ToolbarScreenshareButtonDirective,
ToolbarFullscreenButtonDirective,
ToolbarBackgroundEffectsButtonDirective,
// ToolbarCaptionsButtonDirective,
ToolbarLeaveButtonDirective,
ToolbarRecordingButtonDirective,
ToolbarBroadcastingButtonDirective,
ToolbarParticipantsPanelButtonDirective,
ToolbarChatPanelButtonDirective,
ToolbarActivitiesPanelButtonDirective,
ToolbarDisplayRoomNameDirective,
ToolbarDisplayLogoDirective,
ToolbarSettingsButtonDirective,
ToolbarAdditionalButtonsPossitionDirective,
StreamDisplayParticipantNameDirective,
StreamDisplayAudioDetectionDirective,
StreamVideoControlsDirective,
FallbackLogoDirective,
ToolbarBrandingLogoDirective,
ParticipantPanelItemMuteButtonDirective,
ParticipantNameDirective,
ActivitiesPanelRecordingActivityDirective,
ActivitiesPanelBroadcastingActivityDirective,
AdminDashboardRecordingsListDirective,
AdminLoginTitleDirective,
AdminLoginErrorDirective,
AdminDashboardTitleDirective,
LayoutRemoteParticipantsDirective
]
}) })
export class ApiDirectiveModule {} export class ApiDirectiveModule {}

View File

@ -9,7 +9,8 @@ import { OpenViduComponentsConfigService } from '../../services/config/directive
* @internal * @internal
*/ */
@Directive({ @Directive({
selector: 'img[ovLogo]' selector: 'img[ovLogo]',
standalone: false
}) })
export class FallbackLogoDirective implements OnInit { export class FallbackLogoDirective implements OnInit {
defaultLogo = defaultLogo =
@ -53,7 +54,8 @@ export class FallbackLogoDirective implements OnInit {
* @internal * @internal
*/ */
@Directive({ @Directive({
selector: 'ov-layout[ovRemoteParticipants]' selector: 'ov-layout[ovRemoteParticipants]',
standalone: false
}) })
export class LayoutRemoteParticipantsDirective { export class LayoutRemoteParticipantsDirective {
@Input() set ovRemoteParticipants(value: ParticipantModel[] | undefined) { @Input() set ovRemoteParticipants(value: ParticipantModel[] | undefined) {
@ -85,7 +87,8 @@ export class LayoutRemoteParticipantsDirective {
* @internal * @internal
*/ */
@Directive({ @Directive({
selector: 'ov-videoconference[brandingLogo], ov-toolbar[brandingLogo]' selector: 'ov-videoconference[brandingLogo], ov-toolbar[brandingLogo]',
standalone: false
}) })
export class ToolbarBrandingLogoDirective implements AfterViewInit, OnDestroy { export class ToolbarBrandingLogoDirective implements AfterViewInit, OnDestroy {
/** /**
@ -119,6 +122,405 @@ export class ToolbarBrandingLogoDirective implements AfterViewInit, OnDestroy {
} }
private update(value: string) { private update(value: string) {
this.libService.setBrandingLogo(value); this.libService.updateToolbarConfig({ brandingLogo: value });
}
}
/**
* @internal
*/
@Directive({
selector: 'ov-videoconference[prejoinDisplayParticipantName]',
standalone: false
})
export class PrejoinDisplayParticipantName implements OnDestroy {
/**
* @ignore
*/
@Input() set prejoinDisplayParticipantName(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.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

@ -17,7 +17,8 @@ import { OpenViduComponentsConfigService } from '../../services/config/directive
* <ov-participant-panel-item [muteButton]="false"></ov-participant-panel-item> * <ov-participant-panel-item [muteButton]="false"></ov-participant-panel-item>
*/ */
@Directive({ @Directive({
selector: 'ov-videoconference[participantPanelItemMuteButton], ov-participant-panel-item[muteButton]' selector: 'ov-videoconference[participantPanelItemMuteButton], ov-participant-panel-item[muteButton]',
standalone: false
}) })
export class ParticipantPanelItemMuteButtonDirective implements AfterViewInit, OnDestroy { export class ParticipantPanelItemMuteButtonDirective implements AfterViewInit, OnDestroy {
@Input() set participantPanelItemMuteButton(value: boolean) { @Input() set participantPanelItemMuteButton(value: boolean) {
@ -31,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);
@ -45,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

@ -17,7 +17,8 @@ import { OpenViduComponentsConfigService } from '../../services/config/directive
* <ov-stream [displayParticipantName]="false"></ov-stream> * <ov-stream [displayParticipantName]="false"></ov-stream>
*/ */
@Directive({ @Directive({
selector: 'ov-videoconference[streamDisplayParticipantName], ov-stream[displayParticipantName]' selector: 'ov-videoconference[streamDisplayParticipantName], ov-stream[displayParticipantName]',
standalone: false
}) })
export class StreamDisplayParticipantNameDirective implements AfterViewInit, OnDestroy { export class StreamDisplayParticipantNameDirective implements AfterViewInit, OnDestroy {
@Input() set streamDisplayParticipantName(value: boolean) { @Input() set streamDisplayParticipantName(value: boolean) {
@ -45,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() {
@ -71,7 +70,8 @@ export class StreamDisplayParticipantNameDirective implements AfterViewInit, OnD
* <ov-stream [displayAudioDetection]="false"></ov-stream> * <ov-stream [displayAudioDetection]="false"></ov-stream>
*/ */
@Directive({ @Directive({
selector: 'ov-videoconference[streamDisplayAudioDetection], ov-stream[displayAudioDetection]' selector: 'ov-videoconference[streamDisplayAudioDetection], ov-stream[displayAudioDetection]',
standalone: false
}) })
export class StreamDisplayAudioDetectionDirective implements AfterViewInit, OnDestroy { export class StreamDisplayAudioDetectionDirective implements AfterViewInit, OnDestroy {
@Input() set streamDisplayAudioDetection(value: boolean) { @Input() set streamDisplayAudioDetection(value: boolean) {
@ -98,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);
@ -123,7 +121,8 @@ export class StreamDisplayAudioDetectionDirective implements AfterViewInit, OnDe
* <ov-stream [videoControls]="false"></ov-stream> * <ov-stream [videoControls]="false"></ov-stream>
*/ */
@Directive({ @Directive({
selector: 'ov-videoconference[streamVideoControls], ov-stream[videoControls]' selector: 'ov-videoconference[streamVideoControls], ov-stream[videoControls]',
standalone: false
}) })
export class StreamVideoControlsDirective implements AfterViewInit, OnDestroy { export class StreamVideoControlsDirective implements AfterViewInit, OnDestroy {
@Input() set streamVideoControls(value: boolean) { @Input() set streamVideoControls(value: boolean) {
@ -151,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

@ -2,6 +2,134 @@ import { AfterViewInit, Directive, ElementRef, Input, OnDestroy } from '@angular
import { OpenViduComponentsConfigService } from '../../services/config/directive-config.service'; import { OpenViduComponentsConfigService } from '../../services/config/directive-config.service';
import { ToolbarAdditionalButtonsPosition } from '../../models/toolbar.model'; import { ToolbarAdditionalButtonsPosition } from '../../models/toolbar.model';
/**
* The **cameraButton** directive allows show/hide the camera toolbar button.
*
* Default: `true`
*
* It can be used in the parent element {@link VideoconferenceComponent} specifying the name of the `toolbar` component:
*
* @example
* <ov-videoconference [toolbarCameraButton]="false"></ov-videoconference>
*
* \
* And it also can be used in the {@link ToolbarComponent}.
* @example
* <ov-toolbar [cameraButton]="false"></ov-toolbar>
*/
@Directive({
selector: 'ov-videoconference[toolbarCameraButton], ov-toolbar[cameraButton]',
standalone: false
})
export class ToolbarCameraButtonDirective implements AfterViewInit, OnDestroy {
/**
* @ignore
*/
@Input() set toolbarCameraButton(value: boolean) {
this.cameraValue = value;
this.update(this.cameraValue);
}
/**
* @ignore
*/
@Input() set cameraButton(value: boolean) {
this.cameraValue = value;
this.update(this.cameraValue);
}
private cameraValue: boolean = true;
/**
* @ignore
*/
constructor(
public elementRef: ElementRef,
private libService: OpenViduComponentsConfigService
) {}
ngAfterViewInit() {
this.update(this.cameraValue);
}
ngOnDestroy(): void {
this.clear();
}
private clear() {
this.cameraValue = true;
this.update(true);
}
private update(value: boolean) {
this.libService.updateToolbarConfig({ camera: value });
}
}
/**
* The **microphoneButton** directive allows show/hide the microphone toolbar button.
*
* Default: `true`
*
* It can be used in the parent element {@link VideoconferenceComponent} specifying the name of the `toolbar` component:
*
* @example
* <ov-videoconference [toolbarMicrophoneButton]="false"></ov-videoconference>
*
* \
* And it also can be used in the {@link ToolbarComponent}.
* @example
* <ov-toolbar [microphoneButton]="false"></ov-toolbar>
*/
@Directive({
selector: 'ov-videoconference[toolbarMicrophoneButton], ov-toolbar[microphoneButton]',
standalone: false
})
export class ToolbarMicrophoneButtonDirective implements AfterViewInit, OnDestroy {
/**
* @ignore
*/
@Input() set toolbarMicrophoneButton(value: boolean) {
this.microphoneValue = value;
this.update(this.microphoneValue);
}
/**
* @ignore
*/
@Input() set microphoneButton(value: boolean) {
this.microphoneValue = value;
this.update(this.microphoneValue);
}
private microphoneValue: boolean = true;
/**
* @ignore
*/
constructor(
public elementRef: ElementRef,
private libService: OpenViduComponentsConfigService
) {}
ngAfterViewInit() {
this.update(this.microphoneValue);
}
ngOnDestroy(): void {
this.clear();
}
private clear() {
this.microphoneValue = true;
this.update(true);
}
private update(value: boolean) {
this.libService.updateToolbarConfig({ microphone: value });
}
}
/** /**
* The **screenshareButton** directive allows show/hide the screenshare toolbar button. * The **screenshareButton** directive allows show/hide the screenshare toolbar button.
* *
@ -18,7 +146,8 @@ import { ToolbarAdditionalButtonsPosition } from '../../models/toolbar.model';
* <ov-toolbar [screenshareButton]="false"></ov-toolbar> * <ov-toolbar [screenshareButton]="false"></ov-toolbar>
*/ */
@Directive({ @Directive({
selector: 'ov-videoconference[toolbarScreenshareButton], ov-toolbar[screenshareButton]' selector: 'ov-videoconference[toolbarScreenshareButton], ov-toolbar[screenshareButton]',
standalone: false
}) })
export class ToolbarScreenshareButtonDirective implements AfterViewInit, OnDestroy { export class ToolbarScreenshareButtonDirective implements AfterViewInit, OnDestroy {
/** /**
@ -61,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);
}
} }
} }
@ -83,7 +210,8 @@ export class ToolbarScreenshareButtonDirective implements AfterViewInit, OnDestr
* <ov-toolbar [recordingButton]="false"></ov-toolbar> * <ov-toolbar [recordingButton]="false"></ov-toolbar>
*/ */
@Directive({ @Directive({
selector: 'ov-videoconference[toolbarRecordingButton], ov-toolbar[recordingButton]' selector: 'ov-videoconference[toolbarRecordingButton], ov-toolbar[recordingButton]',
standalone: false
}) })
export class ToolbarRecordingButtonDirective implements AfterViewInit, OnDestroy { export class ToolbarRecordingButtonDirective implements AfterViewInit, OnDestroy {
/** /**
@ -123,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);
}
} }
} }
@ -146,7 +272,8 @@ export class ToolbarRecordingButtonDirective implements AfterViewInit, OnDestroy
* *
*/ */
@Directive({ @Directive({
selector: 'ov-videoconference[toolbarBroadcastingButton], ov-toolbar[broadcastingButton]' selector: 'ov-videoconference[toolbarBroadcastingButton], ov-toolbar[broadcastingButton]',
standalone: false
}) })
export class ToolbarBroadcastingButtonDirective implements AfterViewInit, OnDestroy { export class ToolbarBroadcastingButtonDirective implements AfterViewInit, OnDestroy {
/** /**
@ -186,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);
}
} }
} }
@ -208,7 +333,8 @@ export class ToolbarBroadcastingButtonDirective implements AfterViewInit, OnDest
* <ov-toolbar [fullscreenButton]="false"></ov-toolbar> * <ov-toolbar [fullscreenButton]="false"></ov-toolbar>
*/ */
@Directive({ @Directive({
selector: 'ov-videoconference[toolbarFullscreenButton], ov-toolbar[fullscreenButton]' selector: 'ov-videoconference[toolbarFullscreenButton], ov-toolbar[fullscreenButton]',
standalone: false
}) })
export class ToolbarFullscreenButtonDirective implements AfterViewInit, OnDestroy { export class ToolbarFullscreenButtonDirective implements AfterViewInit, OnDestroy {
/** /**
@ -248,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);
}
} }
} }
@ -270,7 +394,8 @@ export class ToolbarFullscreenButtonDirective implements AfterViewInit, OnDestro
* <ov-toolbar [backgroundEffectsButton]="false"></ov-toolbar> * <ov-toolbar [backgroundEffectsButton]="false"></ov-toolbar>
*/ */
@Directive({ @Directive({
selector: 'ov-videoconference[toolbarBackgroundEffectsButton], ov-toolbar[backgroundEffectsButton]' selector: 'ov-videoconference[toolbarBackgroundEffectsButton], ov-toolbar[backgroundEffectsButton]',
standalone: false
}) })
export class ToolbarBackgroundEffectsButtonDirective implements AfterViewInit, OnDestroy { export class ToolbarBackgroundEffectsButtonDirective implements AfterViewInit, OnDestroy {
/** /**
@ -310,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);
}
} }
} }
@ -391,7 +514,8 @@ export class ToolbarBackgroundEffectsButtonDirective implements AfterViewInit, O
* <ov-toolbar [settingsButton]="false"></ov-toolbar> * <ov-toolbar [settingsButton]="false"></ov-toolbar>
*/ */
@Directive({ @Directive({
selector: 'ov-videoconference[toolbarSettingsButton], ov-toolbar[settingsButton]' selector: 'ov-videoconference[toolbarSettingsButton], ov-toolbar[settingsButton]',
standalone: false
}) })
export class ToolbarSettingsButtonDirective implements AfterViewInit, OnDestroy { export class ToolbarSettingsButtonDirective implements AfterViewInit, OnDestroy {
/** /**
@ -431,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);
}
} }
} }
@ -453,7 +575,8 @@ export class ToolbarSettingsButtonDirective implements AfterViewInit, OnDestroy
* <ov-toolbar [leaveButton]="false"></ov-toolbar> * <ov-toolbar [leaveButton]="false"></ov-toolbar>
*/ */
@Directive({ @Directive({
selector: 'ov-videoconference[toolbarLeaveButton], ov-toolbar[leaveButton]' selector: 'ov-videoconference[toolbarLeaveButton], ov-toolbar[leaveButton]',
standalone: false
}) })
export class ToolbarLeaveButtonDirective implements AfterViewInit, OnDestroy { export class ToolbarLeaveButtonDirective implements AfterViewInit, OnDestroy {
/** /**
@ -494,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);
}
} }
} }
@ -516,7 +637,8 @@ export class ToolbarLeaveButtonDirective implements AfterViewInit, OnDestroy {
* <ov-toolbar [participantsPanelButton]="false"></ov-toolbar> * <ov-toolbar [participantsPanelButton]="false"></ov-toolbar>
*/ */
@Directive({ @Directive({
selector: 'ov-videoconference[toolbarParticipantsPanelButton], ov-toolbar[participantsPanelButton]' selector: 'ov-videoconference[toolbarParticipantsPanelButton], ov-toolbar[participantsPanelButton]',
standalone: false
}) })
export class ToolbarParticipantsPanelButtonDirective implements AfterViewInit, OnDestroy { export class ToolbarParticipantsPanelButtonDirective implements AfterViewInit, OnDestroy {
/** /**
@ -558,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);
}
} }
} }
@ -580,7 +700,8 @@ export class ToolbarParticipantsPanelButtonDirective implements AfterViewInit, O
* <ov-toolbar [chatPanelButton]="false"></ov-toolbar> * <ov-toolbar [chatPanelButton]="false"></ov-toolbar>
*/ */
@Directive({ @Directive({
selector: 'ov-videoconference[toolbarChatPanelButton], ov-toolbar[chatPanelButton]' selector: 'ov-videoconference[toolbarChatPanelButton], ov-toolbar[chatPanelButton]',
standalone: false
}) })
export class ToolbarChatPanelButtonDirective implements AfterViewInit, OnDestroy { export class ToolbarChatPanelButtonDirective implements AfterViewInit, OnDestroy {
/** /**
@ -620,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);
}
} }
} }
@ -642,7 +761,8 @@ export class ToolbarChatPanelButtonDirective implements AfterViewInit, OnDestroy
* <ov-toolbar [activitiesPanelButton]="false"></ov-toolbar> * <ov-toolbar [activitiesPanelButton]="false"></ov-toolbar>
*/ */
@Directive({ @Directive({
selector: 'ov-videoconference[toolbarActivitiesPanelButton], ov-toolbar[activitiesPanelButton]' selector: 'ov-videoconference[toolbarActivitiesPanelButton], ov-toolbar[activitiesPanelButton]',
standalone: false
}) })
export class ToolbarActivitiesPanelButtonDirective implements AfterViewInit, OnDestroy { export class ToolbarActivitiesPanelButtonDirective implements AfterViewInit, OnDestroy {
/** /**
@ -682,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);
}
} }
} }
@ -704,7 +822,8 @@ export class ToolbarActivitiesPanelButtonDirective implements AfterViewInit, OnD
* <ov-toolbar [displayRoomName]="false"></ov-toolbar> * <ov-toolbar [displayRoomName]="false"></ov-toolbar>
*/ */
@Directive({ @Directive({
selector: 'ov-videoconference[toolbarDisplayRoomName], ov-toolbar[displayRoomName]' selector: 'ov-videoconference[toolbarDisplayRoomName], ov-toolbar[displayRoomName]',
standalone: false
}) })
export class ToolbarDisplayRoomNameDirective implements AfterViewInit, OnDestroy { export class ToolbarDisplayRoomNameDirective implements AfterViewInit, OnDestroy {
/** /**
@ -745,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);
}
} }
} }
@ -767,7 +884,8 @@ export class ToolbarDisplayRoomNameDirective implements AfterViewInit, OnDestroy
* <ov-toolbar [displayLogo]="false"></ov-toolbar> * <ov-toolbar [displayLogo]="false"></ov-toolbar>
*/ */
@Directive({ @Directive({
selector: 'ov-videoconference[toolbarDisplayLogo], ov-toolbar[displayLogo]' selector: 'ov-videoconference[toolbarDisplayLogo], ov-toolbar[displayLogo]',
standalone: false
}) })
export class ToolbarDisplayLogoDirective implements AfterViewInit, OnDestroy { export class ToolbarDisplayLogoDirective implements AfterViewInit, OnDestroy {
/** /**
@ -808,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);
}
} }
} }
@ -827,7 +943,8 @@ export class ToolbarDisplayLogoDirective implements AfterViewInit, OnDestroy {
* *
*/ */
@Directive({ @Directive({
selector: '[ovToolbarAdditionalButtonsPosition]' selector: '[ovToolbarAdditionalButtonsPosition]',
standalone: false
}) })
export class ToolbarAdditionalButtonsPossitionDirective implements AfterViewInit, OnDestroy { export class ToolbarAdditionalButtonsPossitionDirective implements AfterViewInit, OnDestroy {
/** /**
@ -864,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

@ -1,4 +1,4 @@
import { Directive, ElementRef, Input, OnDestroy, OnInit } from '@angular/core'; import { AfterViewInit, Directive, ElementRef, Input, OnDestroy, OnInit } from '@angular/core';
import { CaptionsLangOption } from '../../models/caption.model'; import { CaptionsLangOption } from '../../models/caption.model';
// import { CaptionService } from '../../services/caption/caption.service'; // import { CaptionService } from '../../services/caption/caption.service';
import { OpenViduComponentsConfigService } from '../../services/config/directive-config.service'; import { OpenViduComponentsConfigService } from '../../services/config/directive-config.service';
@ -18,7 +18,8 @@ import { StorageService } from '../../services/storage/storage.service';
* <ov-videoconference [livekitUrl]="http://localhost:1234"></ov-videoconference> * <ov-videoconference [livekitUrl]="http://localhost:1234"></ov-videoconference>
*/ */
@Directive({ @Directive({
selector: 'ov-videoconference[livekitUrl]' selector: 'ov-videoconference[livekitUrl]',
standalone: false
}) })
export class LivekitUrlDirective implements OnDestroy { export class LivekitUrlDirective implements OnDestroy {
/** /**
@ -54,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 });
} }
} }
@ -70,7 +71,8 @@ export class LivekitUrlDirective implements OnDestroy {
* <ov-videoconference [token]="token"></ov-videoconference> * <ov-videoconference [token]="token"></ov-videoconference>
*/ */
@Directive({ @Directive({
selector: 'ov-videoconference[token]' selector: 'ov-videoconference[token]',
standalone: false
}) })
export class TokenDirective implements OnDestroy { export class TokenDirective implements OnDestroy {
/** /**
@ -106,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 });
} }
} }
@ -121,7 +123,8 @@ export class TokenDirective implements OnDestroy {
* <ov-videoconference [tokenError]="error"></ov-videoconference> * <ov-videoconference [tokenError]="error"></ov-videoconference>
*/ */
@Directive({ @Directive({
selector: 'ov-videoconference[tokenError]' selector: 'ov-videoconference[tokenError]',
standalone: false
}) })
export class TokenErrorDirective implements OnDestroy { export class TokenErrorDirective implements OnDestroy {
/** /**
@ -157,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 });
} }
} }
@ -172,7 +175,8 @@ export class TokenErrorDirective implements OnDestroy {
* <ov-videoconference [minimal]="true"></ov-videoconference> * <ov-videoconference [minimal]="true"></ov-videoconference>
*/ */
@Directive({ @Directive({
selector: 'ov-videoconference[minimal]' selector: 'ov-videoconference[minimal]',
standalone: false
}) })
export class MinimalDirective implements OnDestroy { export class MinimalDirective implements OnDestroy {
/** /**
@ -208,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);
}
} }
} }
@ -221,7 +223,7 @@ export class MinimalDirective implements OnDestroy {
* *
* **Default:** English `en` * **Default:** English `en`
* *
* **Available:** * **Available Langs:**
* *
* * English: `en` * * English: `en`
* * Spanish: `es` * * Spanish: `es`
@ -238,7 +240,8 @@ export class MinimalDirective implements OnDestroy {
* <ov-videoconference [lang]="'es'"></ov-videoconference> * <ov-videoconference [lang]="'es'"></ov-videoconference>
*/ */
@Directive({ @Directive({
selector: 'ov-videoconference[lang]' selector: 'ov-videoconference[lang]',
standalone: false
}) })
export class LangDirective implements OnDestroy { export class LangDirective implements OnDestroy {
/** /**
@ -307,7 +310,8 @@ export class LangDirective implements OnDestroy {
* <ov-videoconference [langOptions]="[{name:'Spanish', lang: 'es'}]"></ov-videoconference> * <ov-videoconference [langOptions]="[{name:'Spanish', lang: 'es'}]"></ov-videoconference>
*/ */
@Directive({ @Directive({
selector: 'ov-videoconference[langOptions]' selector: 'ov-videoconference[langOptions]',
standalone: false
}) })
export class LangOptionsDirective implements OnDestroy { export class LangOptionsDirective implements OnDestroy {
/** /**
@ -488,9 +492,10 @@ export class LangOptionsDirective implements OnDestroy {
* <ov-videoconference [participantName]="'OpenVidu'"></ov-videoconference> * <ov-videoconference [participantName]="'OpenVidu'"></ov-videoconference>
*/ */
@Directive({ @Directive({
selector: 'ov-videoconference[participantName]' selector: 'ov-videoconference[participantName]',
standalone: false
}) })
export class ParticipantNameDirective implements OnInit { export class ParticipantNameDirective implements AfterViewInit, OnDestroy {
/** /**
* @ignore * @ignore
*/ */
@ -509,7 +514,7 @@ export class ParticipantNameDirective implements OnInit {
/** /**
* @ignore * @ignore
*/ */
ngOnInit(): void { ngAfterViewInit(): void {
this.update(this.participantName); this.update(this.participantName);
} }
@ -531,7 +536,7 @@ export class ParticipantNameDirective implements OnInit {
* @ignore * @ignore
*/ */
update(value: string) { update(value: string) {
this.libService.setParticipantName(value); if (value) this.libService.updateGeneralConfig({ participantName: value });
} }
} }
@ -546,7 +551,8 @@ export class ParticipantNameDirective implements OnInit {
* <ov-videoconference [prejoin]="false"></ov-videoconference> * <ov-videoconference [prejoin]="false"></ov-videoconference>
*/ */
@Directive({ @Directive({
selector: 'ov-videoconference[prejoin]' selector: 'ov-videoconference[prejoin]',
standalone: false
}) })
export class PrejoinDirective implements OnDestroy { export class PrejoinDirective implements OnDestroy {
/** /**
@ -582,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);
}
} }
} }
@ -600,7 +604,8 @@ export class PrejoinDirective implements OnDestroy {
* <ov-videoconference [videoEnabled]="false"></ov-videoconference> * <ov-videoconference [videoEnabled]="false"></ov-videoconference>
*/ */
@Directive({ @Directive({
selector: 'ov-videoconference[videoEnabled]' selector: 'ov-videoconference[videoEnabled]',
standalone: false
}) })
export class VideoEnabledDirective implements OnDestroy { export class VideoEnabledDirective implements OnDestroy {
/** /**
@ -654,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 });
} }
} }
} }
@ -671,7 +676,8 @@ export class VideoEnabledDirective implements OnDestroy {
*/ */
@Directive({ @Directive({
selector: 'ov-videoconference[audioEnabled]' selector: 'ov-videoconference[audioEnabled]',
standalone: false
}) })
export class AudioEnabledDirective implements OnDestroy { export class AudioEnabledDirective implements OnDestroy {
/** /**
@ -721,7 +727,132 @@ 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 });
}
}
}
/**
* The **recordingStreamBaseUrl** directive sets the base URL for retrieving recording streams.
* The complete request URL is dynamically constructed by concatenating the supplied URL, the
* internally managed recordingId, and the `/media` segment.
*
* The final URL format will be:
*
* {recordingStreamBaseUrl}/{recordingId}/media
*
* Default: `"call/api/recordings/{recordingId}/stream"`
*
* Example:
* Given a recordingStreamBaseUrl of `api/recordings`, the resulting URL for a recordingId of `12345` would be:
* `api/recordings/12345/media`
*
* It is essential that the resulting route is declared and configured on your backend, as it is
* used for serving and accessing the recording streams.
*
* @example
* <ov-videoconference [recordingStreamBaseUrl]="'https://myserver.com/api/recordings'">
* </ov-videoconference>
*/
@Directive({
selector: 'ov-videoconference[recordingStreamBaseUrl]',
standalone: false
})
export class RecordingStreamBaseUrlDirective implements AfterViewInit, OnDestroy {
/**
* @ignore
*/
@Input() set recordingStreamBaseUrl(url: string) {
this.update(url);
}
/**
* @ignore
*/
constructor(
private elementRef: ElementRef,
private libService: OpenViduComponentsConfigService
) {}
/**
* @ignore
*/
ngAfterViewInit(): void {
this.update(this.recordingStreamBaseUrl);
}
/**
* @ignore
*/
ngOnDestroy(): void {
this.clear();
}
/**
* @ignore
*/
clear() {
this.update('');
}
/**
* @ignore
*/
update(value: string) {
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

@ -33,7 +33,7 @@
"AUDIO_DEVICE": "音频设备", "AUDIO_DEVICE": "音频设备",
"NO_VIDEO_DEVICE": "未找到视频设备", "NO_VIDEO_DEVICE": "未找到视频设备",
"NO_AUDIO_DEVICE": "未找到音频设备", "NO_AUDIO_DEVICE": "未找到音频设备",
"JOIN": "加入会话", "JOIN": "加入房间",
"PREPARING": "筹备会议" "PREPARING": "筹备会议"
}, },
"TOOLBAR": { "TOOLBAR": {
@ -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": "设置",
@ -100,7 +104,7 @@
"CAPTIONS": "字幕", "CAPTIONS": "字幕",
"DISABLED_AUDIO": "没有音频设备", "DISABLED_AUDIO": "没有音频设备",
"DISABLED_VIDEO": "没有视频设备", "DISABLED_VIDEO": "没有视频设备",
"CAPTIONS_LANG_TEXT": "选择会话参与者将使用的语言。字幕将以该语言显示。" "CAPTIONS_LANG_TEXT": "选择房间参与者将使用的语言。字幕将以该语言显示。"
}, },
"BACKGROUND": { "BACKGROUND": {
"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": "直播",
@ -139,9 +150,17 @@
} }
}, },
"ERRORS": { "ERRORS": {
"SESSION": "连接到会话时有错误", "SESSION": "连接到房间时有错误",
"CONNECTION": "连接丢失", "CONNECTION": "连接丢失",
"RECONNECT": "试图重新连接到会话", "RECONNECT": "试图重新连接到房间",
"DISCONNECT": "您已断开连接",
"NETWORK_DISCONNECT": "由于网络连接问题,您已断开连接",
"SIGNAL_CLOSE": "与服务器的连接意外关闭",
"SERVER_SHUTDOWN": "服务器当前已关闭或正在维护中",
"PARTICIPANT_REMOVED": "您已被移出此房间",
"ROOM_DELETED": "此房间已被删除",
"DUPLICATE_IDENTITY": "由于您的昵称已被分配给另一位参与者,您已被断开连接",
"UNKNOWN_DISCONNECT": "您已从房间断开连接",
"TOGGLE_CAMERA": "切换相机时出现错误", "TOGGLE_CAMERA": "切换相机时出现错误",
"TOGGLE_MICROPHONE": "切换麦克风时出现错误", "TOGGLE_MICROPHONE": "切换麦克风时出现错误",
"SCREEN_SHARING": "分享屏幕出错", "SCREEN_SHARING": "分享屏幕出错",

View File

@ -33,8 +33,8 @@
"AUDIO_DEVICE": "Audiogerät", "AUDIO_DEVICE": "Audiogerät",
"NO_VIDEO_DEVICE": "Video-Gerät nicht gefunden", "NO_VIDEO_DEVICE": "Video-Gerät nicht gefunden",
"NO_AUDIO_DEVICE": "Audio-Gerät nicht gefunden", "NO_AUDIO_DEVICE": "Audio-Gerät nicht gefunden",
"JOIN": "Sitzung beitreten", "JOIN": "Raum beitreten",
"PREPARING": "Sitzung vorbereiten..." "PREPARING": "Raum vorbereiten..."
}, },
"TOOLBAR": { "TOOLBAR": {
"MUTE_AUDIO": "Stummschalten des Audios", "MUTE_AUDIO": "Stummschalten des Audios",
@ -52,10 +52,11 @@
"START_RECORDING": "Aufzeichnung starten", "START_RECORDING": "Aufzeichnung starten",
"STOP_RECORDING": "Aufzeichnung stoppen", "STOP_RECORDING": "Aufzeichnung stoppen",
"SETTINGS": "Einstellungen", "SETTINGS": "Einstellungen",
"LEAVE": "Die Sitzung 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",
@ -77,7 +78,7 @@
"CHAT": { "CHAT": {
"TITLE": "Chat", "TITLE": "Chat",
"YOU": "Sie", "YOU": "Sie",
"SUBTITLE": "Nachrichten werden am Ende der Sitzung entfernt", "SUBTITLE": "Nachrichten werden am Ende der Raum entfernt",
"PLACEHOLDER": "Eine Nachricht senden...", "PLACEHOLDER": "Eine Nachricht senden...",
"SEND": "Senden", "SEND": "Senden",
"MESSAGE_SENT_NOTIFICATION": "Nachricht gesendet", "MESSAGE_SENT_NOTIFICATION": "Nachricht gesendet",
@ -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",
@ -100,7 +103,7 @@
"CAPTIONS": "Untertitel", "CAPTIONS": "Untertitel",
"DISABLED_AUDIO": "Audio deaktiviert", "DISABLED_AUDIO": "Audio deaktiviert",
"DISABLED_VIDEO": "Video deaktiviert", "DISABLED_VIDEO": "Video deaktiviert",
"CAPTIONS_LANG_TEXT": "Wählen Sie die Sprache, die die Teilnehmer der Sitzung verwenden. Die Untertitel werden in dieser Sprache angezeigt." "CAPTIONS_LANG_TEXT": "Wählen Sie die Sprache, die die Teilnehmer der Raum verwenden. Die Untertitel werden in dieser Sprache angezeigt."
}, },
"BACKGROUND": { "BACKGROUND": {
"TITLE": "Hintergrund-Effekte", "TITLE": "Hintergrund-Effekte",
@ -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",
@ -139,9 +144,17 @@
} }
}, },
"ERRORS": { "ERRORS": {
"SESSION": "Es ist ein Fehler beim Verbinden mit der Sitzung aufgetreten", "SESSION": "Es ist ein Fehler beim Verbinden mit der Raum aufgetreten",
"CONNECTION": "Verbindung verloren", "CONNECTION": "Verbindung verloren",
"RECONNECT": "Ich versuche, die Verbindung zur Sitzung wiederherzustellen...", "RECONNECT": "Ich versuche, die Verbindung zur Raum wiederherzustellen...",
"DISCONNECT": "Sie wurden getrennt",
"NETWORK_DISCONNECT": "Sie wurden aufgrund eines Netzwerkproblems getrennt",
"SIGNAL_CLOSE": "Die Verbindung zum Server wurde unerwartet geschlossen",
"SERVER_SHUTDOWN": "Der Server ist derzeit nicht verfügbar oder wird gewartet",
"PARTICIPANT_REMOVED": "Sie wurden aus diesem Raum entfernt",
"ROOM_DELETED": "Dieser Raum wurde gelöscht",
"DUPLICATE_IDENTITY": "Sie wurden getrennt, da Ihr Spitzname einem anderen Teilnehmer zugewiesen wurde",
"UNKNOWN_DISCONNECT": "Sie wurden vom Raum getrennt",
"TOGGLE_CAMERA": "Es gab einen Fehler beim Umschalten der Kamera", "TOGGLE_CAMERA": "Es gab einen Fehler beim Umschalten der Kamera",
"TOGGLE_MICROPHONE": "Es ist ein Fehler beim Umschalten des Mikrofons aufgetreten", "TOGGLE_MICROPHONE": "Es ist ein Fehler beim Umschalten des Mikrofons aufgetreten",
"SCREEN_SHARING": "Fehler beim Teilen des Bildschirms", "SCREEN_SHARING": "Fehler beim Teilen des Bildschirms",

View File

@ -30,8 +30,8 @@
"AUDIO_DEVICE": "Audio device", "AUDIO_DEVICE": "Audio device",
"NO_VIDEO_DEVICE": "Video device not found", "NO_VIDEO_DEVICE": "Video device not found",
"NO_AUDIO_DEVICE": "Audio device not found", "NO_AUDIO_DEVICE": "Audio device not found",
"JOIN": "Join session", "JOIN": "Join room",
"PREPARING": "Preparing session..." "PREPARING": "Preparing room..."
}, },
"ROOM": { "ROOM": {
"JOINING": "Joining room..." "JOINING": "Joining room..."
@ -52,10 +52,12 @@
"START_RECORDING": "Start recording", "START_RECORDING": "Start recording",
"STOP_RECORDING": "Stop recording", "STOP_RECORDING": "Stop recording",
"SETTINGS": "Settings", "SETTINGS": "Settings",
"LEAVE": "Leave the session", "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",
@ -77,7 +79,7 @@
"CHAT": { "CHAT": {
"TITLE": "Chat", "TITLE": "Chat",
"YOU": "You", "YOU": "You",
"SUBTITLE": "Messages will be removed at the end of the session", "SUBTITLE": "Messages will be removed at the end of the room",
"PLACEHOLDER": "Send a message...", "PLACEHOLDER": "Send a message...",
"SEND": "Send", "SEND": "Send",
"MESSAGE_SENT_NOTIFICATION": "message sent", "MESSAGE_SENT_NOTIFICATION": "message sent",
@ -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",
@ -100,7 +104,7 @@
"CAPTIONS": "Captions", "CAPTIONS": "Captions",
"DISABLED_AUDIO": "Audio disabled", "DISABLED_AUDIO": "Audio disabled",
"DISABLED_VIDEO": "Video disabled", "DISABLED_VIDEO": "Video disabled",
"CAPTIONS_LANG_TEXT": "Select the language that the participants of the session will use. The captions will appear in that language." "CAPTIONS_LANG_TEXT": "Select the language that the participants of the room will use. The captions will appear in that language."
}, },
"BACKGROUND": { "BACKGROUND": {
"TITLE": "Background effects", "TITLE": "Background effects",
@ -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",
@ -139,9 +154,17 @@
} }
}, },
"ERRORS": { "ERRORS": {
"SESSION": "There was an error connecting to the session", "SESSION": "There was an error connecting to the room",
"CONNECTION": "Connection lost", "CONNECTION": "Connection lost",
"RECONNECT": "Oops! Trying to reconnect to the session...", "RECONNECT": "Oops! Trying to reconnect to the room...",
"DISCONNECT": "You have been disconnected",
"NETWORK_DISCONNECT": "You were disconnected due to a network connectivity issue",
"SIGNAL_CLOSE": "The connection to the server was unexpectedly closed",
"SERVER_SHUTDOWN": "The server is currently down or under maintenance",
"PARTICIPANT_REMOVED": "You have been removed from this room",
"ROOM_DELETED": "This room has been deleted",
"DUPLICATE_IDENTITY": "You have been disconnected because your alias was assigned to another participant",
"UNKNOWN_DISCONNECT": "You have been disconnected from the room",
"TOGGLE_CAMERA": "There was an error toggling camera", "TOGGLE_CAMERA": "There was an error toggling camera",
"TOGGLE_MICROPHONE": "There was an error toggling microhpone", "TOGGLE_MICROPHONE": "There was an error toggling microhpone",
"SCREEN_SHARING": "Error sharing screen", "SCREEN_SHARING": "Error sharing screen",

View File

@ -33,7 +33,7 @@
"AUDIO_DEVICE": "Dispositivo de audio", "AUDIO_DEVICE": "Dispositivo de audio",
"NO_VIDEO_DEVICE": "Dispositivo de vídeo no encontrado", "NO_VIDEO_DEVICE": "Dispositivo de vídeo no encontrado",
"NO_AUDIO_DEVICE": "Dispositivo de audio no encontrado", "NO_AUDIO_DEVICE": "Dispositivo de audio no encontrado",
"PREPARING": "Preparando la session ...", "PREPARING": "Preparando la sala ...",
"JOIN": "Unirme ahora" "JOIN": "Unirme ahora"
}, },
"TOOLBAR": { "TOOLBAR": {
@ -52,10 +52,12 @@
"START_RECORDING": "Iniciar grabación", "START_RECORDING": "Iniciar grabación",
"STOP_RECORDING": "Detener grabación", "STOP_RECORDING": "Detener grabación",
"SETTINGS": "Configuración", "SETTINGS": "Configuración",
"LEAVE": "Salir de la sesión", "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",
@ -77,7 +79,7 @@
"CHAT": { "CHAT": {
"TITLE": "Chat", "TITLE": "Chat",
"YOU": "Tú", "YOU": "Tú",
"SUBTITLE": "Los mensajes se borrarán al finalizar la sesión", "SUBTITLE": "Los mensajes se borrarán al salir de la sala",
"PLACEHOLDER": "Enviar mensaje...", "PLACEHOLDER": "Enviar mensaje...",
"SEND": "Enviar", "SEND": "Enviar",
"MESSAGE_SENT_NOTIFICATION": "mensaje enviado", "MESSAGE_SENT_NOTIFICATION": "mensaje enviado",
@ -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",
@ -100,7 +104,7 @@
"CAPTIONS": "Subtítulos", "CAPTIONS": "Subtítulos",
"DISABLED_AUDIO": "Audio desactivado", "DISABLED_AUDIO": "Audio desactivado",
"DISABLED_VIDEO": "Video desactivado", "DISABLED_VIDEO": "Video desactivado",
"CAPTIONS_LANG_TEXT": "Selecciona el idioma que usarán los participantes de la sesión. Los subtítulos aparecerán en ese idioma." "CAPTIONS_LANG_TEXT": "Selecciona el idioma que usarán los participantes de la sala. Los subtítulos aparecerán en ese idioma."
}, },
"BACKGROUND": { "BACKGROUND": {
"TITLE": "Efectos de fondo", "TITLE": "Efectos de fondo",
@ -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",
@ -138,9 +149,17 @@
} }
}, },
"ERRORS": { "ERRORS": {
"SESSION": "Hubo un error al conectar a la sesión", "SESSION": "Hubo un error al conectar a la sala",
"CONNECTION": "Sin conexión", "CONNECTION": "Sin conexión",
"RECONNECT": "Intentando reconectar a la sesión...", "RECONNECT": "Intentando reconectar a la sala...",
"DISCONNECT": "Te has desconectado",
"NETWORK_DISCONNECT": "Te desconectaste debido a un problema de conectividad de red",
"SIGNAL_CLOSE": "La conexión con el servidor se cerró inesperadamente",
"SERVER_SHUTDOWN": "El servidor está actualmente fuera de servicio o en mantenimiento",
"PARTICIPANT_REMOVED": "Has sido eliminado de esta sala",
"ROOM_DELETED": "Esta sala ha sido eliminada",
"DUPLICATE_IDENTITY": "Te has desconectado porque tu alias ha sido asignado a otro participante",
"UNKNOWN_DISCONNECT": "Te has desconectado de la sala",
"TOGGLE_CAMERA": "Hubo un error cambiando la cámara", "TOGGLE_CAMERA": "Hubo un error cambiando la cámara",
"TOGGLE_MICROPHONE": "Hubo un error cambiando el micrófono", "TOGGLE_MICROPHONE": "Hubo un error cambiando el micrófono",
"SCREEN_SHARING": "Hubo un error compartiendo pantalla", "SCREEN_SHARING": "Hubo un error compartiendo pantalla",

View File

@ -33,8 +33,8 @@
"AUDIO_DEVICE": "Périphérique audio", "AUDIO_DEVICE": "Périphérique audio",
"NO_VIDEO_DEVICE": "Appareil vidéo introuvable", "NO_VIDEO_DEVICE": "Appareil vidéo introuvable",
"NO_AUDIO_DEVICE": "Appareil audio introuvable", "NO_AUDIO_DEVICE": "Appareil audio introuvable",
"JOIN": "Joindre une session", "JOIN": "Joindre une salle",
"PREPARING": "Préparation de la session ..." "PREPARING": "Préparation de la salle ..."
}, },
"TOOLBAR": { "TOOLBAR": {
"MUTE_AUDIO": "Mettez votre audio en sourdine", "MUTE_AUDIO": "Mettez votre audio en sourdine",
@ -52,10 +52,12 @@
"START_RECORDING": "démarrer l'enregistrement", "START_RECORDING": "démarrer l'enregistrement",
"STOP_RECORDING": "Arrêter l'enregistrement", "STOP_RECORDING": "Arrêter l'enregistrement",
"SETTINGS": "Paramètres", "SETTINGS": "Paramètres",
"LEAVE": "Quitter la session", "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",
@ -77,7 +79,7 @@
"CHAT": { "CHAT": {
"TITLE": "Chat", "TITLE": "Chat",
"YOU": "Vous", "YOU": "Vous",
"SUBTITLE": "Les messages seront supprimés à la fin de la session", "SUBTITLE": "Les messages seront supprimés à la fin de la salle",
"PLACEHOLDER": "Envoyer un message...", "PLACEHOLDER": "Envoyer un message...",
"SEND": "Envoyer", "SEND": "Envoyer",
"MESSAGE_SENT_NOTIFICATION": "message envoyé", "MESSAGE_SENT_NOTIFICATION": "message envoyé",
@ -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",
@ -100,7 +104,7 @@
"CAPTIONS": "Les sous-titres", "CAPTIONS": "Les sous-titres",
"DISABLED_AUDIO": "Désactiver l'audio", "DISABLED_AUDIO": "Désactiver l'audio",
"DISABLED_VIDEO": "Désactiver la vidéo", "DISABLED_VIDEO": "Désactiver la vidéo",
"CAPTIONS_LANG_TEXT": "Sélectionnez la langue que les participants de la session utiliseront. Les sous-titres apparaîtront dans cette langue." "CAPTIONS_LANG_TEXT": "Sélectionnez la langue que les participants de la salle utiliseront. Les sous-titres apparaîtront dans cette langue."
}, },
"BACKGROUND": { "BACKGROUND": {
"TITLE": "Effets de fond", "TITLE": "Effets de fond",
@ -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",
@ -139,9 +150,17 @@
} }
}, },
"ERRORS": { "ERRORS": {
"SESSION": "There was an error connecting to the session", "SESSION": "There was an error connecting to the salle",
"CONNECTION": "Connexion perdue", "CONNECTION": "Connexion perdue",
"RECONNECT": "Oups ! Tentative de reconnexion à la session...", "RECONNECT": "Oups ! Tentative de reconnexion à la salle...",
"DISCONNECT": "Vous avez été déconnecté",
"NETWORK_DISCONNECT": "Vous avez été déconnecté en raison d'un problème de connexion réseau",
"SIGNAL_CLOSE": "La connexion au serveur a été interrompue de manière inattendue",
"SERVER_SHUTDOWN": "Le serveur est actuellement hors service ou en maintenance",
"PARTICIPANT_REMOVED": "Vous avez été retiré de cette salle",
"ROOM_DELETED": "Cette salle a été supprimée",
"DUPLICATE_IDENTITY": "Vous avez été déconnecté car votre pseudo a été attribué à un autre participant",
"UNKNOWN_DISCONNECT": "Vous avez été déconnecté de la salle",
"TOGGLE_CAMERA": "There was an error toggle camera", "TOGGLE_CAMERA": "There was an error toggle camera",
"TOGGLE_MICROPHONE": "There was an error toggling microhpone", "TOGGLE_MICROPHONE": "There was an error toggling microhpone",
"SCREEN_SHARING": "Erreur de partage d'écran", "SCREEN_SHARING": "Erreur de partage d'écran",

View File

@ -33,8 +33,8 @@
"AUDIO_DEVICE": "ऑडियो डिवाइस", "AUDIO_DEVICE": "ऑडियो डिवाइस",
"NO_VIDEO_DEVICE": "वीडियो डिवाइस नहीं मिला", "NO_VIDEO_DEVICE": "वीडियो डिवाइस नहीं मिला",
"NO_AUDIO_DEVICE": "ऑडियो डिवाइस नहीं मिला", "NO_AUDIO_DEVICE": "ऑडियो डिवाइस नहीं मिला",
"JOIN": "सत्र में शामिल हों", "JOIN": "कमरा में शामिल हों",
"PREPARING": "सत्र तैयार कर रहा है ..." "PREPARING": "कमरा तैयार कर रहा है ..."
}, },
"TOOLBAR": { "TOOLBAR": {
"MUTE_AUDIO": "अपनी ऑडियो को मौन करें", "MUTE_AUDIO": "अपनी ऑडियो को मौन करें",
@ -52,10 +52,12 @@
"START_RECORDING": "रिकॉर्डिंग प्रारंभ करें", "START_RECORDING": "रिकॉर्डिंग प्रारंभ करें",
"STOP_RECORDING": "रिकॉर्डिंग रोकें", "STOP_RECORDING": "रिकॉर्डिंग रोकें",
"SETTINGS": "सेटिंग्स", "SETTINGS": "सेटिंग्स",
"LEAVE": "सत्र छोड़ें", "LEAVE": "कमरा छोड़ें",
"PARTICIPANTS": "सदस्य", "PARTICIPANTS": "सदस्य",
"CHAT": "बातचीत", "CHAT": "बातचीत",
"ACTIVITIES": "गतिविधियाँ" "ACTIVITIES": "गतिविधियाँ",
"NO_TRACKS_PUBLISHED": "रिकॉर्डिंग शुरू करने के लिए ऑडियो या वीडियो साझा करें।",
"VIEW_RECORDINGS": "रिकॉर्डिंग देखें"
}, },
"STREAM": { "STREAM": {
"SETTINGS": "सेटिंग्स", "SETTINGS": "सेटिंग्स",
@ -77,7 +79,7 @@
"CHAT": { "CHAT": {
"TITLE": "बातचीत", "TITLE": "बातचीत",
"YOU": "आप", "YOU": "आप",
"SUBTITLE": "सत्र समाप्त होने पर संदेश हटा दिए जाएंगे", "SUBTITLE": "कमरा समाप्त होने पर संदेश हटा दिए जाएंगे",
"PLACEHOLDER": "एक संदेश भेजें ...", "PLACEHOLDER": "एक संदेश भेजें ...",
"SEND": "भेजें", "SEND": "भेजें",
"MESSAGE_SENT_NOTIFICATION": "संदेश भेजा गया", "MESSAGE_SENT_NOTIFICATION": "संदेश भेजा गया",
@ -89,7 +91,9 @@
"MICROPHONE": "माइक्रोफ़ोन", "MICROPHONE": "माइक्रोफ़ोन",
"SCREEN": "स्क्रीन", "SCREEN": "स्क्रीन",
"NO_STREAMS": "कोई_स्ट्रीम_नहीं", "NO_STREAMS": "कोई_स्ट्रीम_नहीं",
"YOU": "आप" "YOU": "आप",
"MUTE": "मौन",
"UNMUTE": "अनमौन"
}, },
"SETTINGS": { "SETTINGS": {
"TITLE": "सेटिंग्स", "TITLE": "सेटिंग्स",
@ -100,7 +104,7 @@
"CAPTIONS": "उपशीर्षक", "CAPTIONS": "उपशीर्षक",
"DISABLED_AUDIO": "ऑडियो अक्षम", "DISABLED_AUDIO": "ऑडियो अक्षम",
"DISABLED_VIDEO": "वीडियो अक्षम", "DISABLED_VIDEO": "वीडियो अक्षम",
"CAPTIONS_LANG_TEXT": "उस भाषा का चयन करें जिसका उपयोग सत्र के प्रतिभागी करेंगे। उपशीर्षक उस भाषा में दिखाई देंगे।" "CAPTIONS_LANG_TEXT": "उस भाषा का चयन करें जिसका उपयोग कमरा के प्रतिभागी करेंगे। उपशीर्षक उस भाषा में दिखाई देंगे।"
}, },
"BACKGROUND": { "BACKGROUND": {
"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": "स्ट्रीमिंग",
@ -139,9 +150,17 @@
} }
}, },
"ERRORS": { "ERRORS": {
"SESSION": "सत्र से जुड़ने में त्रुटि हुई", "SESSION": "कमरा से जुड़ने में त्रुटि हुई",
"CONNECTION": "कनेक्शन खो गया", "CONNECTION": "कनेक्शन खो गया",
"RECONNECT": "ओह! सत्र से फिर से कनेक्ट करने का प्रयास कर रहा है", "RECONNECT": "ओह! कमरा से फिर से कनेक्ट करने का प्रयास कर रहा है",
"DISCONNECT": "आपको डिस्कनेक्ट कर दिया गया है",
"NETWORK_DISCONNECT": "नेटवर्क कनेक्टिविटी समस्या के कारण आपका कनेक्शन टूट गया",
"SIGNAL_CLOSE": "सर्वर से कनेक्शन अप्रत्याशित रूप से बंद हो गया",
"SERVER_SHUTDOWN": "सर्वर अभी बंद है या रखरखाव में है",
"PARTICIPANT_REMOVED": "आपको इस कमरे से हटा दिया गया है",
"ROOM_DELETED": "यह कमरा हटा दिया गया है",
"DUPLICATE_IDENTITY": "आपको डिस्कनेक्ट कर दिया गया क्योंकि आपका उपनाम किसी अन्य प्रतिभागी को सौंपा गया है",
"UNKNOWN_DISCONNECT": "आप कक्ष से डिस्कनेक्ट हो गए हैं",
"TOGGLE_CAMERA": "कैमरा टॉगल करने में त्रुटि हुई", "TOGGLE_CAMERA": "कैमरा टॉगल करने में त्रुटि हुई",
"TOGGLE_MICROPHONE": "माइक्रोफ़ोन को चालू करने में त्रुटि हुई", "TOGGLE_MICROPHONE": "माइक्रोफ़ोन को चालू करने में त्रुटि हुई",
"SCREEN_SHARING": "स्क्रीन साझा करने में त्रुटि", "SCREEN_SHARING": "स्क्रीन साझा करने में त्रुटि",

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