Compare commits

...

642 Commits

Author SHA1 Message Date
GitHub Actions
6f1486493a Revert "Bump version to 3.6.0"
This reverts commit 885cac991b7a26801b85b99286352d19233cd1ca.
2026-03-06 10:45:29 +00:00
GitHub Actions
885cac991b Bump version to 3.6.0 2026-03-06 10:45:26 +00:00
github-actions
462ee4e24b openvidu-components-angular: Bumped version to 3.6.0 2026-03-06 10:16:17 +00:00
GitHub Actions
68d803a4e9 Add version 3.5.0 -> 3.6.0 to get_next_version() function 2026-03-06 10:11:56 +00:00
GitHub Actions
8874d10214 Update openvidu-livekit submodule 2026-03-06 10:11:55 +00:00
pabloFuente
896b49cb95 Fix selenium port conflict in event bus 2026-03-05 14:20:23 +01:00
pabloFuente
9f5d38d0e7 Update selenium in openvidu-test-browsers 2026-03-05 14:09:02 +01:00
pabloFuente
b2f5e460c4 update selenium version to 4.41.0 in pom.xml 2026-03-05 13:44:37 +01:00
CSantosM
1547645e7e ov-components: Updated peerDep @livekit/track-processors to version 0.7.2 2026-03-03 16:37:31 +01:00
CSantosM
fcf7d62b4b ov-components: update maximumError size for anyComponentStyle to 12kb in angular.json 2026-03-02 17:41:12 +01:00
CSantosM
1f4b6f34e8 ov-components: update @livekit/track-processors to version 0.7.2 and clean up imports in openvidu.service.ts 2026-03-02 17:11:47 +01:00
cruizba
df4adc52a3 openvidu-deployment: Pin tool versions in cloud deployment scripts
- yq: v4.44.5 → v4.52.4 (AWS + GCP)
- aws-cli: unversioned → 2.34.0 (AWS)
- cfn-bootstrap: latest → 2.0-38 (AWS)
- azure-cli: InstallAzureCLIDeb → 2.83.0 via versioned apt (Azure)
- google-cloud-cli: unversioned → 558.0.0 (GCP)
2026-03-02 16:45:35 +01:00
cruizba
6f5ba1d533 openvidu-deployment: Skip aws-cli installation if already present
Nvidia Deep Learning AMIs come with AWS CLI pre-installed, causing
the install script to fail under set -e. Check for existing aws-cli
before attempting installation.
2026-02-28 00:37:17 +01:00
cruizba
ae99a2afee openvidu-deployment: Increase Media Node disk size to 100GB for Nvidia instances 2026-02-27 23:25:28 +01:00
cruizba
17de091cfe openvidu-deployment: Add missing instance types to AllowedValues in CloudFormation templates
Add Graviton families (c8gd, m8gd, r6g, r6gd, r7g, r7gd, r8g) and
GPU/Nvidia families (g4dn, g5, g5g, g6, g6e, g6f, g7e, gr6, gr6f,
p4d, p4de, p5, p5e, p5en, p6-b200, p6-b300, p6e-gb200) that were
referenced in Conditions (IsGraviton/IsNvidia) but missing from the
AllowedValues parameter selectors.
2026-02-27 18:49:34 +01:00
cruizba
c4673170b4 openvidu-deployment: Add missing Graviton instances, Nvidia GPU instances, and Nvidia driver AMI selection
- Add missing Graviton/ARM instance families to CloudFormation conditions:
  r6g, r6gd, r7g, r7gd, r8g, c8gd, m8gd
- Add Nvidia GPU instance detection conditions (IsNvidia/IsMasterNodeNvidia/
  IsMediaNodeNvidia) for families: g4dn, g5, g5g, g6, g6e, g6f, gr6, gr6f,
  g7e, p4d, p4de, p5, p5e, p5en, p6-b200, p6-b300, p6e-gb200
- Use Ubuntu Deep Learning AMIs with pre-installed Nvidia drivers when a
  GPU instance type is selected, choosing arm64 or x86_64 variant based
  on the Graviton condition
2026-02-27 18:16:13 +01:00
cruizba
03c9b4178a openvidu-deployment: Bump third-party Docker image versions
Update infrastructure and monitoring images across all deployment scripts
and docker-compose files:
- Minio server: 2025.9.7-debian-12-r3 -> 2025.10.15-debian-12-r9
- Minio client: minio/mc:RELEASE.2025-08-13T08-35-41Z -> openvidu/minio-client:2025.8.13-debian-12-r12
- MongoDB: 8.0.15 -> 8.0.19 (scripts), 8.0.15-r0 -> 8.0.19-r1 (compose)
- Redis: 8.2.2-alpine -> 8.6.1-alpine
- Alpine: 3.23.2 -> 3.23.3
- Prometheus: v3.7.1 -> v3.9.1
- Promtail: 3.5.7 -> 3.5.11
- Loki: 3.5.7 -> 3.5.11
- Grafana Mimir: 2.17.1 -> 3.0.3
- Grafana: 12.2.0 -> 12.3.4
2026-02-24 15:25:04 +01:00
cruizba
d850e01f53 openvidu-deployment: Bump Docker to 29.2.1 and Docker Compose to v5.0.2 2026-02-23 22:24:59 +01:00
cruizba
ec34374254 Add OPENVIDU_AGENT_PRO_SPEECH_PROCESSING_IMAGE to all deployment scripts
Add new agent-speech-processing-sherpa image variable for pro speech
processing across all install scripts and the update script.
2026-02-23 13:04:35 +01:00
CSantosM
b2701311fb ov-components: Uses device constraints for media tracks
Ensures correct audio and video device selection by applying
`exact` or `ideal` constraints when creating or restarting
media tracks.

This improves device handling and avoids potential issues when
switching between different audio or video devices.
2026-02-20 16:42:36 +01:00
CSantosM
4617dfd797 ov-components: Enhances camera/mic switching in prejoin
Refactors camera and microphone switching logic in the prejoin state.
Uses `restartTrack` to preserve track settings and background processor state.
Improves background effect handling during camera changes.
Creates new tracks only when necessary (camera unavailable).
Ensures proper muting behavior based on device settings.
2026-02-20 16:42:36 +01:00
CSantosM
5de74f2567 ov-components: update video preview section and remove unused animation comment 2026-02-20 16:42:36 +01:00
pabloFuente
375ea757dd Fix RTSP server container startup 2026-02-20 15:44:32 +01:00
pabloFuente
48341c21f6 Improve logging for RTSP server media publication failure in OpenViduTestE2e 2026-02-20 14:33:32 +01:00
pabloFuente
bc38b18bc5 Add use_global_cpu_monitoring and min_disk_space_mb to egress config 2026-02-20 14:00:12 +01:00
pabloFuente
3bf155cf93 Make default agent provider vosk 2026-02-12 12:21:18 +01:00
Piwccle
26c60fd1cc openvidu-deployment: fix - correct typo in project ID description in variables.tf GCP 2026-02-10 17:37:57 +01:00
Piwccle
d74c2d03ea openvidu-deployment: Update default instance type to e2-standard-4 for improved performance 2026-02-10 17:36:47 +01:00
cruizba
0894097546 openvidu-deployment: fix install command URL to use the pro version for single node deployments
It was working thanks to the flag `--deployment-type=single-node-pro`, but the script should point to `/pro` instead of `/community`
2026-02-10 17:12:02 +01:00
Piwccle
649fc40666 Update .gitignore to include *.tfvars files 2026-02-10 16:56:09 +01:00
Piwccle
ed51cf7e79 openvidu-deployment: Fix GCS bucket configuration and add ARM image support in GCP deployment 2026-02-09 17:14:48 +01:00
cruizba
b04de2e76e local-meet: Add MEET_BASE_PATH environment variable to local-meet docker-compose files 2026-02-06 18:11:50 +01:00
Piwccle
41b836a05b openvidu-deployment: GCP - Update instance type defaults to e2-standard-4 for improved performance and consistency between clouds 2026-02-05 18:42:34 +01:00
cruizba
969077e755 openvidu-deployment: Update health check endpoint in deployment scripts
- Changed health check URL from http://localhost:7880 to http://localhost:7880/health/caddy in multiple deployment configurations for GCP, AWS, and Azure.

With new rules of redirection `http://localhost:7880` redirects to `/meet`. It is more stable to check the node by asking `http://localhost:7880/health/caddy`
2026-02-05 13:16:31 +01:00
cruizba
10c6e04db5 openvidu-deployment: Update port configuration from 6080 to 9080 across AWS, Azure, and GCP deployment templates 2026-02-05 12:30:37 +01:00
Piwccle
c4cdddd191 openvidu-deployment: add get_value_from_config script in GCP HA deployment 2026-02-03 14:55:21 +01:00
Piwccle
444188d653 openvidu-deployment: Add scripts for managing OpenVidu configuration and secrets in GCP deployment 2026-02-03 13:24:33 +01:00
Piwccle
f083ca0cec openvidu-deployment: Update Azure deployment to include OpenVidu and LiveKit URLs in key vault secrets 2026-02-03 13:23:28 +01:00
cruizba
fa434d2097 local-meet: Route 9080 through caddy to redir to /meet 2026-02-02 23:34:01 +01:00
Piwccle
9a3811d48e openvidu-deployment: Remove unused cluster data container parameters and related logic 2026-02-02 13:12:10 +01:00
Piwccle
98fc2c2d5b openvidu-deployment: Azure - fix update URL generation to use correct domain variable syntax 2026-01-29 17:59:19 +01:00
Piwccle
a492515ce5 openvidu-deployment: Azure deployment scripts to include OpenVidu and LiveKit URLs in key vault secrets 2026-01-29 17:34:25 +01:00
cruizba
5e31998776 openvidu-deployment: gcp - Use main domain for TURN - Remove TURN server configuration parameters and related logic from deployment templates 2026-01-28 21:36:27 +01:00
cruizba
c43b3e86e3 openvidu-deployment: azure - Use main domain for TURN - Remove TURN server configuration parameters and related logic from deployment templates 2026-01-27 22:41:33 +01:00
cruizba
755da724b3 openvidu-deployment: aws - Change IAM policy resources in CloudFormation templates for automation execution change in AWS 2026-01-27 20:00:54 +01:00
Piwccle
942e5d1062 Merge branch 'master' of https://github.com/OpenVidu/openvidu 2026-01-27 16:38:05 +01:00
Piwccle
483fda785a openvidu-deployment: Update default max number of media nodes to 5 to match the other clouds 2026-01-27 16:37:33 +01:00
cruizba
617c642215 openvidu-deployment: aws - Use main domain for TURN - Remove TURN server configuration parameters and related logic from deployment templates 2026-01-27 16:37:33 +01:00
CSantosM
83e38ae88a ov-components: Refactor pre-join component to replace Angular animations with CSS animations and optimize video preview container handling 2026-01-27 16:09:07 +01:00
CSantosM
ed64c3a305 ov-components: Refactor audio and video device components to use reactive signals for improved performance and cleaner code 2026-01-27 16:01:53 +01:00
CSantosM
f4264a2a8a ov-components: Refactors device service for performance
Replaces the old device service with a new implementation using Angular Signals for reactive state management.

This enhances performance by:
- Improving permission requests
- Providing live device detection
- Providing better error handling

The new implementation also integrates with the LiveKit client for track management.
2026-01-27 16:00:39 +01:00
CSantosM
5edb36670b ov-components: standardize device service usage and improve component styles 2026-01-27 15:39:09 +01:00
CSantosM
7208cb3a65 ov-components: Improves virtual background support handling
Adds a more robust mechanism to manage virtual background support, including:

- Displaying a warning message when the browser does not support virtual backgrounds.
- Disabling background selection buttons when virtual backgrounds are not supported.
- Optimizing background processor initialization and attachment for different browsers (handling lazy loading for Firefox).
- Centralizing the check for virtual background support in the OpenViduService.

This change ensures a better user experience by clearly indicating when virtual backgrounds are unavailable and preventing users from attempting to use an unsupported feature.
2026-01-26 13:13:47 +01:00
CSantosM
6cfa44c4f1 ov-components: Refactors Firefox background processor handling
Simplifies the logic for attaching and detaching the background processor in Firefox.

Ensures the processor is stopped when background effects are disabled, optimizing performance.
2026-01-22 19:35:22 +01:00
CSantosM
465403f8cb ov-components: Added getParticipantByIdentity to participantService 2026-01-22 12:31:00 +01:00
CSantosM
b321d5bb69 ov-components: enhance resize handling and add height tracking in LayoutComponent 2026-01-19 17:03:24 +01:00
pabloFuente
81e7400807 openvidu-deployment: update all installation scripts from openvidu/agent-speech-processing to openvidu/agent-speech-processing-cloud 2026-01-19 14:14:26 +01:00
pabloFuente
73f84491a5 udpate.sh from agent-speech-processing to agent-speech-processing-cloud 2026-01-19 12:48:06 +01:00
CSantosM
59e4a14d15 ov-components: enhance background processing for Firefox and add getVideoTrack method 2026-01-18 16:31:07 +01:00
Carlos Santos
8dd058fd50 ov-components: add disconnected state handling and translations for multiple languages 2026-01-15 18:03:27 +01:00
cruizba
4a5afac382 openvidu-deployment: AWS - Update EBS volume type from gp2 to gp3 in CloudFormation templates 2026-01-15 17:55:52 +01:00
cruizba
8336b9e81c local-meet: update docker images to use 'main' 2026-01-09 20:04:55 +01:00
GitHub Actions
d82d8b061a Revert "Bump version to 3.5.0"
This reverts commit 21906c971ec3d6e9dc70776ae13758900326fe88.
2025-12-29 12:59:15 +00:00
GitHub Actions
21906c971e Bump version to 3.5.0 2025-12-29 12:59:11 +00:00
cruizba
06f394232c local-meet: bump docker images to version 3.5.0 and bump alpine version to 3.23.2 2025-12-29 13:20:17 +01:00
github-actions
9dc4236f1e openvidu-components-angular: Bumped version to 3.5.0 2025-12-29 11:54:10 +00:00
cruizba
de15d97839 update openvidu-livekit subproject commit reference 2025-12-29 11:55:19 +01:00
cruizba
14009f6041 openvidu-deployment: add next version 3.5.0 to update.sh 2025-12-29 11:35:18 +01:00
cruizba
5ba3e912c9 openvidu-deployment: Azure & GCP - certificate handling to accept base64 encoded certificates directly
- Updated the OpenVidu deployment scripts for GCP and Azure to eliminate the need for downloading and converting certificate files.
- Modified variable descriptions to specify that public and private certificates should be provided in base64 format.
- Adjusted the installation scripts to use the provided base64 encoded certificates directly for both OpenVidu and TURN server configurations.
2025-12-23 22:11:16 +01:00
cruizba
bcad387fe4 openvidu-deployment: AWS - update OperatingSystem description for clarity across deployment templates 2025-12-23 20:22:53 +01:00
cruizba
cb266952b7 openvidu-deployment: AWS - Use base64 for owned certificates instead of http URLs 2025-12-23 19:35:00 +01:00
Carlos Santos
28da654141 ov-components: adjust top bar height from 80px to 50px for improved layout consistency 2025-12-23 16:51:02 +01:00
Carlos Santos
c2a87459cd ov-components: update status icons visibility and styling for better user feedback 2025-12-23 13:15:11 +01:00
Carlos Santos
ce7dd0aa6f ov-components: replace user media subscription with reactive effect for local participant changes 2025-12-22 19:46:10 +01:00
Carlos Santos
703403f182 ov-components: refactor toolbar panel buttons to use Angular signals for state management and enhance badge styles 2025-12-22 19:42:49 +01:00
Carlos Santos
13cfff1aad ov-components: refine participant name container styles and adjust flex container layout 2025-12-22 19:14:07 +01:00
Carlos Santos
961867941a ov-component: introduce layout management classes and caching mechanism
- Added LayoutDimensionsCache for caching dimension calculations to optimize layout rendering.
- Implemented LayoutRenderer for handling DOM manipulation and rendering of layout elements.
- Created layout-types model to define various layout-related types and constants.
- Developed OpenViduLayout class to orchestrate layout calculations and rendering, maintaining backward compatibility.
- Updated document and layout services to reference new layout model structure.
2025-12-22 18:03:45 +01:00
Carlos Santos
3c02121ebe ov-components: enhance layout flexibility with additional element slots and improved rendering logic 2025-12-22 16:55:57 +01:00
Carlos Santos
ff492a1f22 ov-components: add isPinned getter to determine if participant's video is fixed in UI 2025-12-22 13:21:32 +01:00
Carlos Santos
f300788c1e ov-components: migrate to Angular signals for local and remote participants 2025-12-22 13:20:58 +01:00
Carlos Santos
1b9162b6e5 ov-components: Enhance layout handling by adding support for small elements and adjusting layout calculations 2025-12-19 20:18:50 +01:00
Pablo Fuente Pérez
b266916f7f
Merge pull request #857 from OpenVidu/appmod/java-upgrade-20251218002817
Appmod/java upgrade 20251218002817
2025-12-18 01:37:44 +01:00
pabloFuente
e8a31c3985 openvidu-test-e2e: fix browser user constructors 2025-12-18 01:37:24 +01:00
pabloFuente
72e2873e21 openvidu-test-browsers: update dependencies 2025-12-18 01:34:51 +01:00
pabloFuente
4324ac7c05 openvidu-test-e2e: full dependency upgrade 2025-12-18 01:23:44 +01:00
pabloFuente
3590a8fec3 Fix behavior regressions in OpenViduTestAppE2eTest 2025-12-18 01:15:25 +01:00
pabloFuente
d48e0a8e79 Add explicit okhttp3 dependency 2025-12-18 00:16:57 +01:00
pabloFuente
52d60946bc Upgrade other dependencies 2025-12-18 00:15:11 +01:00
pabloFuente
798df6fdcc Upgrade Spring Boot to 3.5.8 2025-12-18 00:13:50 +01:00
Carlos Santos
d229da9e47 ov-components: Add encryption key mismatch warning in chat panel and update translations 2025-12-17 17:39:04 +01:00
Carlos Santos
ddc7226b64 ov-components: Avoid unnecessary encryption decryption data channels
Livekit client now encrypt and decrypt data channels
Updated imports
2025-12-17 17:38:28 +01:00
Carlos Santos
47253bbda1 ov-components: update link color and font weight in styles 2025-12-17 16:10:08 +01:00
Carlos Santos
db8c6d5af9 ov-components: update dependencies in package.json and package-lock.json 2025-12-17 10:27:47 +01:00
Carlos Santos
86f71fc111 ov-components: update livekit-client to version 2.16.1 in package.json and package-lock.json 2025-12-17 10:22:33 +01:00
cruizba
11dcafd904 openvidu-deployment: Add Dpkg::Lock::Timeout configuration to prevent apt-get lock issues
- Added "DPkg::Lock::Timeout \"-1\";" configuration to various installation scripts across GCP, AWS, and Azure deployments to prevent apt-get lock timeout issues during package installations.
2025-12-16 12:49:50 +01:00
Carlos Santos
e8798a9536 openvidu-components-angular: add E2E testing documentation 2025-12-16 11:53:14 +01:00
Piwccle
84b086d076 openvidu-deployment: GCP - update variables and disk types to use europe-west2 and hyperdisk-balanced 2025-12-15 16:50:52 +01:00
Piwccle
bfa690ae56 openvidu-deployment: GCP - update disk type to pd-ssd for c4a instance types 2025-12-15 16:30:01 +01:00
Piwccle
680494f30a openvidu-deployment: GCP - Removing validations of instance types 2025-12-15 13:37:33 +01:00
pabloFuente
b469cf5455 openvidu-testapp: full track-processors-js capabilities 2025-12-12 15:40:27 +01:00
Carlos Santos
d9ebae88fa ov-components: update hover timeout duration in StreamComponent 2025-12-11 13:49:55 +01:00
Piwccle
118d6d370e openvidu-deployment: GCP - Fixes in ARM64 changes and some cleaning. 2025-12-11 12:44:12 +01:00
Carlos Santos
00a9a21de3 ov-components: update element selectors for screen share functionality in tests and add local participant class in layout 2025-12-11 11:27:06 +01:00
Carlos Santos
f2363eebd8 ov-components: increase sleep duration in screensharing test for stability 2025-12-11 11:17:07 +01:00
Carlos Santos
d9565c07bd ov-components: update chromedriver and selenium-webdriver to latest versions 2025-12-11 11:15:29 +01:00
Carlos Santos
ad80e2b3d3 ov-components: reorder imports and add toggleStreamPin method for stream pinning functionality 2025-12-11 11:14:46 +01:00
Carlos Santos
92511e0535 ov-componentes: improve formatting of browser options in selenium configuration 2025-12-11 10:30:56 +01:00
Carlos Santos
3a5f0d28da ov-components: make changes compile library with latest livekit dependencies 2025-12-11 10:06:18 +01:00
Carlos Santos
7c0333bf19 ov-components: update Angular and related dependencies 2025-12-10 18:49:18 +01:00
Piwccle
c7f73e36eb openvidu-deployment: GCP - Added zone dependency becasue its required 2025-12-10 18:42:57 +01:00
Piwccle
2806cbcf8b openvidu-deployment: GCP - Removed more unnecesary var.zone 2025-12-10 18:29:26 +01:00
Piwccle
1359ec77fe openvidu-deployment: trying to remove dependency from var.zone 2025-12-10 18:15:58 +01:00
Piwccle
05697f7ab3 openvidu-deployment: GCP - Added ARM64 support 2025-12-10 18:12:08 +01:00
Carlos Santos
41dc440ef8 ov-components: remove unnecessary peer flags and update package versions 2025-12-10 17:50:04 +01:00
Piwccle
a32efa876f openvidu-deployment: GCP - Fix to zone variable dependency in Single Node deployments and Elastic 2025-12-10 17:38:08 +01:00
Carlos Santos
8f42f50a01 ov-components: rename workflow to reflect E2E testing 2025-12-10 17:32:39 +01:00
Carlos Santos
87ec92ecc8 ov-components: optimize background processor application for video tracks 2025-12-05 16:41:49 +01:00
Carlos Santos
435db94254 ov-components: streamline virtual background processing 2025-12-05 15:40:40 +01:00
Carlos Santos
007297e4ff ov-components: update dependencies and refactor OpenVidu service
- Updated @livekit/track-processors from 0.6.1 to 0.7.0 in openvidu-components-angular/package.json
- Updated livekit-client from 2.15.15 to 2.16.0 in openvidu-components-angular/package.json
- Updated livekit-client from ^2.15.0 to ^2.16.0 in openvidu-components-angular/projects/openvidu-components-angular/package.json
- Updated @livekit/track-processors from ^0.6.0 to ^0.7.0 in openvidu-components-angular/projects/openvidu-components-angular/package.json
- Refactored OpenViduService to change roomOptions.e2ee to roomOptions.encryption for E2EE configuration
- Reorganized import statements in OpenViduService for better readability
2025-12-05 15:39:55 +01:00
Carlos Santos
895cf0e72c ov-components: Add screen track class to local participant tracks
participant-panel: Bind participant name to participant container
2025-12-04 19:51:48 +01:00
Carlos Santos
3531932c88 ov-components: Fixed directive value update
Ensures consistent identification of participant elements by using the participant identity, facilitating easier manipulation and tracking of elements.

Improves component updates by storing the remote participants value locally.
2025-12-02 21:00:07 +01:00
cruizba
a3810f4f51 openvidu-deployment: Azure - Missed to remove contraints for Media Nodes size. 2025-12-02 20:47:00 +01:00
cruizba
aa08432ea8 openvidu-deployment: Azure - Fix wrong sku and offer for ubuntu 24 2025-12-02 20:04:38 +01:00
cruizba
7bc782b7c0 openvidu-deployment: Azure - Remove unnecessary constraints from Azure UI definition files for OpenVidu deployments. 2025-12-02 19:39:49 +01:00
cruizba
75fc732c69 openvidu-deployment: Refactor Azure VM size parameters for OpenVidu deployment ARM support. 2025-12-02 19:21:16 +01:00
cruizba
0cfc342153 openvidu-deployment: Azure - Add ARM64 instance types and update Ubuntu to ubuntu 24 2025-12-02 18:29:44 +01:00
cruizba
712377a1b5 local-meet: remove unnecessary env var in mongo service 2025-12-02 15:53:33 +01:00
cruizba
4cffa3d8b9 local-meet: Add ARM64 support 2025-12-02 15:50:06 +01:00
cruizba
d49d7ef943 openvidu-deployment: AWS - ha: Increase CloudFormation WaitCondition. 2025-12-01 21:54:57 +01:00
cruizba
72cad07118 openvidu-deployment: AWS - ha: Remove redundant crond.service enable and start commands from deployment script 2025-12-01 21:01:11 +01:00
cruizba
352fa03b0a openvidu-deployment: AWS - Support for ARM64 instances 2025-12-01 20:36:57 +01:00
cruizba
d0b2bab7b1 local-meet: Install tzdata on images and use timezone of host. 2025-11-29 00:18:17 +01:00
Carlos Santos
7c43d73066 ov-components: Refactor GitHub Actions workflow for Angular tests and enhance E2E testing structure 2025-11-27 20:05:16 +01:00
Carlos Santos
9918b07f51 ov-components: Add directive for injecting custom menu items into toolbar more options 2025-11-27 20:05:16 +01:00
Carlos Santos
171a5104ae ov-components: Add directive for injecting custom elements into settings panel 2025-11-27 20:05:16 +01:00
Carlos Santos
e59ed89a0b ov-components: Mark pre-join component tutorial link as internal 2025-11-27 20:05:16 +01:00
cruizba
8cdc71e22f local-meet: Add MEET_MONGO_URI environment variable for OpenVidu Meet service 2025-11-25 12:49:00 +01:00
Carlos Santos
a0de27a78e ov-components: Refine stream video controls styling and improve hover effects 2025-11-21 13:44:39 +01:00
Carlos Santos
9c89adbdee Revert "ov-components: Optimize layout handling with caching and resize improvements"
This reverts commit 0cf5101931405a9810ea1849ce3bef80810ba5cf.
2025-11-20 18:11:40 +01:00
Carlos Santos
0cf5101931 ov-components: Optimize layout handling with caching and resize improvements 2025-11-20 17:39:14 +01:00
Carlos Santos
7c17e19cbb ov-components: Remove debug log for configuration in GlobalConfigService constructor 2025-11-20 17:39:14 +01:00
cruizba
f4c4ca8cec openvidu-deployment: Update Docker version to 29.0.2 in installation scripts 2025-11-20 12:10:23 +01:00
Piwccle
c22e957d4d openvidu-deployment: Refactor GCP Terraform configuration for OpenVidu deployment:
- Conditional creation of regional static IP for Network Load Balancer
- Improved validation messages for variables
- Adjusted default values for max number of media nodes and master nodes disk size
2025-11-19 18:42:44 +01:00
Piwccle
3d3089f479 openvidu-deployment: HA deployment GCP done and tested 2025-11-19 10:41:42 +01:00
cruizba
41fe3d718a openvidu-deployment: Typo docker compose version. 2025-11-18 19:14:28 +01:00
Carlos Santos
3be9dd6741 ov-components: Add getter for room name in ParticipantModel 2025-11-18 18:01:27 +01:00
cruizba
cabb761024 openvidu-deployment: Only restart docker daemon on install if needed. 2025-11-17 14:03:17 +01:00
cruizba
70a9f1b2b0 openvidu-deployment: Update Docker and Docker Compose versions across installation scripts 2025-11-14 19:04:40 +01:00
Carlos Santos
8688211277 ov-components: Improve audio detection in stream UI tests to handle timing issues with retries 2025-11-14 13:44:02 +01:00
Carlos Santos
5a99839ed7 ov-components: Enhance screensharing tests to address pinning bugs and add utility methods for stream management 2025-11-14 13:30:47 +01:00
Carlos Santos
19a5c21162 ov-components: improve stream detection logic in stream UI tests to handle timing races and ensure accurate local participant status 2025-11-14 12:15:52 +01:00
Carlos Santos
bea3b8e70a ov-components: Refactor ActionService and related tests to improve dialog handling and mock implementations 2025-11-14 12:02:42 +01:00
Carlos Santos
0f075008a4 ov-components: Refactor DeviceService to improve permission handling and add utility methods
- Extracted permission strategies into a separate method for better readability.
- Created a method to handle permission strategy attempts and return valid devices.
- Added a utility method to filter out invalid devices.
- Improved error handling in getMediaDevicesFirefox method.

test: Add unit tests for DocumentService

- Implemented comprehensive tests for DocumentService methods including toggleFullscreen, isSmallElement, and fullscreen handling.
- Mocked document and element interactions to ensure proper functionality.

feat: Implement E2EE service with encryption and decryption capabilities

- Developed E2eeService to handle end-to-end encryption with methods for encrypting and decrypting data.
- Added caching for decrypted strings to optimize performance.
- Included tests for various scenarios including encryption failures and binary data handling.

test: Add unit tests for PanelService

- Created tests for PanelService to validate panel opening, closing, and state management.
- Ensured proper emissions from panelStatusObs during panel state changes.

fix: Initialize externalType in PanelService to avoid undefined state

- Set default value for externalType to an empty string to prevent potential issues.
2025-11-13 20:16:01 +01:00
Carlos Santos
b1fb3406a0 ov-components: Added missing translation 2025-11-13 13:41:54 +01:00
Carlos Santos
f3e551fc4a ov-components: Refactor data handling in SessionComponent to improve payload processing and error logging 2025-11-12 17:51:26 +01:00
Carlos Santos
ba80504c9e ov-components: Improve error handling for encryption errors in RoomEvent 2025-11-12 12:01:25 +01:00
Carlos Santos
c50b4a6d2f Remove outdated package-lock.json from openvidu-components-angular project 2025-11-12 11:21:11 +01:00
Carlos Santos
fb1dc9d95a ov-components: Enhance data handling in SessionComponent and add safeJsonParse utility 2025-11-11 17:30:38 +01:00
Carlos Santos
9d75a429a6 ov-components: Updated livekit-client dependency 2025-11-11 16:23:20 +01:00
Carlos Santos
bb62986000 Updates participant name logic
Updates participant name input id.

Improves participant name visibility logic
by considering audio-only streams.
2025-11-10 17:29:58 +01:00
Carlos Santos
48eec08509 ov-components: Implement E2EE support for chat messages and participant names and add E2EE service 2025-11-10 12:49:54 +01:00
Carlos Santos
41bca24bfa Merge branch 'e2ee_support' 2025-11-05 17:51:57 +01:00
Carlos Santos
9950a2ba21 ov-components: Add E2EEKey directive and integrate E2EE configuration in OpenViduService
ov-components: Update E2EE error messages for improved clarity across multiple languages
2025-11-05 17:51:42 +01:00
Carlos Santos
4bf413cffc Merge branch 'gcp' 2025-10-29 18:28:54 +01:00
Piwccle
d68cb4933e Add Terraform configuration for OpenVidu deployment on GCP
Changed structure to be more consistant with the terraform standard and removed some resources to try

