Compare commits

..

No commits in common. "master" and "v3.0.0" have entirely different histories.

247 changed files with 18203 additions and 52729 deletions

113
.github/workflows/openvidu-ce-test.yml vendored Normal file
View File

@ -0,0 +1,113 @@
name: OpenVidu CE Tests
on:
push:
branches:
- master
paths-ignore:
- ".github/workflows/openvidu-components-angular-E2E.yml"
- "openvidu-components-angular/**"
- "openvidu-server/docker/**"
- "openvidu-server/deployments/**"
pull_request:
branches:
- master
workflow_dispatch:
inputs:
TEST_IMAGE:
description: "Docker image where to run the tests"
required: true
default: "openvidu/openvidu-test-e2e:22.04"
KURENTO_JAVA_COMMIT:
description: 'Commit to use in kurento-java dependencies. If "default" the release version declared in property "version.kurento" of openvidu-parent/pom.xml will be used'
required: true
default: "default"
KURENTO_MEDIA_SERVER_IMAGE:
description: "Docker image of kurento-media-server"
required: true
default: "kurento/kurento-media-server:7.0.1"
DOCKER_RECORDING_VERSION:
description: "Force version of openvidu/openvidu-recording container"
required: true
default: "default"
CHROME_VERSION:
description: "Version of Chrome to use. Must be a valid image tag from https://hub.docker.com/r/selenium/standalone-chrome/tags"
required: true
default: "latest"
FIREFOX_VERSION:
description: "Version of Firefox to use. Must be a valid image tag from https://hub.docker.com/r/selenium/standalone-firefox/tags"
required: true
default: "latest"
EDGE_VERSION:
description: "Version of Edge to use. Must be a valid image tag from https://hub.docker.com/r/selenium/standalone-edge/tags"
required: true
default: "latest"
jobs:
main:
runs-on: ubuntu-latest
container:
image: ${{ inputs.TEST_IMAGE || 'openvidu/openvidu-test-e2e:22.04' }}
volumes:
- /var/run/docker.sock:/var/run/docker.sock
- /opt/openvidu:/opt/openvidu
env:
TEST_IMAGE: ${{ inputs.TEST_IMAGE || 'openvidu/openvidu-test-e2e:22.04' }}
KURENTO_SNAPSHOTS_URL: ${{ secrets.KURENTO_SNAPSHOTS_URL }}
KURENTO_MEDIA_SERVER_IMAGE: ${{ inputs.KURENTO_MEDIA_SERVER_IMAGE || 'kurento/kurento-media-server:7.0.1' }}
KURENTO_JAVA_COMMIT: ${{ inputs.KURENTO_JAVA_COMMIT || 'default' }}
DOCKER_RECORDING_VERSION: ${{ inputs.DOCKER_RECORDING_VERSION || 'default' }}
CHROME_VERSION: ${{ inputs.CHROME_VERSION || 'latest' }}
FIREFOX_VERSION: ${{ inputs.FIREFOX_VERSION || 'latest' }}
EDGE_VERSION: ${{ inputs.EDGE_VERSION || 'latest' }}
steps:
- uses: actions/checkout@v3
- name: Setup scripts
run: |
curl -sOJ --output-dir /opt https://raw.githubusercontent.com/OpenVidu/openvidu/master/ci-scripts/commons/build.sh
curl -sOJ --output-dir /opt https://raw.githubusercontent.com/OpenVidu/openvidu/master/ci-scripts/commons/test-utils.sh
cp ci-scripts/openvidu-e2e-tests.sh /opt/openvidu-e2e-tests.sh
find /opt/*.sh -type f -print0 | xargs -0 chmod u+x
- name: Clean environment
run: /opt/build.sh --clean-environment
- name: Prepare test environment
run: /opt/test-utils.sh --prepare-test-environment "${TEST_IMAGE}"
- name: Check and prepare kurento snapshots
run: /opt/build.sh --check-and-prepare-kurento-snapshot
- name: Use specific kurento-java commit
if: ${{ env.KURENTO_JAVA_COMMIT != 'default'}}
run: /opt/test-utils.sh --use-specific-kurento-java-commit
- name: Build openvidu-browser
run: /opt/build.sh --build-openvidu-browser
- name: Build openvidu-node-client
run: /opt/build.sh --build-openvidu-node-client
- name: Build openvidu-java-client
run: /opt/build.sh --build-openvidu-java-client
- name: Build openvidu-parent
run: /opt/build.sh --build-openvidu-parent
- name: Build openvidu-testapp
run: /opt/build.sh --build-openvidu-testapp
- name: Build openvidu-server dashboard
run: /opt/build.sh --build-openvidu-server-dashboard true
- name: Build openvidu-server
run: /opt/build.sh --build-openvidu-server
- name: openvidu-server unit tests
run: /opt/openvidu-e2e-tests.sh --openvidu-server-unit-tests
- name: openvidu-server integration tests
run: /opt/openvidu-e2e-tests.sh --openvidu-server-integration-tests
- name: Environment launch Kurento
run: /opt/openvidu-e2e-tests.sh --environment-launch-kurento
- name: Serve openvidu-testapp
run: /opt/test-utils.sh --serve-openvidu-testapp
- name: OpenVidu E2E Tests Kurento
run: /opt/openvidu-e2e-tests.sh --openvidu-e2e-tests-kurento
- name: Test reports
uses: mikepenz/action-junit-report@v3
if: always() # always run even if the previous step fails
with:
report_paths: "**/target/surefire-reports/TEST-*.xml"
- name: Upload logs
uses: actions/upload-artifact@v3
if: always() # always run even if the previous step fails
with:
name: Logs
path: |
/opt/openvidu/*.log

View File

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

View File

@ -1,56 +0,0 @@
name: OpenVidu integration tests
on:
push:
branches:
- master
paths:
- "openvidu-test-integration/**"
- ".github/workflows/openvidu-integration-tests.yml"
workflow_dispatch:
jobs:
integration-tests:
name: Integration tests
timeout-minutes: 30
runs-on: ubuntu-latest
steps:
- name: Configure OpenVidu Local Deployment
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
- name: Checkout current repository
uses: actions/checkout@v4
with:
path: openvidu
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: 22
- name: Install dependencies
working-directory: ./openvidu/openvidu-test-integration
run: npm ci
- name: Run tests
working-directory: ./openvidu/openvidu-test-integration
run: npm run test:ci
- name: Upload report
uses: actions/upload-artifact@v4
if: always()
with:
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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,277 @@
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

@ -0,0 +1,40 @@
<!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,21 +1,21 @@
// Ideally we'd use an editor or import shaders directly from the API. // Ideally we'd use an editor or import shaders directly from the API.
import { FilterStream } from './filter-stream.js'; import { FilterStream } from './filter-stream.js';
export const monkeyPatchMediaDevices = () => { export default function monkeyPatchMediaDevices() {
const enumerateDevicesFn = MediaDevices.prototype.enumerateDevices; const enumerateDevicesFn = MediaDevices.prototype.enumerateDevices;
const getUserMediaFn = MediaDevices.prototype.getUserMedia; const getUserMediaFn = MediaDevices.prototype.getUserMedia;
const getDisplayMediaFn = MediaDevices.prototype.getDisplayMedia; const getDisplayMediaFn = MediaDevices.prototype.getDisplayMedia;
const fakeVideoDevice = { const fakeVideoDevice = {
deviceId: 'virtual_video', deviceId: 'virtual',
groupId: '', groupID: '',
kind: 'videoinput', kind: 'videoinput',
label: 'custom_fake_video_1' label: 'custom_fake_video_1'
}; };
const fakeAudioDevice = { const fakeAudioDevice = {
deviceId: 'virtual_audio', deviceId: 'virtual',
groupId: '', groupID: '',
kind: 'audioinput', kind: 'audioinput',
label: 'custom_fake_audio_1' label: 'custom_fake_audio_1'
}; };
@ -29,21 +29,8 @@ export const monkeyPatchMediaDevices = () => {
const getUserMediaMonkeyPatch = async function () { const getUserMediaMonkeyPatch = async function () {
const args = arguments[0]; const args = arguments[0];
if (args.audio && (args.audio.deviceId === 'virtual_audio' || args.audio.deviceId?.exact === 'virtual_audio')) {
const constraints = {
audio: {
facingMode: args.facingMode,
advanced: args.audio.advanced,
deviceId: fakeAudioDevice.deviceId
},
video: false
};
const res = await getUserMediaFn.call(navigator.mediaDevices, constraints);
return res;
} else if (args.video && (args.video.deviceId === 'virtual_video' || args.video.deviceId?.exact === 'virtual_video')) {
const { deviceId, advanced, width, height } = args.video; const { deviceId, advanced, width, height } = args.video;
if (deviceId === 'virtual' || deviceId?.exact === 'virtual') {
const constraints = { const constraints = {
video: { video: {
facingMode: args.facingMode, facingMode: args.facingMode,

View File

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

View File

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

View File

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

View File

@ -1,19 +1,20 @@
import { Builder, Key, WebDriver } from 'selenium-webdriver'; import { Builder, Key, WebDriver } from 'selenium-webdriver';
import { TestAppConfig } from './selenium.conf'; import { OPENVIDU_CALL_SERVER } from '../config';
import { OpenViduComponentsPO } from './utils.po.test'; import { WebComponentConfig } from '../selenium.conf';
import { OpenViduComponentsPO } from '../utils.po.test';
const url = TestAppConfig.appUrl; const url = `${WebComponentConfig.appUrl}?OV_URL=${OPENVIDU_CALL_SERVER}`;
describe('Testing videoconference EVENTS', () => { describe('Testing videoconference EVENTS', () => {
let browser: WebDriver; let browser: WebDriver;
let utils: OpenViduComponentsPO; let utils: OpenViduComponentsPO;
const isHeadless: boolean = (TestAppConfig.browserOptions as any).options_.args.includes('--headless'); const isHeadless: boolean = (WebComponentConfig.browserOptions as any).options_.args.includes('--headless');
async function createChromeBrowser(): Promise<WebDriver> { async function createChromeBrowser(): Promise<WebDriver> {
return await new Builder() return await new Builder()
.forBrowser(TestAppConfig.browserName) .forBrowser(WebComponentConfig.browserName)
.withCapabilities(TestAppConfig.browserCapabilities) .withCapabilities(WebComponentConfig.browserCapabilities)
.setChromeOptions(TestAppConfig.browserOptions) .setChromeOptions(WebComponentConfig.browserOptions)
.usingServer(TestAppConfig.seleniumAddress) .usingServer(WebComponentConfig.seleniumAddress)
.build(); .build();
} }
@ -23,10 +24,6 @@ describe('Testing videoconference EVENTS', () => {
}); });
afterEach(async () => { afterEach(async () => {
try {
// leaving room if connected
await utils.leaveRoom();
} catch (error) {}
await browser.quit(); await browser.quit();
}); });
@ -60,6 +57,23 @@ describe('Testing videoconference EVENTS', () => {
expect(await utils.isPresent('#onTokenRequested')).toBeTrue(); 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 () => { it('should receive the onVideoEnabledChanged event when clicking on the prejoin', async () => {
await browser.get(url); await browser.get(url);
await utils.checkPrejoinIsPresent(); await utils.checkPrejoinIsPresent();
@ -599,7 +613,7 @@ describe('Testing videoconference EVENTS', () => {
expect(await utils.isPresent('#onReadyToJoin')).toBeFalse(); expect(await utils.isPresent('#onReadyToJoin')).toBeFalse();
}); });
// PARTICIPANT EVENTS // * PUBLISHER EVENTS
it('should receive onParticipantCreated event from LOCAL participant', async () => { it('should receive onParticipantCreated event from LOCAL participant', async () => {
const participantName = 'TEST_USER'; const participantName = 'TEST_USER';
@ -608,39 +622,22 @@ describe('Testing videoconference EVENTS', () => {
expect(await utils.isPresent(`#${participantName}-onParticipantCreated`)).toBeTrue(); expect(await utils.isPresent(`#${participantName}-onParticipantCreated`)).toBeTrue();
}); });
it('should receive the onParticipantLeft event', async () => { // * ROOM EVENTS
await browser.get(`${url}&prejoin=false&redirectToHome=false`);
it('should receive roomDisconnected event from LOCAL participant', async () => {
const participantName = 'TEST_USER';
let element;
await browser.get(`${url}&prejoin=false&participantName=${participantName}`);
await utils.checkSessionIsPresent(); await utils.checkSessionIsPresent();
await utils.checkToolbarIsPresent(); await utils.checkToolbarIsPresent();
// Clicking to leave button // Checking if leave button is not present
const leaveButton = await utils.waitForElement('#leave-btn'); element = await utils.waitForElement('#leave-btn');
expect(await utils.isPresent('#leave-btn')).toBeTrue(); await element.click();
await leaveButton.click();
await utils.waitForElement('#events'); await utils.waitForElement(`#roomDisconnected`);
// Checking if onParticipantLeft has been received expect(await utils.isPresent(`#roomDisconnected`)).toBeTrue();
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

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

View File

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

View File

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

View File

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

View File

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

View File

@ -6,7 +6,7 @@ if [[ -z "$BASEHREF_VERSION" ]]; then
fi fi
# Replace version from "stable" to the specified one in all TypeDoc links # Replace version from "stable" to the specified one in all TypeDoc links
grep -rl '/latest/' projects src | xargs sed -i -e 's|/latest/|/'${BASEHREF_VERSION}'/|g' grep -rl '/en/stable/' projects src | xargs sed -i -e 's|/en/stable/|/en/'${BASEHREF_VERSION}'/|g'
# Replace testapp README by openvidu-components-angular README # Replace testapp README by openvidu-components-angular README
mv README.md README-testapp.md mv README.md README-testapp.md
@ -16,7 +16,7 @@ cp ./projects/openvidu-components-angular/README.md .
npm run doc:build npm run doc:build
# Return links to "stable" version # Return links to "stable" version
grep -rl '/'${BASEHREF_VERSION}'/' projects src | xargs sed -i -e 's|/'${BASEHREF_VERSION}'/|/latest/|g' grep -rl '/en/'${BASEHREF_VERSION}'/' projects src | xargs sed -i -e 's|/en/'${BASEHREF_VERSION}'/|/en/stable/|g'
# Undo changes with READMEs # Undo changes with READMEs
rm README.md rm README.md

View File

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

View File

@ -8,7 +8,7 @@ Angular Components are the simplest way to create real-time videoconferencing ap
## Getting Started ## Getting Started
To get started with OpenVidu Components Angular, visit our [**Getting Started guide**](https://openvidu.io/latest/docs/ui-components/angular-components/). To get started with OpenVidu Components Angular, visit our [**Getting Started guide**](https://openvidu.io/docs/ui-components/angular-components/).
1. Create an Angular Project (>= 17.0.0) 1. Create an Angular Project (>= 17.0.0)
@ -92,7 +92,7 @@ You can also customize the styles in your `styles.scss` file:
## API Documentation ## API Documentation
For detailed information on OpenVidu Angular Components, refer to our [**API Reference**](https://openvidu.io/latest/docs/reference-docs/openvidu-components-angular/). For detailed information on OpenVidu Angular Components, refer to our [**API Reference**](https://openvidu.io/docs/reference-docs/openvidu-components-angular).
## Support ## Support

View File

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

View File

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

View File

@ -4,16 +4,16 @@
}, },
"name": "openvidu-components-angular", "name": "openvidu-components-angular",
"peerDependencies": { "peerDependencies": {
"@angular/animations": "^17.0.0 || ^18.0.0 || ^19.0.0", "@angular/animations": "^17.0.0 || ^18.0.0",
"@angular/cdk": "^17.0.0 || ^18.0.0 || ^19.0.0", "@angular/cdk": "^17.0.0 || ^18.0.0",
"@angular/common": "^17.0.0 || ^18.0.0 || ^19.0.0", "@angular/common": "^17.0.0 || ^18.0.0",
"@angular/core": "^17.0.0 || ^18.0.0 || ^19.0.0", "@angular/core": "^17.0.0 || ^18.0.0",
"@angular/forms": "^17.0.0 || ^18.0.0 || ^19.0.0", "@angular/forms": "^17.0.0 || ^18.0.0",
"@angular/material": "^17.0.0 || ^18.0.0 || ^19.0.0", "@angular/material": "^17.0.0 || ^18.0.0",
"autolinker": "^4.0.0", "autolinker": "^4.0.0",
"buffer": "^6.0.3", "buffer": "^6.0.3",
"livekit-client": "^2.1.0", "livekit-client": "^2.1.0",
"@livekit/track-processors": "^0.3.2" "@livekit/track-processors": "^0.3.2"
}, },
"version": "3.3.0" "version": "3.0.0"
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -8,8 +8,8 @@ import { DialogData } from '../../models/dialog.model';
*/ */
@Component({ @Component({
selector: 'ov-pro-feature-template', selector: 'ov-pro-feature-template',
template: ` template: `
<h1 mat-dialog-title>{{ data.title }}</h1> <h1 mat-dialog-title>{{ data.title }}</h1>
<div mat-dialog-content>{{ data.description }}</div> <div mat-dialog-content>{{ data.description }}</div>
<div mat-dialog-actions *ngIf="data.showActionButtons"> <div mat-dialog-actions *ngIf="data.showActionButtons">
@ -19,8 +19,7 @@ import { DialogData } from '../../models/dialog.model';
</button> </button>
<button mat-button (click)="close()">{{'PANEL.CLOSE' | translate}}</button> <button mat-button (click)="close()">{{'PANEL.CLOSE' | translate}}</button>
</div> </div>
`, `
standalone: false
}) })
export class ProFeatureDialogTemplateComponent { export class ProFeatureDialogTemplateComponent {
constructor(public dialogRef: MatDialogRef<ProFeatureDialogTemplateComponent>, @Inject(MAT_DIALOG_DATA) public data: DialogData) {} constructor(public dialogRef: MatDialogRef<ProFeatureDialogTemplateComponent>, @Inject(MAT_DIALOG_DATA) public data: DialogData) {}
@ -30,6 +29,6 @@ export class ProFeatureDialogTemplateComponent {
} }
seeMore() { seeMore() {
window.open('https://openvidu.io/pricing/#openvidu-pro', '_blank')?.focus(); window.open('https://docs.openvidu.io/en/stable/openvidu-pro/', '_blank')?.focus();
} }
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,4 +1,4 @@
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, EventEmitter, Input, OnInit, Output } from '@angular/core'; import { ChangeDetectionStrategy, ChangeDetectorRef, Component, OnInit } from '@angular/core';
import { Subscription } from 'rxjs'; import { Subscription } from 'rxjs';
import { BackgroundEffect, EffectType } from '../../../models/background-effect.model'; import { BackgroundEffect, EffectType } from '../../../models/background-effect.model';
import { PanelType } from '../../../models/panel.model'; import { PanelType } from '../../../models/panel.model';
@ -12,13 +12,9 @@ import { VirtualBackgroundService } from '../../../services/virtual-background/v
selector: 'ov-background-effects-panel', selector: 'ov-background-effects-panel',
templateUrl: './background-effects-panel.component.html', templateUrl: './background-effects-panel.component.html',
styleUrls: ['../panel.component.scss', './background-effects-panel.component.scss'], styleUrls: ['../panel.component.scss', './background-effects-panel.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush
standalone: false
}) })
export class BackgroundEffectsPanelComponent implements OnInit { export class BackgroundEffectsPanelComponent implements OnInit {
@Input() mode: 'prejoin' | 'meeting' = 'meeting';
@Output() onClose = new EventEmitter<void>();
backgroundSelectedId: string; backgroundSelectedId: string;
effectType = EffectType; effectType = EffectType;
backgroundImages: BackgroundEffect[] = []; backgroundImages: BackgroundEffect[] = [];
@ -56,14 +52,14 @@ export class BackgroundEffectsPanelComponent implements OnInit {
} }
close() { close() {
if (this.mode === 'prejoin') { this.panelService.togglePanel(PanelType.BACKGROUND_EFFECTS);
this.onClose.emit();
} else {
this.panelService.togglePanel(PanelType.BACKGROUND_EFFECTS);
}
} }
async applyBackground(effect: BackgroundEffect) { async applyBackground(effect: BackgroundEffect) {
await this.backgroundService.applyBackground(effect); if (effect.type === EffectType.NONE) {
await this.backgroundService.removeBackground();
} else {
await this.backgroundService.applyBackground(effect);
}
} }
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,16 +1,5 @@
import { import { ChangeDetectorRef, Component, EventEmitter, HostListener, Input, OnDestroy, OnInit, Output } from '@angular/core';
ChangeDetectionStrategy, import { Subscription } from 'rxjs';
ChangeDetectorRef,
Component,
EventEmitter,
HostListener,
Input,
OnDestroy,
OnInit,
Output
} from '@angular/core';
import { animate, state, style, transition, trigger } from '@angular/animations';
import { filter, Subject, takeUntil, tap } from 'rxjs';
import { ILogger } from '../../models/logger.model'; import { ILogger } from '../../models/logger.model';
import { CdkOverlayService } from '../../services/cdk-overlay/cdk-overlay.service'; import { CdkOverlayService } from '../../services/cdk-overlay/cdk-overlay.service';
import { OpenViduComponentsConfigService } from '../../services/config/directive-config.service'; import { OpenViduComponentsConfigService } from '../../services/config/directive-config.service';
@ -20,6 +9,7 @@ import { TranslateService } from '../../services/translate/translate.service';
import { LocalTrack } from 'livekit-client'; import { LocalTrack } from 'livekit-client';
import { CustomDevice } from '../../models/device.model'; import { CustomDevice } from '../../models/device.model';
import { LangOption } from '../../models/lang.model'; import { LangOption } from '../../models/lang.model';
import { StorageService } from '../../services/storage/storage.service';
/** /**
* @internal * @internal
@ -27,49 +17,7 @@ import { LangOption } from '../../models/lang.model';
@Component({ @Component({
selector: 'ov-pre-join', selector: 'ov-pre-join',
templateUrl: './pre-join.component.html', templateUrl: './pre-join.component.html',
styleUrls: ['./pre-join.component.scss'], styleUrls: ['./pre-join.component.scss']
changeDetection: ChangeDetectionStrategy.OnPush,
standalone: false,
animations: [
trigger('containerResize', [
state(
'normal',
style({
height: '*'
})
),
state(
'compact',
style({
height: '28vh'
})
),
transition('normal => compact', [animate('250ms cubic-bezier(0.25, 0.8, 0.25, 1)')]),
transition('compact => normal', [animate('350ms cubic-bezier(0.25, 0.8, 0.25, 1)')])
]),
trigger('slideInOut', [
transition(':enter', [
style({
opacity: 0
}),
animate(
'300ms cubic-bezier(0.34, 1.56, 0.64, 1)',
style({
opacity: 1
})
)
]),
transition(':leave', [
animate(
'200ms cubic-bezier(0.25, 0.46, 0.45, 0.94)',
style({
opacity: 0,
transform: 'translateY(-10px)'
})
)
])
])
]
}) })
export class PreJoinComponent implements OnInit, OnDestroy { export class PreJoinComponent implements OnInit, OnDestroy {
@Input() set error(error: { name: string; message: string } | undefined) { @Input() set error(error: { name: string; message: string } | undefined) {
@ -83,29 +31,24 @@ export class PreJoinComponent implements OnInit, OnDestroy {
@Output() onReadyToJoin = new EventEmitter<any>(); @Output() onReadyToJoin = new EventEmitter<any>();
_error: string | undefined; _error: string | undefined;
windowSize: number; windowSize: number;
isLoading = true; isLoading = true;
participantName: string | undefined = ''; participantName: string | undefined;
/** /**
* @ignore * @ignore
*/ */
isMinimal: boolean = false; isMinimal: boolean = false;
showCameraButton: boolean = true;
showMicrophoneButton: boolean = true;
showLogo: 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; videoTrack: LocalTrack | undefined;
audioTrack: LocalTrack | undefined; audioTrack: LocalTrack | undefined;
isVideoEnabled: boolean = false;
private tracks: LocalTrack[]; private tracks: LocalTrack[];
private log: ILogger; private log: ILogger;
private destroy$ = new Subject<void>(); private screenShareStateSubscription: Subscription;
private minimalSub: Subscription;
private displayLogoSub: Subscription;
private shouldRemoveTracksWhenComponentIsDestroyed: boolean = true; private shouldRemoveTracksWhenComponentIsDestroyed: boolean = true;
@HostListener('window:resize') @HostListener('window:resize')
@ -118,6 +61,7 @@ export class PreJoinComponent implements OnInit, OnDestroy {
private libService: OpenViduComponentsConfigService, private libService: OpenViduComponentsConfigService,
private cdkSrv: CdkOverlayService, private cdkSrv: CdkOverlayService,
private openviduService: OpenViduService, private openviduService: OpenViduService,
private storageService: StorageService,
private translateService: TranslateService, private translateService: TranslateService,
private changeDetector: ChangeDetectorRef private changeDetector: ChangeDetectorRef
) { ) {
@ -129,28 +73,34 @@ export class PreJoinComponent implements OnInit, OnDestroy {
await this.initializeDevices(); await this.initializeDevices();
this.windowSize = window.innerWidth; this.windowSize = window.innerWidth;
this.isLoading = false; this.isLoading = false;
this.changeDetector.markForCheck();
} }
// ngAfterContentChecked(): void { ngAfterContentChecked(): void {
// // this.changeDetector.detectChanges(); this.changeDetector.detectChanges();
// this.isLoading = false; }
// }
async ngOnDestroy() { async ngOnDestroy() {
this.destroy$.next();
this.destroy$.complete();
this.cdkSrv.setSelector('body'); this.cdkSrv.setSelector('body');
if (this.screenShareStateSubscription) this.screenShareStateSubscription.unsubscribe();
if (this.minimalSub) this.minimalSub.unsubscribe();
if (this.displayLogoSub) this.displayLogoSub.unsubscribe();
if (this.shouldRemoveTracksWhenComponentIsDestroyed) { if (this.shouldRemoveTracksWhenComponentIsDestroyed) {
this.tracks?.forEach((track) => { this.tracks.forEach((track) => {
track.stop(); track.stop();
}); });
} }
} }
private async initializeDevices() { private async initializeDevices() {
await this.initializeDevicesWithRetry(); 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() { onDeviceSelectorClicked() {
@ -159,94 +109,47 @@ export class PreJoinComponent implements OnInit, OnDestroy {
this.cdkSrv.setSelector('#prejoin-container'); this.cdkSrv.setSelector('#prejoin-container');
} }
join() { joinSession() {
if (this.showParticipantName && !this.participantName?.trim()) { if (!this.participantName) {
this._error = this.translateService.translate('PREJOIN.NICKNAME_REQUIRED'); this._error = this.translateService.translate('PREJOIN.NICKNAME_REQUIRED');
return; return;
} }
// Clear any previous errors
this._error = undefined;
// Mark tracks as permanent for avoiding to be removed in ngOnDestroy // Mark tracks as permanent for avoiding to be removed in ngOnDestroy
this.shouldRemoveTracksWhenComponentIsDestroyed = false; this.shouldRemoveTracksWhenComponentIsDestroyed = false;
this.onReadyToJoin.emit();
// Assign participant name to the observable if it is defined
if (this.participantName?.trim()) {
this.libService.updateGeneralConfig({ participantName: this.participantName.trim() });
// Wait for the next tick to ensure the participant name propagates
// through the observable before emitting onReadyToJoin
this.libService.participantName$
.pipe(
takeUntil(this.destroy$),
filter((name) => name === this.participantName?.trim()),
tap(() => this.onReadyToJoin.emit())
)
.subscribe();
} else {
// No participant name to set, emit immediately
this.onReadyToJoin.emit();
}
} }
onParticipantNameChanged(name: string) { onParticipantNameChanged(name: string) {
this.participantName = name?.trim() || ''; this.participantName = name;
// Clear error when user starts typing
if (this._error && this.participantName) {
this._error = undefined;
}
} }
onEnterPressed() { onEnterPressed() {
this.join(); this.joinSession();
} }
private subscribeToPrejoinDirectives() { private subscribeToPrejoinDirectives() {
this.libService.minimal$.pipe(takeUntil(this.destroy$)).subscribe((value: boolean) => { this.minimalSub = this.libService.minimal$.subscribe((value: boolean) => {
this.isMinimal = value; this.isMinimal = value;
this.changeDetector.markForCheck(); // this.cd.markForCheck();
}); });
this.displayLogoSub = this.libService.displayLogo$.subscribe((value: boolean) => {
this.libService.cameraButton$.pipe(takeUntil(this.destroy$)).subscribe((value: boolean) => {
this.showCameraButton = value;
this.changeDetector.markForCheck();
});
this.libService.microphoneButton$.pipe(takeUntil(this.destroy$)).subscribe((value: boolean) => {
this.showMicrophoneButton = value;
this.changeDetector.markForCheck();
});
this.libService.displayLogo$.pipe(takeUntil(this.destroy$)).subscribe((value: boolean) => {
this.showLogo = value; this.showLogo = value;
this.changeDetector.markForCheck(); // this.cd.markForCheck();
}); });
this.libService.participantName$.subscribe((value: string) => {
this.libService.participantName$.pipe(takeUntil(this.destroy$)).subscribe((value: string) => { if (value) this.participantName = value;
if (value) { // this.cd.markForCheck();
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) { async videoEnabledChanged(enabled: boolean) {
this.isVideoEnabled = enabled; if (enabled && !this.videoTrack) {
if (!enabled) {
this.closeBackgroundPanel();
} else if (!this.videoTrack) {
const newVideoTrack = await this.openviduService.createLocalTracks(true, false); const newVideoTrack = await this.openviduService.createLocalTracks(true, false);
this.videoTrack = newVideoTrack[0]; this.videoTrack = newVideoTrack[0];
this.tracks.push(this.videoTrack); this.tracks.push(this.videoTrack);
this.openviduService.setLocalTracks(this.tracks); this.openviduService.setLocalTracks(this.tracks);
} }
this.onVideoEnabledChanged.emit(enabled); this.onVideoEnabledChanged.emit(enabled);
} }
@ -259,68 +162,4 @@ export class PreJoinComponent implements OnInit, OnDestroy {
} }
this.onAudioEnabledChanged.emit(enabled); this.onAudioEnabledChanged.emit(enabled);
} }
/**
* Toggle virtual background panel visibility with smooth animation
*/
toggleBackgroundPanel() {
// Add a small delay to ensure smooth transition
if (!this.showBackgroundPanel) {
// Opening panel
this.showBackgroundPanel = true;
this.changeDetector.markForCheck();
} else {
// Closing panel - add slight delay for smooth animation
setTimeout(() => {
this.showBackgroundPanel = false;
this.changeDetector.markForCheck();
}, 50);
}
}
/**
* Close virtual background panel with smooth animation
*/
closeBackgroundPanel() {
// Add animation delay for smooth closing
setTimeout(() => {
this.showBackgroundPanel = false;
this.changeDetector.markForCheck();
}, 100);
}
/**
* Enhanced error handling with better UX
*/
private handleError(error: any) {
this.log.e('PreJoin component error:', error);
this._error = error.message || 'An unexpected error occurred';
this.changeDetector.markForCheck();
}
/**
* Improved device initialization with error handling
*/
private async initializeDevicesWithRetry(maxRetries: number = 3): Promise<void> {
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
this.tracks = await this.openviduService.createLocalTracks();
this.openviduService.setLocalTracks(this.tracks);
this.videoTrack = this.tracks.find((track) => track.kind === 'video');
this.audioTrack = this.tracks.find((track) => track.kind === 'audio');
this.isVideoEnabled = this.openviduService.isVideoTrackEnabled();
return; // Success, exit retry loop
} catch (error) {
this.log.w(`Device initialization attempt ${attempt} failed:`, error);
if (attempt === maxRetries) {
this.handleError(error);
} else {
// Wait before retrying
await new Promise((resolve) => setTimeout(resolve, 1000 * attempt));
}
}
}
}
} }

View File

@ -16,7 +16,7 @@ import {
import { ILogger } from '../../models/logger.model'; import { ILogger } from '../../models/logger.model';
import { animate, style, transition, trigger } from '@angular/animations'; import { animate, style, transition, trigger } from '@angular/animations';
import { MatDrawerContainer, MatSidenav } from '@angular/material/sidenav'; import { MatDrawerContainer, MatSidenav } from '@angular/material/sidenav';
import { skip, Subject, takeUntil } from 'rxjs'; import { skip, Subscription } from 'rxjs';
import { SidenavMode } from '../../models/layout.model'; import { SidenavMode } from '../../models/layout.model';
import { PanelStatusInfo, PanelType } from '../../models/panel.model'; import { PanelStatusInfo, PanelType } from '../../models/panel.model';
import { DataTopic } from '../../models/data-topic.model'; import { DataTopic } from '../../models/data-topic.model';
@ -46,9 +46,8 @@ import {
RoomEvent, RoomEvent,
Track Track
} from 'livekit-client'; } from 'livekit-client';
import { ParticipantLeftEvent, ParticipantLeftReason, ParticipantModel } from '../../models/participant.model'; import { ParticipantModel } from '../../models/participant.model';
import { RecordingStatus } from '../../models/recording.model'; import { ServiceConfigService } from '../../services/config/service-config.service';
import { TemplateManagerService, SessionTemplateConfiguration } from '../../services/template/template-manager.service';
/** /**
* @internal * @internal
@ -59,43 +58,22 @@ import { TemplateManagerService, SessionTemplateConfiguration } from '../../serv
templateUrl: './session.component.html', templateUrl: './session.component.html',
styleUrls: ['./session.component.scss'], styleUrls: ['./session.component.scss'],
animations: [trigger('sessionAnimation', [transition(':enter', [style({ opacity: 0 }), animate('50ms', style({ opacity: 1 }))])])], animations: [trigger('sessionAnimation', [transition(':enter', [style({ opacity: 0 }), animate('50ms', style({ opacity: 1 }))])])],
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush
standalone: false
}) })
export class SessionComponent implements OnInit, OnDestroy { export class SessionComponent implements OnInit, OnDestroy {
@ContentChild('toolbar', { read: TemplateRef }) toolbarTemplate: TemplateRef<any>; @ContentChild('toolbar', { read: TemplateRef }) toolbarTemplate: TemplateRef<any>;
@ContentChild('panel', { read: TemplateRef }) panelTemplate: TemplateRef<any>; @ContentChild('panel', { read: TemplateRef }) panelTemplate: TemplateRef<any>;
@ContentChild('layout', { read: TemplateRef }) layoutTemplate: TemplateRef<any>; @ContentChild('layout', { read: TemplateRef }) layoutTemplate: TemplateRef<any>;
/** /**
* Provides event notifications that fire when Room is created for the local participant. * Provides event notifications that fire when OpenVidu Room is created.
*
*/ */
@Output() onRoomCreated: EventEmitter<Room> = new EventEmitter<Room>(); @Output() onRoomCreated: EventEmitter<Room> = new EventEmitter<Room>();
/** /**
* Provides event notifications that fire when Room is being reconnected for the local participant. * Provides event notifications that fire when local participant is created.
*/ */
@Output() onRoomReconnecting: EventEmitter<void> = new EventEmitter<void>(); @Output() onParticipantCreated: EventEmitter<ParticipantModel> = new EventEmitter<ParticipantModel>();
/**
* Provides event notifications that fire when Room is reconnected for the local participant.
*/
@Output() onRoomReconnected: EventEmitter<void> = new EventEmitter<void>();
/**
* Provides event notifications that fire when participant is disconnected from Room.
* @deprecated Use {@link SessionComponent.onParticipantLeft} instead.
*/
@Output() onRoomDisconnected: EventEmitter<void> = new EventEmitter<void>();
/**
* Provides event notifications that fire when local participant is connected to the Room.
*/
@Output() onParticipantConnected: EventEmitter<ParticipantModel> = new EventEmitter<ParticipantModel>();
/**
* This event is emitted when the local participant leaves the room.
*/
@Output() onParticipantLeft: EventEmitter<ParticipantLeftEvent> = new EventEmitter<ParticipantLeftEvent>();
room: Room; room: Room;
sideMenu: MatSidenav; sideMenu: MatSidenav;
@ -104,20 +82,17 @@ export class SessionComponent implements OnInit, OnDestroy {
drawer: MatDrawerContainer; drawer: MatDrawerContainer;
loading: boolean = true; loading: boolean = true;
/**
* @internal
* Template configuration managed by the service
*/
templateConfig: SessionTemplateConfiguration = {};
private shouldDisconnectRoomWhenComponentIsDestroyed: boolean = true; private shouldDisconnectRoomWhenComponentIsDestroyed: boolean = true;
private readonly SIDENAV_WIDTH_LIMIT_MODE = 790; private readonly SIDENAV_WIDTH_LIMIT_MODE = 790;
private destroy$ = new Subject<void>(); private menuSubscription: Subscription;
private layoutWidthSubscription: Subscription;
private updateLayoutInterval: NodeJS.Timeout; private updateLayoutInterval: NodeJS.Timeout;
private captionLanguageSubscription: Subscription;
private log: ILogger; private log: ILogger;
private layoutService: LayoutService;
constructor( constructor(
private layoutService: LayoutService, private serviceConfig: ServiceConfigService,
private actionService: ActionService, private actionService: ActionService,
private openviduService: OpenViduService, private openviduService: OpenViduService,
private participantService: ParticipantService, private participantService: ParticipantService,
@ -130,16 +105,15 @@ export class SessionComponent implements OnInit, OnDestroy {
private translateService: TranslateService, private translateService: TranslateService,
// private captionService: CaptionService, // private captionService: CaptionService,
private backgroundService: VirtualBackgroundService, private backgroundService: VirtualBackgroundService,
private cd: ChangeDetectorRef, private cd: ChangeDetectorRef
private templateManagerService: TemplateManagerService
) { ) {
this.log = this.loggerSrv.get('SessionComponent'); this.log = this.loggerSrv.get('SessionComponent');
this.setupTemplates(); this.layoutService = this.serviceConfig.getLayoutService();
} }
@HostListener('window:beforeunload') @HostListener('window:beforeunload')
beforeunloadHandler() { beforeunloadHandler() {
this.disconnectRoom(ParticipantLeftReason.BROWSER_UNLOAD); this.disconnectRoom();
} }
@HostListener('window:resize') @HostListener('window:resize')
@ -188,39 +162,15 @@ export class SessionComponent implements OnInit, OnDestroy {
set layoutContainer(container: ElementRef) { set layoutContainer(container: ElementRef) {
setTimeout(async () => { setTimeout(async () => {
if (container) { if (container) {
if (this.libService.showBackgroundEffectsButton()) { // Apply background from storage when layout container is in DOM
// Apply background from storage when layout container is in DOM only when background effects button is enabled await this.backgroundService.applyBackgroundFromStorage();
await this.backgroundService.applyBackgroundFromStorage();
}
} }
}, 0); }, 0);
} }
async ngOnInit() { async ngOnInit() {
this.shouldDisconnectRoomWhenComponentIsDestroyed = true; this.room = this.openviduService.getRoom();
this.onRoomCreated.emit(this.room);
// Check if room is available before proceeding
if (!this.openviduService.isRoomInitialized()) {
this.log.e('Room is not initialized when SessionComponent starts. This indicates a timing issue.');
this.actionService.openDialog(
this.translateService.translate('ERRORS.SESSION'),
'Room is not ready. Please ensure the token is properly configured.'
);
return;
}
// Get room instance
try {
this.room = this.openviduService.getRoom();
this.log.d('Room successfully obtained for SessionComponent');
} catch (error) {
this.log.e('Unexpected error getting room:', error);
this.actionService.openDialog(
this.translateService.translate('ERRORS.SESSION'),
'Failed to get room instance: ' + (error?.message || error)
);
return;
}
// this.subscribeToCaptionLanguage(); // this.subscribeToCaptionLanguage();
this.subcribeToActiveSpeakersChanged(); this.subcribeToActiveSpeakersChanged();
@ -233,22 +183,19 @@ export class SessionComponent implements OnInit, OnDestroy {
// this.subscribeToParticipantNameChanged(); // this.subscribeToParticipantNameChanged();
this.subscribeToDataMessage(); this.subscribeToDataMessage();
this.subscribeToReconnection(); this.subscribeToReconnection();
this.subscribeToVirtualBackground();
// if (this.libService.isRecordingEnabled()) { if (this.libService.isRecordingEnabled()) {
// this.subscribeToRecordingEvents(); // this.subscribeToRecordingEvents();
// } }
// if (this.libService.isBroadcastingEnabled()) { if (this.libService.isBroadcastingEnabled()) {
// this.subscribeToBroadcastingEvents(); // this.subscribeToBroadcastingEvents();
// } }
try { try {
await this.participantService.connect(); await this.participantService.connect();
// Send room created after participant connect for avoiding to send incomplete room payload
this.onRoomCreated.emit(this.room);
this.cd.markForCheck(); this.cd.markForCheck();
this.loading = false; this.loading = false;
this.onParticipantConnected.emit(this.participantService.getLocalParticipant()); this.onParticipantCreated.emit(this.participantService.getLocalParticipant());
} catch (error) { } catch (error) {
this.log.e('There was an error connecting to the room:', error.code, error.message); this.log.e('There was an error connecting to the room:', error.code, error.message);
this.actionService.openDialog(this.translateService.translate('ERRORS.SESSION'), error?.error || error?.message || error); this.actionService.openDialog(this.translateService.translate('ERRORS.SESSION'), error?.error || error?.message || error);
@ -260,41 +207,22 @@ export class SessionComponent implements OnInit, OnDestroy {
}); });
} }
/**
* @internal
* Sets up all templates using the template manager service
*/
private setupTemplates(): void {
this.templateConfig = this.templateManagerService.setupSessionTemplates(
this.toolbarTemplate,
this.panelTemplate,
this.layoutTemplate
);
}
async ngOnDestroy() { async ngOnDestroy() {
if (this.shouldDisconnectRoomWhenComponentIsDestroyed) { if (this.shouldDisconnectRoomWhenComponentIsDestroyed) {
await this.disconnectRoom(ParticipantLeftReason.LEAVE); await this.disconnectRoom();
} }
if (this.room) this.room.removeAllListeners(); if(this.room) this.room.removeAllListeners();
this.participantService.clear(); this.participantService.clear();
// this.room = undefined; // this.room = undefined;
this.destroy$.next(); if (this.menuSubscription) this.menuSubscription.unsubscribe();
this.destroy$.complete(); if (this.layoutWidthSubscription) this.layoutWidthSubscription.unsubscribe();
// if (this.captionLanguageSubscription) this.captionLanguageSubscription.unsubscribe(); // if (this.captionLanguageSubscription) this.captionLanguageSubscription.unsubscribe();
} }
async disconnectRoom(reason: ParticipantLeftReason) { async disconnectRoom() {
// Mark session as disconnected for avoiding to do it again in ngOnDestroy // Mark session as disconnected for avoiding to do it again in ngOnDestroy
this.shouldDisconnectRoomWhenComponentIsDestroyed = false; this.shouldDisconnectRoomWhenComponentIsDestroyed = false;
await this.openviduService.disconnectRoom(() => { await this.openviduService.disconnectRoom();
this.onParticipantLeft.emit({
roomName: this.openviduService.getRoomName(),
participantName: this.participantService.getLocalParticipant()?.name || '',
identity: this.participantService.getLocalParticipant()?.identity || '',
reason
});
}, false);
} }
private subscribeToTogglingMenu() { private subscribeToTogglingMenu() {
@ -311,7 +239,7 @@ export class SessionComponent implements OnInit, OnDestroy {
this.startUpdateLayoutInterval(); this.startUpdateLayoutInterval();
}); });
this.panelService.panelStatusObs.pipe(skip(1), takeUntil(this.destroy$)).subscribe((ev: PanelStatusInfo) => { this.menuSubscription = this.panelService.panelStatusObs.pipe(skip(1)).subscribe((ev: PanelStatusInfo) => {
if (this.sideMenu) { if (this.sideMenu) {
this.settingsPanelOpened = ev.isOpened && ev.panelType === PanelType.SETTINGS; this.settingsPanelOpened = ev.isOpened && ev.panelType === PanelType.SETTINGS;
@ -330,7 +258,7 @@ export class SessionComponent implements OnInit, OnDestroy {
} }
private subscribeToLayoutWidth() { private subscribeToLayoutWidth() {
this.layoutService.layoutWidthObs.pipe(takeUntil(this.destroy$)).subscribe((width) => { this.layoutWidthSubscription = this.layoutService.layoutWidthObs.subscribe((width) => {
this.sidenavMode = width <= this.SIDENAV_WIDTH_LIMIT_MODE ? SidenavMode.OVER : SidenavMode.SIDE; this.sidenavMode = width <= this.SIDENAV_WIDTH_LIMIT_MODE ? SidenavMode.OVER : SidenavMode.SIDE;
}); });
} }
@ -448,7 +376,7 @@ export class SessionComponent implements OnInit, OnDestroy {
this.log.d(`Data event received: ${topic}`); this.log.d(`Data event received: ${topic}`);
switch (topic) { switch (topic) {
case DataTopic.CHAT: case DataTopic.CHAT:
const participantName = participant?.name || 'Unknown'; const participantName = participant?.identity || 'Unknown';
this.chatService.addRemoteMessage(event.message, participantName); this.chatService.addRemoteMessage(event.message, participantName);
break; break;
case DataTopic.RECORDING_STARTING: case DataTopic.RECORDING_STARTING:
@ -500,12 +428,9 @@ export class SessionComponent implements OnInit, OnDestroy {
case DataTopic.ROOM_STATUS: case DataTopic.ROOM_STATUS:
const { recordingList, isRecordingStarted, isBroadcastingStarted, broadcastingId } = event as RoomStatusData; const { recordingList, isRecordingStarted, isBroadcastingStarted, broadcastingId } = event as RoomStatusData;
if (this.libService.showRecordingActivityRecordingsList()) { this.recordingService.setRecordingList(recordingList);
this.recordingService.setRecordingList(recordingList);
}
if (isRecordingStarted) { if (isRecordingStarted) {
const recordingActive = recordingList.find((recording) => recording.status === RecordingStatus.STARTED); this.recordingService.setRecordingStarted();
this.recordingService.setRecordingStarted(recordingActive);
} }
if (isBroadcastingStarted) { if (isBroadcastingStarted) {
this.broadcastingService.setBroadcastingStarted(broadcastingId); this.broadcastingService.setBroadcastingStarted(broadcastingId);
@ -518,84 +443,28 @@ export class SessionComponent implements OnInit, OnDestroy {
); );
} }
private subscribeToReconnection() { subscribeToReconnection() {
this.room.on(RoomEvent.Reconnecting, () => { this.room.on(RoomEvent.Reconnecting, () => {
this.log.w('Connection lost: Reconnecting'); this.log.w('Connection lost: Reconnecting');
this.actionService.openConnectionDialog( this.actionService.openConnectionDialog(
this.translateService.translate('ERRORS.CONNECTION'), this.translateService.translate('ERRORS.CONNECTION'),
this.translateService.translate('ERRORS.RECONNECT') this.translateService.translate('ERRORS.RECONNECT')
); );
this.onRoomReconnecting.emit();
}); });
this.room.on(RoomEvent.Reconnected, () => { this.room.on(RoomEvent.Reconnected, () => {
this.log.w('Connection lost: Reconnected'); this.log.w('Connection lost: Reconnected');
this.actionService.closeConnectionDialog(); this.actionService.closeConnectionDialog();
this.onRoomReconnected.emit();
}); });
this.room.on(RoomEvent.Disconnected, async (reason: DisconnectReason | undefined) => { this.room.on(RoomEvent.Disconnected, async (reason: DisconnectReason | undefined) => {
this.shouldDisconnectRoomWhenComponentIsDestroyed = false; if (reason === DisconnectReason.SERVER_SHUTDOWN) {
this.actionService.closeConnectionDialog(); this.log.e('Room Disconnected', reason);
const participantLeftEvent: ParticipantLeftEvent = { this.actionService.openConnectionDialog(
roomName: this.openviduService.getRoomName(), this.translateService.translate('ERRORS.CONNECTION'),
participantName: this.participantService.getLocalParticipant()?.name || '', this.translateService.translate('ERRORS.RECONNECT')
identity: this.participantService.getLocalParticipant()?.identity || '',
reason: ParticipantLeftReason.NETWORK_DISCONNECT
};
const messageErrorKey = 'ERRORS.DISCONNECT';
let descriptionErrorKey = '';
switch (reason) {
case DisconnectReason.CLIENT_INITIATED:
// Skip disconnect reason if a default disconnect method has been called
if (!this.openviduService.shouldHandleClientInitiatedDisconnectEvent) return;
participantLeftEvent.reason = ParticipantLeftReason.LEAVE;
break;
case DisconnectReason.DUPLICATE_IDENTITY:
participantLeftEvent.reason = ParticipantLeftReason.DUPLICATE_IDENTITY;
descriptionErrorKey = 'ERRORS.DUPLICATE_IDENTITY';
break;
case DisconnectReason.SERVER_SHUTDOWN:
descriptionErrorKey = 'ERRORS.SERVER_SHUTDOWN';
participantLeftEvent.reason = ParticipantLeftReason.SERVER_SHUTDOWN;
break;
case DisconnectReason.PARTICIPANT_REMOVED:
participantLeftEvent.reason = ParticipantLeftReason.PARTICIPANT_REMOVED;
descriptionErrorKey = 'ERRORS.PARTICIPANT_REMOVED';
break;
case DisconnectReason.ROOM_DELETED:
participantLeftEvent.reason = ParticipantLeftReason.ROOM_DELETED;
descriptionErrorKey = 'ERRORS.ROOM_DELETED';
break;
case DisconnectReason.SIGNAL_CLOSE:
participantLeftEvent.reason = ParticipantLeftReason.SIGNAL_CLOSE;
descriptionErrorKey = 'ERRORS.SIGNAL_CLOSE';
break;
default:
participantLeftEvent.reason = ParticipantLeftReason.OTHER;
descriptionErrorKey = 'ERRORS.DISCONNECT';
break;
}
this.log.d('Participant disconnected', participantLeftEvent);
this.onParticipantLeft.emit(participantLeftEvent);
this.onRoomDisconnected.emit();
if (this.libService.getShowDisconnectionDialog() && descriptionErrorKey) {
this.actionService.openDialog(
this.translateService.translate(messageErrorKey),
this.translateService.translate(descriptionErrorKey)
); );
} }
}); // await this.disconnectRoom();
}
private subscribeToVirtualBackground() {
this.libService.backgroundEffectsButton$.subscribe(async (enable) => {
if (!enable && this.backgroundService.isBackgroundApplied()) {
await this.backgroundService.removeBackground();
if (this.panelService.isBackgroundEffectsPanelOpened()) {
this.panelService.closePanel();
}
}
}); });
} }

View File

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

View File

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

View File

@ -1,4 +1,4 @@
import { Component, EventEmitter, Input, OnDestroy, OnInit, Output } from '@angular/core'; import { Component, EventEmitter, OnDestroy, OnInit, Output } from '@angular/core';
import { Subscription } from 'rxjs'; import { Subscription } from 'rxjs';
import { CustomDevice } from '../../../models/device.model'; import { CustomDevice } from '../../../models/device.model';
import { DeviceService } from '../../../services/device/device.service'; import { DeviceService } from '../../../services/device/device.service';
@ -12,11 +12,9 @@ import { ParticipantModel } from '../../../models/participant.model';
@Component({ @Component({
selector: 'ov-audio-devices-select', selector: 'ov-audio-devices-select',
templateUrl: './audio-devices.component.html', templateUrl: './audio-devices.component.html',
styleUrls: ['./audio-devices.component.scss'], styleUrls: ['./audio-devices.component.scss']
standalone: false
}) })
export class AudioDevicesComponent implements OnInit, OnDestroy { export class AudioDevicesComponent implements OnInit, OnDestroy {
@Input() compact: boolean = false;
@Output() onAudioDeviceChanged = new EventEmitter<CustomDevice>(); @Output() onAudioDeviceChanged = new EventEmitter<CustomDevice>();
@Output() onAudioEnabledChanged = new EventEmitter<boolean>(); @Output() onAudioEnabledChanged = new EventEmitter<boolean>();

View File

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

View File

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

View File

@ -1,33 +1,18 @@
<div class="language-selector-container"> <button id="lang-btn-compact" *ngIf="compact" mat-icon-button [matMenuTriggerFor]="menu">
@if (compact) { <mat-icon>translate</mat-icon>
<!-- Compact version (icon only) --> </button>
<button mat-icon-button [matMenuTriggerFor]="langMenu" class="compact-lang-button" [matTooltip]="'Change language'" disableRipple> <button *ngIf="!compact" mat-flat-button [matMenuTriggerFor]="menu" class="lang-button" id="lang-btn">
<mat-icon>translate</mat-icon> <span id="lang-selected-name">{{ langSelected?.name }}</span>
</button> <mat-icon class="expand-more-icon">expand_more</mat-icon>
} @else { </button>
<!-- Full version (with text) --> <mat-menu #menu="matMenu">
<button mat-flat-button [matMenuTriggerFor]="langMenu" class="full-lang-button"> <button
<!-- <mat-icon class="lang-icon">translate</mat-icon> --> mat-menu-item
<span class="lang-name"> *ngFor="let lang of languages"
{{ langSelected?.name }} (click)="onLangSelected(lang.lang)"
<mat-icon class="expand-icon">expand_more</mat-icon> [attr.id]="'lang-opt-' + lang.lang"
</span> class="lang-menu-opt"
</button> >
} <span>{{ lang.name }}</span>
</button>
<!-- Language Menu --> </mat-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,113 +1,21 @@
:host { $ov-surface-color-lighter: color-mix(in srgb, var(--ov-surface-color), #fff 5%);
display: inline-block;
.language-selector-container { .lang-button {
.compact-lang-button { background-color: var(--ov-primary-action-color) !important;
width: 40px; color: var(--ov-secondary-action-color) !important;
height: 40px; }
background: rgba(255, 255, 255, 0.9); .lang-button .mat-icon {
backdrop-filter: blur(10px); color: var(--ov-secondary-action-color);
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; ::ng-deep .mat-mdc-menu-panel {
width: 18px; border-radius: var(--ov-surface-radius) !important;
height: 18px; background-color: $ov-surface-color-lighter !important;
} box-shadow: 1px 1px 5px 0px rgba(0, 0, 0, 0.2) !important;
}
.full-lang-button {
display: flex;
align-items: center;
gap: 8px;
padding: 12px 16px;
background: var(--ov-surface-color, #ffffff);
border: 2px solid var(--ov-border-color, #e0e0e0);
border-radius: 12px;
transition: all 0.2s ease;
color: var(--ov-text-primary-color, #333);
font-weight: 500;
&:hover {
border-color: var(--ov-primary-action-color, #4285f4);
box-shadow: 0 2px 8px rgba(66, 133, 244, 0.1);
}
.lang-icon {
font-size: 18px;
width: 18px;
height: 18px;
color: var(--ov-text-surface-color, #666);
}
.lang-name {
font-size: 14px;
font-weight: 500;
display: inline-block !important;
color: var(--ov-text-surface-color) !important;
}
.expand-icon {
font-size: 16px;
width: 16px;
height: 16px;
color: var(--ov-text-secondary-color, #666);
transition: transform 0.2s ease;
}
&[aria-expanded='true'] .expand-icon {
transform: rotate(180deg);
}
}
}
} }
::ng-deep .language-menu.mat-mdc-menu-panel { ::ng-deep .mat-mdc-menu-item,
border-radius: 12px; .mat-mdc-menu-item:visited,
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.12); .mat-mdc-menu-item:link {
border: 1px solid var(--ov-border-color, #e0e0e0); color: var(--ov-text-surface-color) !important;
overflow: hidden;
background: var(--ov-surface-color, #ffffff);
.language-option {
display: flex;
align-items: center;
gap: 12px;
padding: 12px 16px;
transition: background-color 0.2s ease;
font-size: 14px;
min-height: 48px;
color: var(--ov-text-surface-color);
&:hover {
background-color: var(--ov-hover-color, #f5f5f5);
}
&.selected {
background-color: rgba(66, 133, 244, 0.08);
color: var(--ov-primary-action-color, #4285f4);
.check-icon {
color: var(--ov-primary-action-color, #4285f4);
}
}
.check-icon {
font-size: 18px;
width: 18px;
height: 18px;
}
.lang-option-name {
flex: 1;
font-weight: 500;
}
&.selected .lang-option-name {
font-weight: 600;
}
}
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,4 +1,4 @@
import { Component, EventEmitter, Input, OnDestroy, OnInit, Output } from '@angular/core'; import { Component, EventEmitter, OnDestroy, OnInit, Output } from '@angular/core';
import { Subscription } from 'rxjs'; import { Subscription } from 'rxjs';
import { CustomDevice } from '../../../models/device.model'; import { CustomDevice } from '../../../models/device.model';
import { DeviceService } from '../../../services/device/device.service'; import { DeviceService } from '../../../services/device/device.service';
@ -12,11 +12,9 @@ import { ParticipantModel } from '../../../models/participant.model';
@Component({ @Component({
selector: 'ov-video-devices-select', selector: 'ov-video-devices-select',
templateUrl: './video-devices.component.html', templateUrl: './video-devices.component.html',
styleUrls: ['./video-devices.component.scss'], styleUrls: ['./video-devices.component.scss']
standalone: false
}) })
export class VideoDevicesComponent implements OnInit, OnDestroy { export class VideoDevicesComponent implements OnInit, OnDestroy {
@Input() compact: boolean = false;
@Output() onVideoDeviceChanged = new EventEmitter<CustomDevice>(); @Output() onVideoDeviceChanged = new EventEmitter<CustomDevice>();
@Output() onVideoEnabledChanged = new EventEmitter<boolean>(); @Output() onVideoEnabledChanged = new EventEmitter<boolean>();

View File

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

View File

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

View File

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

View File

@ -1,12 +1,13 @@
import { Component, ElementRef, Input, OnDestroy, OnInit, ViewChild } from '@angular/core'; import { Component, ElementRef, Input, OnDestroy, OnInit, ViewChild } from '@angular/core';
import { MatMenuPanel, MatMenuTrigger } from '@angular/material/menu'; import { MatMenuPanel, MatMenuTrigger } from '@angular/material/menu';
import { Subject, takeUntil } from 'rxjs'; import { Subscription } from 'rxjs';
import { CdkOverlayService } from '../../services/cdk-overlay/cdk-overlay.service'; import { CdkOverlayService } from '../../services/cdk-overlay/cdk-overlay.service';
import { OpenViduComponentsConfigService } from '../../services/config/directive-config.service'; import { OpenViduComponentsConfigService } from '../../services/config/directive-config.service';
import { LayoutService } from '../../services/layout/layout.service'; import { LayoutService } from '../../services/layout/layout.service';
import { ParticipantService } from '../../services/participant/participant.service'; import { ParticipantService } from '../../services/participant/participant.service';
import { Track } from 'livekit-client'; import { Track } from 'livekit-client';
import { ParticipantTrackPublication } from '../../models/participant.model'; import { ParticipantTrackPublication } from '../../models/participant.model';
import { ServiceConfigService } from '../../services/config/service-config.service';
/** /**
* The **StreamComponent** is hosted inside of the {@link LayoutComponent}. * The **StreamComponent** is hosted inside of the {@link LayoutComponent}.
@ -15,8 +16,7 @@ import { ParticipantTrackPublication } from '../../models/participant.model';
@Component({ @Component({
selector: 'ov-stream', selector: 'ov-stream',
templateUrl: './stream.component.html', templateUrl: './stream.component.html',
styleUrls: ['./stream.component.scss'], styleUrls: ['./stream.component.scss']
standalone: false
}) })
export class StreamComponent implements OnInit, OnDestroy { export class StreamComponent implements OnInit, OnDestroy {
/** /**
@ -68,7 +68,7 @@ export class StreamComponent implements OnInit, OnDestroy {
/** /**
* @ignore * @ignore
*/ */
hoveringTimeout: ReturnType<typeof setTimeout>; hoveringTimeout: NodeJS.Timeout;
/** /**
* @ignore * @ignore
@ -92,27 +92,35 @@ export class StreamComponent implements OnInit, OnDestroy {
} }
private _streamContainer: ElementRef; private _streamContainer: ElementRef;
private destroy$ = new Subject<void>(); private minimalSub: Subscription;
private displayParticipantNameSub: Subscription;
private displayAudioDetectionSub: Subscription;
private videoControlsSub: Subscription;
private readonly HOVER_TIMEOUT = 3000; private readonly HOVER_TIMEOUT = 3000;
private layoutService: LayoutService;
/** /**
* @ignore * @ignore
*/ */
constructor( constructor(
private layoutService: LayoutService, private serviceConfig: ServiceConfigService,
private participantService: ParticipantService, private participantService: ParticipantService,
private cdkSrv: CdkOverlayService, private cdkSrv: CdkOverlayService,
private libService: OpenViduComponentsConfigService private libService: OpenViduComponentsConfigService
) {} ) {
this.layoutService = this.serviceConfig.getLayoutService();
}
ngOnInit() { ngOnInit() {
this.subscribeToStreamDirectives(); this.subscribeToStreamDirectives();
} }
ngOnDestroy() { ngOnDestroy() {
this.destroy$.next();
this.destroy$.complete();
this.cdkSrv.setSelector('body'); this.cdkSrv.setSelector('body');
if (this.videoControlsSub) this.videoControlsSub.unsubscribe();
if (this.displayAudioDetectionSub) this.displayAudioDetectionSub.unsubscribe();
if (this.displayParticipantNameSub) this.displayParticipantNameSub.unsubscribe();
if (this.minimalSub) this.minimalSub.unsubscribe();
} }
/** /**
@ -178,31 +186,20 @@ export class StreamComponent implements OnInit, OnDestroy {
} }
private subscribeToStreamDirectives() { private subscribeToStreamDirectives() {
this.libService.minimal$ this.minimalSub = this.libService.minimal$.subscribe((value: boolean) => {
.pipe(takeUntil(this.destroy$)) this.isMinimal = value;
.subscribe((value: boolean) => { });
this.isMinimal = value; this.displayParticipantNameSub = this.libService.displayParticipantName$.subscribe((value: boolean) => {
}); this.showParticipantName = value;
// this.cd.markForCheck();
this.libService.displayParticipantName$ });
.pipe(takeUntil(this.destroy$)) this.displayAudioDetectionSub = this.libService.displayAudioDetection$.subscribe((value: boolean) => {
.subscribe((value: boolean) => { this.showAudioDetection = value;
this.showParticipantName = value; // this.cd.markForCheck();
// this.cd.markForCheck(); });
}); this.videoControlsSub = this.libService.streamVideoControls$.subscribe((value: boolean) => {
this.showVideoControls = value;
this.libService.displayAudioDetection$ // this.cd.markForCheck();
.pipe(takeUntil(this.destroy$)) });
.subscribe((value: boolean) => {
this.showAudioDetection = value;
// this.cd.markForCheck();
});
this.libService.streamVideoControls$
.pipe(takeUntil(this.destroy$))
.subscribe((value: boolean) => {
this.showVideoControls = value;
// this.cd.markForCheck();
});
} }
} }

View File

@ -1,31 +1,28 @@
<head>
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@24,400,0,0" />
</head>
<mat-toolbar id="toolbar" class="toolbar-container"> <mat-toolbar id="toolbar" class="toolbar-container">
<div id="info-container" class="info-container"> <div id="info-container" class="info-container">
<div> <div>
<img *ngIf="!isMinimal && showLogo" id="branding-logo" [ovLogo]="brandingLogo" /> <img *ngIf="!isMinimal && showLogo" id="branding-logo" src="assets/images/logo.png" ovLogo />
<div <div
id="session-info-container" id="session-info-container"
[class.collapsed]="recordingStatus === _recordingStatus.STARTED || broadcastingStatus === _broadcastingStatus.STARTED" [class.collapsed]="recordingStatus === _recordingStatus.STARTED || broadcastingStatus === _broadcastingStatus.STARTED"
> >
<span id="session-name" *ngIf="!isMinimal && showRoomName">{{ roomName }}</span> <span id="session-name" *ngIf="!isMinimal && room && room.name && showSessionName">{{ room.name }}</span>
<div <div
id="activities-tag" id="activities-tag"
*ngIf="recordingStatus === _recordingStatus.STARTED || broadcastingStatus === _broadcastingStatus.STARTED" *ngIf="recordingStatus === _recordingStatus.STARTED || broadcastingStatus === _broadcastingStatus.STARTED"
> >
@if (recordingStatus === _recordingStatus.STARTED) { <div *ngIf="recordingStatus === _recordingStatus.STARTED" id="recording-tag" class="recording-tag">
<div id="recording-tag" class="recording-tag" (click)="openRecordingActivityPanel()"> <mat-icon class="blink">radio_button_checked</mat-icon>
<mat-icon class="blink">radio_button_checked</mat-icon> <span class="blink">REC</span>
<span class="blink">REC</span> <span *ngIf="recordingTime"> | {{ recordingTime | date: 'H:mm:ss' }}</span>
<span *ngIf="recordingTime"> | {{ recordingTime | date: 'H:mm:ss' }}</span> </div>
</div> <div *ngIf="broadcastingStatus === _broadcastingStatus.STARTED" id="broadcasting-tag" class="broadcasting-tag">
} <mat-icon class="blink">sensors</mat-icon>
<span class="blink">LIVE</span>
@if (broadcastingStatus === _broadcastingStatus.STARTED) { </div>
<!-- Broadcasting tag -->
<div id="broadcasting-tag" class="broadcasting-tag">
<mat-icon class="blink">sensors</mat-icon>
<span class="blink">LIVE</span>
</div>
}
</div> </div>
</div> </div>
</div> </div>
@ -35,7 +32,6 @@
<button <button
id="camera-btn" id="camera-btn"
mat-icon-button mat-icon-button
*ngIf="showCameraButton"
(click)="toggleCamera()" (click)="toggleCamera()"
[disabled]="isConnectionLost || !hasVideoDevices || cameraMuteChanging" [disabled]="isConnectionLost || !hasVideoDevices || cameraMuteChanging"
[class.warn-btn]="!isCameraEnabled" [class.warn-btn]="!isCameraEnabled"
@ -50,7 +46,6 @@
<button <button
id="mic-btn" id="mic-btn"
mat-icon-button mat-icon-button
*ngIf="showMicrophoneButton"
(click)="toggleMicrophone()" (click)="toggleMicrophone()"
[disabled]="isConnectionLost || !hasAudioDevices || microphoneMuteChanging" [disabled]="isConnectionLost || !hasAudioDevices || microphoneMuteChanging"
[class.warn-btn]="!isMicrophoneEnabled" [class.warn-btn]="!isMicrophoneEnabled"
@ -126,36 +121,18 @@
*ngIf="!isMinimal && showRecordingButton" *ngIf="!isMinimal && showRecordingButton"
mat-menu-item mat-menu-item
id="recording-btn" id="recording-btn"
[disabled]=" [disabled]="recordingStatus === _recordingStatus.STARTING || recordingStatus === _recordingStatus.STOPPING"
recordingStatus === _recordingStatus.STARTING ||
recordingStatus === _recordingStatus.STOPPING ||
!hasRoomTracksPublished
"
[matTooltip]="!hasRoomTracksPublished ? ('TOOLBAR.NO_TRACKS_PUBLISHED' | translate) : ''"
(click)="toggleRecording()" (click)="toggleRecording()"
> >
<mat-icon color="warn">radio_button_checked</mat-icon> <mat-icon color="warn">radio_button_checked</mat-icon>
@if ( <span *ngIf="recordingStatus === _recordingStatus.STOPPED || recordingStatus === _recordingStatus.STOPPING">
recordingStatus === _recordingStatus.STOPPED || {{ 'TOOLBAR.START_RECORDING' | translate }}
recordingStatus === _recordingStatus.STOPPING || </span>
recordingStatus === _recordingStatus.FAILED <span *ngIf="recordingStatus === _recordingStatus.STARTED || recordingStatus === _recordingStatus.STARTING">
) { {{ 'TOOLBAR.STOP_RECORDING' | translate }}
<span class="blink"> </span>
{{ 'TOOLBAR.START_RECORDING' | translate }}
</span>
} @else if (recordingStatus === _recordingStatus.STARTED || recordingStatus === _recordingStatus.STARTING) {
<span>{{ 'TOOLBAR.STOP_RECORDING' | translate }}</span>
}
</button> </button>
<!-- View recordings button -->
@if (!isMinimal && showViewRecordingsButton) {
<button mat-menu-item id="view-recordings-btn" (click)="onViewRecordingsClicked.emit()">
<mat-icon>video_library</mat-icon>
<span>{{ 'TOOLBAR.VIEW_RECORDINGS' | translate }}</span>
</button>
}
<!-- Broadcasting button --> <!-- Broadcasting button -->
<button <button
*ngIf="!isMinimal && showBroadcastingButton" *ngIf="!isMinimal && showBroadcastingButton"

View File

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

View File

@ -126,7 +126,6 @@ $ov-recording-blinking-color: #eb5144;
text-align: center; text-align: center;
line-height: 20px; line-height: 20px;
margin: auto; margin: auto;
cursor: pointer;
} }
.recording-tag { .recording-tag {
@ -157,9 +156,6 @@ $ov-recording-blinking-color: #eb5144;
.mat-mdc-icon-button[disabled] { .mat-mdc-icon-button[disabled] {
color: #fff; color: #fff;
} }
::ng-deep .mat-badge-content{
background-color: var(--ov-warn-color);
}
.divider { .divider {
margin: 8px 0px; margin: 8px 0px;
} }

View File

@ -12,7 +12,7 @@ import {
TemplateRef, TemplateRef,
ViewChild ViewChild
} from '@angular/core'; } from '@angular/core';
import { fromEvent, skip, Subject, takeUntil } from 'rxjs'; import { fromEvent, skip, Subscription } from 'rxjs';
import { ChatService } from '../../services/chat/chat.service'; import { ChatService } from '../../services/chat/chat.service';
import { DocumentService } from '../../services/document/document.service'; import { DocumentService } from '../../services/document/document.service';
import { PanelService } from '../../services/panel/panel.service'; import { PanelService } from '../../services/panel/panel.service';
@ -44,12 +44,12 @@ import { ParticipantService } from '../../services/participant/participant.servi
import { PlatformService } from '../../services/platform/platform.service'; import { PlatformService } from '../../services/platform/platform.service';
import { RecordingService } from '../../services/recording/recording.service'; import { RecordingService } from '../../services/recording/recording.service';
import { StorageService } from '../../services/storage/storage.service'; import { StorageService } from '../../services/storage/storage.service';
import { TemplateManagerService, ToolbarTemplateConfiguration } from '../../services/template/template-manager.service';
import { TranslateService } from '../../services/translate/translate.service'; import { TranslateService } from '../../services/translate/translate.service';
import { CdkOverlayService } from '../../services/cdk-overlay/cdk-overlay.service'; import { CdkOverlayService } from '../../services/cdk-overlay/cdk-overlay.service';
import { ParticipantLeftEvent, ParticipantLeftReason, ParticipantModel } from '../../models/participant.model'; import { ParticipantModel } from '../../models/participant.model';
import { Room, RoomEvent } from 'livekit-client'; import { Room, RoomEvent } from 'livekit-client';
import { ToolbarAdditionalButtonsPosition } from '../../models/toolbar.model'; import { ToolbarAdditionalButtonsPosition } from '../../models/toolbar.model';
import { ServiceConfigService } from '../../services/config/service-config.service';
/** /**
* The **ToolbarComponent** is hosted inside of the {@link VideoconferenceComponent}. * The **ToolbarComponent** is hosted inside of the {@link VideoconferenceComponent}.
@ -59,8 +59,7 @@ import { ToolbarAdditionalButtonsPosition } from '../../models/toolbar.model';
selector: 'ov-toolbar', selector: 'ov-toolbar',
templateUrl: './toolbar.component.html', templateUrl: './toolbar.component.html',
styleUrls: ['./toolbar.component.scss'], styleUrls: ['./toolbar.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush
standalone: false
}) })
export class ToolbarComponent implements OnInit, OnDestroy, AfterViewInit { export class ToolbarComponent implements OnInit, OnDestroy, AfterViewInit {
/** /**
@ -78,9 +77,10 @@ export class ToolbarComponent implements OnInit, OnDestroy, AfterViewInit {
*/ */
@ContentChild(ToolbarAdditionalButtonsDirective) @ContentChild(ToolbarAdditionalButtonsDirective)
set externalAdditionalButtons(externalAdditionalButtons: ToolbarAdditionalButtonsDirective) { set externalAdditionalButtons(externalAdditionalButtons: ToolbarAdditionalButtonsDirective) {
this._externalAdditionalButtons = externalAdditionalButtons; // This directive will has value only when ADDITIONAL BUTTONS component (tagged with '*ovToolbarAdditionalButtons' directive)
// is inside of the TOOLBAR component tagged with '*ovToolbar' directive
if (externalAdditionalButtons) { if (externalAdditionalButtons) {
this.updateTemplatesAndMarkForCheck(); this.toolbarAdditionalButtonsTemplate = externalAdditionalButtons.template;
} }
} }
@ -89,23 +89,18 @@ export class ToolbarComponent implements OnInit, OnDestroy, AfterViewInit {
*/ */
@ContentChild(ToolbarAdditionalPanelButtonsDirective) @ContentChild(ToolbarAdditionalPanelButtonsDirective)
set externalAdditionalPanelButtons(externalAdditionalPanelButtons: ToolbarAdditionalPanelButtonsDirective) { set externalAdditionalPanelButtons(externalAdditionalPanelButtons: ToolbarAdditionalPanelButtonsDirective) {
this._externalAdditionalPanelButtons = externalAdditionalPanelButtons; // This directive will has value only when ADDITIONAL PANEL BUTTONS component tagged with '*ovToolbarAdditionalPanelButtons' directive
// is inside of the TOOLBAR component tagged with '*ovToolbar' directive
if (externalAdditionalPanelButtons) { if (externalAdditionalPanelButtons) {
this.updateTemplatesAndMarkForCheck(); this.toolbarAdditionalPanelButtonsTemplate = externalAdditionalPanelButtons.template;
} }
} }
/** /**
* This event is emitted when the room has been disconnected. * This event is emitted when the room has been disconnected.
* @deprecated Use {@link ToolbarComponent.onParticipantLeft} instead.
*/ */
@Output() onRoomDisconnected: EventEmitter<void> = new EventEmitter<void>(); @Output() onRoomDisconnected: EventEmitter<void> = new EventEmitter<void>();
/**
* This event is emitted when the local participant leaves the room.
*/
@Output() onParticipantLeft: EventEmitter<ParticipantLeftEvent> = new EventEmitter<ParticipantLeftEvent>();
/** /**
* This event is emitted when the video state changes, providing information about if the video is enabled (true) or disabled (false). * This event is emitted when the video state changes, providing information about if the video is enabled (true) or disabled (false).
*/ */
@ -144,12 +139,6 @@ export class ToolbarComponent implements OnInit, OnDestroy, AfterViewInit {
@Output() onBroadcastingStopRequested: EventEmitter<BroadcastingStopRequestedEvent> = @Output() onBroadcastingStopRequested: EventEmitter<BroadcastingStopRequestedEvent> =
new EventEmitter<BroadcastingStopRequestedEvent>(); new EventEmitter<BroadcastingStopRequestedEvent>();
/**
* @internal
* This event is fired when the user clicks on the view recordings button.
*/
@Output() onViewRecordingsClicked: EventEmitter<void> = new EventEmitter<void>();
/** /**
* @ignore * @ignore
*/ */
@ -213,14 +202,6 @@ export class ToolbarComponent implements OnInit, OnDestroy, AfterViewInit {
* @ignore * @ignore
*/ */
isMinimal: boolean = false; isMinimal: boolean = false;
/**
* @ignore
*/
showCameraButton: boolean = true;
/**
* @ignore
*/
showMicrophoneButton: boolean = true;
/** /**
* @ignore * @ignore
*/ */
@ -245,11 +226,6 @@ export class ToolbarComponent implements OnInit, OnDestroy, AfterViewInit {
*/ */
showRecordingButton: boolean = true; showRecordingButton: boolean = true;
/**
* @ignore
*/
showViewRecordingsButton: boolean = false;
/** /**
* @ignore * @ignore
*/ */
@ -282,20 +258,10 @@ export class ToolbarComponent implements OnInit, OnDestroy, AfterViewInit {
* @ignore * @ignore
*/ */
showLogo: boolean = true; showLogo: boolean = true;
/** /**
* @ignore * @ignore
*/ */
brandingLogo: string = ''; showSessionName: boolean = true;
/**
* @ignore
*/
showRoomName: boolean = true;
/**
* @ignore
*/
roomName: string = '';
/** /**
* @ignore * @ignore
@ -327,11 +293,6 @@ export class ToolbarComponent implements OnInit, OnDestroy, AfterViewInit {
*/ */
recordingStatus: RecordingStatus = RecordingStatus.STOPPED; recordingStatus: RecordingStatus = RecordingStatus.STOPPED;
/**
* @ignore
*/
isRecordingReadOnlyMode: boolean = false;
/** /**
* @ignore * @ignore
*/ */
@ -359,25 +320,36 @@ export class ToolbarComponent implements OnInit, OnDestroy, AfterViewInit {
*/ */
recordingTime: Date; recordingTime: Date;
/**
* @internal
* Template configuration managed by the service
*/
templateConfig: ToolbarTemplateConfiguration = {};
// Store directive references for template setup
private _externalAdditionalButtons?: ToolbarAdditionalButtonsDirective;
private _externalAdditionalPanelButtons?: ToolbarAdditionalPanelButtonsDirective;
private log: ILogger; private log: ILogger;
private destroy$ = new Subject<void>(); private minimalSub: Subscription;
private panelTogglingSubscription: Subscription;
private chatMessagesSubscription: Subscription;
private localParticipantSubscription: Subscription;
private screenshareButtonSub: Subscription;
private fullscreenButtonSub: Subscription;
private backgroundEffectsButtonSub: Subscription;
private leaveButtonSub: Subscription;
private recordingButtonSub: Subscription;
private broadcastingButtonSub: Subscription;
private recordingSubscription: Subscription;
private broadcastingSubscription: Subscription;
private activitiesPanelButtonSub: Subscription;
private participantsPanelButtonSub: Subscription;
private chatPanelButtonSub: Subscription;
private displayLogoSub: Subscription;
private displayRoomNameSub: Subscription;
private settingsButtonSub: Subscription;
private captionsSubs: Subscription;
private additionalButtonsPositionSub: Subscription;
private fullscreenChangeSubscription: Subscription;
private currentWindowHeight = window.innerHeight; private currentWindowHeight = window.innerHeight;
private layoutService: LayoutService;
/** /**
* @ignore * @ignore
*/ */
constructor( constructor(
private layoutService: LayoutService, private serviceConfig: ServiceConfigService,
private documentService: DocumentService, private documentService: DocumentService,
private chatService: ChatService, private chatService: ChatService,
private panelService: PanelService, private panelService: PanelService,
@ -393,10 +365,10 @@ export class ToolbarComponent implements OnInit, OnDestroy, AfterViewInit {
private broadcastingService: BroadcastingService, private broadcastingService: BroadcastingService,
private translateService: TranslateService, private translateService: TranslateService,
private storageSrv: StorageService, private storageSrv: StorageService,
private cdkOverlayService: CdkOverlayService, private cdkOverlayService: CdkOverlayService
private templateManagerService: TemplateManagerService
) { ) {
this.log = this.loggerSrv.get('ToolbarComponent'); this.log = this.loggerSrv.get('ToolbarComponent');
this.layoutService = this.serviceConfig.getLayoutService();
} }
/** /**
* @ignore * @ignore
@ -424,12 +396,10 @@ export class ToolbarComponent implements OnInit, OnDestroy, AfterViewInit {
async ngOnInit() { async ngOnInit() {
this.room = this.openviduService.getRoom(); this.room = this.openviduService.getRoom();
this.evalAndSetRoomName(this.libService.getRoomName());
this.hasVideoDevices = this.oVDevicesService.hasVideoDeviceAvailable(); this.hasVideoDevices = this.oVDevicesService.hasVideoDeviceAvailable();
this.hasAudioDevices = this.oVDevicesService.hasAudioDeviceAvailable(); this.hasAudioDevices = this.oVDevicesService.hasAudioDeviceAvailable();
this.setupTemplates();
this.subscribeToToolbarDirectives(); this.subscribeToToolbarDirectives();
this.subscribeToUserMediaProperties(); this.subscribeToUserMediaProperties();
@ -447,55 +417,31 @@ export class ToolbarComponent implements OnInit, OnDestroy, AfterViewInit {
ngOnDestroy(): void { ngOnDestroy(): void {
this.panelService.clear(); this.panelService.clear();
this.destroy$.next(); if (this.panelTogglingSubscription) this.panelTogglingSubscription.unsubscribe();
this.destroy$.complete(); if (this.chatMessagesSubscription) this.chatMessagesSubscription.unsubscribe();
if (this.localParticipantSubscription) this.localParticipantSubscription.unsubscribe();
if (this.screenshareButtonSub) this.screenshareButtonSub.unsubscribe();
if (this.fullscreenButtonSub) this.fullscreenButtonSub.unsubscribe();
if (this.backgroundEffectsButtonSub) this.backgroundEffectsButtonSub.unsubscribe();
if (this.leaveButtonSub) this.leaveButtonSub.unsubscribe();
if (this.recordingButtonSub) this.recordingButtonSub.unsubscribe();
if (this.broadcastingButtonSub) this.broadcastingButtonSub.unsubscribe();
if (this.participantsPanelButtonSub) this.participantsPanelButtonSub.unsubscribe();
if (this.chatPanelButtonSub) this.chatPanelButtonSub.unsubscribe();
if (this.displayLogoSub) this.displayLogoSub.unsubscribe();
if (this.displayRoomNameSub) this.displayRoomNameSub.unsubscribe();
if (this.minimalSub) this.minimalSub.unsubscribe();
if (this.activitiesPanelButtonSub) this.activitiesPanelButtonSub.unsubscribe();
if (this.recordingSubscription) this.recordingSubscription.unsubscribe();
if (this.broadcastingSubscription) this.broadcastingSubscription.unsubscribe();
if (this.settingsButtonSub) this.settingsButtonSub.unsubscribe();
if (this.captionsSubs) this.captionsSubs.unsubscribe();
if (this.fullscreenChangeSubscription) this.fullscreenChangeSubscription.unsubscribe();
if (this.additionalButtonsPositionSub) this.additionalButtonsPositionSub.unsubscribe();
this.isFullscreenActive = false; this.isFullscreenActive = false;
this.cdkOverlayService.setSelector('body'); this.cdkOverlayService.setSelector('body');
} }
/**
* @internal
* Sets up all templates using the template manager service
*/
private setupTemplates(): void {
this.templateConfig = this.templateManagerService.setupToolbarTemplates(
this._externalAdditionalButtons,
this._externalAdditionalPanelButtons
);
// Apply templates to component properties for backward compatibility
this.applyTemplateConfiguration();
}
/**
* @internal
* Applies the template configuration to component properties
*/
private applyTemplateConfiguration(): void {
if (this.templateConfig.toolbarAdditionalButtonsTemplate) {
this.toolbarAdditionalButtonsTemplate = this.templateConfig.toolbarAdditionalButtonsTemplate;
}
if (this.templateConfig.toolbarAdditionalPanelButtonsTemplate) {
this.toolbarAdditionalPanelButtonsTemplate = this.templateConfig.toolbarAdditionalPanelButtonsTemplate;
}
}
/**
* @internal
* Updates templates and triggers change detection
*/
private updateTemplatesAndMarkForCheck(): void {
this.setupTemplates();
this.cd.markForCheck();
}
/**
* @internal
*/
get hasRoomTracksPublished(): boolean {
return this.openviduService.hasRoomTracksPublished();
}
/** /**
* @ignore * @ignore
*/ */
@ -550,53 +496,17 @@ export class ToolbarComponent implements OnInit, OnDestroy, AfterViewInit {
} }
/** /**
* The participant leaves the room voluntarily.
* @ignore * @ignore
*/ */
async disconnect() { async disconnect() {
try { await this.openviduService.disconnectRoom();
await this.openviduService.disconnectRoom(() => { this.onRoomDisconnected.emit();
this.onParticipantLeft.emit({
roomName: this.openviduService.getRoomName(),
participantName: this.participantService.getLocalParticipant()?.name || '',
identity: this.participantService.getLocalParticipant()?.identity || '',
reason: ParticipantLeftReason.LEAVE
});
this.onRoomDisconnected.emit();
}, false);
} catch (error) {
this.log.e('There was an error disconnecting:', error.code, error.message);
this.actionService.openDialog(this.translateService.translate('ERRORS.DISCONNECT'), error?.error || error?.message || error);
}
}
/**
* @ignore
*/
openRecordingActivityPanel() {
if (this.showActivitiesPanelButton && !this.isActivitiesOpened) {
this.panelService.togglePanel(PanelType.ACTIVITIES, 'recording');
}
}
/**
* @ignore
*/
openBroadcastingActivityPanel() {
if (this.showActivitiesPanelButton && !this.isActivitiesOpened) {
this.panelService.togglePanel(PanelType.ACTIVITIES, 'broadcasting');
}
} }
/** /**
* @ignore * @ignore
*/ */
toggleRecording() { toggleRecording() {
if (this.recordingStatus === RecordingStatus.FAILED) {
this.openRecordingActivityPanel();
return;
}
const payload: RecordingStartRequestedEvent = { const payload: RecordingStartRequestedEvent = {
roomName: this.openviduService.getRoomName() roomName: this.openviduService.getRoomName()
}; };
@ -606,7 +516,9 @@ export class ToolbarComponent implements OnInit, OnDestroy, AfterViewInit {
this.onRecordingStopRequested.emit(payload); this.onRecordingStopRequested.emit(payload);
} else if (this.recordingStatus === RecordingStatus.STOPPED) { } else if (this.recordingStatus === RecordingStatus.STOPPED) {
this.onRecordingStartRequested.emit(payload); this.onRecordingStartRequested.emit(payload);
this.openRecordingActivityPanel(); if (this.showActivitiesPanelButton && !this.isActivitiesOpened) {
this.toggleActivitiesPanel('recording');
}
} }
} }
@ -623,7 +535,9 @@ export class ToolbarComponent implements OnInit, OnDestroy, AfterViewInit {
this.onBroadcastingStopRequested.emit(payload); this.onBroadcastingStopRequested.emit(payload);
this.broadcastingService.setBroadcastingStopped(); this.broadcastingService.setBroadcastingStopped();
} else if (this.broadcastingStatus === BroadcastingStatus.STOPPED) { } else if (this.broadcastingStatus === BroadcastingStatus.STOPPED) {
this.openBroadcastingActivityPanel(); if (this.showActivitiesPanelButton && !this.isActivitiesOpened) {
this.toggleActivitiesPanel('broadcasting');
}
} }
} }
@ -676,11 +590,7 @@ export class ToolbarComponent implements OnInit, OnDestroy, AfterViewInit {
this.documentService.toggleFullscreen('session-container'); this.documentService.toggleFullscreen('session-container');
} }
/** private toggleActivitiesPanel(expandPanel: string) {
* @internal
* @param expandPanel
*/
toggleActivitiesPanel(expandPanel: string) {
this.panelService.togglePanel(PanelType.ACTIVITIES, expandPanel); this.panelService.togglePanel(PanelType.ACTIVITIES, expandPanel);
} }
@ -695,23 +605,21 @@ export class ToolbarComponent implements OnInit, OnDestroy, AfterViewInit {
} }
private subscribeToFullscreenChanged() { private subscribeToFullscreenChanged() {
fromEvent(document, 'fullscreenchange') this.fullscreenChangeSubscription = fromEvent(document, 'fullscreenchange').subscribe(() => {
.pipe(takeUntil(this.destroy$)) const isFullscreen = Boolean(document.fullscreenElement);
.subscribe(() => { if (isFullscreen) {
const isFullscreen = Boolean(document.fullscreenElement); this.cdkOverlayService.setSelector('#session-container');
if (isFullscreen) { } else {
this.cdkOverlayService.setSelector('#session-container'); this.cdkOverlayService.setSelector('body');
} else { }
this.cdkOverlayService.setSelector('body'); this.isFullscreenActive = isFullscreen;
} this.onFullscreenEnabledChanged.emit(this.isFullscreenActive);
this.isFullscreenActive = isFullscreen; this.cd.detectChanges();
this.onFullscreenEnabledChanged.emit(this.isFullscreenActive); });
this.cd.detectChanges();
});
} }
private subscribeToMenuToggling() { private subscribeToMenuToggling() {
this.panelService.panelStatusObs.pipe(takeUntil(this.destroy$)).subscribe((ev: PanelStatusInfo) => { this.panelTogglingSubscription = this.panelService.panelStatusObs.subscribe((ev: PanelStatusInfo) => {
this.isChatOpened = ev.isOpened && ev.panelType === PanelType.CHAT; this.isChatOpened = ev.isOpened && ev.panelType === PanelType.CHAT;
this.isParticipantsOpened = ev.isOpened && ev.panelType === PanelType.PARTICIPANTS; this.isParticipantsOpened = ev.isOpened && ev.panelType === PanelType.PARTICIPANTS;
this.isActivitiesOpened = ev.isOpened && ev.panelType === PanelType.ACTIVITIES; this.isActivitiesOpened = ev.isOpened && ev.panelType === PanelType.ACTIVITIES;
@ -723,7 +631,7 @@ export class ToolbarComponent implements OnInit, OnDestroy, AfterViewInit {
} }
private subscribeToChatMessages() { private subscribeToChatMessages() {
this.chatService.messagesObs.pipe(skip(1), takeUntil(this.destroy$)).subscribe((messages) => { this.chatMessagesSubscription = this.chatService.messagesObs.pipe(skip(1)).subscribe((messages) => {
if (!this.panelService.isChatPanelOpened()) { if (!this.panelService.isChatPanelOpened()) {
this.unreadMessages++; this.unreadMessages++;
} }
@ -732,7 +640,7 @@ export class ToolbarComponent implements OnInit, OnDestroy, AfterViewInit {
}); });
} }
private subscribeToUserMediaProperties() { private subscribeToUserMediaProperties() {
this.participantService.localParticipant$.pipe(takeUntil(this.destroy$)).subscribe((p: ParticipantModel | undefined) => { this.localParticipantSubscription = this.participantService.localParticipant$.subscribe((p: ParticipantModel | undefined) => {
if (p) { if (p) {
if (this.isCameraEnabled !== p.isCameraEnabled) { if (this.isCameraEnabled !== p.isCameraEnabled) {
this.onVideoEnabledChanged.emit(p.isCameraEnabled); this.onVideoEnabledChanged.emit(p.isCameraEnabled);
@ -756,13 +664,8 @@ export class ToolbarComponent implements OnInit, OnDestroy, AfterViewInit {
} }
private subscribeToRecordingStatus() { private subscribeToRecordingStatus() {
this.libService.recordingActivityReadOnly$.pipe(takeUntil(this.destroy$)).subscribe((readOnly: boolean) => { this.recordingSubscription = this.recordingService.recordingStatusObs.subscribe((event: RecordingStatusInfo) => {
this.isRecordingReadOnlyMode = readOnly; const { status, recordingElapsedTime } = event;
this.cd.markForCheck();
});
this.recordingService.recordingStatusObs.pipe(takeUntil(this.destroy$)).subscribe((event: RecordingStatusInfo) => {
const { status, startedAt } = event;
this.recordingStatus = status; this.recordingStatus = status;
if (status === RecordingStatus.STARTED) { if (status === RecordingStatus.STARTED) {
this.startedRecording = event.recordingList.find((rec) => rec.status === RecordingStatus.STARTED); this.startedRecording = event.recordingList.find((rec) => rec.status === RecordingStatus.STARTED);
@ -770,15 +673,15 @@ export class ToolbarComponent implements OnInit, OnDestroy, AfterViewInit {
this.startedRecording = undefined; this.startedRecording = undefined;
} }
if (startedAt) { if (recordingElapsedTime) {
this.recordingTime = startedAt; this.recordingTime = recordingElapsedTime;
} }
this.cd.markForCheck(); this.cd.markForCheck();
}); });
} }
private subscribeToBroadcastingStatus() { private subscribeToBroadcastingStatus() {
this.broadcastingService.broadcastingStatusObs.pipe(takeUntil(this.destroy$)).subscribe((ev: BroadcastingStatusInfo) => { this.broadcastingSubscription = this.broadcastingService.broadcastingStatusObs.subscribe((ev: BroadcastingStatusInfo) => {
if (!!ev) { if (!!ev) {
this.broadcastingStatus = ev.status; this.broadcastingStatus = ev.status;
this.broadcastingId = ev.broadcastingId; this.broadcastingId = ev.broadcastingId;
@ -788,97 +691,73 @@ export class ToolbarComponent implements OnInit, OnDestroy, AfterViewInit {
} }
private subscribeToToolbarDirectives() { private subscribeToToolbarDirectives() {
this.libService.minimal$.pipe(takeUntil(this.destroy$)).subscribe((value: boolean) => { this.minimalSub = this.libService.minimal$.subscribe((value: boolean) => {
this.isMinimal = value; this.isMinimal = value;
this.cd.markForCheck(); this.cd.markForCheck();
}); });
this.libService.brandingLogo$.pipe(takeUntil(this.destroy$)).subscribe((value: string) => { this.screenshareButtonSub = this.libService.screenshareButton$.subscribe((value: boolean) => {
this.brandingLogo = value;
this.cd.markForCheck();
});
this.libService.toolbarViewRecordingsButton$.pipe(takeUntil(this.destroy$)).subscribe((value: boolean) => {
this.showViewRecordingsButton = value;
this.checkDisplayMoreOptions();
this.cd.markForCheck();
});
this.libService.cameraButton$.pipe(takeUntil(this.destroy$)).subscribe((value: boolean) => {
this.showCameraButton = value;
this.cd.markForCheck();
});
this.libService.microphoneButton$.pipe(takeUntil(this.destroy$)).subscribe((value: boolean) => {
this.showMicrophoneButton = value;
this.cd.markForCheck();
});
this.libService.screenshareButton$.pipe(takeUntil(this.destroy$)).subscribe((value: boolean) => {
this.showScreenshareButton = value && !this.platformService.isMobile(); this.showScreenshareButton = value && !this.platformService.isMobile();
this.cd.markForCheck(); this.cd.markForCheck();
}); });
this.libService.fullscreenButton$.pipe(takeUntil(this.destroy$)).subscribe((value: boolean) => { this.fullscreenButtonSub = this.libService.fullscreenButton$.subscribe((value: boolean) => {
this.showFullscreenButton = value; this.showFullscreenButton = value;
this.checkDisplayMoreOptions(); this.checkDisplayMoreOptions();
this.cd.markForCheck(); this.cd.markForCheck();
}); });
this.libService.leaveButton$.pipe(takeUntil(this.destroy$)).subscribe((value: boolean) => { this.leaveButtonSub = this.libService.leaveButton$.subscribe((value: boolean) => {
this.showLeaveButton = value; this.showLeaveButton = value;
this.cd.markForCheck(); this.cd.markForCheck();
}); });
this.libService.recordingButton$.pipe(takeUntil(this.destroy$)).subscribe((value: boolean) => { this.recordingButtonSub = this.libService.recordingButton$.subscribe((value: boolean) => {
this.showRecordingButton = value; this.showRecordingButton = value;
this.checkDisplayMoreOptions(); this.checkDisplayMoreOptions();
this.cd.markForCheck(); this.cd.markForCheck();
}); });
this.libService.broadcastingButton$.pipe(takeUntil(this.destroy$)).subscribe((value: boolean) => { this.broadcastingButtonSub = this.libService.broadcastingButton$.subscribe((value: boolean) => {
this.showBroadcastingButton = value; this.showBroadcastingButton = value;
this.checkDisplayMoreOptions(); this.checkDisplayMoreOptions();
this.cd.markForCheck(); this.cd.markForCheck();
}); });
this.libService.toolbarSettingsButton$.pipe(takeUntil(this.destroy$)).subscribe((value: boolean) => { this.settingsButtonSub = this.libService.toolbarSettingsButton$.subscribe((value: boolean) => {
this.showSettingsButton = value; this.showSettingsButton = value;
this.checkDisplayMoreOptions(); this.checkDisplayMoreOptions();
this.cd.markForCheck(); this.cd.markForCheck();
}); });
this.libService.chatPanelButton$.pipe(takeUntil(this.destroy$)).subscribe((value: boolean) => { this.chatPanelButtonSub = this.libService.chatPanelButton$.subscribe((value: boolean) => {
this.showChatPanelButton = value; this.showChatPanelButton = value;
this.cd.markForCheck(); this.cd.markForCheck();
}); });
this.libService.participantsPanelButton$.pipe(takeUntil(this.destroy$)).subscribe((value: boolean) => { this.participantsPanelButtonSub = this.libService.participantsPanelButton$.subscribe((value: boolean) => {
this.showParticipantsPanelButton = value; this.showParticipantsPanelButton = value;
this.cd.markForCheck(); this.cd.markForCheck();
}); });
this.libService.activitiesPanelButton$.pipe(takeUntil(this.destroy$)).subscribe((value: boolean) => { this.activitiesPanelButtonSub = this.libService.activitiesPanelButton$.subscribe((value: boolean) => {
this.showActivitiesPanelButton = value; this.showActivitiesPanelButton = value;
this.cd.markForCheck(); this.cd.markForCheck();
}); });
this.libService.backgroundEffectsButton$.pipe(takeUntil(this.destroy$)).subscribe((value: boolean) => { this.backgroundEffectsButtonSub = this.libService.backgroundEffectsButton$.subscribe((value: boolean) => {
this.showBackgroundEffectsButton = value; this.showBackgroundEffectsButton = value;
this.checkDisplayMoreOptions(); this.checkDisplayMoreOptions();
this.cd.markForCheck(); this.cd.markForCheck();
}); });
this.libService.displayLogo$.pipe(takeUntil(this.destroy$)).subscribe((value: boolean) => { this.displayLogoSub = this.libService.displayLogo$.subscribe((value: boolean) => {
this.showLogo = value; this.showLogo = value;
this.cd.markForCheck(); this.cd.markForCheck();
}); });
this.displayRoomNameSub = this.libService.displayRoomName$.subscribe((value: boolean) => {
this.libService.displayRoomName$.pipe(takeUntil(this.destroy$)).subscribe((value: boolean) => { this.showSessionName = value;
this.showRoomName = value; this.cd.markForCheck();
});
this.captionsSubs = this.libService.captionsButton$.subscribe((value: boolean) => {
this.showCaptionsButton = value;
this.cd.markForCheck(); this.cd.markForCheck();
}); });
this.libService.roomName$.pipe(takeUntil(this.destroy$)).subscribe((value: string) => { this.additionalButtonsPositionSub = this.libService.toolbarAdditionalButtonsPosition$.subscribe(
this.evalAndSetRoomName(value); (value: ToolbarAdditionalButtonsPosition) => {
this.cd.markForCheck();
});
// this.libService.captionsButton$.pipe(takeUntil(this.destroy$)).subscribe((value: boolean) => {
// this.showCaptionsButton = value;
// this.cd.markForCheck();
// });
this.libService.toolbarAdditionalButtonsPosition$
.pipe(takeUntil(this.destroy$))
.subscribe((value: ToolbarAdditionalButtonsPosition) => {
// Using Promise.resolve() to defer change detection until the next microtask. // Using Promise.resolve() to defer change detection until the next microtask.
// This ensures that Angular's change detection has the latest value before updating the view. // This ensures that Angular's change detection has the latest value before updating the view.
// Without this, Angular's OnPush strategy might not immediately reflect the change, // Without this, Angular's OnPush strategy might not immediately reflect the change,
@ -888,11 +767,12 @@ export class ToolbarComponent implements OnInit, OnDestroy, AfterViewInit {
this.additionalButtonsPosition = value; this.additionalButtonsPosition = value;
this.cd.markForCheck(); this.cd.markForCheck();
}); });
}); }
);
} }
private subscribeToCaptionsToggling() { private subscribeToCaptionsToggling() {
this.layoutService.captionsTogglingObs.pipe(takeUntil(this.destroy$)).subscribe((value: boolean) => { this.captionsSubs = this.layoutService.captionsTogglingObs.subscribe((value: boolean) => {
this.captionsEnabled = value; this.captionsEnabled = value;
this.cd.markForCheck(); this.cd.markForCheck();
}); });
@ -906,14 +786,4 @@ export class ToolbarComponent implements OnInit, OnDestroy, AfterViewInit {
this.showBroadcastingButton || this.showBroadcastingButton ||
this.showSettingsButton; this.showSettingsButton;
} }
private evalAndSetRoomName(value: string) {
if (!!value) {
this.roomName = value;
} else if (!!this.room && this.room.name) {
this.roomName = this.room.name;
} else {
this.roomName = '';
}
}
} }

View File

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

View File

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

View File

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

View File

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

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