Refactor terraform main file to be more alike with aws and azure scripts and fixed some things that were wrong in the install script. Changed variables.tf and output.tf as needed

Refactor firewall rules and streamline user data scripts for OpenVidu deployment on GCP

added Elastic deployment for GCP and changed default values of instance type in Single Node and Single Node PRO

openvidu-deployment_ gcp - changed output.tf in all deployments to output the link to secret manager; changed the name of the instance resource of openvidu single node pro; fixed some things that were broken in elastic terraform file and adjusted times for the lambda and the cronjob
2025-10-29 18:26:56 +01:00
Carlos Santos
17ed624e40 ov-components: Removes redundant computed property
Simplifies the template by directly using the `showLeaveButton` property.

Removes the now-unnecessary computed property that duplicated the value.
2025-10-28 17:18:18 +01:00
Carlos Santos
6c9a8a1bc2 ov-components: Update imports in global-config.service.ts 2025-10-21 20:19:31 +02:00
Carlos Santos
de8639ad63 ov-components: Updated testapp dependencies 2025-10-21 19:35:26 +02:00
Carlos Santos
c576133b42 ov-components: Adds build and copy scripts
Adds `cpx` and `rimraf` packages.
Updates library build script to remove destination directory and copy distribution files.
Updates library package configurations.
2025-10-21 19:22:12 +02:00
Carlos Santos
bb47c3696c ov-components: Configures root injection for overlay
Configures root injection for the CDK overlay container.

This ensures the overlay container is properly initialized and available throughout the application.
It also makes the overlay available application wide and solves the problem of injecting multiple instances.
2025-10-21 19:10:27 +02:00
cruizba
1a3edb9a61 openvidu-deployment: Update Docker images and versions in deployment scripts 2025-10-17 19:55:18 +02:00
Piwccle
b7e715361e openvidu-deployment: azure - changed "defaultName" of PublicIpAddress and changed Action Group name 2025-10-15 18:45:31 +02:00
cruizba
1d4bcdf54f local-meet: bumpt to 3.4.1 2025-10-13 20:41:05 +02:00
GitHub Actions
6ac6e1f2de Revert "Bump version to 3.4.1"
This reverts commit 6b9e9da2f13dac1d4bcf3cdeb3150659e968554c.
2025-10-13 16:57:47 +00:00
GitHub Actions
6b9e9da2f1 Bump version to 3.4.1 2025-10-13 16:57:44 +00:00
cruizba
56dd99458e openvidu-deployment: add to updater missing env variables 2025-10-12 21:45:05 +02:00
cruizba
9cd2bb9bd7 openvidu-deployment: update updater script to include additional Docker images and validate upgrade paths 2025-10-12 21:37:33 +02:00
cruizba
95f4326f9a openvidu-deployment: elastic & ha - Add security group ingress rules for media and master node communication 7880 2025-10-10 14:51:55 +02:00
Carlos Santos
1ca0e8e4e1 ov-components: update rxjs to version 7.8.2 in package.json and package-lock.json 2025-10-09 16:46:51 +02:00
Carlos Santos
25f9d29ffd ov-components: update dependencies and improve TypeScript configuration 2025-10-09 16:06:44 +02:00
Carlos Santos
45d2f7dd6e ov-components: Updated testapp to Angular 20 2025-10-09 14:30:40 +02:00
pabloFuente
a666659ca0 openvidu-test-e2e: improved backdrop wait 2025-10-08 11:12:46 +02:00
pabloFuente
6acb28a178 openvidu-test-e2e: more backdrops wait 2025-10-08 10:18:03 +02:00
pabloFuente
ade870d843 openvidu-test-e2e: allow for egress check in container's backup folder 2025-10-07 23:32:57 +02:00
pabloFuente
654063a33f openvidu-test-e2e: improved waitForBackdropAndClick 2025-10-07 22:46:44 +02:00
pabloFuente
11063be327 openvidu-testapp: final Angular v20 changes 2025-10-07 22:28:57 +02:00
pabloFuente
2ab28e3d41 openvidu-testapp: update to Angular Material v20 2025-10-07 22:09:18 +02:00
pabloFuente
780635dc9a openvidu-testapp: upgrade to Angular v20 2025-10-07 22:00:11 +02:00
pabloFuente
6a5980e3da openvidu-testapp: update package-lock.json 2025-10-07 21:58:12 +02:00
pabloFuente
b187ee9777 openvidu-test-e2e: better wait for backdrop to disappear management 2025-10-07 18:07:10 +02:00
pabloFuente
6534a847f8 Updated LiveKit openvidu-testapp dependencies 2025-10-07 15:40:25 +02:00
Carlos Santos
f98f853c76 ov-components: Reorganize package.json 2025-10-06 12:26:31 +02:00
Carlos Santos
206bb6e8e8 ov-components: update target in tsconfig.json 2025-10-06 12:23:42 +02:00
Carlos Santos
94a14e3ae2 ov-components: update Angular dependencies to version 19.x and ng-packagr to 19.2.2 2025-10-06 12:21:18 +02:00
Carlos Santos
432953323c ov-components: migrate Angular dependencies to version 18.x 2025-10-06 12:17:56 +02:00
Carlos Santos
eec1678585 ov-components: update Angular peer dependencies to include version 20 2025-10-06 12:11:45 +02:00
Carlos Santos
f097866d38 ci: update dispatch URL to point to openvidu-tutorials repository 2025-10-03 12:59:20 +02:00
Carlos Santos
9c119d6bd2 ov-components: clean css variables 2025-10-01 19:42:00 +02:00
Carlos Santos
65e51decb2 ov-components: update color variables for improved consistency across components 2025-10-01 19:42:00 +02:00
cruizba
431bad226d openvidu-deployment: bump local-meet to 3.4.0 2025-10-01 19:37:30 +02:00
GitHub Actions
a0e156e42c Revert "Bump version to 3.4.0"
This reverts commit 0fd048d030263ffd5e4c8bc62155f39bc3887325.
2025-10-01 16:29:16 +00:00
GitHub Actions
0fd048d030 Bump version to 3.4.0 2025-10-01 16:29:13 +00:00
cruizba
df6c5c6db2 update openvidu-livekit subproject to latest commit 2025-10-01 17:39:50 +02:00
github-actions
4762dbc336 openvidu-components-angular: Bumped version to 3.4.0 2025-10-01 15:30:25 +00:00
Carlos Santos
b2b66477d4 ov-components: update color variables for consistency across components 2025-10-01 12:39:34 +02:00
cruizba
89fc82bfd7 openvidu-deployment: fix update.sh to handle Azure secret naming for OpenVidu version updates 2025-09-30 21:46:28 +02:00
cruizba
8dc1dd329a openvidu-deployment: revert unnecessary RUNNING_SYSTEMD=false prefix from docker pull commands in installation scripts 2025-09-30 13:13:00 +02:00
Carlos Santos
1298e3160d ov-components: update documentation for setTheme method to clarify theme mode application 2025-09-30 12:48:00 +02:00
Carlos Santos
d54e21d726 ov-components: improve layout and styling for participant name and badges 2025-09-30 12:46:44 +02:00
Carlos Santos
e00120b046 ov-components: update CLASSIC to Classic for consistency in theme handling 2025-09-30 12:27:37 +02:00
cruizba
ec4e2a2f76 openvidu-deployment: update installation scripts:
- Use RUNNING_SYSTEMD=false for docker pull to avoid warning.
- Meet installer start after install.
2025-09-30 12:05:56 +02:00
Carlos Santos
0a96ce9323 ov-components: update button color in stream video controls for consistency 2025-09-30 11:50:22 +02:00
cruizba
0b56a76f0c openvidu-deployment: rename install-meet.sh to install_meet.sh 2025-09-30 01:09:02 +02:00
cruizba
76ca051ae3 openvidu-deployment: add install-meet.sh script for setting up OpenVidu deployment with Docker (simplified installer) 2025-09-30 01:06:35 +02:00
cruizba
6e40f239fb openvidu-deployment: local-meet - update webhook URL for meet service in docker-compose files 2025-09-26 19:15:56 +02:00
cruizba
5fec44d44e Revert "openvidu-deployment: update deployment URL logic for meet service in docker-compose"
This reverts commit 6bc21cc27edaa039577a26c36c975ddce85c9799.
2025-09-26 19:15:56 +02:00
Carlos Santos
db20af15e6 ov-components: update color variables to enhance UI consistency across components 2025-09-26 18:26:36 +02:00
pabloFuente
1bbb9784cd Add new egress config [allocation_strategy, disable_cpu_overload_killer] 2025-09-26 13:15:36 +02:00
Carlos Santos
90071667ac ov-components: remove alpha values from color variables in light and dark themes 2025-09-25 18:23:41 +02:00
Carlos Santos
1c21097f59 ov-components: update theme selector test to use togglePanel for settings 2025-09-25 17:12:24 +02:00
Carlos Santos
fe97022182 ov-components: implement showThemeSelector directive and integrate theme selector functionality 2025-09-25 17:03:33 +02:00
Carlos Santos
0cdb46a79c ov-components: load predefined themes dynamically in ThemeSelectorComponent 2025-09-25 17:03:33 +02:00
Carlos Santos
7071385e22 ov-components: move theme initialization to ov-videoconference constructor 2025-09-25 17:03:33 +02:00
cruizba
5c6d8984c2 openvidu-deployment: aws - fix LaunchTemplate reference to use LaunchTemplateId 2025-09-25 15:39:41 +02:00
cruizba
bd3a835ce9 openvidu-deployment: aws - Fix Launch template reference 2025-09-25 15:25:19 +02:00
cruizba
418372367e openvidu-deployment: aws - Fix single node cloudformation to be able to deploy more than one stack in the same region. 2025-09-25 15:13:21 +02:00
Carlos Santos
8ff7b24aa5 ov-components: rename theme constants for clarity 2025-09-25 12:10:32 +02:00
Piwccle
747aee5291 Merge branch 'master' of https://github.com/OpenVidu/openvidu 2025-09-24 13:04:52 +02:00
Piwccle
270bb213ca openvidu-deployment: remove unnecesary .gitignore 2025-09-24 13:04:49 +02:00
cruizba
d309778f76 openvidu-deployment: update Docker and Docker Compose versions across installation scripts. Change Egress image to openvidu Egress fork. 2025-09-24 13:02:52 +02:00
Piwccle
d3086d0c09
Add Terraform files to .gitignore 2025-09-24 13:02:51 +02:00
Piwccle
0641639643 openvidu-deployment: Added GCP Single Node and Single Node PRO deployments 2025-09-24 13:00:35 +02:00
Carlos Santos
246be8bcf2 ov-components: enhance mobile layout for prejoin component and improve participant name input background 2025-09-23 19:44:00 +02:00
Carlos Santos
1b9396ca1b ov-components: improve prejoin component and enhance SCSS styles for better layout and responsiveness 2025-09-23 18:22:41 +02:00
Carlos Santos
0f06c15a78 ov-components: update lib:build script to remove unnecessary directory change 2025-09-23 12:49:11 +02:00
Carlos Santos
faaac23c66 ov-components: enhance mobile responsiveness for panel and session components 2025-09-22 21:17:11 +02:00
Carlos Santos
0407725437 ov-components: add landscape orientation warning component for mobile devices 2025-09-22 21:04:23 +02:00
Carlos Santos
bab8d3eb2a ov-components: enhance layout model with extended options and improved dimension calculations 2025-09-22 20:22:27 +02:00
Carlos Santos
1cef3c17a4 ov-components: enhance layout service with responsive viewport handling and layout options adjustment 2025-09-22 20:06:25 +02:00
Carlos Santos
bd74184799 ov-components: enhance pre-join component for mobile support and viewport handling 2025-09-22 19:22:28 +02:00
Carlos Santos
8416c3f764 ov-components: refactor pre-join component styles for improved structure and loading state handling 2025-09-22 19:12:28 +02:00
Carlos Santos
bb70ddb3d9 ov-components: remove unused input properties from toolbar panel buttons component 2025-09-22 18:17:36 +02:00
Carlos Santos
9d17c14bcd ov-components: add theme selector component and integrate theme management
- Introduced a new theme selector component to allow users to select themes.
- Updated settings panel to include the theme selector.
- Created shared styles for device and theme selectors.
- Refactored existing audio and video device components to use shared styles.
- Enhanced storage service to manage theme preferences.
- Updated theme service to support classic theme alongside light and dark themes.
- Added translations for theme-related strings in multiple languages.
2025-09-22 17:57:04 +02:00
Carlos Santos
776c45be3a ov-components: refine viewport checks for mobile landscape orientation and improve SCSS for toolbar buttons 2025-09-22 14:18:01 +02:00
Carlos Santos
f11f0933b4 ov-components: add landscape orientation warning and improve loading animations 2025-09-22 12:39:07 +02:00
Carlos Santos
d48e44ea55 ov-components: enhance settings panel for responsive design and improve layout handling 2025-09-19 18:31:10 +02:00
Carlos Santos
b35f959394 ov-components: update SCSS for collapsible panels menu and adjust button dimensions 2025-09-19 17:25:36 +02:00
Carlos Santos
b71991a3e4 ov-components: remove unnecessary console logs 2025-09-19 17:04:33 +02:00
Carlos Santos
c80c8d3ab2 ov-components: add translations for "PANELS" in multiple language files 2025-09-19 14:57:19 +02:00
Carlos Santos
4654c48f22 ov-components: refactor leave button implementation for improved template handling 2025-09-19 14:52:01 +02:00
Carlos Santos
6da901e7bb ov-components: enhance toolbar panel buttons with responsive design 2025-09-19 14:50:14 +02:00
Carlos Santos
d223acefcf ov-components: add toolbar panel buttons component with event handling and styling 2025-09-19 11:56:10 +02:00
Carlos Santos
41152de276 ov-components: implement custom leave button directive and enhance toolbar for additional button templates 2025-09-18 19:40:25 +02:00
Carlos Santos
2a9f3a62fa ov-components: enhance toolbar media buttons for responsive design and improved visibility 2025-09-18 12:14:07 +02:00
Carlos Santos
677a9129a2 ov-components: add viewport service and model for responsive design detection 2025-09-18 12:12:52 +02:00
Carlos Santos
c65b5c8a18 ov-components: add toolbar media buttons component 2025-09-18 12:11:04 +02:00
Carlos Santos
0a49ca91d8 ov-components: refactor toolbar component to improve readability and add helper methods for recording and broadcasting status 2025-09-18 12:10:32 +02:00
Carlos Santos
8f48e9f70c ov-components: set recording and broadcasting budget color to white 2025-09-18 12:10:32 +02:00
cruizba
0471e43c2a openvidu-deployment: Fix local-meet pro typo 2025-09-17 20:46:31 +02:00
cruizba
6bc21cc27e openvidu-deployment: update deployment URL logic for meet service in docker-compose 2025-09-17 20:38:53 +02:00
cruizba
9755cd6753 openvidu-deployment: azure - single-node-pro & elastic & ha - Fix store_secret.sh script for empty secrets 2025-09-16 19:17:20 +02:00
cruizba
0c21cacded openvidu-deployment: aws - ha - empty api key if not defined 2025-09-16 17:54:03 +02:00
cruizba
522e0338da openvidu-deployment: azure - single node pro & elastic & ha - add parameters for initial Meet admin user and API key with validation 2025-09-16 17:52:10 +02:00
cruizba
f0a788e44f openvidu-deployment: azure - single node - fix UI params 2025-09-16 16:36:35 +02:00
cruizba
9c52ea2fa4 openvidu-deployment: azure - single node - update labels for initial admin password and API key 2025-09-16 16:07:57 +02:00
cruizba
a6488fe711 openvidu-deployment: azure - single node - typo 2025-09-16 15:13:45 +02:00
cruizba
d8fd8df182 openvidu-deployment: azure - single node - handle empty secrets 2025-09-16 14:46:16 +02:00
cruizba
d47c080aaf openvidu-deployment: azure - single node - build bicep 2025-09-16 14:38:53 +02:00
cruizba
8c11071f2e openvidu-deployment: azure - single node - store none if api key is not defined 2025-09-16 14:28:28 +02:00
cruizba
1b4b14c1d7 openvidu-deployment: azure - single node - fix azure ui labels 2025-09-16 14:08:25 +02:00
cruizba
bb5c7d4e39 openvidu-deployment: azure - single node - add parameters for initial Meet admin user and API key with validation 2025-09-16 13:25:05 +02:00
cruizba
4fef8d73d4 openvidu-deployment: aws - Initial Meet api key fixes. Fix redirect 80 to 443 in HA 2025-09-15 22:51:57 +02:00
cruizba
36665d54b1 openvidu-deployment: aws - add initial admin password and API key parameters for OpenVidu Meet configuration to all cloudformation 2025-09-15 13:40:58 +02:00
cruizba
2908391eee openvidu-deployment: add parameters for initial Meet admin user and API key with validation 2025-09-15 12:30:04 +02:00
cruizba
01df8eab17 openvidu-deployment: ha - azure - fix domain name storage in install script. 2025-09-11 21:03:11 +02:00
cruizba
2304bca8d4 openvidu-deployment: azure - Fix race condition on role assignment 2025-09-11 20:28:23 +02:00
cruizba
45b7ef4284 openvidu-deployment: single node - azure - Remove unnecessary letsenctypt logic 2025-09-11 18:23:01 +02:00
Carlos Santos
7353b4a7c2 ov-components: update livekit-client and @livekit/track-processors dependencies to latest versions 2025-09-11 17:51:11 +02:00
Carlos Santos
90446e43dc ov-components: change PREFIX_KEY to protected visibility 2025-09-11 17:32:51 +02:00
cruizba
bc247b8090 openvidu-deployment: Update initial admin user credentials in deployment scripts
- Changed the initial admin username for OpenVidu Meet from "meetadmin" to "admin" in AWS, Azure Bicep, and JSON deployment files.
2025-09-11 14:40:49 +02:00
Carlos Santos
f0b3c2e2c6 ov-components: Implement theming system
- Added THEME.md documentation detailing the theming system, including usage, service methods, and CSS variables reference.
- Created theme.scss with SCSS mixins for applying OpenVidu themes and integrating with Angular Material.
- Introduced theme.model.ts to define theme modes and variables.
- Developed theme.service.ts to manage theme switching, variable updates, and system theme detection.
- Updated public-api.ts to export new theme model and service.
- Enhanced styles.scss to incorporate OpenVidu theme integration with Angular Material.
- Added support for responsive theme detection based on system preferences.
2025-09-11 14:13:39 +02:00
cruizba
7821f3a75d openvidu-deployment: cleanup aws and azure unnecessary code 2025-09-09 22:26:41 +02:00
cruizba
259585929b openvidu-deployment: ha - azure - support sslip if no domain is defined 2025-09-09 22:06:36 +02:00
cruizba
c1e916adc2 openvidu-deployment: single node pro & elastic - azure - refactor public IP retrieval to use Azure CLI and update related scripts 2025-09-09 22:04:33 +02:00
cruizba
6bb42d813f openvidu-deployment: single node - azure - refactor public IP retrieval to use Azure CLI and update related scripts 2025-09-09 19:49:36 +02:00
cruizba
b30ea404ed Revert "openvidu-deployment: elastic - azure - simplify public IP retrieval and remove get_public_ip.sh script"
This reverts commit 09a791307d763ff773c361fb09c7e7a85be65019.
2025-09-09 19:07:18 +02:00
cruizba
09a791307d openvidu-deployment: elastic - azure - simplify public IP retrieval and remove get_public_ip.sh script 2025-09-09 18:41:50 +02:00
cruizba
1310446c23 openvidu-deployment: elastic - azure - remove letsEncryptEmail parameter from certificate configuration 2025-09-09 17:39:15 +02:00
cruizba
51910582d8 openvidu-deployment: elastic - azure - remove letsEncryptEmail handling from certificate configuration 2025-09-09 17:26:49 +02:00
cruizba
0499e9b121 openvidu-deployment: elastic - azure - update default certificate type to 'letsencrypt' 2025-09-09 16:43:14 +02:00
cruizba
b99b041662 openvidu-deployment: elastic - azure - update createUiDefinition.json to hide 'None' option in public IP address settings 2025-09-09 13:30:32 +02:00
cruizba
be8e615b81 openvidu-deployment: elastic - azure - remove letsEncryptEmail parameter from SSL configuration in createUiDefinition.json 2025-09-09 13:25:02 +02:00
cruizba
4d0e4c4520 openvidu-deployment: elastic - azure - Allow empty domains withj sslip 2025-09-09 13:18:01 +02:00
cruizba
aa1dfd4560 openvidu-deployment: single-node - update tooltip for certificate type to clarify FQDN usage and sslip.io integration 2025-09-09 13:18:01 +02:00
Carlos Santos
0ba70638e6 ov-components: optimize storage service for improved performance and error handling 2025-09-08 15:59:23 +02:00
cruizba
f6d1b6e86c openvidu-deployment: single node pro - azure - remove letsEncryptEmail parameter from SSL configuration 2025-09-08 14:37:04 +02:00
Carlos Santos
1d49017f41 ov-components: refactor screen share options for improved functionality and clarity 2025-09-08 12:23:31 +02:00
Carlos Santos
03962d21df ov-components: Add method to set video track elements pinned state by source 2025-09-08 12:07:55 +02:00
cruizba
5cf8a32190 openvidu-deployment: single-node-pro - azure - Allow empty domains withj sslip 2025-09-07 19:56:14 +02:00
cruizba
a2098d1e85 openvidu-deployment: update yq and aws-cli installation scripts for architecture compatibility 2025-09-07 03:56:21 +02:00
cruizba
3794237883 openvidu-deployment: single node - azure - build bicep 2025-09-07 03:34:53 +02:00
cruizba
e6757f5987 openvidu-deployment: single node - azure - fix public ip address selector 2025-09-07 03:12:26 +02:00
cruizba
ecb6e2d011 openvidu-deployment: single node - azure - refactor public IP resource creation logic 2025-09-07 02:52:05 +02:00
cruizba
fb465fa09f openvidu-deployment: single-node - azure - Allow empty domains withj sslip 2025-09-07 02:06:11 +02:00
cruizba
5436087745 openvidu-deployment: single-node: Remove letsencrypt email and clarify descriptions 2025-09-07 01:22:50 +02:00
cruizba
88b8ac1e9e openvidu-deployment: single node - Clarify CertificateType descriptions for better understanding 2025-09-07 01:19:37 +02:00
cruizba
6da05e19a7 openvidu-deployment: Update CertificateType descriptions for clarity 2025-09-06 21:54:25 +02:00
cruizba
b549fdba51 openvidu-deployment: single node & elastic - reorganize parameter groups 2025-09-06 21:44:34 +02:00
cruizba
08cfbcce64 openvidu-deployment: Missing pip install 2025-09-06 20:44:36 +02:00
cruizba
591a8845fa openvidu-deployment: Improve cfn-bootstrap installation by detecting Ubuntu version in CloudFormation templates 2025-09-06 20:41:39 +02:00
cruizba
1efc9850a5 openvidu-deployment: Elastic - Remove letsencrypt email and use sslip.io if domain is not defined. 2025-09-06 20:39:44 +02:00
cruizba
3b13910a06 openvidu-deployment: Missing LIVEKIT_URL to secret updates for improved WebSocket connection 2025-09-02 17:30:50 +02:00
cruizba
e3cd657c97 openvidu-deployment: single-node pro - Remove letsencrypt email and use sslip.io if domain is not defined. 2025-09-02 17:01:11 +02:00
cruizba
8f0db18c4b openvidu-deployment: single-node - community UX improvements. Create EIP if domain is not defined. 2025-09-02 12:01:54 +02:00
cruizba
cf498ee9d5 openvidu-deployment: single-node - community Remove letsencrypt email and use sslip.io if domain is not defined 2025-09-02 01:11:37 +02:00
cruizba
9a549cfa2a openvidu-deployment: local-meet - Remove container names to avoid container conflicts. 2025-08-28 18:34:56 +02:00
cruizba
897c74c6ab openvidu-deployment: local-meet - Add operator service to docker-compose and update volumes 2025-08-28 17:37:12 +02:00
cruizba
6fd6fc241c openvidu-deployment: local-meet - Rename ready-check service to openvidu-meet-init and update dependencies in docker-compose files 2025-08-28 17:31:35 +02:00
cruizba
fdbb9dd87b openvidu-deployment: local-meet - Refactor environment variable definitions in docker-compose files for consistency and flexibility 2025-08-28 15:17:50 +02:00
cruizba
f8e904a08d openvidu-deployment: local-meet - Update MinIO and MongoDB images to use OpenVidu repositories 2025-08-28 14:10:54 +02:00
cruizba
679447a4df openvidu-deployment: Update cloud deployments:
- Bump Bicep version from 0.36.1 to 0.37.4 and update template hash.
- Modify install script to include new parameters for Meet module: MEET_INITIAL_ADMIN_USER, MEET_INITIAL_ADMIN_PASSWORD, and MEET_INITIAL_API_KEY.
2025-08-28 13:58:03 +02:00
cruizba
536c2122df openvidu-deployment: local-mett - Add OPENVIDU_ENVIRONMENT for ready-check container 2025-08-28 13:40:06 +02:00
cruizba
059c983274 openvidu-deployment: local-meet - update egress service image to version 1.10.0 in docker-compose files 2025-08-27 20:02:14 +02:00
cruizba
a5e829408b openvidu-deployment: Fix environment variable name for initial API key in docker-compose files 2025-08-27 19:52:27 +02:00
cruizba
2e7a85efa9 openvidu-deployment: Add local-meet 2025-08-27 18:51:24 +02:00
Carlos Santos
8a236e1b67 ov-components: update audio device change handling and improve track retrieval logic 2025-08-23 11:32:00 +02:00
Carlos Santos
5d9dbc114b ov-components: add conditional rendering for letter display in avatar profile 2025-08-22 19:43:11 +02:00
Carlos Santos
488811f132 ov-components: Refactor device handling and improve error logging in audio and video components 2025-08-22 19:23:37 +02:00
cruizba
4c37d8cf7c openvidu-deployment: Update LiveKit Egress server image to version 1.10.0 across deployment scripts 2025-08-22 15:21:50 +02:00
Carlos Santos
82630f3508 ov-components: Increase Jasmine default timeout interval to 60 seconds and remove obsolete test for BACKGROUND panel on prejoin page 2025-08-22 14:06:09 +02:00
Carlos Santos
dfb297a6d2 ov-components: Add E2E tests for virtual backgrounds with setup and cleanup steps 2025-08-22 13:32:18 +02:00
Carlos Santos
c8f53a48ce ov-components: Add virtual backgrounds tests and enhance utility functions for background effects 2025-08-22 13:29:27 +02:00
Carlos Santos
1b9277145c ov-components: Fix test case for recording start and stop events by enabling it in events test suite 2025-08-22 12:36:16 +02:00
Carlos Santos
ee30c3ce95 ov-components: Update device selection logic and improve UI handling for audio and video devices 2025-08-22 12:30:05 +02:00
Carlos Santos
03fb7c0a93 ov-components: Refactor recording stop event logging to include room name for better traceability 2025-08-22 12:23:21 +02:00
Carlos Santos
1762c43769 ov-components: Refactor prejoin logic and improve observable handling in configuration service 2025-08-21 19:01:51 +02:00
Carlos Santos
bcbf24b84d ov-components: Refactor recording list display logic in RecordingActivityComponent 2025-08-21 16:49:50 +02:00
Carlos Santos
fc73aca4a2 Revert "ov-components: Update RecordingActivityComponent to avoid showing recordings when not expanded"
This reverts commit 9e9684c4dbf9cf896e09c9af480c2e6d9a666a0a.
2025-08-21 14:30:44 +02:00
Carlos Santos
f92ee9b886 ov-components: Improve prejoin card styles 2025-08-21 13:08:31 +02:00
Carlos Santos
68d855a245 ov-components: Enhanced camera track handling and processor application in virtual background service 2025-08-21 13:08:31 +02:00
Carlos Santos
72e7469012 ov-components: Enhanced prejoin component
- Introduced background effect feature with options for 'none', 'blur', 'office', and 'nature'.
- Enhanced error handling during device initialization with retry logic and user feedback.
- Updated participant name handling to trim whitespace and clear errors on input change.

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

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

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

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

style(lang-selector): improve language selection UI

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

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

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

style: general UI improvements across components

- Enhanced overall styling for better consistency and user experience across various components.
2025-08-21 13:08:31 +02:00
cruizba
622a2f6707 openvidu-deployment: Change MinIO and Mimir images to use OpenVidu registry instead of bitnami. 2025-08-20 22:38:37 +02:00
Carlos Santos
72697bafa5 ov-components:Enhance logging by adding verbose level and updating log calls in TemplateManagerService 2025-08-19 11:25:56 +02:00
Carlos Santos
4fb4878342 ov-components: Refactor LoggerService to improve logger function creation and cache handling 2025-08-19 10:50:13 +02:00
Carlos Santos
57e76fe69b ov-components: Update participant name handling in Session and Toolbar components 2025-08-18 18:28:13 +02:00
Carlos Santos
e1f16a6179 ov-components: Update MATERIAL_ICONS_URL to remove unnecessary icon names 2025-08-18 14:28:20 +02:00
Carlos Santos
9e9684c4db ov-components: Update RecordingActivityComponent to avoid showing recordings when not expanded 2025-08-18 14:00:16 +02:00
Carlos Santos
efada4c166 ov-components: (test) Add check for speaker element when local participant is muted 2025-08-14 12:54:30 +02:00
Carlos Santos
3fc0193260 Revert "ov-components: (test) Enhance element counting method to handle timeouts gracefully"
This reverts commit 61a3589dd7a1c0edc7f7e6e3bffe65dcea6ae3d7.
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 2083c078fd1b27e882a930cdb388994ba45f134c.
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 26b790bfd865e5a17b466d5e4d1e6dfa991cb78f.
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 a9e727fd517616d8d37e15a7106aa6173629a6fa.
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 c275381cfda0a7a87a6c060acfe38eea853fda3f.
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
github-actions
d3520e9098 openvidu-components-angular: Bumped version to 3.1.0 2025-02-17 14:42:29 +00:00
cruizba
b29b9102a9 update openvidu-livekit subproject to latest commit 2025-02-17 15:23:37 +01:00
Carlos Santos
64fbaa2a42 ov-components: Fixed nested event test 2025-02-13 12:43:22 +01:00
Carlos Santos
4c4380a87f ov-components e2e: Added participantLeft listener 2025-02-13 12:33:04 +01:00
Carlos Santos
d8452c42ad ov-components: Fired room and participantLeft events 2025-02-13 12:31:50 +01:00
cruizba
09ba39642e openvidu-test-e2e: Disable flaky audio ingress tests for RTSP 2025-02-11 13:35:20 +01:00
pabloFuente
11c6f2f965 openvidu-testapp: remove unused imports 2025-02-11 12:47:02 +01:00
cruizba
9a01c0e179 openvidu-test-e2e: Fix file URL selection logic for lossless video encoding 2025-02-11 12:33:44 +01:00
cruizba
06fc88f2b0 openvidu-test-e2e: Add lossless flag to getFileUrl method 2025-02-11 12:30:56 +01:00
cruizba
f19a9129ba openvidu-test-e2e: Improve CPU performance by using losless codec ffv1 and flac. Fix some tests also because of mp3 used as source instead of flac. 2025-02-11 02:28:22 +01:00
Carlos Santos
f785dfd7ad ov-components: subscribe to vc directives in constructor 2025-02-07 17:21:09 +01:00
pabloFuente
8185324a0a openvidu-test-e2e: disable audio only opus RTSP test 2025-02-07 11:14:06 +01:00
pabloFuente
88214676a8 openvidu-test-e2e: add -crf flags to video encodings with ffmpeg 2025-02-06 23:16:36 +01:00
pabloFuente
9f9eda5ce4 openvidu-test-e2e: improve waiters resilience 2025-02-06 20:49:42 +01:00
pabloFuente
75bb35d00a openvidu-test-e2e: fix opus codec with "-ac 2" flag 2025-02-06 20:43:47 +01:00
pabloFuente
d4006421e9 openvidu-test-e2e: add AAC audio codec RTSP tests 2025-02-06 20:27:32 +01:00
pabloFuente
0fe47c4b13 openvidu-test-e2e: test RTSP with all codecs 2025-02-06 20:07:17 +01:00
pabloFuente
5c97301c6d openvidu-testapp: allow configuring URI for Ingress URL types 2025-02-06 18:59:29 +01:00
pabloFuente
de5f67dd3c openvidu-test-e2e: parameterized rtsp/srt port 2025-02-03 19:17:50 +01:00
pabloFuente
da108a4031 openvidu-test-e2e: update rtstp server container to network host 2025-02-03 19:10:44 +01:00
pabloFuente
5fb3be503c openvidu-test-e2e: add RTSP and SRT tests 2025-02-03 15:36:37 +01:00
pabloFuente
6ea42e82c0 openvidu-testapp: add ingress URL type selector 2025-02-03 15:35:52 +01:00
Carlos Santos
6e1d9c7ee7 ov-components: Updated logo directive 2025-01-28 21:37:53 +01:00
Carlos Santos
4cdb761737 ov-components: Allowed Angular 19 2025-01-27 17:09:13 +01:00
Carlos Santos
51e3abd483 ov-components: Updated npm script 2025-01-27 17:04:11 +01:00
332 changed files with 85553 additions and 38623 deletions

File diff suppressed because it is too large Load Diff

View File

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

6
.gitignore vendored
View File

@ -27,3 +27,9 @@ nbactions.xml
*/.tscache/*
.factorypath
.terraform
.terraform.lock.hcl
*.tfstate
*.tfstate.backup
*.tfstate.lock.info
*.tfvars

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

View File

@ -10,9 +10,4 @@
node_modules
dist/
docs/
openvidu-webcomponent/
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",
"options": {
"outputPath": {
"base": "dist/openvidu-components-testapp"
"base": "dist/openvidu-components-testapp",
"browser": ""
},
"index": "src/index.html",
"polyfills": ["zone.js"],
@ -58,7 +59,7 @@
{
"type": "anyComponentStyle",
"maximumWarning": "6kb",
"maximumError": "10kb"
"maximumError": "12kb"
}
]
}
@ -148,84 +149,35 @@
}
}
}
},
"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": {
"analytics": false
}
},
"schematics": {
"@schematics/angular:component": {
"type": "component"
},
"@schematics/angular:directive": {
"type": "directive"
},
"@schematics/angular:service": {
"type": "service"
},
"@schematics/angular:guard": {
"typeSeparator": "."
},
"@schematics/angular:interceptor": {
"typeSeparator": "."
},
"@schematics/angular:module": {
"typeSeparator": "."
},
"@schematics/angular:pipe": {
"typeSeparator": "."
},
"@schematics/angular:resolver": {
"typeSeparator": "."
}
}
}

View File

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

View File

@ -1,29 +1,35 @@
import { Builder, WebDriver } from 'selenium-webdriver';
import { OPENVIDU_CALL_SERVER } from '../config';
import { WebComponentConfig } from '../selenium.conf';
import { OpenViduComponentsPO } from '../utils.po.test';
import { TestAppConfig } from './selenium.conf';
import { OpenViduComponentsPO } from './utils.po.test';
const url = `${WebComponentConfig.appUrl}?OV_URL=${OPENVIDU_CALL_SERVER}`;
let url = '';
describe('Testing API Directives', () => {
let browser: WebDriver;
let utils: OpenViduComponentsPO;
async function createChromeBrowser(): Promise<WebDriver> {
return await new Builder()
.forBrowser(WebComponentConfig.browserName)
.withCapabilities(WebComponentConfig.browserCapabilities)
.setChromeOptions(WebComponentConfig.browserOptions)
.usingServer(WebComponentConfig.seleniumAddress)
.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=API_DIRECTIVES_${Math.floor(Math.random() * 1000)}`;
});
afterEach(async () => {
// 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();
});
@ -83,7 +89,7 @@ describe('Testing API Directives', () => {
await utils.checkPrejoinIsPresent();
await utils.waitForElement('#lang-btn-compact');
await utils.waitForElement('.language-selector');
const element = await utils.waitForElement('#join-button');
expect(await element.getText()).toEqual('Unirme ahora');
@ -102,20 +108,20 @@ describe('Testing API Directives', () => {
const panelTitle = await utils.waitForElement('.panel-title');
expect(await panelTitle.getText()).toEqual('Configuración');
const element = await utils.waitForElement('#lang-selected-name');
expect(await element.getAttribute('innerText')).toEqual('Español');
const element = await utils.waitForElement('.lang-name');
expect(await element.getAttribute('innerText')).toEqual('Español expand_more');
});
it('should override the LANG OPTIONS', async () => {
await browser.get(`${url}&prejoin=true&langOptions=true`);
await utils.checkPrejoinIsPresent();
await utils.waitForElement('#lang-btn-compact');
await utils.clickOn('#lang-btn-compact');
await utils.waitForElement('.language-selector');
await utils.clickOn('.language-selector');
await browser.sleep(500);
expect(await utils.getNumberOfElements('.lang-menu-opt')).toEqual(2);
expect(await utils.getNumberOfElements('.language-option')).toEqual(2);
await utils.clickOn('.lang-menu-opt');
await utils.clickOn('.language-option');
await browser.sleep(500);
await utils.clickOn('#join-button');
@ -130,12 +136,12 @@ describe('Testing API Directives', () => {
await browser.sleep(500);
await utils.waitForElement('#settings-container');
await utils.waitForElement('.lang-button');
await utils.clickOn('.lang-button');
await utils.waitForElement('.full-lang-button');
await utils.clickOn('.full-lang-button');
await browser.sleep(500);
expect(await utils.getNumberOfElements('.lang-menu-opt')).toEqual(2);
expect(await utils.getNumberOfElements('.language-option')).toEqual(2);
});
it('should show the PREJOIN page', async () => {
@ -168,7 +174,7 @@ describe('Testing API Directives', () => {
});
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}`);
// Checking if prejoin page exist
@ -198,7 +204,7 @@ describe('Testing API Directives', () => {
});
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}`);
// Checking if session container is present
@ -232,11 +238,11 @@ describe('Testing API Directives', () => {
await utils.checkSessionIsPresent();
await utils.waitForElement('#video-poster');
expect(await utils.getNumberOfElements('video')).toEqual(0);
await utils.waitForElement('#videocam_off');
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 () => {
@ -281,6 +287,7 @@ describe('Testing API Directives', () => {
it('should run the app with AUDIO DISABLED and WITHOUT PREJOIN page', async () => {
await browser.get(`${url}&prejoin=false&audioEnabled=false`);
await browser.sleep(1000);
await utils.checkSessionIsPresent();
// Checking if video is displayed
@ -292,6 +299,30 @@ describe('Testing API Directives', () => {
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 () => {
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 () => {
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 utils.checkSessionIsPresent();
@ -527,8 +558,9 @@ describe('Testing API Directives', () => {
expect(await utils.isPresent('#remote-participant-item')).toBeFalse();
// 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.sleep(10000);
// Go to first tab
const tabs = await browser.getAllWindowHandles();

Binary file not shown.

View File

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

View File

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

View File

@ -1,4 +1,3 @@
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 = 60000;

View File

@ -1,20 +1,19 @@
import { Builder, Key, WebDriver } from 'selenium-webdriver';
import { OPENVIDU_CALL_SERVER } from '../config';
import { WebComponentConfig } from '../selenium.conf';
import { OpenViduComponentsPO } from '../utils.po.test';
import { TestAppConfig } from './selenium.conf';
import { OpenViduComponentsPO } from './utils.po.test';
const url = `${WebComponentConfig.appUrl}?OV_URL=${OPENVIDU_CALL_SERVER}`;
const url = TestAppConfig.appUrl;
describe('Testing videoconference EVENTS', () => {
let browser: WebDriver;
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> {
return await new Builder()
.forBrowser(WebComponentConfig.browserName)
.withCapabilities(WebComponentConfig.browserCapabilities)
.setChromeOptions(WebComponentConfig.browserOptions)
.usingServer(WebComponentConfig.seleniumAddress)
.forBrowser(TestAppConfig.browserName)
.withCapabilities(TestAppConfig.browserCapabilities)
.setChromeOptions(TestAppConfig.browserOptions)
.usingServer(TestAppConfig.seleniumAddress)
.build();
}
@ -24,6 +23,10 @@ describe('Testing videoconference EVENTS', () => {
});
afterEach(async () => {
try {
// leaving room if connected
await utils.leaveRoom();
} catch (error) {}
await browser.quit();
});
@ -57,23 +60,6 @@ describe('Testing videoconference EVENTS', () => {
expect(await utils.isPresent('#onTokenRequested')).toBeTrue();
});
it('should receive the onRoomDisconnected event', async () => {
await browser.get(`${url}&prejoin=false`);
await utils.checkSessionIsPresent();
await utils.checkToolbarIsPresent();
// Clicking to leave button
const leaveButton = await utils.waitForElement('#leave-btn');
expect(await utils.isPresent('#leave-btn')).toBeTrue();
await leaveButton.click();
// Checking if onRoomDisconnected has been received
await utils.waitForElement('#onRoomDisconnected');
expect(await utils.isPresent('#onRoomDisconnected')).toBeTrue();
});
it('should receive the onVideoEnabledChanged event when clicking on the prejoin', async () => {
await browser.get(url);
await utils.checkPrejoinIsPresent();
@ -106,35 +92,12 @@ describe('Testing videoconference EVENTS', () => {
expect(await utils.isPresent('#onVideoEnabledChanged-true')).toBeTrue();
});
it('should receive the onVideoEnabledChanged event when clicking on the settings panel', async () => {
await browser.get(`${url}&prejoin=false`);
await utils.checkSessionIsPresent();
await utils.checkToolbarIsPresent();
await utils.togglePanel('settings');
await browser.sleep(500);
await utils.waitForElement('#settings-container');
await utils.clickOn('#video-opt');
await utils.waitForElement('ov-video-devices-select');
await utils.clickOn('ov-video-devices-select #camera-button');
// Checking if onVideoEnabledChanged has been received
await utils.waitForElement('#onVideoEnabledChanged-false');
expect(await utils.isPresent('#onVideoEnabledChanged-false')).toBeTrue();
await utils.clickOn('ov-video-devices-select #camera-button');
await utils.waitForElement('#onVideoEnabledChanged-true');
expect(await utils.isPresent('#onVideoEnabledChanged-true')).toBeTrue();
});
it('should receive the onVideoDeviceChanged event on prejoin', async () => {
await browser.get(`${url}&fakeDevices=true`);
await utils.checkPrejoinIsPresent();
await utils.waitForElement('#video-devices-form');
await utils.clickOn('#video-devices-form');
await utils.waitForElement('#video-dropdown');
await utils.clickOn('#video-dropdown');
await utils.waitForElement('#option-custom_fake_video_1');
await utils.clickOn('#option-custom_fake_video_1');
@ -156,8 +119,8 @@ describe('Testing videoconference EVENTS', () => {
await utils.clickOn('#video-opt');
await utils.waitForElement('ov-video-devices-select');
await utils.waitForElement('#video-devices-form');
await utils.clickOn('#video-devices-form');
await utils.waitForElement('#video-dropdown');
await utils.clickOn('#video-dropdown');
await utils.waitForElement('#option-custom_fake_video_1');
await utils.clickOn('#option-custom_fake_video_1');
@ -198,35 +161,12 @@ describe('Testing videoconference EVENTS', () => {
expect(await utils.isPresent('#onAudioEnabledChanged-true')).toBeTrue();
});
it('should receive the onAudioEnabledChanged event when clicking on the settings panel', async () => {
await browser.get(`${url}&prejoin=false`);
await utils.checkSessionIsPresent();
await utils.checkToolbarIsPresent();
await utils.togglePanel('settings');
await browser.sleep(500);
await utils.waitForElement('#settings-container');
await utils.clickOn('#audio-opt');
await utils.waitForElement('ov-audio-devices-select');
await utils.clickOn('ov-audio-devices-select #microphone-button');
// Checking if onAudioEnabledChanged has been received
await utils.waitForElement('#onAudioEnabledChanged-false');
expect(await utils.isPresent('#onAudioEnabledChanged-false')).toBeTrue();
await utils.clickOn('ov-audio-devices-select #microphone-button');
await utils.waitForElement('#onAudioEnabledChanged-true');
expect(await utils.isPresent('#onAudioEnabledChanged-true')).toBeTrue();
});
it('should receive the onAudioDeviceChanged event on prejoin', async () => {
await browser.get(`${url}&fakeDevices=true`);
await utils.checkPrejoinIsPresent();
await utils.waitForElement('#audio-devices-form');
await utils.clickOn('#audio-devices-form');
await utils.waitForElement('#audio-dropdown');
await utils.clickOn('#audio-dropdown');
await utils.waitForElement('#option-custom_fake_audio_1');
await utils.clickOn('#option-custom_fake_audio_1');
@ -248,8 +188,8 @@ describe('Testing videoconference EVENTS', () => {
await utils.clickOn('#audio-opt');
await utils.waitForElement('ov-audio-devices-select');
await utils.waitForElement('#audio-devices-form');
await utils.clickOn('#audio-devices-form');
await utils.waitForElement('#audio-dropdown');
await utils.clickOn('#audio-dropdown');
await utils.waitForElement('#option-custom_fake_audio_1');
await utils.clickOn('#option-custom_fake_audio_1');
@ -262,8 +202,8 @@ describe('Testing videoconference EVENTS', () => {
await browser.get(`${url}`);
await utils.checkPrejoinIsPresent();
await utils.waitForElement('#lang-btn-compact');
await utils.clickOn('#lang-btn-compact');
await utils.waitForElement('.language-selector');
await utils.clickOn('.language-selector');
await browser.sleep(500);
await utils.clickOn('#lang-opt-es');
@ -283,8 +223,8 @@ describe('Testing videoconference EVENTS', () => {
await browser.sleep(500);
await utils.waitForElement('#settings-container');
await utils.waitForElement('.lang-button');
await utils.clickOn('.lang-button');
await utils.waitForElement('.full-lang-button');
await utils.clickOn('.full-lang-button');
await browser.sleep(500);
await utils.clickOn('#lang-opt-es');
@ -412,7 +352,7 @@ describe('Testing videoconference EVENTS', () => {
expect(await utils.isPresent('#onSettingsPanelStatusChanged-false')).toBeTrue();
});
it('should receive the onRecordingStartRequested event when clicking toolbar button', async () => {
it('should receive the onRecordingStartRequested and onRecordingStopRequested event when clicking toolbar button', async () => {
const roomName = 'recordingToolbarEvent';
await browser.get(`${url}&prejoin=false&roomName=${roomName}`);
@ -424,9 +364,15 @@ describe('Testing videoconference EVENTS', () => {
// Checking if onRecordingStartRequested has been received
await utils.waitForElement(`#onRecordingStartRequested-${roomName}`);
expect(await utils.isPresent(`#onRecordingStartRequested-${roomName}`)).toBeTrue();
});
xit('should receive the onRecordingStopRequested event when clicking toolbar button', async () => {});
await utils.waitForElement('.activity-status.started');
await utils.toggleRecordingFromToolbar();
// Checking if onRecordingStopRequested has been received
await utils.waitForElement(`#onRecordingStopRequested-${roomName}`);
expect(await utils.isPresent(`#onRecordingStopRequested-${roomName}`)).toBeTrue();
});
xit('should receive the onBroadcastingStopRequested event when clicking toolbar button', async () => {
await browser.get(`${url}&prejoin=false`);
@ -460,7 +406,7 @@ describe('Testing videoconference EVENTS', () => {
expect(await utils.isPresent('#onBroadcastingStopRequested')).toBeTrue();
});
it('should receive the onRecordingStartRequested when clicking from activities panel', async () => {
it('should receive the onRecordingStartRequested and onRecordingStopRequested when clicking from activities panel', async () => {
const roomName = 'recordingActivitiesEvent';
await browser.get(`${url}&prejoin=false&roomName=${roomName}`);
@ -486,8 +432,6 @@ describe('Testing videoconference EVENTS', () => {
expect(await utils.isPresent(`#onRecordingStartRequested-${roomName}`)).toBeTrue();
});
xit('should receive the onRecordingStopRequested when clicking from activities panel', async () => {});
xit('should receive the onRecordingDeleteRequested event', async () => {
let element;
const roomName = 'deleteRecordingEvent';
@ -613,7 +557,7 @@ describe('Testing videoconference EVENTS', () => {
expect(await utils.isPresent('#onReadyToJoin')).toBeFalse();
});
// * PUBLISHER EVENTS
// PARTICIPANT EVENTS
it('should receive onParticipantCreated event from LOCAL participant', async () => {
const participantName = 'TEST_USER';
@ -622,22 +566,39 @@ describe('Testing videoconference EVENTS', () => {
expect(await utils.isPresent(`#${participantName}-onParticipantCreated`)).toBeTrue();
});
// * ROOM EVENTS
it('should receive roomDisconnected event from LOCAL participant', async () => {
const participantName = 'TEST_USER';
let element;
await browser.get(`${url}&prejoin=false&participantName=${participantName}`);
it('should receive the onParticipantLeft event', async () => {
await browser.get(`${url}&prejoin=false&redirectToHome=false`);
await utils.checkSessionIsPresent();
await utils.checkToolbarIsPresent();
// Checking if leave button is not present
element = await utils.waitForElement('#leave-btn');
await element.click();
// Clicking to leave button
const leaveButton = await utils.waitForElement('#leave-btn');
expect(await utils.isPresent('#leave-btn')).toBeTrue();
await leaveButton.click();
await utils.waitForElement(`#roomDisconnected`);
expect(await utils.isPresent(`#roomDisconnected`)).toBeTrue();
await utils.waitForElement('#events');
// Checking if onParticipantLeft has been received
await utils.waitForElement('#onParticipantLeft');
expect(await utils.isPresent('#onParticipantLeft')).toBeTrue();
});
// * ROOM EVENTS
//TODO: Implement a mechanism to emulate network disconnection
// it('should receive the onRoomDisconnected event', async () => {
// await browser.get(`${url}&prejoin=false`);
// await utils.checkSessionIsPresent();
// await utils.checkToolbarIsPresent();
// // Emulate network disconnection
// await utils.forceCloseWebsocket();
// // Checking if onRoomDisconnected has been received
// await utils.waitForElement('#onRoomDisconnected');
// expect(await utils.isPresent('#onRoomDisconnected')).toBeTrue();
// });
});

View File

@ -0,0 +1,97 @@
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();
});
it('should show/hide theme selector with showThemeSelector directive', async () => {
await browser.get(`${url}&prejoin=false&showThemeSelector=true`);
await utils.checkSessionIsPresent();
await utils.togglePanel('settings');
expect(await utils.isPresent('.theme-selector-container')).toBeTrue();
await browser.get(`${url}&prejoin=false`);
await browser.navigate().refresh();
await utils.checkSessionIsPresent();
await utils.togglePanel('settings');
expect(await utils.isPresent('.theme-selector-container')).toBeFalse();
});
});

View File

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

View File

@ -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;
describe('Testing EVENTS', () => {
describe('OpenVidu Components EVENTS', () => {
let browser: WebDriver;
let utils: OpenViduComponentsPO;
@ -24,10 +24,13 @@ describe('Testing EVENTS', () => {
});
afterEach(async () => {
try {
await utils.leaveRoom();
} catch (error) {}
await browser.quit();
});
it('should receive the onRoomDisconnected event', async () => {
it('should receive the onParticipantLeft event', async () => {
await browser.get(`${url}`);
await utils.clickOn('#ovToolbar-checkbox');
@ -40,8 +43,8 @@ describe('Testing EVENTS', () => {
await utils.clickOn('#leave-btn');
// Checking if onLeaveButtonClicked has been received
await utils.waitForElement('#onRoomDisconnected');
expect(await utils.isPresent('#onRoomDisconnected')).toBeTrue();
await utils.waitForElement('#onParticipantLeft');
expect(await utils.isPresent('#onParticipantLeft')).toBeTrue();
});
it('should receive the onVideoEnabledChanged event', async () => {

View File

@ -1,20 +1,19 @@
import { Builder, Key, WebDriver } from 'selenium-webdriver';
import { OPENVIDU_CALL_SERVER } from '../config';
import { getBrowserOptionsWithoutDevices, WebComponentConfig } from '../selenium.conf';
import { OpenViduComponentsPO } from '../utils.po.test';
import { Builder, WebDriver } from 'selenium-webdriver';
import { TestAppConfig } from './selenium.conf';
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 utils: OpenViduComponentsPO;
async function createChromeBrowser(): Promise<WebDriver> {
return await new Builder()
.forBrowser(WebComponentConfig.browserName)
.withCapabilities(WebComponentConfig.browserCapabilities)
.setChromeOptions(WebComponentConfig.browserOptions)
.usingServer(WebComponentConfig.seleniumAddress)
.forBrowser(TestAppConfig.browserName)
.withCapabilities(TestAppConfig.browserCapabilities)
.setChromeOptions(TestAppConfig.browserOptions)
.usingServer(TestAppConfig.seleniumAddress)
.build();
}
@ -24,201 +23,110 @@ describe('Testing panels', () => {
});
afterEach(async () => {
try {
await utils.leaveRoom();
} catch (error) {}
await browser.quit();
});
/**
* TODO
* It only works with OpenVidu PRO because this is a PRO feature
*/
// it('should toggle BACKGROUND panel on prejoin page when VIDEO is MUTED', async () => {
// let element;
// await browser.get(`${url}`);
// element = await utils.waitForElement('#pre-join-container');
// expect(await utils.isPresent('#pre-join-container')).toBeTrue();
// const backgroundButton = await utils.waitForElement('#background-effects-btn');
// expect(await utils.isPresent('#background-effects-btn')).toBeTrue();
// expect(await backgroundButton.isEnabled()).toBeTrue();
// await backgroundButton.click();
// await browser.sleep(500);
// await utils.waitForElement('#background-effects-container');
// expect(await utils.isPresent('#background-effects-container')).toBeTrue();
// element = await utils.waitForElement('#camera-button');
// expect(await utils.isPresent('#camera-button')).toBeTrue();
// expect(await element.isEnabled()).toBeTrue();
// await element.click();
// await browser.sleep(500);
// element = await utils.waitForElement('#video-poster');
// expect(await utils.isPresent('#video-poster')).toBeTrue();
// expect(await backgroundButton.isDisplayed()).toBeTrue();
// expect(await backgroundButton.isEnabled()).toBeFalse();
// expect(await utils.isPresent('#background-effects-container')).toBeFalse();
// });
it('should toggle CHAT panel', async () => {
it('should open and close the CHAT panel and verify its content', async () => {
await browser.get(`${url}&prejoin=false`);
await utils.checkLayoutPresent();
const chatButton = await utils.waitForElement('#chat-panel-btn');
await chatButton.click();
await utils.waitForElement('.sidenav-menu');
await utils.waitForElement('.input-container');
expect(await utils.isPresent('.input-container')).toBeTrue();
await utils.waitForElement('.messages-container');
expect(await utils.isPresent('.messages-container')).toBeTrue();
await chatButton.click();
expect(await utils.isPresent('.input-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 utils.checkLayoutPresent();
const participantBtn = await utils.waitForElement('#participants-panel-btn');
await participantBtn.click();
await utils.waitForElement('.sidenav-menu');
await utils.waitForElement('.local-participant-container');
expect(await utils.isPresent('.local-participant-container')).toBeTrue();
await utils.waitForElement('ov-participant-panel-item');
expect(await utils.isPresent('ov-participant-panel-item')).toBeTrue();
await participantBtn.click();
expect(await utils.isPresent('.local-participant-container')).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 utils.checkLayoutPresent();
// Get activities button and click into it
const activitiesBtn = await utils.waitForElement('#activities-panel-btn');
await activitiesBtn.click();
await utils.waitForElement('.sidenav-menu');
await utils.waitForElement('#activities-container');
expect(await utils.isPresent('#activities-container')).toBeTrue();
await utils.waitForElement('#recording-activity');
expect(await utils.isPresent('#recording-activity')).toBeTrue();
await activitiesBtn.click();
expect(await utils.isPresent('#activities-container')).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;
await browser.get(`${url}&prejoin=false`);
await utils.checkLayoutPresent();
// Checking if toolbar is present
await utils.checkToolbarIsPresent();
await utils.togglePanel('settings');
element = await utils.waitForElement('.sidenav-menu');
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 utils.checkSessionIsPresent();
await utils.checkToolbarIsPresent();
// Open chat panel
const chatButton = await utils.waitForElement('#chat-panel-btn');
await chatButton.click();
await utils.waitForElement('.sidenav-menu');
expect(await utils.isPresent('.sidenav-menu')).toBeTrue();
await utils.waitForElement('.input-container');
expect(await utils.isPresent('.input-container')).toBeTrue();
expect(await utils.isPresent('.messages-container')).toBeTrue();
// Open participants panel
const participantBtn = await utils.waitForElement('#participants-panel-btn');
await participantBtn.click();
await utils.waitForElement('.sidenav-menu');
expect(await utils.isPresent('.local-participant-container')).toBeTrue();
expect(await utils.isPresent('ov-participant-panel-item')).toBeTrue();
// Switch to chat panel
await chatButton.click();
await utils.waitForElement('.sidenav-menu');
expect(await utils.isPresent('.input-container')).toBeTrue();
expect(await utils.isPresent('.messages-container')).toBeTrue();
expect(await utils.isPresent('.local-participant-container')).toBeFalse();
expect(await utils.isPresent('ov-participant-panel-item')).toBeFalse();
// Close chat panel
await chatButton.click();
expect(await utils.getNumberOfElements('.input-container')).toEqual(0);
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;
await browser.get(`${url}&prejoin=false`);
await utils.checkToolbarIsPresent();
// Checking if toolbar is present
await utils.checkToolbarIsPresent();
// Open more options menu
await utils.togglePanel('settings');
await utils.waitForElement('.sidenav-menu');
expect(await utils.isPresent('.sidenav-menu')).toBeTrue();
// Check if general section is shown
await browser.sleep(500);
element = await utils.waitForElement('#general-opt');
await element.click();
expect(await utils.isPresent('ov-participant-name-input')).toBeTrue();
// Check if video section is shown
element = await utils.waitForElement('#video-opt');
await element.click();
expect(await utils.isPresent('ov-video-devices-select')).toBeTrue();
// Check if audio section is shown
element = await utils.waitForElement('#audio-opt');
await element.click();
expect(await utils.isPresent('ov-audio-devices-select')).toBeTrue();
});
});

View File

@ -0,0 +1,355 @@
import { Builder, WebDriver } from 'selenium-webdriver';
import { TestAppConfig } from './selenium.conf';
import { OpenViduComponentsPO } from './utils.po.test';
const url = TestAppConfig.appUrl;
describe('E2E: Screensharing features', () => {
let browser: WebDriver;
let utils: OpenViduComponentsPO;
async function createChromeBrowser(): Promise<WebDriver> {
return await new Builder()
.forBrowser(TestAppConfig.browserName)
.withCapabilities(TestAppConfig.browserCapabilities)
.setChromeOptions(TestAppConfig.browserOptions)
.usingServer(TestAppConfig.seleniumAddress)
.build();
}
beforeEach(async () => {
browser = await createChromeBrowser();
utils = new OpenViduComponentsPO(browser);
});
afterEach(async () => {
try {
await utils.leaveRoom();
} catch (error) {}
await browser.quit();
});
it('should toggle screensharing on and off twice, updating video count', async () => {
await browser.get(`${url}&prejoin=false`);
await utils.checkLayoutPresent();
// Enable screensharing
await utils.waitForElement('#screenshare-btn');
await utils.clickOn('#screenshare-btn');
await browser.sleep(500);
await utils.waitForElement('.OV_big');
expect(await utils.getNumberOfElements('video')).toEqual(2);
// Disable screensharing
await utils.disableScreenShare();
expect(await utils.getNumberOfElements('video')).toEqual(1);
// Enable again
await utils.clickOn('#screenshare-btn');
await browser.sleep(500);
await utils.waitForElement('.OV_big');
expect(await utils.getNumberOfElements('video')).toEqual(2);
// Disable again
await utils.disableScreenShare();
expect(await utils.getNumberOfElements('video')).toEqual(1);
});
it('should show screenshare and muted camera (camera off, screenshare on)', async () => {
await browser.get(`${url}&prejoin=false`);
await utils.checkLayoutPresent();
// Mute camera
await utils.waitForElement('#camera-btn');
await utils.clickOn('#camera-btn');
// Enable screensharing
const screenshareButton = await utils.waitForElement('#screenshare-btn');
expect(await screenshareButton.isDisplayed()).toBeTrue();
await screenshareButton.click();
await browser.sleep(500);
await utils.waitForElement('.OV_big');
expect(await utils.getNumberOfElements('video')).toEqual(2);
// Disable screensharing
await utils.disableScreenShare();
expect(await utils.getNumberOfElements('video')).toEqual(1);
});
it('should display screensharing with a single pinned video', async () => {
await browser.get(`${url}&prejoin=false`);
await utils.checkLayoutPresent();
// Enable screensharing
const screenshareButton = await utils.waitForElement('#screenshare-btn');
expect(await screenshareButton.isDisplayed()).toBeTrue();
await screenshareButton.click();
await utils.waitForElement('.OV_big');
expect(await utils.getNumberOfElements('.OV_big')).toEqual(1);
});
it('should replace pinned video when a second participant starts screensharing', async () => {
const roomName = 'screensharingE2E';
const fixedUrl = `${url}&roomName=${roomName}&prejoin=false`;
await browser.get(fixedUrl);
await utils.checkLayoutPresent();
// First participant screenshares
await utils.waitForElement('#screenshare-btn');
await utils.clickOn('#screenshare-btn');
await utils.waitForElement('.OV_big');
expect(await utils.getNumberOfElements('.OV_big')).toEqual(1);
// Second participant joins and screenshares
const newTabScript = `window.open("${fixedUrl}")`;
await browser.executeScript(newTabScript);
const tabs = await browser.getAllWindowHandles();
await browser.switchTo().window(tabs[1]);
await utils.checkLayoutPresent();
await utils.waitForElement('#screenshare-btn');
await utils.clickOn('#screenshare-btn');
await browser.sleep(500);
expect(await utils.getNumberOfElements('video')).toEqual(4);
await utils.waitForElement('.OV_big');
expect(await utils.getNumberOfElements('.OV_big')).toEqual(1);
// Switch back to first tab and check
await browser.switchTo().window(tabs[0]);
await browser.sleep(500);
expect(await utils.getNumberOfElements('video')).toEqual(4);
await utils.waitForElement('.OV_big');
expect(await utils.getNumberOfElements('.OV_big')).toEqual(1);
});
it('should unpin screensharing and restore previous pinned video when disabled', async () => {
const roomName = 'screensharingtwoE2E';
const fixedUrl = `${url}&roomName=${roomName}&prejoin=false`;
await browser.get(fixedUrl);
await utils.checkLayoutPresent();
// First participant screenshares
await utils.waitForElement('#screenshare-btn');
await utils.clickOn('#screenshare-btn');
await browser.sleep(500);
await utils.waitForElement('.OV_big');
expect(await utils.getNumberOfElements('.OV_big')).toEqual(1);
// Second participant joins and screenshares
const tabs = await utils.openTab(fixedUrl);
await browser.switchTo().window(tabs[1]);
await utils.checkLayoutPresent();
await utils.waitForElement('#screenshare-btn');
await utils.clickOn('#screenshare-btn');
await browser.sleep(500);
expect(await utils.getNumberOfElements('video')).toEqual(4);
await utils.waitForElement('.OV_big');
expect(await utils.getNumberOfElements('.OV_big')).toEqual(1);
// Disable screensharing for second participant
await utils.disableScreenShare();
expect(await utils.getNumberOfElements('video')).toEqual(3);
await utils.waitForElement('.OV_big');
expect(await utils.getNumberOfElements('.OV_big')).toEqual(1);
// Switch back to first tab and check
await browser.switchTo().window(tabs[0]);
await browser.sleep(500);
expect(await utils.getNumberOfElements('video')).toEqual(3);
await utils.waitForElement('.OV_big');
expect(await utils.getNumberOfElements('.OV_big')).toEqual(1);
});
it('should correctly share screen with microphone muted and maintain proper track state', async () => {
// Helper for inspecting stream tracks
const getMediaTracks = (className: string) => {
return `
const tracks = document.getElementsByClassName('${className}')[0].srcObject.getTracks();
return tracks.map(track => ({
kind: track.kind,
enabled: track.enabled,
id: track.id,
label: track.label
}));`;
};
// Setup: Navigate to room and skip prejoin
await browser.get(`${url}&prejoin=false`);
await utils.checkLayoutPresent();
// Step 1: First mute the microphone
const micButton = await utils.waitForElement('#mic-btn');
await micButton.click();
// Step 2: Start screen sharing
await utils.clickOn('#screenshare-btn');
// Step 3: Verify both streams are present
await utils.waitForElement('.screen-type');
expect(await utils.getNumberOfElements('video')).toEqual(2);
// Step 4: Verify screen share track properties
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();
// Step 5: Verify microphone status indicators for both streams
// await utils.waitForElement('#status-mic');
// const micStatusCount = await utils.getNumberOfElements('#status-mic');
// expect(micStatusCount).toEqual(2);
// 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);
});
// ==================== PIN/UNPIN TESTS ====================
// These tests demonstrate bugs in the pin system:
// 1. Multiple screens can be auto-pinned simultaneously
// 2. Manual unpins can be overridden by auto-pin logic when participants join
it('should NOT have multiple screens pinned when both participants share screen', async () => {
const roomName = 'pinBugCase1';
const fixedUrl = `${url}&roomName=${roomName}&prejoin=false`;
// Participant A joins and shares screen
await browser.get(fixedUrl);
await utils.checkLayoutPresent();
await utils.waitForElement('#screenshare-btn');
await utils.clickOn('#screenshare-btn');
await browser.sleep(500);
// Verify A's screen is pinned
await utils.waitForElement('.OV_big');
expect(await utils.getNumberOfPinnedStreams()).toEqual(1);
const pinnedCountA1 = await utils.getNumberOfPinnedStreams();
console.log(`[Tab A] After A shares: ${pinnedCountA1} pinned stream(s)`);
// Participant B joins
const tabs = await utils.openTab(fixedUrl);
await browser.switchTo().window(tabs[1]);
await utils.checkLayoutPresent();
await browser.sleep(1000);
// B should see A's screen pinned
expect(await utils.getNumberOfElements('video')).toEqual(3); // 2 cameras + 1 screen
expect(await utils.getNumberOfPinnedStreams()).toEqual(1);
const pinnedCountB1 = await utils.getNumberOfPinnedStreams();
console.log(`[Tab B] After B joins: ${pinnedCountB1} pinned stream(s)`);
// B shares screen
await utils.waitForElement('#screenshare-btn');
await utils.clickOn('#screenshare-btn');
await browser.sleep(500);
// B should see only their own screen pinned (auto-pin + unpin previous)
expect(await utils.getNumberOfElements('video')).toEqual(4); // 2 cameras + 2 screens
await utils.waitForElement('.OV_big');
const pinnedCountB2 = await utils.getNumberOfPinnedStreams();
console.log(`[Tab B] After B shares: ${pinnedCountB2} pinned stream(s)`);
expect(pinnedCountB2).toEqual(1); // Should be 1, but implementation might show different
// Switch to Tab A and check
await browser.switchTo().window(tabs[0]);
await browser.sleep(1000);
expect(await utils.getNumberOfElements('video')).toEqual(4); // 2 cameras + 2 screens
// BUG: In A's view, BOTH screens are pinned
const pinnedCountA2 = await utils.getNumberOfPinnedStreams();
console.log(`[Tab A] After B shares: ${pinnedCountA2} pinned stream(s)`);
// EXPECTED: Only B's screen should be pinned (the most recent one)
// ACTUAL: Both A's and B's screens are pinned
expect(pinnedCountA2).toEqual(1, 'BUG DETECTED: Multiple screens are pinned. Expected only the most recent screen to be pinned.');
});
it('should NOT re-pin manually unpinned screen when new participant joins', async () => {
const roomName = 'pinBugCase2';
const fixedUrl = `${url}&roomName=${roomName}&prejoin=false`;
// Participant A joins and shares screen
await browser.get(fixedUrl);
await utils.checkLayoutPresent();
await utils.waitForElement('#screenshare-btn');
await utils.clickOn('#screenshare-btn');
await browser.sleep(500);
// Verify A's screen is auto-pinned
await utils.waitForElement('.OV_big');
expect(await utils.getNumberOfPinnedStreams()).toEqual(1);
// Participant B joins and shares screen
const tabs = await utils.openTab(fixedUrl);
await browser.switchTo().window(tabs[1]);
await utils.checkLayoutPresent();
await browser.sleep(1000);
await utils.waitForElement('#screenshare-btn');
await utils.clickOn('#screenshare-btn');
await browser.sleep(500);
// B should see their own screen pinned
expect(await utils.getNumberOfElements('video')).toEqual(4); // 2 cameras + 2 screens
await utils.waitForElement('.OV_big');
let pinnedCountB = await utils.getNumberOfPinnedStreams();
console.log(`[Tab B] After B shares: ${pinnedCountB} pinned stream(s)`);
// B manually unpins their own screen
const screenStreams = await utils.getScreenShareStreams();
if (screenStreams.length > 0) {
// Find B's own screen (it should be the pinned one)
await utils.toggleStreamPin('.OV_big');
await browser.sleep(1000);
}
// Verify B's screen is now unpinned
pinnedCountB = await utils.getNumberOfPinnedStreams();
console.log(`[Tab B] After manually unpinning B's screen: ${pinnedCountB} pinned stream(s)`);
expect(pinnedCountB).toEqual(0, 'B should have no pinned streams after manual unpin');
// B manually pins A's screen
const screenElements = await utils.getScreenShareStreams();
if (screenElements.length >= 2) {
// Pin the first screen that is not already pinned (should be A's screen)
await utils.toggleStreamPin('.OV_stream.remote .screen-type');
await utils.toggleStreamPin('#pin-btn');
await browser.sleep(500);
}
// Verify A's screen is now pinned in B's view
pinnedCountB = await utils.getNumberOfPinnedStreams();
console.log(`[Tab B] After manually pinning A's screen: ${pinnedCountB} pinned stream(s)`);
expect(pinnedCountB).toEqual(1, "Only A's screen should be pinned");
// Participant C joins the room
const tab3 = await utils.openTab(fixedUrl);
await browser.switchTo().window(tab3[2]);
await utils.checkLayoutPresent();
await browser.sleep(1500);
// Switch back to B's tab
await browser.switchTo().window(tabs[1]);
await browser.sleep(1000);
// B's screen should still be unpinned, but might get re-pinned automatically
pinnedCountB = await utils.getNumberOfPinnedStreams();
console.log(`[Tab B] After C joins: ${pinnedCountB} pinned stream(s)`);
// EXPECTED: No screens should be pinned (B manually unpinned everything)
// ACTUAL: B's screen gets re-pinned automatically
expect(pinnedCountB).toEqual(1, 'BUG DETECTED: Only one screen should be pinned after C joins.');
// Switch back to A's tab to verify
await browser.switchTo().window(tabs[0]);
await browser.sleep(500);
const pinnedCountA2 = await utils.getNumberOfPinnedStreams();
console.log(`[Tab A] After C joins: ${pinnedCountA2} pinned stream(s)`);
// EXPECTED: Only A's screen should be pinned
// ACTUAL: A's screen remains pinned
expect(pinnedCountA2).toEqual(1, "BUG DETECTED: A's screen should remain pinned after C joins.");
});
});

View File

@ -10,12 +10,14 @@ interface BrowserConfig {
browserName: string;
}
const audioPath = LAUNCH_MODE === 'CI' ? `e2e-assets/audio_test.wav` : 'e2e/assets/audio_test.wav';
const chromeArguments = [
'--window-size=1300,1000',
'--headless',
// '--headless',
'--use-fake-ui-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 = [
'--window-size=1300,1000',
@ -29,7 +31,10 @@ const chromeArgumentsCI = [
'--disable-background-networking',
'--disable-default-apps',
'--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 chromeArgumentsWithoutMediaDevicesCI = [
@ -46,12 +51,13 @@ const chromeArgumentsWithoutMediaDevicesCI = [
'--deny-permission-prompts'
];
export const WebComponentConfig: BrowserConfig = {
appUrl: 'http://localhost:8080/',
export const TestAppConfig: BrowserConfig = {
appUrl: 'http://localhost:4200/#/call?staticVideos=false',
seleniumAddress: LAUNCH_MODE === 'CI' ? 'http://localhost:4444/wd/hub' : '',
browserName: 'chrome',
browserCapabilities: Capabilities.chrome().set('acceptInsecureCerts', true),
browserOptions: new chrome.Options().addArguments(...(LAUNCH_MODE === 'CI' ? chromeArgumentsCI : chromeArguments))
browserOptions: new chrome.Options()
.addArguments(...(LAUNCH_MODE === 'CI' ? chromeArgumentsCI : chromeArguments)) as chrome.Options
};
export const NestedConfig: BrowserConfig = {
@ -59,13 +65,14 @@ export const NestedConfig: BrowserConfig = {
seleniumAddress: LAUNCH_MODE === 'CI' ? 'http://localhost:4444/wd/hub' : '',
browserName: 'Chrome',
browserCapabilities: Capabilities.chrome().set('acceptInsecureCerts', true),
browserOptions: new chrome.Options().addArguments(...(LAUNCH_MODE === 'CI' ? chromeArgumentsCI : chromeArguments))
browserOptions: new chrome.Options()
.addArguments(...(LAUNCH_MODE === 'CI' ? chromeArgumentsCI : chromeArguments)) as chrome.Options
};
export function getBrowserOptionsWithoutDevices() {
if (LAUNCH_MODE === 'CI') {
return new chrome.Options().addArguments(...chromeArgumentsWithoutMediaDevicesCI);
return new chrome.Options().addArguments(...chromeArgumentsWithoutMediaDevicesCI) as chrome.Options;
} else {
return new chrome.Options().addArguments(...chromeArgumentsWithoutMediaDevices);
return new chrome.Options().addArguments(...chromeArgumentsWithoutMediaDevices) as chrome.Options;
}
}

View File

@ -1,19 +1,18 @@
import { Builder, ILocation, IRectangle, ISize, WebDriver } from 'selenium-webdriver';
import { OPENVIDU_CALL_SERVER } from '../config';
import { WebComponentConfig } from '../selenium.conf';
import { OpenViduComponentsPO } from '../utils.po.test';
import { Builder, IRectangle, WebDriver } from 'selenium-webdriver';
import { TestAppConfig } from './selenium.conf';
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 utils: OpenViduComponentsPO;
async function createChromeBrowser(): Promise<WebDriver> {
return await new Builder()
.forBrowser(WebComponentConfig.browserName)
.withCapabilities(WebComponentConfig.browserCapabilities)
.setChromeOptions(WebComponentConfig.browserOptions)
.usingServer(WebComponentConfig.seleniumAddress)
.forBrowser(TestAppConfig.browserName)
.withCapabilities(TestAppConfig.browserCapabilities)
.setChromeOptions(TestAppConfig.browserOptions)
.usingServer(TestAppConfig.seleniumAddress)
.build();
}
@ -23,10 +22,13 @@ describe('Checking stream elements by disabling/enabling the media', () => {
});
afterEach(async () => {
try {
await utils.leaveRoom();
} catch (error) {}
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 utils.checkPrejoinIsPresent();
@ -39,7 +41,7 @@ describe('Checking stream elements by disabling/enabling the media', () => {
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 utils.checkPrejoinIsPresent();
@ -52,7 +54,7 @@ describe('Checking stream elements by disabling/enabling the media', () => {
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 utils.checkPrejoinIsPresent();
@ -65,7 +67,7 @@ describe('Checking stream elements by disabling/enabling the media', () => {
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 utils.checkPrejoinIsPresent();
@ -79,7 +81,7 @@ describe('Checking stream elements by disabling/enabling the media', () => {
await utils.clickOn('#screenshare-btn');
await browser.sleep(1000);
await utils.waitForElement('#local-element-screen_share');
await utils.waitForElement('.local_participant.OV_screen');
expect(await utils.getNumberOfElements('.OV_stream')).toEqual(2);
expect(await utils.getNumberOfElements('video')).toEqual(1); //screen sharse video
expect(await utils.getNumberOfElements('audio')).toEqual(1); //screen share audio
@ -90,7 +92,7 @@ describe('Checking stream elements by disabling/enabling the media', () => {
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 utils.checkPrejoinIsPresent();
@ -104,7 +106,7 @@ describe('Checking stream elements by disabling/enabling the media', () => {
await utils.clickOn('#screenshare-btn');
await browser.sleep(1000);
await utils.waitForElement('#local-element-screen_share');
await utils.waitForElement('.local_participant.OV_screen');
expect(await utils.getNumberOfElements('.OV_stream')).toEqual(2);
expect(await utils.getNumberOfElements('video')).toEqual(2);
expect(await utils.getNumberOfElements('audio')).toEqual(2); //screen share audio and local audio
@ -115,9 +117,9 @@ describe('Checking stream elements by disabling/enabling the media', () => {
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 fixedUrl = `${url}&roomName=${roomName}&prejoin=false&videoEnabled=false&audioEnabled=false`;
await browser.get(fixedUrl);
@ -145,7 +147,7 @@ describe('Checking stream elements by disabling/enabling the media', () => {
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 fixedUrl = `${url}&roomName=${roomName}&prejoin=false&videoEnabled=true&audioEnabled=false`;
await browser.get(fixedUrl);
@ -158,6 +160,8 @@ describe('Checking stream elements by disabling/enabling the media', () => {
expect(await utils.getNumberOfElements('audio')).toEqual(0);
const tabs = await utils.openTab(fixedUrl);
await browser.sleep(1000);
await browser.switchTo().window(tabs[0]);
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);
});
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 fixedUrl = `${url}&roomName=${roomName}&prejoin=false&videoEnabled=false`;
await browser.get(fixedUrl);
@ -186,6 +190,7 @@ describe('Checking stream elements by disabling/enabling the media', () => {
expect(await utils.getNumberOfElements('audio')).toEqual(1);
const tabs = await utils.openTab(fixedUrl);
await browser.sleep(1000);
await browser.switchTo().window(tabs[0]);
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);
});
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 fixedUrl = `${url}&roomName=${roomName}&prejoin=false&videoEnabled=false&audioEnabled=false`;
await browser.get(fixedUrl);
@ -216,7 +221,7 @@ describe('Checking stream elements by disabling/enabling the media', () => {
await utils.clickOn('#screenshare-btn');
await browser.sleep(1000);
await utils.waitForElement('#local-element-screen_share');
await utils.waitForElement('.local_participant.OV_screen');
expect(await utils.getNumberOfElements('.OV_stream')).toEqual(3);
expect(await utils.getNumberOfElements('video')).toEqual(1);
expect(await utils.getNumberOfElements('audio')).toEqual(1); // screen share audios
@ -240,7 +245,7 @@ describe('Checking stream elements by disabling/enabling the media', () => {
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 fixedUrl = `${url}&roomName=${roomName}&prejoin=false&videoEnabled=true&audioEnabled=true`;
await browser.get(fixedUrl);
@ -255,7 +260,7 @@ describe('Checking stream elements by disabling/enabling the media', () => {
await utils.clickOn('#screenshare-btn');
await browser.sleep(1000);
await utils.waitForElement('#local-element-screen_share');
await utils.waitForElement('.local_participant.OV_screen');
expect(await utils.getNumberOfElements('.OV_stream')).toEqual(3);
expect(await utils.getNumberOfElements('video')).toEqual(3);
expect(await utils.getNumberOfElements('audio')).toEqual(3); // screen share audios and local audio and remote audio
@ -279,7 +284,7 @@ describe('Checking stream elements by disabling/enabling the media', () => {
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 fixedUrl = `${url}&roomName=${roomName}&prejoin=false&videoEnabled=false&audioEnabled=false`;
await browser.get(fixedUrl);
@ -299,7 +304,7 @@ describe('Checking stream elements by disabling/enabling the media', () => {
await utils.clickOn('#screenshare-btn');
await browser.sleep(500);
await utils.waitForElement('#local-element-screen_share');
await utils.waitForElement('.local_participant.OV_screen');
expect(await utils.getNumberOfElements('.OV_stream')).toEqual(4);
expect(await utils.getNumberOfElements('video')).toEqual(2);
expect(await utils.getNumberOfElements('audio')).toEqual(2); // screen share audios
@ -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 utils: OpenViduComponentsPO;
async function createChromeBrowser(): Promise<WebDriver> {
return await new Builder()
.forBrowser(WebComponentConfig.browserName)
.withCapabilities(WebComponentConfig.browserCapabilities)
.setChromeOptions(WebComponentConfig.browserOptions)
.usingServer(WebComponentConfig.seleniumAddress)
.forBrowser(TestAppConfig.browserName)
.withCapabilities(TestAppConfig.browserCapabilities)
.setChromeOptions(TestAppConfig.browserOptions)
.usingServer(TestAppConfig.seleniumAddress)
.build();
}
@ -341,6 +346,9 @@ describe('Testing stream features', () => {
});
afterEach(async () => {
try {
await utils.leaveRoom();
} catch (error) {}
await browser.quit();
});
@ -658,7 +666,7 @@ describe('Testing stream features', () => {
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 fixedUrl = `${url}&roomName=${roomName}&prejoin=false`;
await browser.get(`${fixedUrl}&audioEnabled=false`);
@ -668,25 +676,65 @@ describe('Testing stream features', () => {
// Starting new browser for adding the second participant
const newTabScript = `window.open("${fixedUrl}")`;
await browser.executeScript(newTabScript);
await browser.sleep(1000);
const tabs = await browser.getAllWindowHandles();
await browser.switchTo().window(tabs[0]);
await utils.waitForElement('.OV_stream.remote.speaking');
expect(await utils.getNumberOfElements('.OV_stream.remote.speaking')).toEqual(1);
expect(await utils.getNumberOfElements('.OV_stream.speaking')).toEqual(1);
// Wait with retries for audio detection to appear (handles timing issues)
const maxRetries = 5;
const retryInterval = 1000;
let audioDetected = false;
for (let i = 0; i < maxRetries && !audioDetected; i++) {
await browser.sleep(retryInterval);
const remoteSpeakingCount = await utils.getNumberOfElements('.OV_stream.remote.speaking');
if (remoteSpeakingCount >= 1) {
audioDetected = true;
console.log(`[Audio Detection] Detected after ${i + 1} attempt(s)`);
} else {
console.log(`[Audio Detection] Attempt ${i + 1}/${maxRetries}: No audio detected yet`);
}
}
// Ensure at least one remote speaker element is present (timing-sensitive)
expect(audioDetected).toBeTrue();
if (!audioDetected) {
console.error('Audio detection indicator did not appear within timeout');
}
// The local participant is muted; poll briefly to ensure the local stream is not
// marked as speaking. This handles timing races where classes may be applied
// or removed slightly later.
const timeout = 2000;
const interval = 200;
const start = Date.now();
let localNotSpeaking = false;
while (Date.now() - start < timeout) {
const localCount = await utils.getNumberOfElements('.OV_stream.local.speaking');
if (localCount === 0) {
localNotSpeaking = true;
break;
}
await browser.sleep(interval);
}
expect(localNotSpeaking).toBeTrue();
if (!localNotSpeaking) {
console.error('Local stream should not be marked as speaking when muted');
}
});
});
describe('Testing video is playing', () => {
describe('Video playback reliability with different media states', () => {
let browser: WebDriver;
let utils: OpenViduComponentsPO;
async function createChromeBrowser(): Promise<WebDriver> {
return await new Builder()
.forBrowser(WebComponentConfig.browserName)
.withCapabilities(WebComponentConfig.browserCapabilities)
.setChromeOptions(WebComponentConfig.browserOptions)
.usingServer(WebComponentConfig.seleniumAddress)
.forBrowser(TestAppConfig.browserName)
.withCapabilities(TestAppConfig.browserCapabilities)
.setChromeOptions(TestAppConfig.browserOptions)
.usingServer(TestAppConfig.seleniumAddress)
.build();
}
@ -696,6 +744,9 @@ describe('Testing video is playing', () => {
});
afterEach(async () => {
try {
await utils.leaveRoom();
} catch (error) {}
await browser.quit();
});

View File

@ -1,19 +1,18 @@
import { Builder, WebDriver } from 'selenium-webdriver';
import { OPENVIDU_CALL_SERVER } from '../config';
import { WebComponentConfig } from '../selenium.conf';
import { OpenViduComponentsPO } from '../utils.po.test';
import { TestAppConfig } from './selenium.conf';
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 utils: OpenViduComponentsPO;
async function createChromeBrowser(): Promise<WebDriver> {
return await new Builder()
.forBrowser(WebComponentConfig.browserName)
.withCapabilities(WebComponentConfig.browserCapabilities)
.setChromeOptions(WebComponentConfig.browserOptions)
.usingServer(WebComponentConfig.seleniumAddress)
.forBrowser(TestAppConfig.browserName)
.withCapabilities(TestAppConfig.browserCapabilities)
.setChromeOptions(TestAppConfig.browserOptions)
.usingServer(TestAppConfig.seleniumAddress)
.build();
}
@ -23,10 +22,13 @@ describe('Testing TOOLBAR features', () => {
});
afterEach(async () => {
try {
await utils.leaveRoom();
} catch (error) {}
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 utils.checkLayoutPresent();
@ -43,7 +45,7 @@ describe('Testing TOOLBAR features', () => {
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 utils.checkLayoutPresent();

View File

@ -1,4 +1,8 @@
import * as fs from 'fs';
import pixelmatch from 'pixelmatch';
import { PNG } from 'pngjs';
import { By, until, WebDriver, WebElement } from 'selenium-webdriver';
type PNGWithMetadata = PNG & { data: Buffer };
export class OpenViduComponentsPO {
private TIMEOUT = 10 * 1000;
@ -136,6 +140,42 @@ export class OpenViduComponentsPO {
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) {
switch (panelName) {
case 'activities':
@ -152,6 +192,16 @@ export class OpenViduComponentsPO {
await this.waitForElement('#participants-panel-btn');
await this.clickOn('#participants-panel-btn');
break;
case 'backgrounds':
await this.waitForElement('#more-options-btn');
await this.clickOn('#more-options-btn');
await this.browser.sleep(500);
await this.waitForElement('#virtual-bg-btn');
await this.clickOn('#virtual-bg-btn');
await this.browser.sleep(1000);
break;
case 'settings':
await this.toggleToolbarMoreOptions();
@ -159,5 +209,120 @@ export class OpenViduComponentsPO {
await this.clickOn('#toolbar-settings-btn');
break;
}
await this.browser.sleep(500);
}
async applyBackground(bgId: string) {
await this.waitForElement('ov-background-effects-panel');
await this.browser.sleep(1000);
await this.waitForElement(`#effect-${bgId}`);
await this.clickOn(`#effect-${bgId}`);
await this.browser.sleep(2000);
}
async applyVirtualBackgroundFromPrejoin(bgId: string): Promise<void> {
await this.waitForElement('#backgrounds-button');
await this.clickOn('#backgrounds-button');
await this.applyBackground(bgId);
await this.clickOn('#backgrounds-button');
}
async saveScreenshot(filename: string, element: WebElement) {
const image = await element.takeScreenshot();
fs.writeFileSync(filename, image, 'base64');
}
async expectVirtualBackgroundApplied(
img1Name: string,
img2Name: string,
{
threshold = 0.4,
minDiffPixels = 500,
debug = false
}: {
threshold?: number;
minDiffPixels?: number;
debug?: boolean;
} = {}
): Promise<void> {
const beforeImg = PNG.sync.read(fs.readFileSync(img1Name));
const afterImg = PNG.sync.read(fs.readFileSync(img2Name));
const { width, height } = beforeImg;
const diff = new PNG({ width, height });
// const numDiffPixels = pixelmatch(img1.data, img2.data, diff.data, width, height, {
// threshold: 0.4
// // alpha: 0.5,
// // includeAA: false,
// // diffColor: [255, 0, 0]
// });
const numDiffPixels = pixelmatch(beforeImg.data, afterImg.data, diff.data, width, height, {
threshold
// includeAA: true
});
if (numDiffPixels <= minDiffPixels) {
// Sólo guardar los archivos de debug si falla la prueba
if (debug) {
fs.writeFileSync('before.png', PNG.sync.write(beforeImg));
fs.writeFileSync('after.png', PNG.sync.write(afterImg));
fs.writeFileSync('diff.png', PNG.sync.write(diff));
}
}
expect(numDiffPixels).toBeGreaterThan(minDiffPixels, 'The virtual background was not applied correctly');
// fs.writeFileSync('diff.png', PNG.sync.write(diff));
// expect(numDiffPixels).to.be.greaterThan(500, 'The virtual background was not applied correctly');
}
/**
* Pins or unpins a stream by clicking on it
* @param streamSelector CSS selector for the stream element (e.g., '.screen-type', '.camera-type')
*/
async toggleStreamPin(streamSelector: string): Promise<void> {
const stream = await this.waitForElement(streamSelector);
await stream.click();
await this.clickOn('#pin-btn');
await this.browser.sleep(300);
}
/**
* Gets the number of pinned streams (elements with class .OV_big)
*/
async getNumberOfPinnedStreams(): Promise<number> {
return await this.getNumberOfElements('.OV_big');
}
/**
* Checks if a specific stream is pinned
* @param streamSelector CSS selector for the stream element
*/
async isStreamPinned(streamSelector: string): Promise<boolean> {
try {
const stream = await this.waitForElement(streamSelector);
const classes = await stream.getAttribute('class');
return classes.includes('OV_big');
} catch (error) {
return false;
}
}
/**
* Gets all screen share streams
*/
async getScreenShareStreams(): Promise<WebElement[]> {
return await this.browser.findElements(By.css('.screen-type'));
}
/**
* Gets all camera streams
*/
async getCameraStreams(): Promise<WebElement[]> {
return await this.browser.findElements(By.css('.camera-type'));
}
}

View File

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

View File

@ -1,277 +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('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) => {
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,310 +0,0 @@
import { Builder, WebDriver } from 'selenium-webdriver';
import { OPENVIDU_CALL_SERVER } from '../config';
import { WebComponentConfig } from '../selenium.conf';
import { OpenViduComponentsPO } from '../utils.po.test';
const url = `${WebComponentConfig.appUrl}?OV_URL=${OPENVIDU_CALL_SERVER}`;
describe('Testing screenshare features', () => {
let browser: WebDriver;
let utils: OpenViduComponentsPO;
async function createChromeBrowser(): Promise<WebDriver> {
return await new Builder()
.forBrowser(WebComponentConfig.browserName)
.withCapabilities(WebComponentConfig.browserCapabilities)
.setChromeOptions(WebComponentConfig.browserOptions)
.usingServer(WebComponentConfig.seleniumAddress)
.build();
}
beforeEach(async () => {
browser = await createChromeBrowser();
utils = new OpenViduComponentsPO(browser);
});
afterEach(async () => {
await browser.quit();
});
it('should toggle screensharing twice', async () => {
await browser.get(`${url}&prejoin=false`);
await utils.checkLayoutPresent();
// Clicking to screensharing button
await utils.waitForElement('#screenshare-btn');
await utils.clickOn('#screenshare-btn');
await browser.sleep(500);
await utils.waitForElement('.OV_big');
expect(await utils.getNumberOfElements('video')).toEqual(2);
// expect(await utils.getNumberOfElements('.OV_stream.speaking')).toEqual(1);
await utils.disableScreenShare();
expect(await utils.getNumberOfElements('video')).toEqual(1);
// toggle screenshare again
await utils.clickOn('#screenshare-btn');
await browser.sleep(500);
await utils.waitForElement('.OV_big');
expect(await utils.getNumberOfElements('video')).toEqual(2);
await utils.disableScreenShare();
expect(await utils.getNumberOfElements('video')).toEqual(1);
});
it('should show screen and muted camera', async () => {
await browser.get(`${url}&prejoin=false`);
await utils.checkLayoutPresent();
await utils.waitForElement('#camera-btn');
await utils.clickOn('#camera-btn');
// Clicking to screensharing button
const screenshareButton = await utils.waitForElement('#screenshare-btn');
expect(await screenshareButton.isDisplayed()).toBeTrue();
await screenshareButton.click();
await browser.sleep(500);
await utils.waitForElement('.OV_big');
expect(await utils.getNumberOfElements('video')).toEqual(2);
await utils.disableScreenShare();
expect(await utils.getNumberOfElements('video')).toEqual(1);
});
it('should screensharing with PINNED video', async () => {
await browser.get(`${url}&prejoin=false`);
await utils.checkLayoutPresent();
// Clicking to screensharing button
const screenshareButton = await utils.waitForElement('#screenshare-btn');
expect(await screenshareButton.isDisplayed()).toBeTrue();
await screenshareButton.click();
await utils.waitForElement('.OV_big');
expect(await utils.getNumberOfElements('.OV_big')).toEqual(1);
});
it('should screensharing with PINNED video and replace the existing one', async () => {
const roomName = 'screensharingE2E';
const fixedUrl = `${url}&roomName=${roomName}&prejoin=false`;
await browser.get(fixedUrl);
await utils.checkLayoutPresent();
// Clicking to screensharing button
await utils.waitForElement('#screenshare-btn');
await utils.clickOn('#screenshare-btn');
await utils.waitForElement('.OV_big');
expect(await utils.getNumberOfElements('.OV_big')).toEqual(1);
// Starting new browser for adding the second participant
const newTabScript = `window.open("${fixedUrl}")`;
await browser.executeScript(newTabScript);
const tabs = await browser.getAllWindowHandles();
await browser.switchTo().window(tabs[1]);
await utils.checkLayoutPresent();
// Clicking to screensharing button
await utils.waitForElement('#screenshare-btn');
await utils.clickOn('#screenshare-btn');
await browser.sleep(500);
expect(await utils.getNumberOfElements('video')).toEqual(4);
await utils.waitForElement('.OV_big');
expect(await utils.getNumberOfElements('.OV_big')).toEqual(1);
// Go to first tab
await browser.switchTo().window(tabs[0]);
await browser.sleep(500);
expect(await utils.getNumberOfElements('video')).toEqual(4);
await utils.waitForElement('.OV_big');
expect(await utils.getNumberOfElements('.OV_big')).toEqual(1);
});
it('should disabled a screensharing and pinned the previous one', async () => {
const roomName = 'screensharingtwoE2E';
const fixedUrl = `${url}&roomName=${roomName}&prejoin=false`;
await browser.get(fixedUrl);
await utils.checkLayoutPresent();
// Clicking to screensharing button
await utils.waitForElement('#screenshare-btn');
await utils.clickOn('#screenshare-btn');
await browser.sleep(500);
await utils.waitForElement('.OV_big');
expect(await utils.getNumberOfElements('.OV_big')).toEqual(1);
// Starting new browser for adding the second participant
const tabs = await utils.openTab(fixedUrl);
await browser.switchTo().window(tabs[1]);
await utils.checkLayoutPresent();
// Clicking to screensharing button
await utils.waitForElement('#screenshare-btn');
await utils.clickOn('#screenshare-btn');
await browser.sleep(500);
expect(await utils.getNumberOfElements('video')).toEqual(4);
await utils.waitForElement('.OV_big');
expect(await utils.getNumberOfElements('.OV_big')).toEqual(1);
// Disable screensharing
await utils.disableScreenShare();
expect(await utils.getNumberOfElements('video')).toEqual(3);
await utils.waitForElement('.OV_big');
expect(await utils.getNumberOfElements('.OV_big')).toEqual(1);
// Go to first tab
await browser.switchTo().window(tabs[0]);
await browser.sleep(500);
expect(await utils.getNumberOfElements('video')).toEqual(3);
await utils.waitForElement('.OV_big');
expect(await utils.getNumberOfElements('.OV_big')).toEqual(1);
});
// it('should screensharing with audio muted', async () => {
// let isAudioEnabled;
// const getAudioScript = (className: string) => {
// return `return document.getElementsByClassName('${className}')[0].srcObject.getAudioTracks()[0].enabled;`;
// };
// await browser.get(`${url}&prejoin=false`);
// await utils.checkLayoutPresent();
// const micButton = await utils.waitForElement('#mic-btn');
// await micButton.click();
// // Clicking to screensharing button
// const screenshareButton = await utils.waitForElement('#screenshare-btn');
// expect(await utils.isPresent('#screenshare-btn')).toBeTrue();
// await screenshareButton.click();
// await utils.waitForElement('.screen-type');
// expect(await utils.getNumberOfElements('video')).toEqual(2);
// isAudioEnabled = await browser.executeScript(getAudioScript('screen-type'));
// expect(isAudioEnabled).toBeFalse();
// await utils.waitForElement('#status-mic');
// expect(await utils.getNumberOfElements('#status-mic')).toEqual(2);
// // Clicking to screensharing button
// await screenshareButton.click();
// expect(await utils.getNumberOfElements('video')).toEqual(1);
// });
// it('should show and hide CAMERA stream when muting video with screensharing', async () => {
// await browser.get(`${url}&prejoin=false`);
// await utils.checkLayoutPresent();
// // Clicking to screensharing button
// const screenshareButton = await utils.waitForElement('#screenshare-btn');
// expect(await screenshareButton.isDisplayed()).toBeTrue();
// await screenshareButton.click();
// await utils.waitForElement('.OV_big');
// expect(await utils.getNumberOfElements('video')).toEqual(2);
// const muteVideoButton = await utils.waitForElement('#camera-btn');
// await muteVideoButton.click();
// expect(await utils.getNumberOfElements('video')).toEqual(1);
// });
// it('should screenshare has audio active when camera is muted', async () => {
// let isAudioEnabled;
// const audioEnableScript = 'return document.getElementsByTagName("video")[0].srcObject.getAudioTracks()[0].enabled;';
// await browser.get(`${url}&prejoin=false`);
// await utils.checkLayoutPresent();
// // Clicking to screensharing button
// const screenshareButton = await utils.waitForElement('#screenshare-btn');
// expect(await utils.isPresent('#screenshare-btn')).toBeTrue();
// await screenshareButton.click();
// await utils.waitForElement('.OV_big');
// expect(await utils.getNumberOfElements('video')).toEqual(2);
// expect(await utils.getNumberOfElements('#status-mic')).toEqual(1);
// // Muting camera video
// const muteVideoButton = await utils.waitForElement('#camera-btn');
// await muteVideoButton.click();
// expect(await utils.getNumberOfElements('video')).toEqual(1);
// await browser.sleep(500);
// expect(await utils.isPresent('#status-mic')).toBeFalse();
// // Checking if audio is muted after join the room
// isAudioEnabled = await browser.executeScript(audioEnableScript);
// expect(isAudioEnabled).toBeTrue();
// // Unmuting camera
// await muteVideoButton.click();
// await browser.sleep(1000);
// await utils.waitForElement('.camera-type');
// expect(await utils.getNumberOfElements('video')).toEqual(2);
// expect(await utils.getNumberOfElements('#status-mic')).toEqual(1);
// });
// it('should camera come back with audio muted when screensharing', async () => {
// let element, isAudioEnabled;
// const getAudioScript = (className: string) => {
// return `return document.getElementsByClassName('${className}')[0].srcObject.getAudioTracks()[0].enabled;`;
// };
// await browser.get(`${url}&prejoin=false`);
// await utils.checkLayoutPresent();
// // Clicking to screensharing button
// const screenshareButton = await utils.waitForElement('#screenshare-btn');
// await screenshareButton.click();
// await utils.waitForElement('.screen-type');
// expect(await utils.getNumberOfElements('video')).toEqual(2);
// expect(await utils.getNumberOfElements('#status-mic')).toEqual(1);
// // Mute camera
// const muteVideoButton = await utils.waitForElement('#camera-btn');
// await muteVideoButton.click();
// expect(await utils.getNumberOfElements('video')).toEqual(1);
// expect(await utils.isPresent('#status-mic')).toBeFalse();
// // Checking if audio is muted after join the room
// isAudioEnabled = await browser.executeScript(getAudioScript('screen-type'));
// expect(isAudioEnabled).toBeTrue();
// // Mute audio
// const muteAudioButton = await utils.waitForElement('#mic-btn');
// await muteAudioButton.click();
// await utils.waitForElement('#status-mic');
// expect(await utils.getNumberOfElements('#status-mic')).toEqual(1);
// isAudioEnabled = await browser.executeScript(getAudioScript('screen-type'));
// expect(isAudioEnabled).toBeFalse();
// // Unmute camera
// await muteVideoButton.click();
// await utils.waitForElement('.camera-type');
// expect(await utils.getNumberOfElements('video')).toEqual(2);
// expect(await utils.getNumberOfElements('#status-mic')).toEqual(2);
// isAudioEnabled = await browser.executeScript(getAudioScript('camera-type'));
// expect(isAudioEnabled).toBeFalse();
// });
});

View File

@ -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,110 +1,110 @@
{
"dependencies": {
"@angular/animations": "18.2.5",
"@angular/cdk": "18.2.5",
"@angular/common": "18.2.5",
"@angular/core": "18.2.5",
"@angular/forms": "18.2.5",
"@angular/material": "18.2.5",
"@angular/platform-browser": "18.2.5",
"@angular/platform-browser-dynamic": "18.2.5",
"@angular/router": "18.2.5",
"@livekit/track-processors": "0.3.2",
"autolinker": "4.0.0",
"livekit-client": "2.5.2",
"rxjs": "7.8.1",
"tslib": "2.7.0",
"zone.js": "^0.14.6"
},
"devDependencies": {
"@angular-devkit/build-angular": "18.2.5",
"@angular/cli": "18.2.5",
"@angular/compiler": "18.2.5",
"@angular/compiler-cli": "18.2.5",
"@angular/elements": "18.2.5",
"@compodoc/compodoc": "^1.1.25",
"@types/dom-mediacapture-transform": "0.1.9",
"@types/dom-webcodecs": "0.1.11",
"@types/jasmine": "^5.1.4",
"@types/node": "20.12.14",
"@types/selenium-webdriver": "4.1.16",
"@types/ws": "^8.5.12",
"chromedriver": "129.0.0",
"concat": "^1.0.3",
"cross-env": "^7.0.3",
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-prettier": "^5.2.1",
"http-server": "14.1.1",
"husky": "^9.1.6",
"jasmine": "^5.3.1",
"jasmine-core": "5.3.0",
"jasmine-spec-reporter": "7.0.0",
"karma": "^6.4.4",
"karma-chrome-launcher": "3.2.0",
"karma-coverage": "^2.2.1",
"karma-coverage-istanbul-reporter": "3.0.3",
"karma-jasmine": "5.1.0",
"karma-jasmine-html-reporter": "2.1.0",
"karma-junit-reporter": "2.0.1",
"karma-mocha-reporter": "2.2.5",
"karma-notify-reporter": "1.3.0",
"lint-staged": "^15.2.10",
"ng-packagr": "18.2.1",
"npm-watch": "^0.13.0",
"prettier": "3.3.3",
"selenium-webdriver": "4.25.0",
"ts-node": "10.9.2",
"tslint": "6.1.3",
"typescript": "5.4.5",
"webpack-bundle-analyzer": "^4.10.2"
},
"name": "openvidu-components-testapp",
"private": true,
"watch": {
"doc:serve": {
"patterns": [
"projects",
"src"
],
"extensions": "ts,html,scss,css,md",
"quiet": false
}
},
"scripts": {
"start": "ng serve --configuration development --open",
"start-prod": "npx http-server ./dist/openvidu-components-testapp/browser --port 4200",
"start:ssl": "ng serve --ssl --configuration development --host 0.0.0.0 --port 5080",
"build": "ng build openvidu-components-testapp --configuration production",
"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:generate-directives-tutorials": "node ./projects/openvidu-components-angular/doc/scripts/generate-directive-tutorials.js",
"doc:generate-directive-tables": "node ./projects/openvidu-components-angular/doc/scripts/generate-directive-tables.js",
"doc:clean-copy": "rm -rf ../../openvidu.io/docs/docs/reference-docs/openvidu-components-angular && cp -r ./docs/openvidu-components-angular/ ../../openvidu.io/docs/docs/reference-docs/openvidu-components-angular",
"doc:serve": "npx compodoc -c ../openvidu-components-angular/projects/openvidu-components-angular/doc/.compodocrc.json --serve --port 7000",
"doc:serve-watch": "npm-watch doc:serve",
"lib:serve": "ng build openvidu-components-angular --watch",
"lib:build": "ng build openvidu-components-angular --configuration production && cd ./dist/openvidu-components-angular",
"lib:pack": "cd ./dist/openvidu-components-angular && npm pack",
"lib:copy": "cp dist/openvidu-components-angular/openvidu-components-angular-*.tgz ../../openvidu-call/openvidu-call-front",
"lib:test": "ng test openvidu-components-angular --no-watch --code-coverage",
"e2e:nested-all": "tsc --project ./e2e && npx jasmine --fail-fast ./e2e/dist/nested-components/*.test.js",
"e2e:nested-events": "tsc --project ./e2e && npx jasmine --fail-fast ./e2e/dist/nested-components/events.test.js",
"e2e:nested-directives": "tsc --project ./e2e && npx jasmine --fail-fast ./e2e/dist/nested-components/directives.test.js",
"e2e:webcomponent-all": "tsc --project ./e2e && npx jasmine --fail-fast ./e2e/dist/webcomponent-e2e/**/*.test.js",
"e2e:webcomponent-directives": "tsc --project ./e2e && npx jasmine --fail-fast ./e2e/dist/webcomponent-e2e/api-directives.test.js",
"e2e:webcomponent-captions": "tsc --project ./e2e && npx jasmine --fail-fast ./e2e/dist/webcomponent-e2e/captions.test.js",
"e2e:webcomponent-chat": "tsc --project ./e2e && npx jasmine --fail-fast ./e2e/dist/webcomponent-e2e/chat.test.js",
"e2e:webcomponent-events": "tsc --project ./e2e && npx jasmine --fail-fast ./e2e/dist/webcomponent-e2e/events.test.js",
"e2e:webcomponent-media-devices": "tsc --project ./e2e && npx jasmine --fail-fast ./e2e/dist/webcomponent-e2e/media-devices.test.js",
"e2e:webcomponent-panels": "tsc --project ./e2e && npx jasmine --fail-fast ./e2e/dist/webcomponent-e2e/panels.test.js",
"e2e:webcomponent-screensharing": "tsc --project ./e2e && npx jasmine --fail-fast ./e2e/dist/webcomponent-e2e/screensharing.test.js",
"e2e:webcomponent-stream": "tsc --project ./e2e && npx jasmine --fail-fast ./e2e/dist/webcomponent-e2e/stream.test.js",
"e2e:webcomponent-toolbar": "tsc --project ./e2e && npx jasmine --fail-fast ./e2e/dist/webcomponent-e2e/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",
"husky": "cd .. && husky install"
},
"version": "3.0.1-beta1"
"dependencies": {
"@angular/animations": "20.3.15",
"@angular/cdk": "20.2.14",
"@angular/common": "20.3.15",
"@angular/core": "20.3.15",
"@angular/forms": "20.3.15",
"@angular/material": "20.2.14",
"@angular/platform-browser": "20.3.15",
"@angular/platform-browser-dynamic": "20.3.15",
"@angular/router": "20.3.15",
"@livekit/track-processors": "0.7.2",
"@types/dom-mediacapture-transform": "0.1.11",
"autolinker": "4.1.5",
"livekit-client": "2.16.1",
"rxjs": "7.8.2",
"tslib": "2.8.1",
"zone.js": "0.15.1"
},
"devDependencies": {
"@angular-devkit/build-angular": "20.3.13",
"@angular/cli": "20.3.13",
"@angular/compiler": "20.3.15",
"@angular/compiler-cli": "20.3.15",
"@compodoc/compodoc": "1.1.32",
"@types/jasmine": "5.1.13",
"@types/node": "22.19.3",
"@types/pngjs": "6.0.5",
"@types/selenium-webdriver": "4.35.4",
"@types/ws": "8.18.1",
"chromedriver": "143.0.1",
"concat": "1.0.3",
"cpx": "1.5.0",
"cross-env": "7.0.3",
"eslint-config-prettier": "9.1.2",
"eslint-plugin-prettier": "5.2.6",
"http-server": "14.1.1",
"husky": "9.1.7",
"jasmine": "5.13.0",
"jasmine-core": "5.13.0",
"jasmine-spec-reporter": "7.0.0",
"karma": "6.4.4",
"karma-chrome-launcher": "3.2.0",
"karma-coverage": "2.2.1",
"karma-coverage-istanbul-reporter": "3.0.3",
"karma-jasmine": "5.1.0",
"karma-jasmine-html-reporter": "2.1.0",
"karma-junit-reporter": "2.0.1",
"karma-mocha-reporter": "2.2.5",
"karma-notify-reporter": "1.3.0",
"lint-staged": "15.5.2",
"ng-packagr": "20.3.2",
"npm-watch": "0.13.0",
"pixelmatch": "7.1.0",
"pngjs": "7.0.0",
"prettier": "3.7.4",
"rimraf": "6.1.2",
"selenium-webdriver": "4.39.0",
"ts-node": "10.9.2",
"tslint": "6.1.3",
"typescript": "5.8.3",
"webpack-bundle-analyzer": "4.10.2"
},
"name": "openvidu-components-testapp",
"private": true,
"watch": {
"doc:serve": {
"patterns": [
"projects",
"src"
],
"extensions": "ts,html,scss,css,md",
"quiet": false
}
},
"scripts": {
"start": "ng serve --configuration development --open",
"start-prod": "npx http-server ./dist/openvidu-components-testapp/browser --port 4200",
"start:ssl": "ng serve --ssl --configuration development --host 0.0.0.0 --port 5080",
"build": "ng build openvidu-components-testapp --configuration production",
"doc:build": "npx compodoc -c ./projects/openvidu-components-angular/doc/.compodocrc.json",
"doc:generate-directives-tutorials": "node ./projects/openvidu-components-angular/doc/scripts/generate-directive-tutorials.js",
"doc:generate-directive-tables": "node ./projects/openvidu-components-angular/doc/scripts/generate-directive-tables.js",
"doc:clean-copy": "rm -rf ../../openvidu.io/docs/docs/reference-docs/openvidu-components-angular && cp -r ./docs/openvidu-components-angular/ ../../openvidu.io/docs/docs/reference-docs/openvidu-components-angular",
"doc:serve": "npx compodoc -c ../openvidu-components-angular/projects/openvidu-components-angular/doc/.compodocrc.json --serve --port 7000",
"doc:serve-watch": "npm-watch doc:serve",
"lib:serve": "ng build openvidu-components-angular --watch",
"lib:build": "ng build openvidu-components-angular --configuration production && rimraf dist/openvidu-components-angular && cpx \"projects/openvidu-components-angular/dist/**/*\" dist/openvidu-components-angular",
"lib:pack": "cd ./dist/openvidu-components-angular && npm pack",
"lib:copy": "cp dist/openvidu-components-angular/openvidu-components-angular-*.tgz ../../openvidu-call/frontend",
"lib:test": "ng test openvidu-components-angular --no-watch --code-coverage",
"e2e:nested-all": "tsc --project ./e2e && npx jasmine --fail-fast ./e2e/dist/nested-components/*.test.js",
"e2e:nested-events": "tsc --project ./e2e && npx jasmine --fail-fast ./e2e/dist/nested-components/events.test.js",
"e2e:nested-structural-directives": "tsc --project ./e2e && npx jasmine --fail-fast ./e2e/dist/nested-components/structural-directives.test.js",
"e2e:nested-attribute-directives": "tsc --project ./e2e && npx jasmine --fail-fast ./e2e/dist/nested-components/attribute-directives.test.js",
"e2e:lib-directives": "tsc --project ./e2e && npx jasmine --fail-fast ./e2e/dist/api-directives.test.js",
"e2e:lib-internal-directives": "tsc --project ./e2e && npx jasmine --fail-fast ./e2e/dist/internal-directives.test.js",
"e2e:lib-chat": "tsc --project ./e2e && npx jasmine --fail-fast ./e2e/dist/chat.test.js",
"e2e:lib-events": "tsc --project ./e2e && npx jasmine ./e2e/dist/events.test.js",
"e2e:lib-media-devices": "tsc --project ./e2e && npx jasmine --fail-fast ./e2e/dist/media-devices.test.js",
"e2e:lib-panels": "tsc --project ./e2e && npx jasmine --fail-fast ./e2e/dist/panels.test.js",
"e2e:lib-screensharing": "tsc --project ./e2e && npx jasmine --fail-fast ./e2e/dist/screensharing.test.js",
"e2e:lib-stream": "tsc --project ./e2e && npx jasmine --fail-fast ./e2e/dist/stream.test.js",
"e2e:lib-toolbar": "tsc --project ./e2e && npx jasmine --fail-fast ./e2e/dist/toolbar.test.js",
"e2e:lib-virtual-backgrounds": "tsc --project ./e2e && npx jasmine --fail-fast ./e2e/dist/virtual-backgrounds.test.js",
"simulate:multiparty": "livekit-cli load-test --url ws://localhost:7880 --api-key devkey --api-secret secret --room daily-call --publishers 8 --audio-publishers 8 --identity-prefix Participant --identity publisher",
"husky": "cd .. && husky install"
},
"version": "3.6.0"
}

View File

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

View File

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

View File

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

View File

@ -1,418 +0,0 @@
{
"name": "openvidu-components-angular",
"version": "3.0.1-beta1",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "openvidu-components-angular",
"version": "3.0.1-beta1",
"dependencies": {
"tslib": "^2.3.0"
},
"peerDependencies": {
"@angular/animations": "^17.0.0 || ^18.0.0",
"@angular/cdk": "^17.0.0 || ^18.0.0",
"@angular/common": "^17.0.0 || ^18.0.0",
"@angular/core": "^17.0.0 || ^18.0.0",
"@angular/forms": "^17.0.0 || ^18.0.0",
"@angular/material": "^17.0.0 || ^18.0.0",
"@livekit/track-processors": "^0.3.2",
"autolinker": "^4.0.0",
"buffer": "^6.0.3",
"livekit-client": "^2.1.0"
}
},
"node_modules/@angular/animations": {
"version": "18.2.8",
"resolved": "https://registry.npmjs.org/@angular/animations/-/animations-18.2.8.tgz",
"integrity": "sha512-dMSn2hg70siv3lhP+vqhMbgc923xw6XBUvnpCPEzhZqFHvPXfh/LubmsD5RtqHmjWebXtgVcgS+zg3Gq3jB2lg==",
"license": "MIT",
"peer": true,
"dependencies": {
"tslib": "^2.3.0"
},
"engines": {
"node": "^18.19.1 || ^20.11.1 || >=22.0.0"
},
"peerDependencies": {
"@angular/core": "18.2.8"
}
},
"node_modules/@angular/cdk": {
"version": "18.2.8",
"resolved": "https://registry.npmjs.org/@angular/cdk/-/cdk-18.2.8.tgz",
"integrity": "sha512-J8A2FkwTBzLleAEWz6EgW73dEoeq87GREBPjTv8+2JV09LX+V3hnbgNk6zWq5k4OXtQNg9WrWP9QyRbUyA597g==",
"license": "MIT",
"peer": true,
"dependencies": {
"tslib": "^2.3.0"
},
"optionalDependencies": {
"parse5": "^7.1.2"
},
"peerDependencies": {
"@angular/common": "^18.0.0 || ^19.0.0",
"@angular/core": "^18.0.0 || ^19.0.0",
"rxjs": "^6.5.3 || ^7.4.0"
}
},
"node_modules/@angular/common": {
"version": "18.2.8",
"resolved": "https://registry.npmjs.org/@angular/common/-/common-18.2.8.tgz",
"integrity": "sha512-TYsKtE5nVaIScWSLGSO34Skc+s3hB/BujSddnfQHoNFvPT/WR0dfmdlpVCTeLj+f50htFoMhW11tW99PbK+whQ==",
"license": "MIT",
"peer": true,
"dependencies": {
"tslib": "^2.3.0"
},
"engines": {
"node": "^18.19.1 || ^20.11.1 || >=22.0.0"
},
"peerDependencies": {
"@angular/core": "18.2.8",
"rxjs": "^6.5.3 || ^7.4.0"
}
},
"node_modules/@angular/core": {
"version": "18.2.8",
"resolved": "https://registry.npmjs.org/@angular/core/-/core-18.2.8.tgz",
"integrity": "sha512-NwIuX/Iby1jT6Iv1/s6S3wOFf8xfuQR3MPGvKhGgNtjXLbHG+TXceK9+QPZC0s9/Z8JR/hz+li34B79GrIKgUg==",
"license": "MIT",
"peer": true,
"dependencies": {
"tslib": "^2.3.0"
},
"engines": {
"node": "^18.19.1 || ^20.11.1 || >=22.0.0"
},
"peerDependencies": {
"rxjs": "^6.5.3 || ^7.4.0",
"zone.js": "~0.14.10"
}
},
"node_modules/@angular/forms": {
"version": "18.2.8",
"resolved": "https://registry.npmjs.org/@angular/forms/-/forms-18.2.8.tgz",
"integrity": "sha512-JCLki7KC6D5vF6dE6yGlBmW33khIgpHs8N9SzuiJtkQqNDTIQA8cPsGV6qpLpxflxASynQOX5lDkWYdQyfm77Q==",
"license": "MIT",
"peer": true,
"dependencies": {
"tslib": "^2.3.0"
},
"engines": {
"node": "^18.19.1 || ^20.11.1 || >=22.0.0"
},
"peerDependencies": {
"@angular/common": "18.2.8",
"@angular/core": "18.2.8",
"@angular/platform-browser": "18.2.8",
"rxjs": "^6.5.3 || ^7.4.0"
}
},
"node_modules/@angular/material": {
"version": "18.2.8",
"resolved": "https://registry.npmjs.org/@angular/material/-/material-18.2.8.tgz",
"integrity": "sha512-wQGMVsfQ9lQfih2VsWAvV4z3S3uBxrxc61owlE+K0T1BxH9u/jo3A/rnRitIdvR/L4NnYlfhCnmrW9K+Pl+WCg==",
"license": "MIT",
"peer": true,
"dependencies": {
"tslib": "^2.3.0"
},
"peerDependencies": {
"@angular/animations": "^18.0.0 || ^19.0.0",
"@angular/cdk": "18.2.8",
"@angular/common": "^18.0.0 || ^19.0.0",
"@angular/core": "^18.0.0 || ^19.0.0",
"@angular/forms": "^18.0.0 || ^19.0.0",
"@angular/platform-browser": "^18.0.0 || ^19.0.0",
"rxjs": "^6.5.3 || ^7.4.0"
}
},
"node_modules/@angular/platform-browser": {
"version": "18.2.8",
"resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-18.2.8.tgz",
"integrity": "sha512-EPai4ZPqSq3ilLJUC85kPi9wo5j5suQovwtgRyjM/75D9Qy4TV19g8hkVM5Co/zrltO8a2G6vDscCNI5BeGw2A==",
"license": "MIT",
"peer": true,
"dependencies": {
"tslib": "^2.3.0"
},
"engines": {
"node": "^18.19.1 || ^20.11.1 || >=22.0.0"
},
"peerDependencies": {
"@angular/animations": "18.2.8",
"@angular/common": "18.2.8",
"@angular/core": "18.2.8"
},
"peerDependenciesMeta": {
"@angular/animations": {
"optional": true
}
}
},
"node_modules/@bufbuild/protobuf": {
"version": "1.10.0",
"resolved": "https://registry.npmjs.org/@bufbuild/protobuf/-/protobuf-1.10.0.tgz",
"integrity": "sha512-QDdVFLoN93Zjg36NoQPZfsVH9tZew7wKDKyV5qRdj8ntT4wQCOradQjRaTdwMhWUYsgKsvCINKKm87FdEk96Ag==",
"license": "(Apache-2.0 AND BSD-3-Clause)",
"peer": true
},
"node_modules/@livekit/protocol": {
"version": "1.24.0",
"resolved": "https://registry.npmjs.org/@livekit/protocol/-/protocol-1.24.0.tgz",
"integrity": "sha512-9dCsqnkMn7lvbI4NGh18zhLDsrXyUcpS++TEFgEk5Xv1WM3R2kT3EzqgL1P/mr3jaabM6rJ8wZA/KJLuQNpF5w==",
"license": "Apache-2.0",
"peer": true,
"dependencies": {
"@bufbuild/protobuf": "^1.10.0"
}
},
"node_modules/@livekit/track-processors": {
"version": "0.3.2",
"resolved": "https://registry.npmjs.org/@livekit/track-processors/-/track-processors-0.3.2.tgz",
"integrity": "sha512-4JUCzb7yIKoVsTo8J6FTzLZJHcI6DihfX/pGRDg0SOGaxprcDPrt8jaDBBTsnGBSXHeMxl2ugN+xQjdCWzLKEA==",
"license": "Apache-2.0",
"peer": true,
"dependencies": {
"@mediapipe/holistic": "0.5.1675471629",
"@mediapipe/tasks-vision": "0.10.9"
},
"peerDependencies": {
"livekit-client": "^1.12.0 || ^2.1.0"
}
},
"node_modules/@mediapipe/holistic": {
"version": "0.5.1675471629",
"resolved": "https://registry.npmjs.org/@mediapipe/holistic/-/holistic-0.5.1675471629.tgz",
"integrity": "sha512-qY+cxtDeSOvVtevrLgnodiwXYaAtPi7dHZtNv/bUCGEjFicAOYtMmrZSqMmbPkTB2+4jLnPF1vgshkAqQRSYAw==",
"license": "Apache-2.0",
"peer": true
},
"node_modules/@mediapipe/tasks-vision": {
"version": "0.10.9",
"resolved": "https://registry.npmjs.org/@mediapipe/tasks-vision/-/tasks-vision-0.10.9.tgz",
"integrity": "sha512-/gFguyJm1ng4Qr7VVH2vKO+zZcQd8wc3YafUfvBuYFX0Y5+CvrV+VNPEVkl5W/gUZF5KNKNZAiaHPULGPCIjyQ==",
"license": "Apache-2.0",
"peer": true
},
"node_modules/autolinker": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/autolinker/-/autolinker-4.0.0.tgz",
"integrity": "sha512-fl5Kh6BmEEZx+IWBfEirnRUU5+cOiV0OK7PEt0RBKvJMJ8GaRseIOeDU3FKf4j3CE5HVefcjHmhYPOcaVt0bZw==",
"license": "MIT",
"peer": true,
"dependencies": {
"tslib": "^2.3.0"
}
},
"node_modules/base64-js": {
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
"integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
],
"license": "MIT",
"peer": true
},
"node_modules/buffer": {
"version": "6.0.3",
"resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz",
"integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
],
"license": "MIT",
"peer": true,
"dependencies": {
"base64-js": "^1.3.1",
"ieee754": "^1.2.1"
}
},
"node_modules/entities": {
"version": "4.5.0",
"resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz",
"integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==",
"license": "BSD-2-Clause",
"optional": true,
"peer": true,
"engines": {
"node": ">=0.12"
},
"funding": {
"url": "https://github.com/fb55/entities?sponsor=1"
}
},
"node_modules/events": {
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz",
"integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">=0.8.x"
}
},
"node_modules/ieee754": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
"integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
],
"license": "BSD-3-Clause",
"peer": true
},
"node_modules/livekit-client": {
"version": "2.5.9",
"resolved": "https://registry.npmjs.org/livekit-client/-/livekit-client-2.5.9.tgz",
"integrity": "sha512-oDpK6SKYB1F+mNO+25DA0bF0cD2XoOJeD8ji4YQpzDBQv2IxeyKrQhoqXAqrYgIKuiMNkImSf+yg2v7EHSl4Og==",
"license": "Apache-2.0",
"peer": true,
"dependencies": {
"@livekit/protocol": "1.24.0",
"events": "^3.3.0",
"loglevel": "^1.8.0",
"sdp-transform": "^2.14.1",
"ts-debounce": "^4.0.0",
"tslib": "2.7.0",
"typed-emitter": "^2.1.0",
"webrtc-adapter": "^9.0.0"
}
},
"node_modules/loglevel": {
"version": "1.9.2",
"resolved": "https://registry.npmjs.org/loglevel/-/loglevel-1.9.2.tgz",
"integrity": "sha512-HgMmCqIJSAKqo68l0rS2AanEWfkxaZ5wNiEFb5ggm08lDs9Xl2KxBlX3PTcaD2chBM1gXAYf491/M2Rv8Jwayg==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">= 0.6.0"
},
"funding": {
"type": "tidelift",
"url": "https://tidelift.com/funding/github/npm/loglevel"
}
},
"node_modules/parse5": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/parse5/-/parse5-7.2.0.tgz",
"integrity": "sha512-ZkDsAOcxsUMZ4Lz5fVciOehNcJ+Gb8gTzcA4yl3wnc273BAybYWrQ+Ks/OjCjSEpjvQkDSeZbybK9qj2VHHdGA==",
"license": "MIT",
"optional": true,
"peer": true,
"dependencies": {
"entities": "^4.5.0"
},
"funding": {
"url": "https://github.com/inikulin/parse5?sponsor=1"
}
},
"node_modules/rxjs": {
"version": "7.8.1",
"resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz",
"integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==",
"license": "Apache-2.0",
"peer": true,
"dependencies": {
"tslib": "^2.1.0"
}
},
"node_modules/sdp": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/sdp/-/sdp-3.2.0.tgz",
"integrity": "sha512-d7wDPgDV3DDiqulJjKiV2865wKsJ34YI+NDREbm+FySq6WuKOikwyNQcm+doLAZ1O6ltdO0SeKle2xMpN3Brgw==",
"license": "MIT",
"peer": true
},
"node_modules/sdp-transform": {
"version": "2.14.2",
"resolved": "https://registry.npmjs.org/sdp-transform/-/sdp-transform-2.14.2.tgz",
"integrity": "sha512-icY6jVao7MfKCieyo1AyxFYm1baiM+fA00qW/KrNNVlkxHAd34riEKuEkUe4bBb3gJwLJZM+xT60Yj1QL8rHiA==",
"license": "MIT",
"peer": true,
"bin": {
"sdp-verify": "checker.js"
}
},
"node_modules/ts-debounce": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/ts-debounce/-/ts-debounce-4.0.0.tgz",
"integrity": "sha512-+1iDGY6NmOGidq7i7xZGA4cm8DAa6fqdYcvO5Z6yBevH++Bdo9Qt/mN0TzHUgcCcKv1gmh9+W5dHqz8pMWbCbg==",
"license": "MIT",
"peer": true
},
"node_modules/tslib": {
"version": "2.7.0",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.7.0.tgz",
"integrity": "sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA==",
"license": "0BSD"
},
"node_modules/typed-emitter": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/typed-emitter/-/typed-emitter-2.1.0.tgz",
"integrity": "sha512-g/KzbYKbH5C2vPkaXGu8DJlHrGKHLsM25Zg9WuC9pMGfuvT+X25tZQWo5fK1BjBm8+UrVE9LDCvaY0CQk+fXDA==",
"license": "MIT",
"peer": true,
"optionalDependencies": {
"rxjs": "*"
}
},
"node_modules/webrtc-adapter": {
"version": "9.0.1",
"resolved": "https://registry.npmjs.org/webrtc-adapter/-/webrtc-adapter-9.0.1.tgz",
"integrity": "sha512-1AQO+d4ElfVSXyzNVTOewgGT/tAomwwztX/6e3totvyyzXPvXIIuUUjAmyZGbKBKbZOXauuJooZm3g6IuFuiNQ==",
"license": "BSD-3-Clause",
"peer": true,
"dependencies": {
"sdp": "^3.2.0"
},
"engines": {
"node": ">=6.0.0",
"npm": ">=3.10.0"
}
},
"node_modules/zone.js": {
"version": "0.14.10",
"resolved": "https://registry.npmjs.org/zone.js/-/zone.js-0.14.10.tgz",
"integrity": "sha512-YGAhaO7J5ywOXW6InXNlLmfU194F8lVgu7bRntUF3TiG8Y3nBK0x1UJJuHUP/e8IyihkjCYqhCScpSwnlaSRkQ==",
"license": "MIT",
"peer": true
}
}
}

View File

@ -1,19 +1,22 @@
{
"name": "openvidu-components-angular",
"main": "dist/fesm2022/openvidu-components-angular.mjs",
"module": "dist/fesm2022/openvidu-components-angular.mjs",
"typings": "dist/index.d.ts",
"dependencies": {
"tslib": "^2.3.0"
},
"name": "openvidu-components-angular",
"peerDependencies": {
"@angular/animations": "^17.0.0 || ^18.0.0",
"@angular/cdk": "^17.0.0 || ^18.0.0",
"@angular/common": "^17.0.0 || ^18.0.0",
"@angular/core": "^17.0.0 || ^18.0.0",
"@angular/forms": "^17.0.0 || ^18.0.0",
"@angular/material": "^17.0.0 || ^18.0.0",
"@angular/animations": "^17.0.0 || ^18.0.0 || ^19.0.0 || ^20.0.0",
"@angular/cdk": "^17.0.0 || ^18.0.0 || ^19.0.0 || ^20.0.0",
"@angular/common": "^17.0.0 || ^18.0.0 || ^19.0.0 || ^20.0.0",
"@angular/core": "^17.0.0 || ^18.0.0 || ^19.0.0 || ^20.0.0",
"@angular/forms": "^17.0.0 || ^18.0.0 || ^19.0.0 || ^20.0.0",
"@angular/material": "^17.0.0 || ^18.0.0 || ^19.0.0 || ^20.0.0",
"autolinker": "^4.0.0",
"buffer": "^6.0.3",
"livekit-client": "^2.1.0",
"@livekit/track-processors": "^0.3.2"
"livekit-client": "^2.16.0",
"@livekit/track-processors": "^0.7.2"
},
"version": "3.0.1-beta1"
"version": "3.6.0"
}

View File

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

View File

@ -4,7 +4,7 @@
.header {
height: 50px;
background-color: var(--ov-secondary-action-color);
background-color: var(--ov-primary-action-color);
color: var(--ov-text-primary-color);
}
@ -42,7 +42,7 @@
#sort-menu-btn {
margin-left: 5px;
background-color: var(--ov-surface-color);
color: var(--ov-text-primary-color);
color: var(--ov-text-surface-color);
}
.search-bar {
@ -140,7 +140,7 @@
}
.video-btns button #play {
color: var(--ov-text-primary-color);
color: var(--ov-primary-action-color);
}
.video-btns button #download {
color: var(--ov-accent-action-color);
@ -167,7 +167,7 @@
.video-card-tag {
font-size: 13px;
color: var(--ov-text-primary-color);
color: var(--ov-text-surface-color);
font-weight: bold;
}
@ -236,13 +236,11 @@
text-align: center;
color: var(--ov-text-primary-color);
}
/* TODO(mdc-migration): The following rule targets internal classes of form-field that may no longer apply for the MDC version. */
::ng-deep .mat-form-field-appearance-fill .mat-form-field-flex {
padding: 0px !important;
background-color: var(--ov-light-color) !important;
}
/* TODO(mdc-migration): The following rule targets internal classes of form-field that may no longer apply for the MDC version. */
::ng-deep .mat-form-field-wrapper {
height: 100% !important;
}

View File

@ -8,7 +8,8 @@ import { RecordingService } from '../../services/recording/recording.service';
@Component({
selector: 'ov-admin-dashboard',
templateUrl: './admin-dashboard.component.html',
styleUrls: ['./admin-dashboard.component.scss']
styleUrls: ['./admin-dashboard.component.scss'],
standalone: false
})
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** |
|:--------------------------------: | :-------: | :---------------------------------------------: |
| **error** | `any` | [AdminLoginErrorDirective](../directives/AdminLoginErrorDirective.html) |
| **navbarTitle** | `any` | [AdminLoginTitleDirective](../directives/AdminLoginTitleDirective.html) |
<!-- end-dynamic-api-directives-content -->

View File

@ -19,7 +19,6 @@
max-width: 300px;
min-width: 300px;
border-radius: 10px;
box-shadow: 0px 0px 10px rgba(0, 0, 0, 0.1);
}
.form-btn {
@ -33,5 +32,5 @@
}
::ng-deep .mat-mdc-progress-spinner {
--mdc-circular-progress-active-indicator-color: var(--ov-accent-action-color);
--mat-progress-spinner-active-indicator-color: var(--ov-accent-action-color);
}

View File

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

View File

@ -38,7 +38,7 @@
margin: auto;
height: 80%;
width: 3px;
background: var(--ov-secondary-action-color);
background: var(--ov-text-primary-color);
border-radius: 8px;
}

View File

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

View File

@ -1,31 +0,0 @@
.poster {
height: 100%;
width: 100%;
background-color: #000000;
position: absolute;
z-index: 888;
border-radius: var(--ov-video-radius);
}
.initial {
position: absolute;
display: inline-grid;
z-index: 1;
margin: auto;
bottom: 0;
right: 0;
left: 0;
top: 0;
height: 70px;
width: 70px;
border-radius: var(--ov-video-radius);
border: 2px solid var(--ov-text-primary-color);
color: #000000;
}
#poster-text {
padding: 0px !important;
font-weight: bold;
font-size: 40px;
margin: auto;
}

View File

@ -1,28 +0,0 @@
import { Component, Input } from '@angular/core';
/**
* @internal
*/
@Component({
selector: 'ov-avatar-profile',
template: `
<div class="poster" id="video-poster">
<div class="initial" [ngStyle]="{ 'background-color': color }">
<span id="poster-text">{{ letter }}</span>
</div>
</div>
`,
styleUrls: ['./avatar-profile.component.scss']
})
export class AvatarProfileComponent {
letter: string;
@Input()
set name(name: string) {
if (name) this.letter = name[0];
}
@Input() color;
constructor() {}
}

View File

@ -43,7 +43,7 @@ mat-spinner {
}
::ng-deep .mat-mdc-progress-spinner {
--mdc-circular-progress-active-indicator-color: var(--ov-accent-action-color);
--mat-progress-spinner-active-indicator-color: var(--ov-accent-action-color);
}
/*

View File

@ -5,8 +5,8 @@ import { MatDialogRef } from '@angular/material/dialog';
* @internal
*/
@Component({
selector: 'app-delete-dialog',
template: `
selector: 'app-delete-dialog',
template: `
<div mat-dialog-content>{{ 'PANEL.RECORDING.DELETE_QUESTION' | translate }}</div>
<div mat-dialog-actions>
<button mat-button [disableRipple]="true" (click)="close()">{{ 'PANEL.RECORDING.CANCEL' | translate }}</button>
@ -15,8 +15,8 @@ import { MatDialogRef } from '@angular/material/dialog';
</button>
</div>
`,
styles: [
`
styles: [
`
::ng-deep .mat-mdc-dialog-content {
color: var(--ov-text-surface-color) !important;
}
@ -26,17 +26,18 @@ import { MatDialogRef } from '@angular/material/dialog';
}
#delete-recording-confirm-btn {
background-color: var(--ov-error-color) !important;
color: var(--ov-secondary-action-color);
color: var(--ov-primary-action-color);
}
.mat-mdc-button,
.mat-mdc-button:not(:disabled),
::ng-deep .mat-mdc-button .mat-mdc-button-persistent-ripple::before {
color: var(--ov-secondary-action-color);
color: var(--ov-text-primary-color) !important;
background-color: var(--ov-primary-action-color) !important;
border-radius: var(--ov-surface-radius);
}
`
]
],
standalone: false
})
export class DeleteDialogComponent {
constructor(public dialogRef: MatDialogRef<DeleteDialogComponent>) {}

View File

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

View File

@ -8,8 +8,8 @@ import { DialogData } from '../../models/dialog.model';
*/
@Component({
selector: 'ov-pro-feature-template',
template: `
selector: 'ov-pro-feature-template',
template: `
<h1 mat-dialog-title>{{ data.title }}</h1>
<div mat-dialog-content>{{ data.description }}</div>
<div mat-dialog-actions *ngIf="data.showActionButtons">
@ -19,7 +19,8 @@ import { DialogData } from '../../models/dialog.model';
</button>
<button mat-button (click)="close()">{{'PANEL.CLOSE' | translate}}</button>
</div>
`
`,
standalone: false
})
export class ProFeatureDialogTemplateComponent {
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
*/
@Component({
selector: 'app-recording-dialog',
template: `
selector: 'app-recording-dialog',
template: `
<div mat-dialog-content>
<video #videoElement controls autoplay [src]="src" (error)="handleError()"></video>
</div>
@ -15,8 +15,8 @@ import { RecordingDialogData } from '../../models/dialog.model';
<button mat-button [disableRipple]="true" (click)="close()">{{ 'PANEL.CLOSE' | translate }}</button>
</div>
`,
styles: [
`
styles: [
`
::ng-deep .mat-mdc-dialog-content {
color: var(--ov-text-surface-color) !important;
}
@ -33,12 +33,13 @@ import { RecordingDialogData } from '../../models/dialog.model';
.mat-mdc-button,
.mat-mdc-button:not(:disabled),
::ng-deep .mat-mdc-button .mat-mdc-button-persistent-ripple::before {
color: var(--ov-secondary-action-color);
color: var(--ov-text-primary-color);
background-color: var(--ov-primary-action-color) !important;
border-radius: var(--ov-surface-radius);
}
`
]
],
standalone: false
})
export class RecordingDialogComponent {
@ViewChild('videoElement', { static: true }) videoElement: ElementRef<HTMLVideoElement>;

View File

@ -0,0 +1,7 @@
<!-- Landscape orientation warning for mobile devices -->
<div id="landscape-warning" [@inOutAnimation]>
<div class="warning-message">
<mat-icon class="warning-icon">screen_rotation</mat-icon>
<span>{{ 'ROOM.LANDSCAPE_WARNING' | translate }}</span>
</div>
</div>

View File

@ -0,0 +1,33 @@
#landscape-warning {
width: 100%;
height: 100%;
background-color: var(--ov-background-color);
opacity: 95%;
align-content: space-evenly;
text-align: center;
color: var(--ov-text-primary-color);
.warning-message {
display: inline-grid;
display: -moz-inline-grid;
place-items: center;
}
mat-icon {
width: 50px;
height: 50px;
font-size: 50px;
margin: auto;
margin-bottom: 10px;
animation: boomerang 1.2s ease-in-out infinite alternate;
@keyframes boomerang {
from {
transform: rotate(0deg);
}
to {
transform: rotate(45deg);
}
}
}
}

View File

@ -0,0 +1,20 @@
import { animate, style, transition, trigger } from '@angular/animations';
import { Component } from '@angular/core';
/**
* Component to display a landscape orientation warning on mobile devices.
* @internal
*/
@Component({
selector: 'ov-landscape-warning',
templateUrl: './landscape-warning.component.html',
styleUrl: './landscape-warning.component.scss',
standalone: false,
animations: [
trigger('inOutAnimation', [
transition(':enter', [style({ opacity: 0 }), animate('200ms', style({ opacity: 1 }))]),
transition(':leave', [animate('200ms', style({ opacity: 0 }))])
])
]
})
export class LandscapeWarningComponent {}

View File

@ -1,16 +1,23 @@
<div class="container" [ngClass]="{ withCaptions: captionsEnabled, withMargin: localParticipant.isMinimized }">
<div id="layout" class="layout" #layout>
<!-- Top slot: Render elements that should appear at the top -->
@if (layoutAdditionalElementsTemplate && templateConfig.layoutAdditionalElementsSlot === 'top') {
<ng-container *ngTemplateOutlet="layoutAdditionalElementsTemplate"></ng-container>
}
<div
#localLayoutElement
*ngFor="let track of localParticipant.tracks; trackBy: trackParticipantElement"
[ngClass]="{
local_participant: true,
OV_root: !track.isAudioTrack && !track.isMinimized,
OV_publisher: !track.isAudioTrack && !track.isMinimized,
OV_minimized: track.isMinimized,
OV_big: track.isPinned,
OV_ignored: track.isAudioTrack && !track.participant.onlyHasAudioTracks
OV_ignored: track.isAudioTrack && !track.participant.onlyHasAudioTracks,
OV_screen: track.isScreenTrack
}"
[id]="'local-element-' + track.source"
[id]="'participant-' + track.participant.identity"
cdkDrag
cdkDragBoundary=".layout"
[cdkDragDisabled]="!track.isMinimized"
@ -19,18 +26,30 @@
<ng-container *ngTemplateOutlet="streamTemplate; context: { $implicit: track }"></ng-container>
</div>
<!-- Default slot: Render additional layout elements (backward compatibility and default position) -->
@if (layoutAdditionalElementsTemplate && (templateConfig.layoutAdditionalElementsSlot === 'default' || !templateConfig.layoutAdditionalElementsSlot)) {
<ng-container *ngTemplateOutlet="layoutAdditionalElementsTemplate"></ng-container>
}
<div
*ngFor="let track of remoteParticipants | tracks; trackBy: trackParticipantElement"
class="remote-participant"
[id]="'participant-' + track.participant.identity"
[ngClass]="{
OV_root: !track.isAudioTrack,
OV_publisher: !track.isAudioTrack,
OV_big: track.isPinned,
OV_ignored: track.isAudioTrack && !track.participant.onlyHasAudioTracks
OV_ignored: track.isAudioTrack && !track.participant.onlyHasAudioTracks,
OV_screen: track.isScreenTrack
}"
>
<ng-container *ngTemplateOutlet="streamTemplate; context: { $implicit: track }"></ng-container>
</div>
<!-- Bottom slot: Render elements that should appear at the bottom -->
@if (layoutAdditionalElementsTemplate && templateConfig.layoutAdditionalElementsSlot === 'bottom') {
<ng-container *ngTemplateOutlet="layoutAdditionalElementsTemplate"></ng-container>
}
</div>
<!-- <ov-captions *ngIf="captionsEnabled" class="OV_ignored"></ov-captions> -->

View File

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

View File

@ -31,7 +31,7 @@
.OV_root,
.OV_root * {
color: var(--ov-secondary-action-color);
color: var(--ov-text-primary-color);
margin: 0;
padding: 0;
border: 0;

View File

@ -1,3 +1,6 @@
import { LayoutAdditionalElementsDirective } from '../../directives/template/internals.directive';
import { CdkDrag } from '@angular/cdk/drag-drop';
import {
AfterViewInit,
ChangeDetectionStrategy,
@ -11,16 +14,15 @@ import {
ViewChild,
ViewContainerRef
} from '@angular/core';
import { combineLatest, map, Subscription } from 'rxjs';
import { combineLatest, map, Subject, takeUntil } from 'rxjs';
import { StreamDirective } from '../../directives/template/openvidu-components-angular.directive';
import { ParticipantTrackPublication, ParticipantModel } from '../../models/participant.model';
import { LayoutService } from '../../services/layout/layout.service';
import { ParticipantService } from '../../services/participant/participant.service';
import { CdkDrag } from '@angular/cdk/drag-drop';
import { PanelService } from '../../services/panel/panel.service';
import { GlobalConfigService } from '../../services/config/global-config.service';
import { ServiceConfigService } from '../../services/config/service-config.service';
import { ParticipantModel, ParticipantTrackPublication } from '../../models/participant.model';
import { OpenViduComponentsConfigService } from '../../services/config/directive-config.service';
import { GlobalConfigService } from '../../services/config/global-config.service';
import { LayoutService } from '../../services/layout/layout.service';
import { PanelService } from '../../services/panel/panel.service';
import { ParticipantService } from '../../services/participant/participant.service';
import { LayoutTemplateConfiguration, TemplateManagerService } from '../../services/template/template-manager.service';
/**
*
@ -31,7 +33,8 @@ import { OpenViduComponentsConfigService } from '../../services/config/directive
selector: 'ov-layout',
templateUrl: './layout.component.html',
styleUrls: ['./layout.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush
changeDetection: ChangeDetectionStrategy.OnPush,
standalone: false
})
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>;
/**
* @ignore
*/
@ContentChild('layoutAdditionalElements', { read: TemplateRef }) layoutAdditionalElementsTemplate: TemplateRef<any>;
/**
* @ignore
*/
@ -62,9 +70,27 @@ export class LayoutComponent implements OnInit, OnDestroy, AfterViewInit {
// is inside of the layout component tagged with '*ovLayout' directive
if (externalStream) {
this.streamTemplate = externalStream.template;
this.updateTemplatesAndMarkForCheck();
}
}
/**
* @ignore
*/
@ContentChild(LayoutAdditionalElementsDirective) set externalAdditionalElements(
externalAdditionalElements: LayoutAdditionalElementsDirective
) {
if (externalAdditionalElements) {
this._externalLayoutAdditionalElements = externalAdditionalElements;
this.updateTemplatesAndMarkForCheck();
}
}
/**
* @ignore
*/
templateConfig: LayoutTemplateConfiguration = {};
localParticipant: ParticipantModel | undefined;
remoteParticipants: ParticipantModel[] = [];
/**
@ -72,31 +98,32 @@ export class LayoutComponent implements OnInit, OnDestroy, AfterViewInit {
*/
captionsEnabled = true;
private localParticipantSubs: Subscription;
private remoteParticipantsSubs: Subscription;
private captionsSubs: Subscription;
private _externalStream?: StreamDirective;
private _externalLayoutAdditionalElements?: LayoutAdditionalElementsDirective;
private destroy$ = new Subject<void>();
private resizeObserver: ResizeObserver;
private cdkSubscription: Subscription;
private resizeTimeout: NodeJS.Timeout;
private videoIsAtRight: boolean = false;
private lastLayoutWidth: number = 0;
private layoutService: LayoutService;
private lastLayoutHeight: number = 0;
/**
* @ignore
*/
constructor(
private serviceConfig: ServiceConfigService,
private layoutService: LayoutService,
private panelService: PanelService,
private participantService: ParticipantService,
private globalService: GlobalConfigService,
private directiveService: OpenViduComponentsConfigService,
private cd: ChangeDetectorRef
) {
this.layoutService = this.serviceConfig.getLayoutService();
}
private cd: ChangeDetectorRef,
private templateManagerService: TemplateManagerService
) {}
ngOnInit(): void {
this.setupTemplates();
this.subscribeToParticipants();
this.subscribeToCaptions();
}
@ -104,19 +131,19 @@ export class LayoutComponent implements OnInit, OnDestroy, AfterViewInit {
ngAfterViewInit() {
console.log('LayoutComponent.ngAfterViewInit');
this.layoutService.initialize(this.layoutContainer.element.nativeElement);
this.lastLayoutWidth = this.layoutContainer.element.nativeElement.getBoundingClientRect().width;
const rect = this.layoutContainer.element.nativeElement.getBoundingClientRect();
this.lastLayoutWidth = rect.width;
this.lastLayoutHeight = rect.height;
this.listenToResizeLayout();
this.listenToCdkDrag();
}
ngOnDestroy() {
this.destroy$.next();
this.destroy$.complete();
this.localParticipant = undefined;
this.remoteParticipants = [];
this.resizeObserver?.disconnect();
this.localParticipantSubs?.unsubscribe();
this.remoteParticipantsSubs?.unsubscribe();
this.captionsSubs?.unsubscribe();
this.cdkSubscription?.unsubscribe();
this.layoutService.clear();
}
@ -129,8 +156,36 @@ export class LayoutComponent implements OnInit, OnDestroy, AfterViewInit {
return track;
}
private setupTemplates() {
this.templateConfig = this.templateManagerService.setupLayoutTemplates(
this._externalStream,
this._externalLayoutAdditionalElements
);
// Apply templates to component properties for backward compatibility
this.applyTemplateConfiguration();
}
private applyTemplateConfiguration() {
if (this.templateConfig.layoutStreamTemplate) {
this.streamTemplate = this.templateConfig.layoutStreamTemplate;
}
if (this.templateConfig.layoutAdditionalElementsTemplate) {
this.layoutAdditionalElementsTemplate = this.templateConfig.layoutAdditionalElementsTemplate;
}
}
/**
* @internal
* Updates templates and triggers change detection
*/
private updateTemplatesAndMarkForCheck(): void {
this.setupTemplates();
this.cd.markForCheck();
}
private subscribeToCaptions() {
this.captionsSubs = this.layoutService.captionsTogglingObs.subscribe((value: boolean) => {
this.layoutService.captionsTogglingObs.pipe(takeUntil(this.destroy$)).subscribe((value: boolean) => {
this.captionsEnabled = value;
this.cd.markForCheck();
this.layoutService.update();
@ -138,7 +193,7 @@ export class LayoutComponent implements OnInit, OnDestroy, AfterViewInit {
}
private subscribeToParticipants() {
this.localParticipantSubs = this.participantService.localParticipant$.subscribe((p) => {
this.participantService.localParticipant$.pipe(takeUntil(this.destroy$)).subscribe((p) => {
if (p) {
this.localParticipant = p;
if (!this.localParticipant?.isMinimized) {
@ -149,29 +204,37 @@ export class LayoutComponent implements OnInit, OnDestroy, AfterViewInit {
}
});
this.remoteParticipantsSubs = combineLatest([
this.participantService.remoteParticipants$,
this.directiveService.layoutRemoteParticipants$
])
.pipe(
map(([serviceParticipants, directiveParticipants]) =>
directiveParticipants !== undefined ? directiveParticipants : serviceParticipants
combineLatest([this.participantService.remoteParticipants$, this.directiveService.layoutRemoteParticipants$])
.pipe(
map(([serviceParticipants, directiveParticipants]) =>
directiveParticipants !== undefined ? directiveParticipants : serviceParticipants
),
takeUntil(this.destroy$)
)
)
.subscribe((participants) => {
this.remoteParticipants = participants;
this.layoutService.update();
this.cd.markForCheck();
});
.subscribe((participants) => {
this.remoteParticipants = participants;
this.layoutService.update();
this.cd.markForCheck();
});
}
private listenToResizeLayout() {
this.resizeObserver = new ResizeObserver((entries) => {
const { width: parentWidth, height: parentHeight } = entries[0].contentRect;
clearTimeout(this.resizeTimeout);
this.resizeTimeout = setTimeout(() => {
// Always update layout when container size changes
// This ensures layout recalculates when parent containers change
const widthDiff = Math.abs(this.lastLayoutWidth - parentWidth);
const heightDiff = Math.abs(this.lastLayoutHeight - parentHeight);
if (widthDiff > 1 || heightDiff > 1) {
this.layoutService.update();
this.cd.markForCheck();
}
// Handle minimized participant positioning
if (this.localParticipant?.isMinimized) {
const { width: parentWidth } = entries[0].contentRect;
if (this.panelService.isPanelOpened()) {
if (this.lastLayoutWidth < parentWidth) {
// Layout is bigger than before. Maybe the settings panel(wider) has been transitioned to another panel.
@ -190,8 +253,10 @@ export class LayoutComponent implements OnInit, OnDestroy, AfterViewInit {
this.moveStreamToRight(parentWidth);
}
}
this.lastLayoutWidth = parentWidth;
}
this.lastLayoutWidth = parentWidth;
this.lastLayoutHeight = parentHeight;
}, 100);
});
@ -208,7 +273,6 @@ export class LayoutComponent implements OnInit, OnDestroy, AfterViewInit {
const handler = (event) => {
if (!this.panelService.isPanelOpened()) return;
const { x, width } = this.localLayoutElement.nativeElement.getBoundingClientRect();
console.log(x);
const { width: parentWidth } = this.layoutContainer.element.nativeElement.getBoundingClientRect();
if (x === 0) {
// Video is at the left
@ -221,7 +285,8 @@ export class LayoutComponent implements OnInit, OnDestroy, AfterViewInit {
this.videoIsAtRight = false;
}
};
this.cdkSubscription = this.cdkDrag.released.subscribe(handler);
this.cdkDrag.released.pipe(takeUntil(this.destroy$)).subscribe(handler);
if (this.globalService.isProduction()) return;
// Just for allow E2E testing with drag and drop

View File

@ -9,5 +9,5 @@ video {
border: 0;
font-size: 100%;
border-radius: var(--ov-video-radius);
background-color: #000000;
background-color: var(--ov-video-background, var(--ov-primary-action-color));
}

View File

@ -1,5 +1,5 @@
import { animate, style, transition, trigger } from '@angular/animations';
import { AfterViewInit, Component, ElementRef, Input, ViewChild } from '@angular/core';
import { AfterViewInit, Component, ElementRef, Input, OnDestroy, ViewChild } from '@angular/core';
import { Track } from 'livekit-client';
/**
@ -8,7 +8,13 @@ import { Track } from 'livekit-client';
@Component({
selector: 'ov-media-element',
template: `
<ov-avatar-profile @posterAnimation *ngIf="showAvatar" [name]="avatarName" [color]="avatarColor"></ov-avatar-profile>
<ov-video-poster
@posterAnimation
[showAvatar]="showAvatar"
[nickname]="avatarName"
[color]="avatarColor"
[hasEncryptionError]="hasEncryptionError"
></ov-video-poster>
<video #videoElement *ngIf="_track?.kind === 'video'" class="OV_video-element" [attr.id]="_track?.sid"></video>
<audio #audioElement *ngIf="_track?.kind === 'audio'" [attr.id]="_track?.sid"></audio>
`,
@ -18,38 +24,46 @@ import { Track } from 'livekit-client';
transition(':enter', [style({ opacity: 0 }), animate('100ms', style({ opacity: 1 }))]),
transition(':leave', [style({ opacity: 1 }), animate('200ms', style({ opacity: 0 }))])
])
]
],
standalone: false
})
export class MediaElementComponent implements AfterViewInit {
export class MediaElementComponent implements AfterViewInit, OnDestroy {
_track: Track;
_videoElement: ElementRef;
_audioElement: ElementRef;
type: Track.Source = Track.Source.Camera;
private _muted: boolean = false;
private previousTrack: Track | null = null;
@Input() showAvatar: boolean;
@Input() avatarColor: string;
@Input() avatarName: string;
@Input() isLocal: boolean;
@Input() showAvatar: boolean = false;
@Input() avatarColor: string = '#000000';
@Input() avatarName: string = 'User';
@Input() isLocal: boolean = false;
@Input() hasEncryptionError: boolean = false;
@ViewChild('videoElement', { static: false })
set videoElement(element: ElementRef) {
this._videoElement = element;
this.attachTracks();
}
@ViewChild('audioElement', { static: false })
set audioElement(element: ElementRef) {
this._audioElement = element;
this.attachTracks();
}
@Input()
set track(track: Track) {
if (!track) return;
// Detach previous track if it's different
if (this.previousTrack && this.previousTrack !== track) {
this.detachPreviousTrack();
}
this._track = track;
this.previousTrack = track;
this.attachTracks();
}
@ -68,6 +82,23 @@ export class MediaElementComponent implements AfterViewInit {
});
}
ngOnDestroy() {
this.detachPreviousTrack();
}
private detachPreviousTrack() {
if (this.previousTrack) {
// Detach from video element
if (this.isVideoTrack() && this._videoElement?.nativeElement) {
this.previousTrack.detach(this._videoElement.nativeElement);
}
// Detach from audio element
if (this.isAudioTrack() && this._audioElement?.nativeElement) {
this.previousTrack.detach(this._audioElement.nativeElement);
}
}
}
private updateVideoStyles() {
this.type = this._track.source;
if (this.type === Track.Source.ScreenShare) {

View File

@ -1,6 +1,6 @@
<div class="panel-container" id="activities-container">
<div class="panel-header-container">
<h3 class="panel-title">Activities</h3>
<h3 class="panel-title">{{ 'PANEL.ACTIVITIES.TITLE' | translate }}</h3>
<button class="panel-close-button" mat-icon-button matTooltip="{{ 'PANEL.CLOSE' | translate }}" (click)="close()">
<mat-icon>close</mat-icon>
</button>
@ -17,6 +17,8 @@
(onRecordingDeleteRequested)="onRecordingDeleteRequested.emit($event)"
(onRecordingDownloadClicked)="onRecordingDownloadClicked.emit($event)"
(onRecordingPlayClicked)="onRecordingPlayClicked.emit($event)"
(onViewRecordingClicked)="onViewRecordingClicked.emit($event)"
(onViewRecordingsClicked)="onViewRecordingsClicked.emit()"
></ov-recording-activity>
<ov-broadcasting-activity
*ngIf="showBroadcastingActivity"

View File

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

View File

@ -1,5 +1,3 @@
$ov-activity-status-color: #afafaf;
:host {
.activities-body-container {
display: block !important;
@ -14,12 +12,12 @@ $ov-activity-status-color: #afafaf;
padding: 3px;
font-size: 11px;
border-radius: var(--ov-surface-radius);
background-color: $ov-activity-status-color;
background-color: var(--ov-secondary-action-color);
}
.activity-icon {
display: inherit;
background-color: $ov-activity-status-color;;
background-color: var(--ov-secondary-action-color);
border-radius: var(--ov-surface-radius);
margin: 10px 0px !important;
padding: 10px;
@ -91,6 +89,10 @@ $ov-activity-status-color: #afafaf;
display: none !important;
}
::ng-deep .mat-expansion-indicator svg {
fill: var(--ov-text-surface-color) !important;
}
::ng-deep
.mdc-list-item--with-leading-icon.mdc-list-item--with-two-lines.mdc-list-item--with-trailing-meta
.mdc-list-item__end::before {

View File

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

View File

@ -1,8 +1,3 @@
$ov-broadcasting-color: #5903ca;
$ov-input-color: #cccccc;
.time-container {
padding: 2px;
}
@ -14,29 +9,17 @@ $ov-input-color: #cccccc;
}
#broadcasting-icon {
color: $ov-broadcasting-color !important;
color: var(--ov-broadcasting-color, #5903ca) !important;
}
.broadcasting-duration {
background-color: var(--ov-secondary-action-color);
padding: 4px 8px;
border-radius: var(--ov-surface-radius);
font-weight: 500;
}
.broadcasting-duration mat-icon {
font-size: 18px;
width: 18px;
height: 18px;
}
.started {
background-color: $ov-broadcasting-color !important;
background-color: var(--ov-broadcasting-color, #5903ca) !important;
color: var(--ov-text-primary-color);
}
.activity-icon.started {
background-color: $ov-broadcasting-color !important;
background-color: var(--ov-broadcasting-color, #5903ca) !important;
color: var(--ov-text-primary-color);
}
@ -119,10 +102,10 @@ mat-expansion-panel {
.input-container {
height: 25px;
display: flex;
background-color: $ov-input-color;
padding: 10px;
margin: 10px;
border-radius: var(--ov-surface-radius);
border: 1px solid var(--ov-border-color);
order: 3;
justify-content: space-evenly;
align-items: center;
@ -131,6 +114,7 @@ mat-expansion-panel {
.input-container input {
width: 100%;
height: 16px;
color: var(--ov-text-surface-color);
margin: auto;
background-color: transparent;
display: block;
@ -143,5 +127,19 @@ mat-expansion-panel {
-webkit-box-shadow: none;
-moz-box-shadow: none;
box-shadow: none;
font-family: 'Roboto', 'RobotoDraft', Helvetica, Arial, sans-serif;
&::placeholder {
color: var(--ov-text-surface-color);
opacity: 1; /* Firefox */
}
}
#broadcasting-btn {
color: var(--ov-text-secondary-color);
&:disabled {
// background-color: var(--ov-disabled-color) !important;
color: var(--ov-text-disabled-color) !important;
cursor: not-allowed !important;
}
}

View File

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

View File

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

View File

@ -1,10 +1,24 @@
<div class="panel-container" id="background-effects-container">
<div class="panel-header-container">
<h3 class="panel-title">{{ 'PANEL.BACKGROUND.TITLE' | translate }}</h3>
<div class="panel-container" id="background-effects-container" [class.prejoin-mode]="mode === 'prejoin'">
@if (mode === 'meeting') {
<div class="panel-header-container">
<h3 class="panel-title">{{ 'PANEL.BACKGROUND.TITLE' | translate }}</h3>
<button class="panel-close-button" mat-icon-button matTooltip="{{ 'PANEL.CLOSE' | translate }}" (click)="close()">
<mat-icon>close</mat-icon>
</button>
</div>
} @else {
<button class="panel-close-button" mat-icon-button matTooltip="{{ 'PANEL.CLOSE' | translate }}" (click)="close()">
<mat-icon>close</mat-icon>
<mat-icon>arrow_back</mat-icon>
</button>
</div>
}
@if (!isVirtualBackgroundSupported()) {
<div class="not-supported-message">
<p class="warning-title">{{ 'PANEL.BACKGROUND.NOT_SUPPORTED' | translate }}</p>
<p class="warning-description">{{ 'PANEL.BACKGROUND.NOT_SUPPORTED_DESCRIPTION' | translate }}</p>
</div>
}
<div class="effects-container" fxFlex="100%" fxLayoutAlign="space-evenly none">
<div>
@ -17,6 +31,7 @@
[class.active-effect-btn]="backgroundSelectedId === effect.id"
(click)="applyBackground(effect)"
[attr.id]="effect.id + '-btn'"
[disabled]="!isVirtualBackgroundSupported()"
[matTooltip]="
effect.type === effectType.NONE
? ('PANEL.BACKGROUND.NO_EFFECTS' | translate)
@ -37,7 +52,8 @@
class="effect-button"
[id]="'effect-' + effect.id"
[class.active-effect-btn]="backgroundSelectedId === effect.id"
(click)="applyBackground(effect)"
[class.disabled]="!isVirtualBackgroundSupported()"
(click)="isVirtualBackgroundSupported() && applyBackground(effect)"
>
<img [src]="effect.thumbnail" />
</div>

View File

@ -1,6 +1,37 @@
.prejoin-mode {
margin: 0 10px 0px 10px;
max-height: 100%;
min-height: 100%;
.panel-close-button {
margin: 0;
}
}
.background-title {
color: var(--ov-text-surface-color);
margin: 10px 0;
font-weight: 300;
}
.not-supported-message {
padding: 15px;
margin: 10px;
background-color: var(--ov-warn-color, #ff9800);
border-radius: var(--ov-surface-radius);
color: var(--ov-text-surface-color);
.warning-title {
font-weight: 500;
margin: 0 0 8px 0;
}
.warning-description {
font-size: 0.9em;
margin: 0;
opacity: 0.9;
}
}
.effects-container {
display: block !important;
overflow-y: auto;
@ -12,7 +43,7 @@
margin: 5px;
border-radius: var(--ov-surface-radius);
background-color: var(--ov-secondary-action-color);
color: var(--ov-primary-action-color);
color: var(--ov-text-surface-color);
width: 60px;
height: 60px;
line-height: inherit;
@ -22,6 +53,11 @@
cursor: pointer;
}
.effect-button.disabled {
opacity: 0.5;
cursor: not-allowed;
}
.active-effect-btn {
border: 2px solid var(--ov-accent-action-color);
}

View File

@ -1,4 +1,4 @@
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, OnInit } from '@angular/core';
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, computed, EventEmitter, Input, OnInit, Output, Signal } from '@angular/core';
import { Subscription } from 'rxjs';
import { BackgroundEffect, EffectType } from '../../../models/background-effect.model';
import { PanelType } from '../../../models/panel.model';
@ -12,14 +12,18 @@ import { VirtualBackgroundService } from '../../../services/virtual-background/v
selector: 'ov-background-effects-panel',
templateUrl: './background-effects-panel.component.html',
styleUrls: ['../panel.component.scss', './background-effects-panel.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush
changeDetection: ChangeDetectionStrategy.OnPush,
standalone: false
})
export class BackgroundEffectsPanelComponent implements OnInit {
@Input() mode: 'prejoin' | 'meeting' = 'meeting';
@Output() onClose = new EventEmitter<void>();
backgroundSelectedId: string;
effectType = EffectType;
backgroundImages: BackgroundEffect[] = [];
noEffectAndBlurredBackground: BackgroundEffect[] = [];
private backgrounds: BackgroundEffect[];
private backgrounds: BackgroundEffect[] = [];
private backgroundSubs: Subscription;
/**
@ -34,6 +38,14 @@ export class BackgroundEffectsPanelComponent implements OnInit {
private cd: ChangeDetectorRef
) {}
/**
* Computed signal that reactively tracks if virtual background is supported.
* Updates automatically when browser support changes.
*/
readonly isVirtualBackgroundSupported: Signal<boolean> = computed(() =>
this.backgroundService.isVirtualBackgroundSupported()
);
ngOnInit(): void {
this.subscribeToBackgroundSelected();
this.backgrounds = this.backgroundService.getBackgrounds();
@ -52,14 +64,14 @@ export class BackgroundEffectsPanelComponent implements OnInit {
}
close() {
this.panelService.togglePanel(PanelType.BACKGROUND_EFFECTS);
if (this.mode === 'prejoin') {
this.onClose.emit();
} else {
this.panelService.togglePanel(PanelType.BACKGROUND_EFFECTS);
}
}
async applyBackground(effect: BackgroundEffect) {
if (effect.type === EffectType.NONE) {
await this.backgroundService.removeBackground();
} else {
await this.backgroundService.applyBackground(effect);
}
await this.backgroundService.applyBackground(effect);
}
}

View File

@ -6,6 +6,13 @@
</button>
</div>
@if (hasEncryptionKeyMismatch()) {
<div class="encryption-warning">
<mat-icon>warning</mat-icon>
<p>{{ 'PANEL.CHAT.ENCRYPTION_KEY_MISMATCH' | translate }}</p>
</div>
}
<div class="text-container">
<p class="text-info">{{ 'PANEL.CHAT.SUBTITLE' | translate }}</p>
</div>

View File

@ -1,4 +1,28 @@
$ov-selection-color: #d4d6d7;
.encryption-warning {
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
padding: 12px;
margin: 10px;
background-color: rgba(255, 152, 0, 0.1);
border: 1px solid rgba(255, 152, 0, 0.5);
border-radius: var(--ov-surface-radius);
color: var(--ov-warn-color, #ff9800);
font-size: 13px;
mat-icon {
font-size: 20px;
width: 20px;
height: 20px;
}
p {
margin: 0;
line-height: 1.4;
width: fit-content;
}
}
.text-container {
color: var(--ov-text-primary-color);
@ -29,15 +53,14 @@ $ov-selection-color: #d4d6d7;
.input-container {
height: 65px;
display: flex;
background-color: var(--ov-surface-color);
border: 1px solid $ov-selection-color;
border: 1px solid var(--ov-secondary-action-color);
padding: 10px 5px 10px 10px;
margin: 10px;
border-radius: var(--ov-surface-radius);
order: 3;
justify-content: space-evenly;
align-items: none;
color: var(--ov-text-surface-color);
}
.input-container textarea {
@ -59,6 +82,10 @@ $ov-selection-color: #d4d6d7;
box-shadow: none;
font-family: 'Roboto', 'RobotoDraft', Helvetica, Arial, sans-serif;
color: var(--ov-text-surface-color);
&::placeholder {
color: var(--ov-text-surface-color);
}
}
.message {
@ -87,18 +114,18 @@ $ov-selection-color: #d4d6d7;
position: relative;
border-radius: var(--ov-surface-radius);
padding: 8px;
color: var(--ov-secondary-action-color);
color: var(--ov-text-surface-color);
width: auto;
max-width: 95%;
font-size: 14px;
word-break: break-all;
background-color: var(--ov-primary-action-color);
background-color: var(--ov-secondary-action-color);
}
#send-btn {
border-radius: var(--ov-surface-radius);
color: var(--ov-secondary-action-color);
background-color: var(--ov-primary-action-color);
color: var(--ov-text-primary-color);
border-radius: var(--ov-surface-radius);
align-self: center;
height: 75px;
}
@ -126,5 +153,6 @@ $ov-selection-color: #d4d6d7;
}
::ng-deep a:-webkit-any-link {
color: var(--ov-accent-action-color);
color: #37a1f8;
font-weight: 500;
}

View File

@ -1,9 +1,11 @@
import { AfterViewInit, ChangeDetectionStrategy, ChangeDetectorRef, Component, ElementRef, OnInit, ViewChild } from '@angular/core';
import { Subscription } from 'rxjs';
import { AfterViewInit, ChangeDetectionStrategy, ChangeDetectorRef, Component, computed, ElementRef, OnInit, ViewChild } from '@angular/core';
import { Subject, takeUntil } from 'rxjs';
import { ChatMessage } from '../../../models/chat.model';
import { PanelType } from '../../../models/panel.model';
import { ChatService } from '../../../services/chat/chat.service';
import { E2eeService } from '../../../services/e2ee/e2ee.service';
import { PanelService } from '../../../services/panel/panel.service';
import { ParticipantService } from '../../../services/participant/participant.service';
/**
*
@ -13,27 +15,28 @@ import { PanelService } from '../../../services/panel/panel.service';
selector: 'ov-chat-panel',
templateUrl: './chat-panel.component.html',
styleUrls: ['../panel.component.scss', './chat-panel.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush
changeDetection: ChangeDetectionStrategy.OnPush,
standalone: false
})
export class ChatPanelComponent implements OnInit, AfterViewInit {
/**
* @ignore
*/
@ViewChild('chatScroll') chatScroll: ElementRef;
@ViewChild('chatScroll') chatScroll: ElementRef = new ElementRef(null);
/**
* @ignore
*/
@ViewChild('chatInput') chatInput: ElementRef;
@ViewChild('chatInput') chatInput: ElementRef = new ElementRef(null);
/**
* @ignore
*/
message: string;
message: string = '';
/**
* @ignore
*/
messageList: ChatMessage[] = [];
private chatMessageSubscription: Subscription;
private destroy$ = new Subject<void>();
/**
* @ignore
@ -41,8 +44,10 @@ export class ChatPanelComponent implements OnInit, AfterViewInit {
constructor(
private chatService: ChatService,
private panelService: PanelService,
private cd: ChangeDetectorRef
) {}
private cd: ChangeDetectorRef,
private e2eeService: E2eeService,
private participantService: ParticipantService
) { }
/**
* @ignore
@ -65,13 +70,14 @@ export class ChatPanelComponent implements OnInit, AfterViewInit {
* @ignore
*/
ngOnDestroy(): void {
if (this.chatMessageSubscription) this.chatMessageSubscription.unsubscribe();
this.destroy$.next();
this.destroy$.complete();
}
/**
* @ignore
*/
eventKeyPress(event) {
eventKeyPress(event: KeyboardEvent): void {
// Pressed 'Enter' key
if (event && event.keyCode === 13) {
event.preventDefault();
@ -107,8 +113,21 @@ export class ChatPanelComponent implements OnInit, AfterViewInit {
this.panelService.togglePanel(PanelType.CHAT);
}
/**
* @ignore
*/
hasEncryptionKeyMismatch = computed(() => {
if (!this.e2eeService.isEnabled) {
return false;
}
const remoteParticipants = this.participantService.remoteParticipantsSignal();
return remoteParticipants.some(p => p.hasEncryptionError);
});
private subscribeToMessages() {
this.chatMessageSubscription = this.chatService.messagesObs.subscribe((messages: ChatMessage[]) => {
this.chatService.chatMessages$.pipe(takeUntil(this.destroy$)).subscribe((messages: ChatMessage[]) => {
this.messageList = messages;
if (this.panelService.isChatPanelOpened()) {
this.scrollToBottom();

View File

@ -9,6 +9,16 @@
align-items: stretch;
}
// Mobile responsiveness - ensure panels occupy full width on mobile devices
@media only screen and (max-width: 768px) {
.panel-container {
margin: 8px;
max-height: calc(100% - 16px);
min-height: calc(100% - 16px);
border-radius: 8px;
}
}
.panel-header-container {
padding: 10px;
flex: inherit;
@ -37,12 +47,12 @@
}
::-webkit-scrollbar-thumb {
background: #a7a7a7;
background: var(--ov-selection-color-btn);
border-radius: 4px;
}
::-webkit-scrollbar-thumb:hover {
background: #7c7c7c;
background: var(--ov-text-disabled-color);
}
::-webkit-scrollbar-track {

View File

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

View File

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

View File

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

View File

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

View File

@ -13,8 +13,10 @@ import {
import { ParticipantService } from '../../../../services/participant/participant.service';
import { PanelService } from '../../../../services/panel/panel.service';
import { ParticipantPanelItemDirective } from '../../../../directives/template/openvidu-components-angular.directive';
import { Subscription } from 'rxjs';
import { Subject, takeUntil } from 'rxjs';
import { ParticipantModel } from '../../../../models/participant.model';
import { TemplateManagerService, ParticipantsPanelTemplateConfiguration } from '../../../../services/template/template-manager.service';
import { OpenViduComponentsConfigService } from '../../../../services/config/directive-config.service';
/**
* The **ParticipantsPanelComponent** is hosted inside of the {@link PanelComponent}.
@ -25,7 +27,8 @@ import { ParticipantModel } from '../../../../models/participant.model';
selector: 'ov-participants-panel',
templateUrl: './participants-panel.component.html',
styleUrls: ['../../panel.component.scss', './participants-panel.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush
changeDetection: ChangeDetectionStrategy.OnPush,
standalone: false
})
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>;
/**
* @ignore
*/
@ContentChild('participantPanelAfterLocalParticipant', { read: TemplateRef })
participantPanelAfterLocalParticipantTemplate: TemplateRef<any>;
/**
* @ignore
*/
@ContentChild(ParticipantPanelItemDirective)
set externalParticipantPanelItem(externalParticipantPanelItem: ParticipantPanelItemDirective) {
// This directive will has value only when PARTICIPANT PANEL ITEM component tagged with '*ovParticipantPanelItem'
// is inside of the PARTICIPANTS PANEL component tagged with '*ovParticipantsPanel'
this._externalParticipantPanelItem = externalParticipantPanelItem;
if (externalParticipantPanelItem) {
this.participantPanelItemTemplate = externalParticipantPanelItem.template;
this.updateTemplatesAndMarkForCheck();
}
}
private localParticipantSubs: Subscription;
private remoteParticipantsSubs: Subscription;
/**
* @internal
* Template configuration managed by the service
*/
templateConfig: ParticipantsPanelTemplateConfiguration = {};
// Store directive references for template setup
private _externalParticipantPanelItem?: ParticipantPanelItemDirective;
private destroy$ = new Subject<void>();
/**
* @ignore
@ -68,32 +84,26 @@ export class ParticipantsPanelComponent implements OnInit, OnDestroy, AfterViewI
constructor(
private participantService: ParticipantService,
private panelService: PanelService,
private cd: ChangeDetectorRef
private cd: ChangeDetectorRef,
private templateManagerService: TemplateManagerService,
private libService: OpenViduComponentsConfigService
) {}
/**
* @ignore
*/
ngOnInit(): void {
this.localParticipantSubs = this.participantService.localParticipant$.subscribe((p: ParticipantModel | undefined) => {
if (p) {
this.localParticipant = p;
this.cd.markForCheck();
}
});
this.setupTemplates();
this.remoteParticipantsSubs = this.participantService.remoteParticipants$.subscribe((p: ParticipantModel[]) => {
this.remoteParticipants = p;
this.cd.markForCheck();
});
this.subscribeToParticipantsChanges();
}
/**
* @ignore
*/
ngOnDestroy() {
if (this.localParticipantSubs) this.localParticipantSubs.unsubscribe();
if (this.remoteParticipantsSubs) this.remoteParticipantsSubs.unsubscribe;
this.destroy$.next();
this.destroy$.complete();
}
/**
@ -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
*/

View File

@ -1,4 +1,4 @@
<div class="panel-container" id="settings-container">
<div class="panel-container" id="settings-container" [class.vertical-layout]="isVerticalLayout" [class.compact-view]="isCompactView">
<div class="panel-header-container">
<h3 class="panel-title">{{ 'PANEL.SETTINGS.TITLE' | translate }}</h3>
<button class="panel-close-button" mat-icon-button matTooltip="{{ 'PANEL.CLOSE' | translate }}" (click)="close()">
@ -6,8 +6,8 @@
</button>
</div>
<div class="settings-container">
<div class="item-menu" [ngClass]="{ mobile: isMobile }">
<div class="settings-container" [class.vertical-layout]="isVerticalLayout">
<div class="item-menu" [class.compact]="isCompactView" [class.icons-only]="shouldHideMenuText">
<mat-selection-list
#optionList
(selectionChange)="onSelectionChanged(optionList.selectedOptions.selected[0]?.value)"
@ -20,27 +20,35 @@
id="general-opt"
[selected]="selectedOption === settingsOptions.GENERAL"
[value]="settingsOptions.GENERAL"
matTooltip="{{ shouldHideMenuText ? ('PANEL.SETTINGS.GENERAL' | translate) : '' }}"
[matTooltipDisabled]="!shouldHideMenuText"
>
<mat-icon matListItemIcon>manage_accounts</mat-icon>
<div mat-line *ngIf="!isMobile">{{ 'PANEL.SETTINGS.GENERAL' | translate }}</div>
<div *ngIf="!shouldHideMenuText" class="option-text">{{ 'PANEL.SETTINGS.GENERAL' | translate }}</div>
</mat-list-option>
<mat-list-option
*ngIf="showCameraButton"
class="option"
id="video-opt"
[selected]="selectedOption === settingsOptions.VIDEO"
[value]="settingsOptions.VIDEO"
matTooltip="{{ shouldHideMenuText ? ('PANEL.SETTINGS.VIDEO' | translate) : '' }}"
[matTooltipDisabled]="!shouldHideMenuText"
>
<mat-icon matListItemIcon>videocam</mat-icon>
<div mat-line *ngIf="!isMobile">{{ 'PANEL.SETTINGS.VIDEO' | translate }}</div>
<div *ngIf="!shouldHideMenuText" class="option-text">{{ 'PANEL.SETTINGS.VIDEO' | translate }}</div>
</mat-list-option>
<mat-list-option
*ngIf="showMicrophoneButton"
class="option"
id="audio-opt"
[selected]="selectedOption === settingsOptions.AUDIO"
[value]="settingsOptions.AUDIO"
matTooltip="{{ shouldHideMenuText ? ('PANEL.SETTINGS.AUDIO' | translate) : '' }}"
[matTooltipDisabled]="!shouldHideMenuText"
>
<mat-icon matListItemIcon>mic</mat-icon>
<div mat-line *ngIf="!isMobile">{{ 'PANEL.SETTINGS.AUDIO' | translate }}</div>
<div *ngIf="!shouldHideMenuText" class="option-text">{{ 'PANEL.SETTINGS.AUDIO' | translate }}</div>
</mat-list-option>
<!-- <mat-list-option
*ngIf="showCaptions"
@ -48,36 +56,65 @@
[selected]="selectedOption === settingsOptions.CAPTIONS"
[value]="settingsOptions.CAPTIONS"
id="captions-opt"
matTooltip="{{ shouldHideMenuText ? ('PANEL.SETTINGS.CAPTIONS' | translate) : '' }}"
[matTooltipDisabled]="!shouldHideMenuText"
>
<mat-icon matListItemIcon>closed_caption</mat-icon>
<div mat-line *ngIf="!isMobile">{{ 'PANEL.SETTINGS.CAPTIONS' | translate }}</div>
<div mat-line *ngIf="!shouldHideMenuText">{{ 'PANEL.SETTINGS.CAPTIONS' | translate }}</div>
</mat-list-option> -->
</mat-selection-list>
</div>
<div class="item-content">
<div *ngIf="selectedOption === settingsOptions.GENERAL">
<mat-label class="input-label">{{ 'PREJOIN.NICKNAME' | translate }}</mat-label>
<ov-participant-name-input></ov-participant-name-input>
<mat-list>
<mat-list-item class="lang-selector">
<mat-icon matListItemIcon>translate</mat-icon>
<div matListItemTitle>{{ 'PANEL.SETTINGS.LANGUAGE' | translate }}</div>
<ov-lang-selector matListItemMeta (onLangChanged)="onLangChanged.emit($event)"></ov-lang-selector>
</mat-list-item>
</mat-list>
<div class="item-content" [class.full-width]="isVerticalLayout">
<div *ngIf="selectedOption === settingsOptions.GENERAL" class="general-settings">
<div class="nickname-section">
<mat-label class="input-label">{{ 'PREJOIN.NICKNAME' | translate }}</mat-label>
<div class="nickname-input-container">
<ov-participant-name-input></ov-participant-name-input>
</div>
</div>
<div class="language-section">
<mat-list>
<mat-list-item class="lang-selector">
<mat-icon matListItemIcon>translate</mat-icon>
<div matListItemTitle>{{ 'PANEL.SETTINGS.LANGUAGE' | translate }}</div>
<ov-lang-selector matListItemMeta (onLangChanged)="onLangChanged.emit($event)"></ov-lang-selector>
</mat-list-item>
</mat-list>
</div>
@if (showThemeSelector) {
<div class="theme-section">
<mat-list>
<mat-list-item class="theme-selector">
<mat-icon matListItemIcon class="material-symbols-outlined">routine</mat-icon>
<div matListItemTitle>{{ 'PANEL.SETTINGS.THEME' | translate }}</div>
<ov-theme-selector matListItemMeta></ov-theme-selector>
</mat-list-item>
</mat-list>
</div>
}
<!-- Additional elements injected via directive -->
@if (generalAdditionalElementsTemplate) {
<div class="additional-elements-section">
<ng-container *ngTemplateOutlet="generalAdditionalElementsTemplate"></ng-container>
</div>
}
</div>
<ov-video-devices-select
*ngIf="selectedOption === settingsOptions.VIDEO"
(onVideoDeviceChanged)="onVideoDeviceChanged.emit($event)"
(onVideoEnabledChanged)="onVideoEnabledChanged.emit($event)"
></ov-video-devices-select>
<ov-audio-devices-select
*ngIf="selectedOption === settingsOptions.AUDIO"
(onAudioDeviceChanged)="onAudioDeviceChanged.emit($event)"
(onAudioEnabledChanged)="onAudioEnabledChanged.emit($event)"
></ov-audio-devices-select>
<!-- <ov-captions-settings *ngIf="selectedOption === settingsOptions.CAPTIONS && showCaptions"></ov-captions-settings> -->
<div *ngIf="showCameraButton && selectedOption === settingsOptions.VIDEO" class="video-settings">
<ov-video-devices-select
(onVideoDeviceChanged)="onVideoDeviceChanged.emit($event)"
(onVideoEnabledChanged)="onVideoEnabledChanged.emit($event)"
></ov-video-devices-select>
</div>
<div *ngIf="showMicrophoneButton && selectedOption === settingsOptions.AUDIO" class="audio-settings">
<ov-audio-devices-select
(onAudioDeviceChanged)="onAudioDeviceChanged.emit($event)"
(onAudioEnabledChanged)="onAudioEnabledChanged.emit($event)"
></ov-audio-devices-select>
</div>
<!-- <div *ngIf="selectedOption === settingsOptions.CAPTIONS && showCaptions" class="captions-settings">
<ov-captions-settings></ov-captions-settings>
</div> -->
</div>
</div>
</div>

View File

@ -1,39 +1,192 @@
#settings-container {
display: flex !important;
flex-direction: column;
height: 100%;
min-height: 0;
// Base layout - horizontal (desktop/tablet)
.settings-container {
display: flex;
padding: 10px;
flex: 1;
width: auto;
justify-content: space-between;
align-items: stretch;
}
.item-menu {
padding-right: 5px;
border-right: 1px solid var(--ov-secondary-action-color);
width: 170px;
}
.item-menu.mobile {
width: 50px !important;
}
.item-content {
min-height: 0;
padding: 16px;
flex-grow: 1;
width: min-content;
gap: 16px;
align-items: stretch;
box-sizing: border-box;
}
.lang-container button {
// Vertical layout for mobile
&.vertical-layout .settings-container {
flex-direction: column;
gap: 16px;
padding: 12px;
}
// Menu styling - Desktop/Tablet default
.item-menu {
flex-shrink: 0;
border-right: 1px solid var(--ov-secondary-action-color);
width: 180px;
min-width: 180px;
padding-right: 16px;
// Compact view (tablet)
&.compact {
width: 140px;
min-width: 140px;
padding-right: 12px;
.option {
display: grid;
justify-content: center;
}
}
// Icons only (mobile/small tablets) - ahora con mejor padding
&.icons-only {
width: 80px;
min-width: 80px;
padding-right: 8px;
}
}
// Mobile vertical layout - no side border, full width menu
&.vertical-layout .item-menu {
width: 100%;
min-width: auto;
border-right: none;
border-bottom: 1px solid var(--ov-border-color);
padding-right: 0;
padding-bottom: 16px;
margin-bottom: 4px;
// Make menu horizontal on mobile with better spacing
mat-selection-list {
display: flex;
flex-direction: row;
justify-content: space-around;
gap: 8px;
padding: 0 8px;
}
mat-list-option {
flex: 1;
min-width: auto;
min-height: 60px;
border-radius: var(--ov-surface-radius);
// Estructura vertical: icono arriba, texto abajo
::ng-deep .mdc-list-item__content {
display: flex !important;
flex-direction: column !important;
align-items: center !important;
justify-content: center !important;
gap: 4px;
}
// Resetear el margin del icono para layout vertical
::ng-deep .mdc-list-item--with-leading-icon .mdc-list-item__start {
margin-right: 0 !important;
}
// Mejor presentación en mobile con layout vertical
.option-text {
font-size: 11px;
text-align: center;
line-height: 1.1;
font-weight: 500;
white-space: nowrap;
max-width: 100%;
overflow: hidden;
text-overflow: ellipsis;
}
mat-icon {
font-size: 20px;
width: 20px;
height: 20px;
}
}
}
// Content area styling
.item-content {
flex: 1;
min-width: 0;
padding: 8px 16px;
overflow-y: auto;
overflow-x: hidden;
box-sizing: border-box;
&.full-width {
padding: 8px 12px;
}
}
// General settings specific styling
.general-settings {
display: flex;
flex-direction: column;
gap: 24px;
padding: 8px 0;
.nickname-section {
.input-label {
display: block;
margin-bottom: 12px;
font-weight: 500;
color: var(--ov-text-surface-color);
font-size: 14px;
}
.nickname-input-container {
width: 100%;
max-width: 100%;
box-sizing: border-box;
// Ensure the input takes full width of its container
::ng-deep ov-participant-name-input {
width: 100%;
.participant-name-input-container {
width: 100%;
box-sizing: border-box;
.participant-name-input {
width: 100% !important;
box-sizing: border-box;
}
}
}
}
}
.language-section {
box-sizing: border-box;
mat-list {
padding: 0;
}
}
}
// Video and Audio settings containers
.video-settings,
.audio-settings,
.captions-settings {
width: 100%;
max-width: 100%;
box-sizing: border-box;
}
// List option styling
mat-list-option[aria-selected='true'] {
background: var(--ov-accent-action-color) !important;
border-radius: var(--ov-surface-radius);
::ng-deep .mat-mdc-list-item-unscoped-content,
mat-icon {
color: var(--ov-secondary-action-color) !important;
color: var(--ov-text-primary-color) !important;
}
}
@ -42,23 +195,224 @@
mat-icon {
color: var(--ov-text-surface-color) !important;
}
&:hover {
background-color: rgba(var(--ov-accent-action-color-rgb), 0.1) !important;
}
}
// Icon spacing
::ng-deep .mdc-list-item--with-leading-icon .mdc-list-item__start {
margin-right: 15px !important;
}
// Remove focus state layer
.mat-mdc-list-base {
--mdc-list-list-item-focus-state-layer-color: transparent !important;
--mat-list-list-item-focus-state-layer-color: transparent !important;
}
// Language selector styling
::ng-deep .lang-selector {
.expand-more-icon,
mat-icon {
color: var(--ov-text-surface-color) !important;
}
div {
color: var(--ov-text-surface-color) !important;
}
}
::ng-deep .mat-mdc-list-item-meta.mdc-list-item__end {
margin: 0 !important;
}
::ng-deep .lang-selector .expand-more-icon,
::ng-deep .lang-selector mat-icon {
color: var(--ov-secondary-action-color) !important;
::ng-deep .lang-selector mat-icon,
::ng-deep .theme-selector .expand-more-icon,
::ng-deep .theme-selector mat-icon {
color: var(--ov-text-surface-color) !important;
}
::ng-deep .lang-selector div,
::ng-deep .theme-selector div,
.input-label {
color: var(--ov-text-surface-color) !important;
}
// Icons-only mode styling for compact menu items
&.compact-view .item-menu.icons-only {
mat-list-option {
min-height: 52px;
padding: 8px;
justify-content: center;
border-radius: var(--ov-surface-radius);
::ng-deep .mdc-list-item__content {
justify-content: center;
flex-direction: column;
align-items: center;
}
::ng-deep .mdc-list-item--with-leading-icon .mdc-list-item__start {
margin-right: 0 !important;
margin-bottom: 4px;
}
mat-icon {
font-size: 20px;
width: 20px;
height: 20px;
}
}
}
// Mejor transición para cambios de estado
mat-list-option {
transition: all 0.2s ease-in-out;
&:hover:not([aria-selected='true']) {
transform: translateY(-1px);
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
}
// Mejora en la tipografía y espaciado
.option-text {
font-weight: 500;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
}
// Responsive breakpoints optimizados
@media (max-width: 1024px) {
#settings-container {
.settings-container {
padding: 14px;
gap: 14px;
}
}
}
@media (max-width: 768px) {
#settings-container {
.settings-container {
padding: 12px;
}
.item-content {
padding: 8px 14px;
}
.general-settings {
gap: 20px;
.nickname-input-container {
max-width: none;
}
}
// Ajustes para tablet en modo portrait
.item-menu {
width: 140px;
min-width: 140px;
padding-right: 10px;
}
}
}
@media (max-width: 640px) {
#settings-container {
.settings-container {
padding: 10px;
}
.item-content {
padding: 8px 10px;
}
.general-settings {
gap: 18px;
}
}
}
@media (max-width: 480px) {
#settings-container {
.settings-container {
padding: 8px;
}
.item-content {
padding: 6px 8px;
}
.general-settings {
gap: 16px;
padding: 4px 0;
}
// Mobile horizontal menu adjustments mejorados
&.vertical-layout .item-menu {
mat-selection-list {
gap: 6px;
padding: 0 6px;
}
mat-list-option {
min-height: 56px;
padding: 6px 4px;
.option-text {
font-size: 10px;
}
mat-icon {
font-size: 18px;
width: 18px;
height: 18px;
}
}
}
}
}
// High DPI / small screens optimization
@media (max-width: 360px) {
#settings-container {
.settings-container {
padding: 6px;
}
.item-content {
padding: 4px 6px;
}
.general-settings {
gap: 14px;
.nickname-section .input-label {
font-size: 13px;
margin-bottom: 10px;
}
}
&.vertical-layout .item-menu {
mat-list-option {
min-height: 52px;
padding: 4px 2px;
mat-icon {
font-size: 18px;
width: 18px;
height: 18px;
}
.option-text {
font-size: 11px;
}
}
}
}
}

View File

@ -1,11 +1,13 @@
import { Component, EventEmitter, OnInit, Output } from '@angular/core';
import { Subscription } from 'rxjs';
import { Component, ContentChild, EventEmitter, OnInit, Output, TemplateRef } from '@angular/core';
import { Subject, takeUntil } from 'rxjs';
import { PanelStatusInfo, PanelSettingsOptions, PanelType } from '../../../models/panel.model';
import { OpenViduComponentsConfigService } from '../../../services/config/directive-config.service';
import { PanelService } from '../../../services/panel/panel.service';
import { PlatformService } from '../../../services/platform/platform.service';
import { ViewportService } from '../../../services/viewport/viewport.service';
import { CustomDevice } from '../../../models/device.model';
import { LangOption } from '../../../models/lang.model';
import { SettingsPanelGeneralAdditionalElementsDirective } from '../../../directives/template/internals.directive';
/**
* @internal
@ -13,7 +15,8 @@ import { LangOption } from '../../../models/lang.model';
@Component({
selector: 'ov-settings-panel',
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 {
@Output() onVideoEnabledChanged = new EventEmitter<boolean>();
@ -21,17 +24,50 @@ export class SettingsPanelComponent implements OnInit {
@Output() onAudioEnabledChanged = new EventEmitter<boolean>();
@Output() onAudioDeviceChanged = new EventEmitter<CustomDevice>();
@Output() onLangChanged = new EventEmitter<LangOption>();
/**
* @internal
* ContentChild for custom elements in general section
*/
@ContentChild(SettingsPanelGeneralAdditionalElementsDirective)
externalGeneralAdditionalElements!: SettingsPanelGeneralAdditionalElementsDirective;
settingsOptions: typeof PanelSettingsOptions = PanelSettingsOptions;
selectedOption: PanelSettingsOptions = PanelSettingsOptions.GENERAL;
showCameraButton: boolean = true;
showMicrophoneButton: boolean = true;
showCaptions: boolean = true;
panelSubscription: Subscription;
showThemeSelector: boolean = false;
isMobile: boolean = false;
private captionsSubs: Subscription;
private destroy$ = new Subject<void>();
/**
* @internal
* Gets the template for additional elements in general section
*/
get generalAdditionalElementsTemplate(): TemplateRef<any> | undefined {
return this.externalGeneralAdditionalElements?.template;
}
constructor(
private panelService: PanelService,
private platformService: PlatformService,
private libService: OpenViduComponentsConfigService
private libService: OpenViduComponentsConfigService,
public viewportService: ViewportService
) {}
// Computed properties for responsive behavior
get isCompactView(): boolean {
return this.viewportService.isMobileView() || this.viewportService.isTabletDown();
}
get isVerticalLayout(): boolean {
return this.viewportService.isMobileView();
}
get shouldHideMenuText(): boolean {
return !this.viewportService.isMobileView() && this.viewportService.isTablet();
}
ngOnInit() {
this.isMobile = this.platformService.isMobile();
this.subscribeToPanelToggling();
@ -39,7 +75,8 @@ export class SettingsPanelComponent implements OnInit {
}
ngOnDestroy() {
if (this.captionsSubs) this.captionsSubs.unsubscribe();
this.destroy$.next();
this.destroy$.complete();
}
close() {
@ -50,13 +87,14 @@ export class SettingsPanelComponent implements OnInit {
}
private subscribeToDirectives() {
this.captionsSubs = this.libService.captionsButton$.subscribe((value: boolean) => {
this.showCaptions = value;
});
this.libService.cameraButton$.pipe(takeUntil(this.destroy$)).subscribe((value: boolean) => (this.showCameraButton = value));
this.libService.microphoneButton$.pipe(takeUntil(this.destroy$)).subscribe((value: boolean) => (this.showMicrophoneButton = value));
this.libService.captionsButton$.pipe(takeUntil(this.destroy$)).subscribe((value: boolean) => (this.showCaptions = value));
this.libService.showThemeSelector$.pipe(takeUntil(this.destroy$)).subscribe((value: boolean) => (this.showThemeSelector = value));
}
private subscribeToPanelToggling() {
this.panelSubscription = this.panelService.panelStatusObs.subscribe((ev: PanelStatusInfo) => {
this.panelService.panelStatusObs.pipe(takeUntil(this.destroy$)).subscribe((ev: PanelStatusInfo) => {
if (ev.panelType === PanelType.SETTINGS && !!ev.subOptionType) {
this.selectedOption = ev.subOptionType as PanelSettingsOptions;
}

View File

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

View File

@ -1,15 +1,25 @@
import { ChangeDetectorRef, Component, EventEmitter, HostListener, Input, OnDestroy, OnInit, Output } from '@angular/core';
import { Subscription } from 'rxjs';
import {
ChangeDetectionStrategy,
ChangeDetectorRef,
Component,
EventEmitter,
HostListener,
Input,
OnDestroy,
OnInit,
Output
} from '@angular/core';
import { LocalTrack, Track } from 'livekit-client';
import { filter, Subject, take, takeUntil } from 'rxjs';
import { CustomDevice } from '../../models/device.model';
import { LangOption } from '../../models/lang.model';
import { ILogger } from '../../models/logger.model';
import { CdkOverlayService } from '../../services/cdk-overlay/cdk-overlay.service';
import { OpenViduComponentsConfigService } from '../../services/config/directive-config.service';
import { LoggerService } from '../../services/logger/logger.service';
import { OpenViduService } from '../../services/openvidu/openvidu.service';
import { TranslateService } from '../../services/translate/translate.service';
import { LocalTrack } from 'livekit-client';
import { CustomDevice } from '../../models/device.model';
import { LangOption } from '../../models/lang.model';
import { StorageService } from '../../services/storage/storage.service';
import { ViewportService } from '../../services/viewport/viewport.service';
/**
* @internal
@ -17,7 +27,9 @@ import { StorageService } from '../../services/storage/storage.service';
@Component({
selector: 'ov-pre-join',
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 {
@Input() set error(error: { name: string; message: string } | undefined) {
@ -31,24 +43,30 @@ export class PreJoinComponent implements OnInit, OnDestroy {
@Output() onReadyToJoin = new EventEmitter<any>();
_error: string | undefined;
windowSize: number;
windowSize!: number;
isLoading = true;
participantName: string | undefined;
participantName: string | undefined = '';
/**
* @ignore
*/
isMinimal: boolean = false;
showCameraButton: boolean = true;
showMicrophoneButton: boolean = true;
showLogo: boolean = true;
showParticipantName: boolean = true;
// Future feature preparation
backgroundEffectEnabled: boolean = true; // Enable virtual backgrounds by default
showBackgroundPanel: boolean = false;
videoTrack: LocalTrack | undefined;
audioTrack: LocalTrack | undefined;
private tracks: LocalTrack[];
isVideoEnabled: boolean = false;
hasVideoDevices: boolean = true;
private tracks: LocalTrack[] = [];
private log: ILogger;
private screenShareStateSubscription: Subscription;
private minimalSub: Subscription;
private displayLogoSub: Subscription;
private destroy$ = new Subject<void>();
private shouldRemoveTracksWhenComponentIsDestroyed: boolean = true;
@HostListener('window:resize')
@ -61,98 +79,177 @@ export class PreJoinComponent implements OnInit, OnDestroy {
private libService: OpenViduComponentsConfigService,
private cdkSrv: CdkOverlayService,
private openviduService: OpenViduService,
private storageService: StorageService,
private translateService: TranslateService,
private changeDetector: ChangeDetectorRef
private changeDetector: ChangeDetectorRef,
protected viewportService: ViewportService
) {
this.log = this.loggerSrv.get('PreJoinComponent');
}
async ngOnInit() {
this.subscribeToPrejoinDirectives();
await this.initializeDevices();
await this.initializeDevicesWithRetry();
this.windowSize = window.innerWidth;
this.isLoading = false;
this.changeDetector.markForCheck();
}
ngAfterContentChecked(): void {
this.changeDetector.detectChanges();
}
// ngAfterContentChecked(): void {
// // this.changeDetector.detectChanges();
// this.isLoading = false;
// }
async ngOnDestroy() {
this.destroy$.next();
this.destroy$.complete();
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) {
this.tracks.forEach((track) => {
this.tracks?.forEach((track) => {
track.stop();
});
}
}
private async initializeDevices() {
try {
this.tracks = await this.openviduService.createLocalTracks();
this.openviduService.setLocalTracks(this.tracks);
this.videoTrack = this.tracks.find((track) => track.kind === 'video');
this.audioTrack = this.tracks.find((track) => track.kind === 'audio');
} catch (error) {
this.log.e('Error creating local tracks:', error);
}
}
onDeviceSelectorClicked() {
// Some devices as iPhone do not show the menu panels correctly
// Updating the container where the panel is added fix the problem.
this.cdkSrv.setSelector('#prejoin-container');
}
joinSession() {
if (!this.participantName) {
join() {
if (this.showParticipantName && !this.participantName?.trim()) {
this._error = this.translateService.translate('PREJOIN.NICKNAME_REQUIRED');
return;
}
// Clear any previous errors
this._error = undefined;
// Mark tracks as permanent for avoiding to be removed in ngOnDestroy
this.shouldRemoveTracksWhenComponentIsDestroyed = false;
this.onReadyToJoin.emit();
// Assign participant name to the observable if it is defined
if (this.participantName?.trim()) {
this.libService.updateGeneralConfig({ participantName: this.participantName.trim() });
this.libService.participantName$
.pipe(
filter((name) => name === this.participantName?.trim()),
take(1)
)
.subscribe(() => this.onReadyToJoin.emit());
} else {
// No participant name to set, emit immediately
this.onReadyToJoin.emit();
}
}
onParticipantNameChanged(name: string) {
this.participantName = name;
this.participantName = name?.trim() || '';
// Clear error when user starts typing
if (this._error && this.participantName) {
this._error = undefined;
}
}
onEnterPressed() {
this.joinSession();
this.join();
}
private subscribeToPrejoinDirectives() {
this.minimalSub = this.libService.minimal$.subscribe((value: boolean) => {
this.libService.minimal$.pipe(takeUntil(this.destroy$)).subscribe((value: boolean) => {
this.isMinimal = value;
// this.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.cd.markForCheck();
this.changeDetector.markForCheck();
});
this.libService.participantName$.subscribe((value: string) => {
if (value) this.participantName = value;
// this.cd.markForCheck();
this.libService.participantName$.pipe(takeUntil(this.destroy$)).subscribe((value: string) => {
if (value) {
this.participantName = value;
this.changeDetector.markForCheck();
}
});
this.libService.prejoinDisplayParticipantName$.pipe(takeUntil(this.destroy$)).subscribe((value: boolean) => {
this.showParticipantName = value;
this.changeDetector.markForCheck();
});
}
async videoEnabledChanged(enabled: boolean) {
if (enabled && !this.videoTrack) {
this.isVideoEnabled = enabled;
if (!enabled) {
this.closeBackgroundPanel();
} else if (!this.videoTrack) {
const newVideoTrack = await this.openviduService.createLocalTracks(true, false);
this.videoTrack = newVideoTrack[0];
this.tracks.push(this.videoTrack);
this.openviduService.setLocalTracks(this.tracks);
}
this.onVideoEnabledChanged.emit(enabled);
}
async videoDeviceChanged(device: CustomDevice) {
try {
this.log.d('Video device changed to:', device);
// Get the updated tracks from the service
const updatedTracks = this.openviduService.getLocalTracks();
// Find the new video track
const newVideoTrack = updatedTracks.find((track) => track.kind === Track.Kind.Video);
// if (newVideoTrack && newVideoTrack !== this.videoTrack) {
this.tracks = updatedTracks;
this.videoTrack = newVideoTrack;
this.onVideoDeviceChanged.emit(device);
} catch (error) {
this.log.e('Error handling video device change:', error);
this.handleError(error);
}
}
onVideoDevicesLoaded(devices: CustomDevice[]) {
this.hasVideoDevices = devices.length > 0;
}
audioDeviceChanged(device: CustomDevice) {
try {
this.log.d('Audio device changed to:', device);
// Get the updated tracks from the service
const updatedTracks = this.openviduService.getLocalTracks();
// Find the new audio track
const newAudioTrack = updatedTracks.find((track) => track.kind === Track.Kind.Audio);
this.tracks = updatedTracks;
this.audioTrack = newAudioTrack;
this.onAudioDeviceChanged.emit(device);
} catch (error) {
this.log.e('Error handling audio device change:', error);
this.handleError(error);
}
}
async audioEnabledChanged(enabled: boolean) {
if (enabled && !this.audioTrack) {
const newAudioTrack = await this.openviduService.createLocalTracks(false, true);
@ -162,4 +259,68 @@ export class PreJoinComponent implements OnInit, OnDestroy {
}
this.onAudioEnabledChanged.emit(enabled);
}
/**
* Toggle virtual background panel visibility with smooth animation
*/
toggleBackgroundPanel() {
// Add a small delay to ensure smooth transition
if (!this.showBackgroundPanel) {
// Opening panel
this.showBackgroundPanel = true;
this.changeDetector.markForCheck();
} else {
// Closing panel - add slight delay for smooth animation
setTimeout(() => {
this.showBackgroundPanel = false;
this.changeDetector.markForCheck();
}, 50);
}
}
/**
* Close virtual background panel with smooth animation
*/
closeBackgroundPanel() {
// Add animation delay for smooth closing
setTimeout(() => {
this.showBackgroundPanel = false;
this.changeDetector.markForCheck();
}, 100);
}
/**
* Enhanced error handling with better UX
*/
private handleError(error: any) {
this.log.e('PreJoin component error:', error);
this._error = error.message || 'An unexpected error occurred';
this.changeDetector.markForCheck();
}
/**
* Improved device initialization with error handling
*/
private async initializeDevicesWithRetry(maxRetries: number = 3): Promise<void> {
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
this.tracks = await this.openviduService.createLocalTracks();
this.openviduService.setLocalTracks(this.tracks);
this.videoTrack = this.tracks.find((track) => track.kind === Track.Kind.Video);
this.audioTrack = this.tracks.find((track) => track.kind === Track.Kind.Audio);
this.isVideoEnabled = this.openviduService.isVideoTrackEnabled();
return; // Success, exit retry loop
} catch (error) {
this.log.w(`Device initialization attempt ${attempt} failed:`, error);
if (attempt === maxRetries) {
this.handleError(error);
} else {
// Wait before retrying
await new Promise((resolve) => setTimeout(resolve, 1000 * attempt));
}
}
}
}
}

View File

@ -1,31 +1,36 @@
<div id="spinner" *ngIf="loading" @sessionAnimation>
<mat-spinner [diameter]="50"></mat-spinner>
<span>{{ 'ROOM.JOINING' | translate }}</span>
</div>
<div id="session-container" *ngIf="!loading" @sessionAnimation>
<mat-sidenav-container #container #videoContainer class="sidenav-container">
<mat-sidenav
#sidenav
mode="{{ sidenavMode }}"
position="end"
class="sidenav-menu"
[ngClass]="{big: settingsPanelOpened}"
fixedInViewport="true"
fixedTopGap="0"
fixedBottomGap="0"
>
<ng-container *ngTemplateOutlet="panelTemplate"></ng-container>
</mat-sidenav>
<mat-sidenav-content class="sidenav-main">
<div id="layout-container" #layoutContainer>
<ng-container *ngTemplateOutlet="layoutTemplate"></ng-container>
</div>
</mat-sidenav-content>
</mat-sidenav-container>
<div id="footer-container" *ngIf="toolbarTemplate">
<ng-container *ngTemplateOutlet="toolbarTemplate"></ng-container>
@if (loading) {
<div id="spinner" [@inOutAnimation]>
<mat-spinner [diameter]="50"></mat-spinner>
<span>{{ 'ROOM.JOINING' | translate }}</span>
</div>
</div>
} @else {
@if (viewportService.shouldShowLandscapeWarning()) {
<ov-landscape-warning></ov-landscape-warning>
}
<div id="session-container" [@inOutAnimation]>
<mat-sidenav-container #container #videoContainer class="sidenav-container">
<mat-sidenav
#sidenav
mode="{{ sidenavMode }}"
position="end"
class="sidenav-menu"
[ngClass]="{ big: settingsPanelOpened }"
fixedInViewport="true"
fixedTopGap="0"
fixedBottomGap="0"
>
<ng-container *ngTemplateOutlet="panelTemplate"></ng-container>
</mat-sidenav>
<mat-sidenav-content class="sidenav-main">
<div id="layout-container" #layoutContainer>
<ng-container *ngTemplateOutlet="layoutTemplate"></ng-container>
</div>
</mat-sidenav-content>
</mat-sidenav-container>
<div id="footer-container" *ngIf="toolbarTemplate">
<ng-container *ngTemplateOutlet="toolbarTemplate"></ng-container>
</div>
</div>
}

View File

@ -3,6 +3,7 @@
/* min-width: 400px; */
height: 100%;
}
#spinner {
position: absolute;
top: 40%;
@ -84,29 +85,37 @@
width: 100%;
height: 100%;
z-index: 1000;
background-color: black;
background-color: var(--ov-video-background, var(--ov-primary-action-color));
opacity: 80%;
position: absolute;
}
@media only screen and (max-width: 600px) {
@media only screen and (max-width: 768px) {
#session-container {
width: 100%;
/* position: fixed; */
}
.sidenav-menu {
width: 100% !important;
max-width: 100% !important;
}
.big {
width: 100% !important;
max-width: 100% !important;
}
}
/* TODO(mdc-migration): The following rule targets internal classes of button that may no longer apply for the MDC version. */
::ng-deep .mat-button-toggle-appearance-standard .mat-button-toggle-label-content {
padding: 1px !important;
}
::ng-deep .mat-mdc-input-element {
caret-color: #000000;
caret-color: var(--ov-video-background, var(--ov-primary-action-color));
}
/* TODO(mdc-migration): The following rule targets internal classes of option that may no longer apply for the MDC version. */
::ng-deep .mat-primary .mat-mdc-option.mat-selected:not(.mat-option-disabled) {
color: #000000;
color: var(--ov-video-background, var(--ov-primary-action-color)) !important;
}
/* TODO(mdc-migration): The following rule targets internal classes of form-field that may no longer apply for the MDC version. */

View File

@ -3,7 +3,7 @@ import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
import { ActionService } from '../../services/action/action.service';
import { ActionServiceMock } from '../../services/action/action.service.mock';
import { ActionServiceMock } from '../../../test-helpers/action.service.mock';
import { ChatService } from '../../services/chat/chat.service';
import { ChatServiceMock } from '../../services/chat/chat.service.mock';

View File

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

View File

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

View File

@ -1,10 +1,10 @@
import { Component, EventEmitter, OnDestroy, OnInit, Output } from '@angular/core';
import { Subscription } from 'rxjs';
import { Component, effect, EventEmitter, Input, OnInit, Output, Signal, WritableSignal } from '@angular/core';
import { CustomDevice } from '../../../models/device.model';
import { ILogger } from '../../../models/logger.model';
import { DeviceService } from '../../../services/device/device.service';
import { LoggerService } from '../../../services/logger/logger.service';
import { ParticipantService } from '../../../services/participant/participant.service';
import { StorageService } from '../../../services/storage/storage.service';
import { ParticipantModel } from '../../../models/participant.model';
/**
* @internal
@ -12,39 +12,46 @@ import { ParticipantModel } from '../../../models/participant.model';
@Component({
selector: 'ov-audio-devices-select',
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 {
@Input() compact: boolean = false;
@Output() onAudioDeviceChanged = new EventEmitter<CustomDevice>();
@Output() onAudioEnabledChanged = new EventEmitter<boolean>();
microphoneStatusChanging: boolean;
hasAudioDevices: boolean;
isMicrophoneEnabled: boolean;
microphoneSelected: CustomDevice | undefined;
microphones: CustomDevice[] = [];
private localParticipantSubscription: Subscription;
microphoneStatusChanging: boolean = false;
isMicrophoneEnabled: boolean = false;
private log: ILogger;
// Expose signals directly from service (reactive)
protected readonly microphones: WritableSignal<CustomDevice[]>;
protected readonly microphoneSelected: WritableSignal<CustomDevice | undefined>;
protected readonly hasAudioDevices: Signal<boolean>;
constructor(
private deviceSrv: DeviceService,
private storageSrv: StorageService,
private participantService: ParticipantService
) {}
private participantService: ParticipantService,
private loggerSrv: LoggerService
) {
this.log = this.loggerSrv.get('AudioDevicesComponent');
this.microphones = this.deviceSrv.microphones;
this.microphoneSelected = this.deviceSrv.microphoneSelected;
this.hasAudioDevices = this.deviceSrv.hasAudioDevices;
async ngOnInit() {
this.subscribeToParticipantMediaProperties();
this.hasAudioDevices = this.deviceSrv.hasAudioDeviceAvailable();
if (this.hasAudioDevices) {
this.microphones = this.deviceSrv.getMicrophones();
this.microphoneSelected = this.deviceSrv.getMicrophoneSelected();
}
this.isMicrophoneEnabled = this.participantService.isMyMicrophoneEnabled();
// Use effect instead of subscription for reactive updates
effect(() => {
const participant = this.participantService.localParticipantSignal();
if (participant) {
this.isMicrophoneEnabled = participant.isMicrophoneEnabled;
this.storageSrv.setMicrophoneEnabled(this.isMicrophoneEnabled);
}
});
}
ngOnDestroy() {
this.microphones = [];
if (this.localParticipantSubscription) this.localParticipantSubscription.unsubscribe();
async ngOnInit() {
this.isMicrophoneEnabled = this.participantService.isMyMicrophoneEnabled();
}
async toggleMic(event: any) {
@ -58,14 +65,18 @@ export class AudioDevicesComponent implements OnInit, OnDestroy {
}
async onMicrophoneSelected(event: any) {
const device: CustomDevice = event?.value;
if (this.deviceSrv.needUpdateAudioTrack(device)) {
this.microphoneStatusChanging = true;
await this.participantService.switchMicrophone(device.device);
this.deviceSrv.setMicSelected(device.device);
this.microphoneSelected = this.deviceSrv.getMicrophoneSelected();
try {
const device: CustomDevice = event?.value;
if (this.deviceSrv.needUpdateAudioTrack(device)) {
this.microphoneStatusChanging = true;
await this.participantService.switchMicrophone(device.device);
this.deviceSrv.setMicSelected(device.device);
this.onAudioDeviceChanged.emit(this.microphoneSelected());
}
} catch (error) {
this.log.e('Error switching microphone', error);
} finally {
this.microphoneStatusChanging = false;
this.onAudioDeviceChanged.emit(this.microphoneSelected);
}
}
@ -76,17 +87,4 @@ export class AudioDevicesComponent implements OnInit, OnDestroy {
compareObjectDevices(o1: CustomDevice, o2: CustomDevice): boolean {
return o1.label === o2.label;
}
/**
* This subscription is necessary to update the microphone status when the user changes it from toolbar and
* the settings panel is opened. With this, the microphone status is updated in the settings panel.
*/
private subscribeToParticipantMediaProperties() {
this.localParticipantSubscription = this.participantService.localParticipant$.subscribe((p: ParticipantModel | undefined) => {
if (p) {
this.isMicrophoneEnabled = p.isMicrophoneEnabled;
this.storageSrv.setMicrophoneEnabled(this.isMicrophoneEnabled);
}
});
}
}

View File

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

View File

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

View File

@ -1,21 +1,32 @@
$ov-surface-color-lighter: color-mix(in srgb, var(--ov-surface-color), #fff 5%);
@use '../selector-shared' as shared;
.lang-button {
background-color: var(--ov-primary-action-color) !important;
color: var(--ov-secondary-action-color) !important;
}
.lang-button .mat-icon {
color: var(--ov-secondary-action-color);
:host {
display: inline-block;
}
::ng-deep .mat-mdc-menu-panel {
border-radius: var(--ov-surface-radius) !important;
background-color: $ov-surface-color-lighter !important;
box-shadow: 1px 1px 5px 0px rgba(0, 0, 0, 0.2) !important;
.language-selector-container {
.compact-lang-button {
width: 40px;
height: 40px;
background: rgba(255, 255, 255, 0.9);
backdrop-filter: blur(10px);
border: 1px solid var(--ov-border-color, #e0e0e0);
border-radius: 10px;
transition: all 0.2s ease;
color: var(--ov-text-secondary-color, #666);
mat-icon {
font-size: 18px;
width: 18px;
height: 18px;
}
}
.full-lang-button {
@include shared.selector-button('lang-icon', 'lang-name', 'expand-icon');
}
}
}
::ng-deep .mat-mdc-menu-item,
.mat-mdc-menu-item:visited,
.mat-mdc-menu-item:link {
color: var(--ov-text-surface-color) !important;
::ng-deep .language-menu.mat-mdc-menu-panel {
@include shared.selector-menu('language-option', 'lang-option-name');
}

View File

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

View File

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

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