Compare commits

..

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

209 changed files with 15902 additions and 43738 deletions

View File

@ -45,7 +45,7 @@ jobs:
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"'"}}'
nested_events:
nested_components_e2e_events:
needs: test_setup
name: Nested events
runs-on: ubuntu-latest
@ -58,33 +58,74 @@ jobs:
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
run: docker run --network=host -d -p 4444:4444 selenium/standalone-chrome:127.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
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 and Serve openvidu-components-angular Testapp
uses: OpenVidu/actions/start-openvidu-components-testapp@main
- 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 event tests
env:
LAUNCH_MODE: CI
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
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
steps:
- name: Checkout Repository
@ -95,29 +136,53 @@ jobs:
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
run: docker run --network=host -d -p 4444:4444 selenium/standalone-chrome:127.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 nested structural directives tests
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-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:
LAUNCH_MODE: CI
run: npm run e2e:nested-structural-directives --prefix openvidu-components-angular
- name: Cleanup
if: always()
uses: OpenVidu/actions/cleanup@main
run: npm run e2e:nested-directives --prefix openvidu-components-angular
nested_attribute_directives:
webcomponent_e2e_directives:
needs: test_setup
name: Nested Attribute Directives
name: Webcomponent directives
runs-on: ubuntu-latest
steps:
- name: Checkout Repository
@ -128,318 +193,439 @@ jobs:
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
run: docker run --network=host -d -p 4444:4444 selenium/standalone-chrome:127.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 nested attribute directives tests
env:
LAUNCH_MODE: CI
run: npm run e2e:nested-attribute-directives --prefix openvidu-components-angular
- name: Cleanup
if: always()
uses: OpenVidu/actions/cleanup@main
e2e_directives:
needs: test_setup
name: API 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-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
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:lib-toolbar --prefix openvidu-components-angular
- name: Cleanup
if: always()
uses: OpenVidu/actions/cleanup@main
run: npm run e2e:webcomponent-directives --prefix openvidu-components-angular
webcomponent_e2e_chat:
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

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

View File

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

View File

@ -10,4 +10,9 @@
node_modules
dist/
docs/
openvidu-webcomponent/
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",
"options": {
"outputPath": {
"base": "dist/openvidu-components-testapp",
"browser": ""
"base": "dist/openvidu-components-testapp"
},
"index": "src/index.html",
"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": {

View File

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

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;
describe('E2E: Toolbar structural directive scenarios', () => {
describe('Testing TOOLBAR STRUCTURAL DIRECTIVES', () => {
let browser: WebDriver;
let utils: OpenViduComponentsPO;
async function createChromeBrowser(): Promise<WebDriver> {
@ -24,13 +24,10 @@ describe('E2E: Toolbar structural directive scenarios', () => {
afterEach(async () => {
// console.log('data:image/png;base64,' + await browser.takeScreenshot());
try {
await utils.leaveRoom();
} catch (error) {}
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 utils.clickOn('#ovToolbar-checkbox');
@ -48,7 +45,7 @@ describe('E2E: Toolbar structural directive scenarios', () => {
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 utils.clickOn('#ovToolbar-checkbox');
@ -72,7 +69,7 @@ describe('E2E: Toolbar structural directive scenarios', () => {
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 utils.clickOn('#ovToolbar-checkbox');
@ -96,7 +93,7 @@ describe('E2E: Toolbar structural directive scenarios', () => {
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;
await browser.get(`${url}`);
@ -119,7 +116,7 @@ describe('E2E: Toolbar structural directive scenarios', () => {
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;
await browser.get(`${url}`);
@ -136,14 +133,14 @@ describe('E2E: Toolbar structural directive scenarios', () => {
expect(await utils.isPresent('#custom-toolbar-additional-panel-buttons')).toBeTrue();
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
expect(await utils.isPresent('#custom-toolbar')).toBeFalse();
});
});
describe('E2E: Panel structural directive scenarios', () => {
describe('Testing PANEL STRUCTURAL DIRECTIVES', () => {
let browser: WebDriver;
let utils: OpenViduComponentsPO;
@ -162,49 +159,10 @@ describe('E2E: Panel structural directive scenarios', () => {
});
afterEach(async () => {
try {
await utils.leaveRoom();
} catch (error) {}
await browser.quit();
});
it('should render an additional custom panel with default panels', async () => {
let element;
await browser.get(`${url}`);
await utils.clickOn('#ovAdditionalPanels-checkbox');
await utils.clickOn('#apply-btn');
// Check if toolbar panel buttons are present
await utils.checkToolbarIsPresent();
// Open additional panel
await utils.clickOn('#toolbar-additional-panel-btn');
await browser.sleep(500);
// Check if custom panel is present
expect(await utils.isPresent('#custom-additional-panel')).toBeTrue();
element = await utils.waitForElement('#additional-panel-title');
expect(await element.getAttribute('innerText')).toEqual('NEW PANEL');
// Open the participants panel
await utils.clickOn('#participants-panel-btn');
await browser.sleep(500);
// Check if default panel is present
expect(await utils.isPresent('#default-participants-panel')).toBeTrue();
// Open additional panel again
await utils.clickOn('#toolbar-additional-panel-btn');
expect(await utils.isPresent('#custom-additional-panel')).toBeTrue();
// Close the additional panel
await utils.clickOn('#toolbar-additional-panel-btn');
expect(await utils.isPresent('#custom-additional-panel')).toBeFalse();
});
it('should render only the custom panel container (no children, no default panels)', async () => {
it('should inject the CUSTOM PANEL without children', async () => {
await browser.get(`${url}`);
await utils.clickOn('#ovPanel-checkbox');
@ -240,7 +198,7 @@ describe('E2E: Panel structural directive scenarios', () => {
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;
await browser.get(`${url}`);
@ -269,7 +227,7 @@ describe('E2E: Panel structural directive scenarios', () => {
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 utils.clickOn('#ovPanel-checkbox');
@ -308,7 +266,7 @@ describe('E2E: Panel structural directive scenarios', () => {
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;
await browser.get(`${url}`);
@ -335,7 +293,7 @@ describe('E2E: Panel structural directive scenarios', () => {
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 utils.clickOn('#ovPanel-checkbox');
@ -373,7 +331,7 @@ describe('E2E: Panel structural directive scenarios', () => {
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 utils.clickOn('#ovPanel-checkbox');
@ -412,7 +370,7 @@ describe('E2E: Panel structural directive scenarios', () => {
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 utils.clickOn('#ovPanel-checkbox');
@ -457,7 +415,7 @@ describe('E2E: Panel structural directive scenarios', () => {
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 utils.clickOn('#ovActivitiesPanel-checkbox');
@ -482,7 +440,7 @@ describe('E2E: Panel structural directive scenarios', () => {
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;
await browser.get(`${url}`);
@ -508,7 +466,7 @@ describe('E2E: Panel structural directive scenarios', () => {
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 utils.clickOn('#ovChatPanel-checkbox');
@ -546,7 +504,7 @@ describe('E2E: Panel structural directive scenarios', () => {
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 utils.clickOn('#ovParticipantsPanel-checkbox');
@ -584,7 +542,7 @@ describe('E2E: Panel structural directive scenarios', () => {
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 utils.clickOn('#ovParticipantPanelItem-checkbox');
@ -625,7 +583,7 @@ describe('E2E: Panel structural directive scenarios', () => {
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 utils.clickOn('#ovParticipantPanelItemElements-checkbox');
@ -663,7 +621,7 @@ describe('E2E: Panel structural directive scenarios', () => {
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 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 utils: OpenViduComponentsPO;
@ -723,13 +681,10 @@ describe('E2E: Layout and stream structural directive scenarios', () => {
});
afterEach(async () => {
try {
await utils.leaveRoom();
} catch (error) {}
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 utils.clickOn('#ovLayout-checkbox');
@ -750,7 +705,7 @@ describe('E2E: Layout and stream structural directive scenarios', () => {
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 utils.clickOn('#ovLayout-checkbox');
@ -778,7 +733,7 @@ describe('E2E: Layout and stream structural directive scenarios', () => {
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 utils.clickOn('#ovStream-checkbox');
@ -804,3 +759,293 @@ describe('E2E: Layout and stream structural directive scenarios', () => {
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;
describe('OpenVidu Components EVENTS', () => {
describe('Testing EVENTS', () => {
let browser: WebDriver;
let utils: OpenViduComponentsPO;
@ -24,9 +24,6 @@ describe('OpenVidu Components EVENTS', () => {
});
afterEach(async () => {
try {
await utils.leaveRoom();
} catch (error) {}
await browser.quit();
});

View File

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

View File

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

View File

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

View File

@ -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.
import { FilterStream } from './filter-stream.js';
export const monkeyPatchMediaDevices = () => {
export default function monkeyPatchMediaDevices() {
const enumerateDevicesFn = MediaDevices.prototype.enumerateDevices;
const getUserMediaFn = MediaDevices.prototype.getUserMedia;
const getDisplayMediaFn = MediaDevices.prototype.getDisplayMedia;
const fakeVideoDevice = {
deviceId: 'virtual_video',
groupId: '',
deviceId: 'virtual',
groupID: '',
kind: 'videoinput',
label: 'custom_fake_video_1'
};
const fakeAudioDevice = {
deviceId: 'virtual_audio',
groupId: '',
deviceId: 'virtual',
groupID: '',
kind: 'audioinput',
label: 'custom_fake_audio_1'
};
@ -29,21 +29,8 @@ export const monkeyPatchMediaDevices = () => {
const getUserMediaMonkeyPatch = async function () {
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;
if (deviceId === 'virtual' || deviceId?.exact === 'virtual') {
const constraints = {
video: {
facingMode: args.facingMode,

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,18 +1,19 @@
import { Builder, ILocation, IRectangle, ISize, WebDriver } from 'selenium-webdriver';
import { TestAppConfig } from './selenium.conf';
import { OpenViduComponentsPO } from './utils.po.test';
import { OPENVIDU_CALL_SERVER } from '../config';
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 utils: OpenViduComponentsPO;
async function createChromeBrowser(): Promise<WebDriver> {
return await new Builder()
.forBrowser(TestAppConfig.browserName)
.withCapabilities(TestAppConfig.browserCapabilities)
.setChromeOptions(TestAppConfig.browserOptions)
.usingServer(TestAppConfig.seleniumAddress)
.forBrowser(WebComponentConfig.browserName)
.withCapabilities(WebComponentConfig.browserCapabilities)
.setChromeOptions(WebComponentConfig.browserOptions)
.usingServer(WebComponentConfig.seleniumAddress)
.build();
}
@ -22,13 +23,10 @@ describe('Stream rendering and media toggling scenarios', () => {
});
afterEach(async () => {
try {
await utils.leaveRoom();
} catch (error) {}
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 utils.checkPrejoinIsPresent();
@ -41,7 +39,7 @@ describe('Stream rendering and media toggling scenarios', () => {
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 utils.checkPrejoinIsPresent();
@ -54,7 +52,7 @@ describe('Stream rendering and media toggling scenarios', () => {
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 utils.checkPrejoinIsPresent();
@ -67,7 +65,7 @@ describe('Stream rendering and media toggling scenarios', () => {
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 utils.checkPrejoinIsPresent();
@ -92,7 +90,7 @@ describe('Stream rendering and media toggling scenarios', () => {
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 utils.checkPrejoinIsPresent();
@ -117,9 +115,9 @@ describe('Stream rendering and media toggling scenarios', () => {
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 fixedUrl = `${url}&roomName=${roomName}&prejoin=false&videoEnabled=false&audioEnabled=false`;
await browser.get(fixedUrl);
@ -147,7 +145,7 @@ describe('Stream rendering and media toggling scenarios', () => {
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 fixedUrl = `${url}&roomName=${roomName}&prejoin=false&videoEnabled=true&audioEnabled=false`;
await browser.get(fixedUrl);
@ -160,8 +158,6 @@ describe('Stream rendering and media toggling scenarios', () => {
expect(await utils.getNumberOfElements('audio')).toEqual(0);
const tabs = await utils.openTab(fixedUrl);
await browser.sleep(1000);
await browser.switchTo().window(tabs[0]);
await utils.waitForElement('.OV_stream.remote');
@ -177,7 +173,7 @@ describe('Stream rendering and media toggling scenarios', () => {
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 fixedUrl = `${url}&roomName=${roomName}&prejoin=false&videoEnabled=false`;
await browser.get(fixedUrl);
@ -190,7 +186,6 @@ describe('Stream rendering and media toggling scenarios', () => {
expect(await utils.getNumberOfElements('audio')).toEqual(1);
const tabs = await utils.openTab(fixedUrl);
await browser.sleep(1000);
await browser.switchTo().window(tabs[0]);
await utils.waitForElement('.OV_stream.remote');
@ -206,7 +201,7 @@ describe('Stream rendering and media toggling scenarios', () => {
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 fixedUrl = `${url}&roomName=${roomName}&prejoin=false&videoEnabled=false&audioEnabled=false`;
await browser.get(fixedUrl);
@ -245,7 +240,7 @@ describe('Stream rendering and media toggling scenarios', () => {
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 fixedUrl = `${url}&roomName=${roomName}&prejoin=false&videoEnabled=true&audioEnabled=true`;
await browser.get(fixedUrl);
@ -284,7 +279,7 @@ describe('Stream rendering and media toggling scenarios', () => {
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 fixedUrl = `${url}&roomName=${roomName}&prejoin=false&videoEnabled=false&audioEnabled=false`;
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 utils: OpenViduComponentsPO;
async function createChromeBrowser(): Promise<WebDriver> {
return await new Builder()
.forBrowser(TestAppConfig.browserName)
.withCapabilities(TestAppConfig.browserCapabilities)
.setChromeOptions(TestAppConfig.browserOptions)
.usingServer(TestAppConfig.seleniumAddress)
.forBrowser(WebComponentConfig.browserName)
.withCapabilities(WebComponentConfig.browserCapabilities)
.setChromeOptions(WebComponentConfig.browserOptions)
.usingServer(WebComponentConfig.seleniumAddress)
.build();
}
@ -346,9 +341,6 @@ describe('Stream UI controls and interaction features', () => {
});
afterEach(async () => {
try {
await utils.leaveRoom();
} catch (error) {}
await browser.quit();
});
@ -666,7 +658,7 @@ describe('Stream UI controls and interaction features', () => {
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 fixedUrl = `${url}&roomName=${roomName}&prejoin=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
const newTabScript = `window.open("${fixedUrl}")`;
await browser.executeScript(newTabScript);
await browser.sleep(1000);
const tabs = await browser.getAllWindowHandles();
await browser.switchTo().window(tabs[0]);
await utils.waitForElement('.OV_stream.remote.speaking');
expect(await utils.getNumberOfElements('.OV_stream.remote.speaking')).toEqual(1);
// 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);
});
});
describe('Video playback reliability with different media states', () => {
describe('Testing video is playing', () => {
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)
.forBrowser(WebComponentConfig.browserName)
.withCapabilities(WebComponentConfig.browserCapabilities)
.setChromeOptions(WebComponentConfig.browserOptions)
.usingServer(WebComponentConfig.seleniumAddress)
.build();
}
@ -709,9 +696,6 @@ describe('Video playback reliability with different media states', () => {
});
afterEach(async () => {
try {
await utils.leaveRoom();
} catch (error) {}
await browser.quit();
});

View File

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

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

View File

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

View File

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

View File

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

View File

@ -15,5 +15,5 @@
"livekit-client": "^2.1.0",
"@livekit/track-processors": "^0.3.2"
},
"version": "3.3.0"
"version": "3.1.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 -->
| **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 -->

View File

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

View File

@ -7,8 +7,7 @@ import { OpenViduComponentsConfigService } from '../../services/config/directive
@Component({
selector: 'ov-admin-login',
templateUrl: './admin-login.component.html',
styleUrls: ['./admin-login.component.scss'],
standalone: false
styleUrls: ['./admin-login.component.scss']
})
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 normal play"></div>
</div>`,
styleUrls: ['./audio-wave.component.scss'],
standalone: false
styleUrls: ['./audio-wave.component.scss']
})
export class AudioWaveComponent {}

View File

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

View File

@ -36,8 +36,7 @@ import { MatDialogRef } from '@angular/material/dialog';
border-radius: var(--ov-surface-radius);
}
`
],
standalone: false
]
})
export class DeleteDialogComponent {
constructor(public dialogRef: MatDialogRef<DeleteDialogComponent>) {}

View File

@ -33,8 +33,7 @@ import { DialogData } from '../../models/dialog.model';
border-radius: var(--ov-surface-radius);
}
`
],
standalone: false
]
})
export class DialogTemplateComponent {
constructor(

View File

@ -19,8 +19,7 @@ import { DialogData } from '../../models/dialog.model';
</button>
<button mat-button (click)="close()">{{'PANEL.CLOSE' | translate}}</button>
</div>
`,
standalone: false
`
})
export class ProFeatureDialogTemplateComponent {
constructor(public dialogRef: MatDialogRef<ProFeatureDialogTemplateComponent>, @Inject(MAT_DIALOG_DATA) public data: DialogData) {}

View File

@ -38,8 +38,7 @@ import { RecordingDialogData } from '../../models/dialog.model';
border-radius: var(--ov-surface-radius);
}
`
],
standalone: false
]
})
export class RecordingDialogComponent {
@ViewChild('videoElement', { static: true }) videoElement: ElementRef<HTMLVideoElement>;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -12,8 +12,7 @@ import { VirtualBackgroundService } from '../../../services/virtual-background/v
selector: 'ov-background-effects-panel',
templateUrl: './background-effects-panel.component.html',
styleUrls: ['../panel.component.scss', './background-effects-panel.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
standalone: false
changeDetection: ChangeDetectionStrategy.OnPush
})
export class BackgroundEffectsPanelComponent implements OnInit {
backgroundSelectedId: string;
@ -57,6 +56,10 @@ export class BackgroundEffectsPanelComponent implements OnInit {
}
async applyBackground(effect: BackgroundEffect) {
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 { Subject, takeUntil } from 'rxjs';
import { Subscription } from 'rxjs';
import { ChatMessage } from '../../../models/chat.model';
import { PanelType } from '../../../models/panel.model';
import { ChatService } from '../../../services/chat/chat.service';
@ -13,8 +13,7 @@ import { PanelService } from '../../../services/panel/panel.service';
selector: 'ov-chat-panel',
templateUrl: './chat-panel.component.html',
styleUrls: ['../panel.component.scss', './chat-panel.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
standalone: false
changeDetection: ChangeDetectionStrategy.OnPush
})
export class ChatPanelComponent implements OnInit, AfterViewInit {
/**
@ -34,7 +33,7 @@ export class ChatPanelComponent implements OnInit, AfterViewInit {
*/
messageList: ChatMessage[] = [];
private destroy$ = new Subject<void>();
private chatMessageSubscription: Subscription;
/**
* @ignore
@ -66,8 +65,7 @@ export class ChatPanelComponent implements OnInit, AfterViewInit {
* @ignore
*/
ngOnDestroy(): void {
this.destroy$.next();
this.destroy$.complete();
if (this.chatMessageSubscription) this.chatMessageSubscription.unsubscribe();
}
/**
@ -110,7 +108,7 @@ export class ChatPanelComponent implements OnInit, AfterViewInit {
}
private subscribeToMessages() {
this.chatService.messagesObs.pipe(takeUntil(this.destroy$)).subscribe((messages: ChatMessage[]) => {
this.chatMessageSubscription = this.chatService.messagesObs.subscribe((messages: ChatMessage[]) => {
this.messageList = messages;
if (this.panelService.isChatPanelOpened()) {
this.scrollToBottom();

View File

@ -8,7 +8,7 @@ import {
Output,
TemplateRef
} from '@angular/core';
import { skip, Subject, takeUntil } from 'rxjs';
import { skip, Subscription } from 'rxjs';
import {
ActivitiesPanelDirective,
AdditionalPanelsDirective,
@ -25,7 +25,6 @@ import {
} from '../../models/panel.model';
import { PanelService } from '../../services/panel/panel.service';
import { BackgroundEffect } from '../../models/background-effect.model';
import { TemplateManagerService, PanelTemplateConfiguration } from '../../services/template/template-manager.service';
/**
*
@ -38,8 +37,7 @@ import { TemplateManagerService, PanelTemplateConfiguration } from '../../servic
selector: 'ov-panel',
templateUrl: './panel.component.html',
styleUrls: ['./panel.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
standalone: false
changeDetection: ChangeDetectionStrategy.OnPush
})
export class PanelComponent implements OnInit {
/**
@ -76,20 +74,42 @@ export class PanelComponent implements OnInit {
*/
@ContentChild(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) {
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
*/
@ContentChild(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) {
this.updateTemplatesAndMarkForCheck();
this.activitiesPanelTemplate = externalActivitiesPanel.template;
}
}
@ -98,9 +118,10 @@ export class PanelComponent implements OnInit {
*/
@ContentChild(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) {
this.updateTemplatesAndMarkForCheck();
this.chatPanelTemplate = externalChatPanel.template;
}
}
@ -109,9 +130,10 @@ export class PanelComponent implements OnInit {
*/
@ContentChild(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) {
this.updateTemplatesAndMarkForCheck();
this.additionalPanelsTemplate = externalAdditionalPanels.template;
}
}
@ -172,20 +194,7 @@ export class PanelComponent implements OnInit {
* @internal
*/
isExternalPanelOpened: boolean;
/**
* @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 panelSubscription: Subscription;
private panelEmitersHandler: Map<
PanelType,
@ -197,78 +206,30 @@ export class PanelComponent implements OnInit {
*/
constructor(
private panelService: PanelService,
private cd: ChangeDetectorRef,
private templateManagerService: TemplateManagerService
private cd: ChangeDetectorRef
) {}
/**
* @ignore
*/
ngOnInit(): void {
this.setupTemplates();
this.subscribeToPanelToggling();
this.panelEmitersHandler.set(PanelType.CHAT, this.onChatPanelStatusChanged);
this.panelEmitersHandler.set(PanelType.PARTICIPANTS, this.onParticipantsPanelStatusChanged);
this.panelEmitersHandler.set(PanelType.SETTINGS, this.onSettingsPanelStatusChanged);
this.panelEmitersHandler.set(PanelType.ACTIVITIES, this.onActivitiesPanelStatusChanged);
}
/**
* @internal
* Sets up all templates using the template manager service
*/
private setupTemplates(): void {
this.templateConfig = this.templateManagerService.setupPanelTemplates(
this._externalParticipantPanel,
this._externalChatPanel,
this._externalActivitiesPanel,
this._externalAdditionalPanels
);
// Apply templates to component properties for backward compatibility
this.applyTemplateConfiguration();
}
/**
* @internal
* Applies the template configuration to component properties
*/
private applyTemplateConfiguration(): void {
if (this.templateConfig.participantsPanelTemplate) {
this.participantsPanelTemplate = this.templateConfig.participantsPanelTemplate;
}
if (this.templateConfig.chatPanelTemplate) {
this.chatPanelTemplate = this.templateConfig.chatPanelTemplate;
}
if (this.templateConfig.activitiesPanelTemplate) {
this.activitiesPanelTemplate = this.templateConfig.activitiesPanelTemplate;
}
if (this.templateConfig.additionalPanelsTemplate) {
this.additionalPanelsTemplate = this.templateConfig.additionalPanelsTemplate;
}
}
/**
* @internal
* Updates templates and triggers change detection
*/
private updateTemplatesAndMarkForCheck(): void {
this.setupTemplates();
this.cd.markForCheck();
}
/**
* @ignore
*/
ngOnDestroy() {
this.isChatPanelOpened = false;
this.isParticipantsPanelOpened = false;
this.destroy$.next();
this.destroy$.complete();
if (this.panelSubscription) this.panelSubscription.unsubscribe();
}
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.isParticipantsPanelOpened = ev.isOpened && ev.panelType === PanelType.PARTICIPANTS;
this.isBackgroundEffectsPanelOpened = ev.isOpened && ev.panelType === PanelType.BACKGROUND_EFFECTS;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -22,27 +22,25 @@
[value]="settingsOptions.GENERAL"
>
<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
*ngIf="showCameraButton"
class="option"
id="video-opt"
[selected]="selectedOption === settingsOptions.VIDEO"
[value]="settingsOptions.VIDEO"
>
<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
*ngIf="showMicrophoneButton"
class="option"
id="audio-opt"
[selected]="selectedOption === settingsOptions.AUDIO"
[value]="settingsOptions.AUDIO"
>
<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
*ngIf="showCaptions"
@ -70,12 +68,12 @@
</mat-list>
</div>
<ov-video-devices-select
*ngIf="showCameraButton && selectedOption === settingsOptions.VIDEO"
*ngIf="selectedOption === settingsOptions.VIDEO"
(onVideoDeviceChanged)="onVideoDeviceChanged.emit($event)"
(onVideoEnabledChanged)="onVideoEnabledChanged.emit($event)"
></ov-video-devices-select>
<ov-audio-devices-select
*ngIf="showMicrophoneButton && selectedOption === settingsOptions.AUDIO"
*ngIf="selectedOption === settingsOptions.AUDIO"
(onAudioDeviceChanged)="onAudioDeviceChanged.emit($event)"
(onAudioEnabledChanged)="onAudioEnabledChanged.emit($event)"
></ov-audio-devices-select>

View File

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

View File

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

View File

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

View File

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

View File

@ -16,7 +16,7 @@ import {
import { ILogger } from '../../models/logger.model';
import { animate, style, transition, trigger } from '@angular/animations';
import { MatDrawerContainer, MatSidenav } from '@angular/material/sidenav';
import { skip, Subject, takeUntil } from 'rxjs';
import { skip, Subscription } from 'rxjs';
import { SidenavMode } from '../../models/layout.model';
import { PanelStatusInfo, PanelType } from '../../models/panel.model';
import { DataTopic } from '../../models/data-topic.model';
@ -46,9 +46,8 @@ import {
RoomEvent,
Track
} from 'livekit-client';
import { ParticipantLeftEvent, ParticipantLeftReason, ParticipantModel } from '../../models/participant.model';
import { RecordingStatus } from '../../models/recording.model';
import { TemplateManagerService, SessionTemplateConfiguration } from '../../services/template/template-manager.service';
import { ParticipantModel } from '../../models/participant.model';
import { ServiceConfigService } from '../../services/config/service-config.service';
/**
* @internal
@ -59,43 +58,37 @@ import { TemplateManagerService, SessionTemplateConfiguration } from '../../serv
templateUrl: './session.component.html',
styleUrls: ['./session.component.scss'],
animations: [trigger('sessionAnimation', [transition(':enter', [style({ opacity: 0 }), animate('50ms', style({ opacity: 1 }))])])],
changeDetection: ChangeDetectionStrategy.OnPush,
standalone: false
changeDetection: ChangeDetectionStrategy.OnPush
})
export class SessionComponent implements OnInit, OnDestroy {
@ContentChild('toolbar', { read: TemplateRef }) toolbarTemplate: TemplateRef<any>;
@ContentChild('panel', { read: TemplateRef }) panelTemplate: 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>();
/**
* Provides event notifications that fire when Room is being reconnected for the local participant.
* Provides event notifications that fire when OpenVidu Room is disconnected.
*/
@Output() onRoomReconnecting: EventEmitter<void> = new EventEmitter<void>();
/**
* Provides event notifications that fire when Room is reconnected for the local participant.
* Provides event notifications that fire when OpenVidu Room is reconnected.
*/
@Output() onRoomReconnected: EventEmitter<void> = new EventEmitter<void>();
/**
* Provides event notifications that fire when participant is disconnected from Room.
* @deprecated Use {@link SessionComponent.onParticipantLeft} instead.
* Provides event notifications that fire when OpenVidu Room is disconnected.
*/
@Output() onRoomDisconnected: EventEmitter<void> = new EventEmitter<void>();
/**
* Provides event notifications that fire when local participant is connected to the Room.
* Provides event notifications that fire when local participant is created.
*/
@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>();
@Output() onParticipantCreated: EventEmitter<ParticipantModel> = new EventEmitter<ParticipantModel>();
room: Room;
sideMenu: MatSidenav;
@ -104,20 +97,17 @@ export class SessionComponent implements OnInit, OnDestroy {
drawer: MatDrawerContainer;
loading: boolean = true;
/**
* @internal
* Template configuration managed by the service
*/
templateConfig: SessionTemplateConfiguration = {};
private shouldDisconnectRoomWhenComponentIsDestroyed: boolean = true;
private readonly SIDENAV_WIDTH_LIMIT_MODE = 790;
private destroy$ = new Subject<void>();
private menuSubscription: Subscription;
private layoutWidthSubscription: Subscription;
private updateLayoutInterval: NodeJS.Timeout;
private captionLanguageSubscription: Subscription;
private log: ILogger;
private layoutService: LayoutService;
constructor(
private layoutService: LayoutService,
private serviceConfig: ServiceConfigService,
private actionService: ActionService,
private openviduService: OpenViduService,
private participantService: ParticipantService,
@ -130,16 +120,15 @@ export class SessionComponent implements OnInit, OnDestroy {
private translateService: TranslateService,
// private captionService: CaptionService,
private backgroundService: VirtualBackgroundService,
private cd: ChangeDetectorRef,
private templateManagerService: TemplateManagerService
private cd: ChangeDetectorRef
) {
this.log = this.loggerSrv.get('SessionComponent');
this.setupTemplates();
this.layoutService = this.serviceConfig.getLayoutService();
}
@HostListener('window:beforeunload')
beforeunloadHandler() {
this.disconnectRoom(ParticipantLeftReason.BROWSER_UNLOAD);
this.disconnectRoom();
}
@HostListener('window:resize')
@ -188,39 +177,15 @@ export class SessionComponent implements OnInit, OnDestroy {
set layoutContainer(container: ElementRef) {
setTimeout(async () => {
if (container) {
if (this.libService.showBackgroundEffectsButton()) {
// Apply background from storage when layout container is in DOM only when background effects button is enabled
// Apply background from storage when layout container is in DOM
await this.backgroundService.applyBackgroundFromStorage();
}
}
}, 0);
}
async ngOnInit() {
this.shouldDisconnectRoomWhenComponentIsDestroyed = true;
// 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.onRoomCreated.emit(this.room);
// this.subscribeToCaptionLanguage();
this.subcribeToActiveSpeakersChanged();
@ -233,22 +198,19 @@ export class SessionComponent implements OnInit, OnDestroy {
// this.subscribeToParticipantNameChanged();
this.subscribeToDataMessage();
this.subscribeToReconnection();
this.subscribeToVirtualBackground();
// if (this.libService.isRecordingEnabled()) {
if (this.libService.isRecordingEnabled()) {
// this.subscribeToRecordingEvents();
// }
}
// if (this.libService.isBroadcastingEnabled()) {
if (this.libService.isBroadcastingEnabled()) {
// this.subscribeToBroadcastingEvents();
// }
}
try {
await this.participantService.connect();
// Send room created after participant connect for avoiding to send incomplete room payload
this.onRoomCreated.emit(this.room);
this.cd.markForCheck();
this.loading = false;
this.onParticipantConnected.emit(this.participantService.getLocalParticipant());
this.onParticipantCreated.emit(this.participantService.getLocalParticipant());
} catch (error) {
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);
@ -260,41 +222,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() {
if (this.shouldDisconnectRoomWhenComponentIsDestroyed) {
await this.disconnectRoom(ParticipantLeftReason.LEAVE);
await this.disconnectRoom();
}
if (this.room) this.room.removeAllListeners();
this.participantService.clear();
// this.room = undefined;
this.destroy$.next();
this.destroy$.complete();
if (this.menuSubscription) this.menuSubscription.unsubscribe();
if (this.layoutWidthSubscription) this.layoutWidthSubscription.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
this.shouldDisconnectRoomWhenComponentIsDestroyed = false;
await this.openviduService.disconnectRoom(() => {
this.onParticipantLeft.emit({
roomName: this.openviduService.getRoomName(),
participantName: this.participantService.getLocalParticipant()?.name || '',
identity: this.participantService.getLocalParticipant()?.identity || '',
reason
});
}, false);
await this.openviduService.disconnectRoom();
}
private subscribeToTogglingMenu() {
@ -311,7 +254,7 @@ export class SessionComponent implements OnInit, OnDestroy {
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) {
this.settingsPanelOpened = ev.isOpened && ev.panelType === PanelType.SETTINGS;
@ -330,7 +273,7 @@ export class SessionComponent implements OnInit, OnDestroy {
}
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;
});
}
@ -448,7 +391,7 @@ export class SessionComponent implements OnInit, OnDestroy {
this.log.d(`Data event received: ${topic}`);
switch (topic) {
case DataTopic.CHAT:
const participantName = participant?.name || 'Unknown';
const participantName = participant?.identity || 'Unknown';
this.chatService.addRemoteMessage(event.message, participantName);
break;
case DataTopic.RECORDING_STARTING:
@ -500,12 +443,9 @@ export class SessionComponent implements OnInit, OnDestroy {
case DataTopic.ROOM_STATUS:
const { recordingList, isRecordingStarted, isBroadcastingStarted, broadcastingId } = event as RoomStatusData;
if (this.libService.showRecordingActivityRecordingsList()) {
this.recordingService.setRecordingList(recordingList);
}
if (isRecordingStarted) {
const recordingActive = recordingList.find((recording) => recording.status === RecordingStatus.STARTED);
this.recordingService.setRecordingStarted(recordingActive);
this.recordingService.setRecordingStarted();
}
if (isBroadcastingStarted) {
this.broadcastingService.setBroadcastingStarted(broadcastingId);
@ -518,7 +458,7 @@ export class SessionComponent implements OnInit, OnDestroy {
);
}
private subscribeToReconnection() {
subscribeToReconnection() {
this.room.on(RoomEvent.Reconnecting, () => {
this.log.w('Connection lost: Reconnecting');
this.actionService.openConnectionDialog(
@ -534,68 +474,15 @@ export class SessionComponent implements OnInit, OnDestroy {
});
this.room.on(RoomEvent.Disconnected, async (reason: DisconnectReason | undefined) => {
this.shouldDisconnectRoomWhenComponentIsDestroyed = false;
this.actionService.closeConnectionDialog();
const participantLeftEvent: ParticipantLeftEvent = {
roomName: this.openviduService.getRoomName(),
participantName: this.participantService.getLocalParticipant()?.name || '',
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)
if (reason === DisconnectReason.SERVER_SHUTDOWN) {
this.log.e('Room Disconnected', reason);
this.actionService.openConnectionDialog(
this.translateService.translate('ERRORS.CONNECTION'),
this.translateService.translate('ERRORS.RECONNECT')
);
this.onRoomDisconnected.emit();
}
});
}
private subscribeToVirtualBackground() {
this.libService.backgroundEffectsButton$.subscribe(async (enable) => {
if (!enable && this.backgroundService.isBackgroundApplied()) {
await this.backgroundService.removeBackground();
if (this.panelService.isBackgroundEffectsPanelOpened()) {
this.panelService.closePanel();
}
}
// await this.disconnectRoom();
});
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -48,10 +48,10 @@
mat-icon-button
id="pin-btn"
(click)="toggleVideoPinned()"
[class.active-btn]="_track.isPinned"
[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 *ngIf="!_track.isPinned" id="status-pinned" fontIcon="push_pin"></mat-icon>
<mat-icon>push_pin</mat-icon>
</button>
<button
*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 -->
| **Parameter** | **Type** | **Reference** |
|:--------------------------------: | :-------: | :---------------------------------------------: |
| **displayAudioDetection** | `boolean` | [StreamDisplayAudioDetectionDirective](../directives/StreamDisplayAudioDetectionDirective.html) |
| **displayParticipantName** | `boolean` | [StreamDisplayParticipantNameDirective](../directives/StreamDisplayParticipantNameDirective.html) |
| **displayAudioDetection** | `boolean` | [StreamDisplayAudioDetectionDirective](../directives/StreamDisplayAudioDetectionDirective.html) |
| **videoControls** | `boolean` | [StreamVideoControlsDirective](../directives/StreamVideoControlsDirective.html) |
<!-- 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 {
/* Fixes layout bug. The OV_root is created with the entire layout width and it has a weird UX behaviour */
.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 {
color: var(--ov-error-color) !important;
}
@ -76,15 +79,11 @@ $ov-video-elements-bg-color: var(--ov-primary-action-color);
left: 0;
line-height: 0;
#status-mic,
#status-muted-forcibly,
#status-pinned {
font-size: 24px;
margin: 5px;
}
#status-mic,
#status-muted-forcibly {
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 { 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 { OpenViduComponentsConfigService } from '../../services/config/directive-config.service';
import { LayoutService } from '../../services/layout/layout.service';
import { ParticipantService } from '../../services/participant/participant.service';
import { Track } from 'livekit-client';
import { ParticipantTrackPublication } from '../../models/participant.model';
import { ServiceConfigService } from '../../services/config/service-config.service';
/**
* The **StreamComponent** is hosted inside of the {@link LayoutComponent}.
@ -15,8 +16,7 @@ import { ParticipantTrackPublication } from '../../models/participant.model';
@Component({
selector: 'ov-stream',
templateUrl: './stream.component.html',
styleUrls: ['./stream.component.scss'],
standalone: false
styleUrls: ['./stream.component.scss']
})
export class StreamComponent implements OnInit, OnDestroy {
/**
@ -68,7 +68,7 @@ export class StreamComponent implements OnInit, OnDestroy {
/**
* @ignore
*/
hoveringTimeout: ReturnType<typeof setTimeout>;
hoveringTimeout: NodeJS.Timeout;
/**
* @ignore
@ -92,27 +92,35 @@ export class StreamComponent implements OnInit, OnDestroy {
}
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 layoutService: LayoutService;
/**
* @ignore
*/
constructor(
private layoutService: LayoutService,
private serviceConfig: ServiceConfigService,
private participantService: ParticipantService,
private cdkSrv: CdkOverlayService,
private libService: OpenViduComponentsConfigService
) {}
) {
this.layoutService = this.serviceConfig.getLayoutService();
}
ngOnInit() {
this.subscribeToStreamDirectives();
}
ngOnDestroy() {
this.destroy$.next();
this.destroy$.complete();
this.cdkSrv.setSelector('body');
if (this.videoControlsSub) this.videoControlsSub.unsubscribe();
if (this.displayAudioDetectionSub) this.displayAudioDetectionSub.unsubscribe();
if (this.displayParticipantNameSub) this.displayParticipantNameSub.unsubscribe();
if (this.minimalSub) this.minimalSub.unsubscribe();
}
/**
@ -178,29 +186,18 @@ export class StreamComponent implements OnInit, OnDestroy {
}
private subscribeToStreamDirectives() {
this.libService.minimal$
.pipe(takeUntil(this.destroy$))
.subscribe((value: boolean) => {
this.minimalSub = this.libService.minimal$.subscribe((value: boolean) => {
this.isMinimal = value;
});
this.libService.displayParticipantName$
.pipe(takeUntil(this.destroy$))
.subscribe((value: boolean) => {
this.displayParticipantNameSub = this.libService.displayParticipantName$.subscribe((value: boolean) => {
this.showParticipantName = value;
// this.cd.markForCheck();
});
this.libService.displayAudioDetection$
.pipe(takeUntil(this.destroy$))
.subscribe((value: boolean) => {
this.displayAudioDetectionSub = this.libService.displayAudioDetection$.subscribe((value: boolean) => {
this.showAudioDetection = value;
// this.cd.markForCheck();
});
this.libService.streamVideoControls$
.pipe(takeUntil(this.destroy$))
.subscribe((value: boolean) => {
this.videoControlsSub = this.libService.streamVideoControls$.subscribe((value: boolean) => {
this.showVideoControls = value;
// this.cd.markForCheck();
});

View File

@ -1,3 +1,6 @@
<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">
<div id="info-container" class="info-container">
<div>
@ -6,26 +9,20 @@
id="session-info-container"
[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
id="activities-tag"
*ngIf="recordingStatus === _recordingStatus.STARTED || broadcastingStatus === _broadcastingStatus.STARTED"
>
@if (recordingStatus === _recordingStatus.STARTED) {
<div id="recording-tag" class="recording-tag" (click)="openRecordingActivityPanel()">
<div *ngIf="recordingStatus === _recordingStatus.STARTED" id="recording-tag" class="recording-tag">
<mat-icon class="blink">radio_button_checked</mat-icon>
<span class="blink">REC</span>
<span *ngIf="recordingTime"> | {{ recordingTime | date: 'H:mm:ss' }}</span>
</div>
}
@if (broadcastingStatus === _broadcastingStatus.STARTED) {
<!-- Broadcasting tag -->
<div id="broadcasting-tag" class="broadcasting-tag">
<div *ngIf="broadcastingStatus === _broadcastingStatus.STARTED" id="broadcasting-tag" class="broadcasting-tag">
<mat-icon class="blink">sensors</mat-icon>
<span class="blink">LIVE</span>
</div>
}
</div>
</div>
</div>
@ -35,7 +32,6 @@
<button
id="camera-btn"
mat-icon-button
*ngIf="showCameraButton"
(click)="toggleCamera()"
[disabled]="isConnectionLost || !hasVideoDevices || cameraMuteChanging"
[class.warn-btn]="!isCameraEnabled"
@ -50,7 +46,6 @@
<button
id="mic-btn"
mat-icon-button
*ngIf="showMicrophoneButton"
(click)="toggleMicrophone()"
[disabled]="isConnectionLost || !hasAudioDevices || microphoneMuteChanging"
[class.warn-btn]="!isMicrophoneEnabled"
@ -126,36 +121,18 @@
*ngIf="!isMinimal && showRecordingButton"
mat-menu-item
id="recording-btn"
[disabled]="
recordingStatus === _recordingStatus.STARTING ||
recordingStatus === _recordingStatus.STOPPING ||
!hasRoomTracksPublished
"
[matTooltip]="!hasRoomTracksPublished ? ('TOOLBAR.NO_TRACKS_PUBLISHED' | translate) : ''"
[disabled]="recordingStatus === _recordingStatus.STARTING || recordingStatus === _recordingStatus.STOPPING"
(click)="toggleRecording()"
>
<mat-icon color="warn">radio_button_checked</mat-icon>
@if (
recordingStatus === _recordingStatus.STOPPED ||
recordingStatus === _recordingStatus.STOPPING ||
recordingStatus === _recordingStatus.FAILED
) {
<span class="blink">
<span *ngIf="recordingStatus === _recordingStatus.STOPPED || recordingStatus === _recordingStatus.STOPPING">
{{ 'TOOLBAR.START_RECORDING' | translate }}
</span>
} @else if (recordingStatus === _recordingStatus.STARTED || recordingStatus === _recordingStatus.STARTING) {
<span>{{ 'TOOLBAR.STOP_RECORDING' | translate }}</span>
}
<span *ngIf="recordingStatus === _recordingStatus.STARTED || recordingStatus === _recordingStatus.STARTING">
{{ 'TOOLBAR.STOP_RECORDING' | translate }}
</span>
</button>
<!-- View recordings button -->
@if (!isMinimal && showViewRecordingsButton) {
<button mat-menu-item id="view-recordings-btn" (click)="onViewRecordingsClicked.emit()">
<mat-icon>video_library</mat-icon>
<span>{{ 'TOOLBAR.VIEW_RECORDINGS' | translate }}</span>
</button>
}
<!-- Broadcasting button -->
<button
*ngIf="!isMinimal && showBroadcastingButton"

View File

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

View File

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

View File

@ -12,7 +12,7 @@ import {
TemplateRef,
ViewChild
} from '@angular/core';
import { fromEvent, skip, Subject, takeUntil } from 'rxjs';
import { fromEvent, skip, Subscription } from 'rxjs';
import { ChatService } from '../../services/chat/chat.service';
import { DocumentService } from '../../services/document/document.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 { RecordingService } from '../../services/recording/recording.service';
import { StorageService } from '../../services/storage/storage.service';
import { TemplateManagerService, ToolbarTemplateConfiguration } from '../../services/template/template-manager.service';
import { TranslateService } from '../../services/translate/translate.service';
import { CdkOverlayService } from '../../services/cdk-overlay/cdk-overlay.service';
import { ParticipantLeftEvent, ParticipantLeftReason, ParticipantModel } from '../../models/participant.model';
import { ParticipantLeftEvent, ParticipantModel } from '../../models/participant.model';
import { Room, RoomEvent } from 'livekit-client';
import { ToolbarAdditionalButtonsPosition } from '../../models/toolbar.model';
import { ServiceConfigService } from '../../services/config/service-config.service';
/**
* The **ToolbarComponent** is hosted inside of the {@link VideoconferenceComponent}.
@ -59,8 +59,7 @@ import { ToolbarAdditionalButtonsPosition } from '../../models/toolbar.model';
selector: 'ov-toolbar',
templateUrl: './toolbar.component.html',
styleUrls: ['./toolbar.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
standalone: false
changeDetection: ChangeDetectionStrategy.OnPush
})
export class ToolbarComponent implements OnInit, OnDestroy, AfterViewInit {
/**
@ -78,9 +77,10 @@ export class ToolbarComponent implements OnInit, OnDestroy, AfterViewInit {
*/
@ContentChild(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) {
this.updateTemplatesAndMarkForCheck();
this.toolbarAdditionalButtonsTemplate = externalAdditionalButtons.template;
}
}
@ -89,20 +89,15 @@ export class ToolbarComponent implements OnInit, OnDestroy, AfterViewInit {
*/
@ContentChild(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) {
this.updateTemplatesAndMarkForCheck();
this.toolbarAdditionalPanelButtonsTemplate = externalAdditionalPanelButtons.template;
}
}
/**
* This event is emitted when the room has been disconnected.
* @deprecated Use {@link ToolbarComponent.onParticipantLeft} instead.
*/
@Output() onRoomDisconnected: EventEmitter<void> = new EventEmitter<void>();
/**
* This event is emitted when the local participant leaves the room.
* This event is emitted when a participant leaves the room.
*/
@Output() onParticipantLeft: EventEmitter<ParticipantLeftEvent> = new EventEmitter<ParticipantLeftEvent>();
@ -144,12 +139,6 @@ export class ToolbarComponent implements OnInit, OnDestroy, AfterViewInit {
@Output() onBroadcastingStopRequested: EventEmitter<BroadcastingStopRequestedEvent> =
new EventEmitter<BroadcastingStopRequestedEvent>();
/**
* @internal
* This event is fired when the user clicks on the view recordings button.
*/
@Output() onViewRecordingsClicked: EventEmitter<void> = new EventEmitter<void>();
/**
* @ignore
*/
@ -213,14 +202,6 @@ export class ToolbarComponent implements OnInit, OnDestroy, AfterViewInit {
* @ignore
*/
isMinimal: boolean = false;
/**
* @ignore
*/
showCameraButton: boolean = true;
/**
* @ignore
*/
showMicrophoneButton: boolean = true;
/**
* @ignore
*/
@ -245,11 +226,6 @@ export class ToolbarComponent implements OnInit, OnDestroy, AfterViewInit {
*/
showRecordingButton: boolean = true;
/**
* @ignore
*/
showViewRecordingsButton: boolean = false;
/**
* @ignore
*/
@ -290,12 +266,7 @@ export class ToolbarComponent implements OnInit, OnDestroy, AfterViewInit {
/**
* @ignore
*/
showRoomName: boolean = true;
/**
* @ignore
*/
roomName: string = '';
showSessionName: boolean = true;
/**
* @ignore
@ -327,11 +298,6 @@ export class ToolbarComponent implements OnInit, OnDestroy, AfterViewInit {
*/
recordingStatus: RecordingStatus = RecordingStatus.STOPPED;
/**
* @ignore
*/
isRecordingReadOnlyMode: boolean = false;
/**
* @ignore
*/
@ -359,25 +325,37 @@ export class ToolbarComponent implements OnInit, OnDestroy, AfterViewInit {
*/
recordingTime: Date;
/**
* @internal
* Template configuration managed by the service
*/
templateConfig: ToolbarTemplateConfiguration = {};
// Store directive references for template setup
private _externalAdditionalButtons?: ToolbarAdditionalButtonsDirective;
private _externalAdditionalPanelButtons?: ToolbarAdditionalPanelButtonsDirective;
private log: ILogger;
private 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 brandingLogoSub: Subscription;
private displayRoomNameSub: Subscription;
private settingsButtonSub: Subscription;
private captionsSubs: Subscription;
private additionalButtonsPositionSub: Subscription;
private fullscreenChangeSubscription: Subscription;
private currentWindowHeight = window.innerHeight;
private layoutService: LayoutService;
/**
* @ignore
*/
constructor(
private layoutService: LayoutService,
private serviceConfig: ServiceConfigService,
private documentService: DocumentService,
private chatService: ChatService,
private panelService: PanelService,
@ -393,10 +371,10 @@ export class ToolbarComponent implements OnInit, OnDestroy, AfterViewInit {
private broadcastingService: BroadcastingService,
private translateService: TranslateService,
private storageSrv: StorageService,
private cdkOverlayService: CdkOverlayService,
private templateManagerService: TemplateManagerService
private cdkOverlayService: CdkOverlayService
) {
this.log = this.loggerSrv.get('ToolbarComponent');
this.layoutService = this.serviceConfig.getLayoutService();
}
/**
* @ignore
@ -424,12 +402,10 @@ export class ToolbarComponent implements OnInit, OnDestroy, AfterViewInit {
async ngOnInit() {
this.room = this.openviduService.getRoom();
this.evalAndSetRoomName(this.libService.getRoomName());
this.hasVideoDevices = this.oVDevicesService.hasVideoDeviceAvailable();
this.hasAudioDevices = this.oVDevicesService.hasAudioDeviceAvailable();
this.setupTemplates();
this.subscribeToToolbarDirectives();
this.subscribeToUserMediaProperties();
@ -447,55 +423,32 @@ export class ToolbarComponent implements OnInit, OnDestroy, AfterViewInit {
ngOnDestroy(): void {
this.panelService.clear();
this.destroy$.next();
this.destroy$.complete();
if (this.panelTogglingSubscription) this.panelTogglingSubscription.unsubscribe();
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.brandingLogoSub) this.brandingLogoSub.unsubscribe();
if (this.displayRoomNameSub) this.displayRoomNameSub.unsubscribe();
if (this.minimalSub) this.minimalSub.unsubscribe();
if (this.activitiesPanelButtonSub) this.activitiesPanelButtonSub.unsubscribe();
if (this.recordingSubscription) this.recordingSubscription.unsubscribe();
if (this.broadcastingSubscription) this.broadcastingSubscription.unsubscribe();
if (this.settingsButtonSub) this.settingsButtonSub.unsubscribe();
if (this.captionsSubs) this.captionsSubs.unsubscribe();
if (this.fullscreenChangeSubscription) this.fullscreenChangeSubscription.unsubscribe();
if (this.additionalButtonsPositionSub) this.additionalButtonsPositionSub.unsubscribe();
this.isFullscreenActive = false;
this.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
*/
@ -550,53 +503,26 @@ export class ToolbarComponent implements OnInit, OnDestroy, AfterViewInit {
}
/**
* The participant leaves the room voluntarily.
* @ignore
*/
async disconnect() {
try {
await this.openviduService.disconnectRoom(() => {
this.onParticipantLeft.emit({
const event: ParticipantLeftEvent = {
roomName: this.openviduService.getRoomName(),
participantName: this.participantService.getLocalParticipant()?.name || '',
identity: this.participantService.getLocalParticipant()?.identity || '',
reason: ParticipantLeftReason.LEAVE
});
this.onRoomDisconnected.emit();
}, false);
participantId: this.participantService.getLocalParticipant()?.identity || ''
};
try {
await this.openviduService.disconnectRoom();
this.onParticipantLeft.emit(event);
} 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
*/
toggleRecording() {
if (this.recordingStatus === RecordingStatus.FAILED) {
this.openRecordingActivityPanel();
return;
}
const payload: RecordingStartRequestedEvent = {
roomName: this.openviduService.getRoomName()
};
@ -606,7 +532,9 @@ export class ToolbarComponent implements OnInit, OnDestroy, AfterViewInit {
this.onRecordingStopRequested.emit(payload);
} else if (this.recordingStatus === RecordingStatus.STOPPED) {
this.onRecordingStartRequested.emit(payload);
this.openRecordingActivityPanel();
if (this.showActivitiesPanelButton && !this.isActivitiesOpened) {
this.toggleActivitiesPanel('recording');
}
}
}
@ -623,7 +551,9 @@ export class ToolbarComponent implements OnInit, OnDestroy, AfterViewInit {
this.onBroadcastingStopRequested.emit(payload);
this.broadcastingService.setBroadcastingStopped();
} else if (this.broadcastingStatus === BroadcastingStatus.STOPPED) {
this.openBroadcastingActivityPanel();
if (this.showActivitiesPanelButton && !this.isActivitiesOpened) {
this.toggleActivitiesPanel('broadcasting');
}
}
}
@ -676,11 +606,7 @@ export class ToolbarComponent implements OnInit, OnDestroy, AfterViewInit {
this.documentService.toggleFullscreen('session-container');
}
/**
* @internal
* @param expandPanel
*/
toggleActivitiesPanel(expandPanel: string) {
private toggleActivitiesPanel(expandPanel: string) {
this.panelService.togglePanel(PanelType.ACTIVITIES, expandPanel);
}
@ -695,9 +621,7 @@ export class ToolbarComponent implements OnInit, OnDestroy, AfterViewInit {
}
private subscribeToFullscreenChanged() {
fromEvent(document, 'fullscreenchange')
.pipe(takeUntil(this.destroy$))
.subscribe(() => {
this.fullscreenChangeSubscription = fromEvent(document, 'fullscreenchange').subscribe(() => {
const isFullscreen = Boolean(document.fullscreenElement);
if (isFullscreen) {
this.cdkOverlayService.setSelector('#session-container');
@ -711,7 +635,7 @@ export class ToolbarComponent implements OnInit, OnDestroy, AfterViewInit {
}
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.isParticipantsOpened = ev.isOpened && ev.panelType === PanelType.PARTICIPANTS;
this.isActivitiesOpened = ev.isOpened && ev.panelType === PanelType.ACTIVITIES;
@ -723,7 +647,7 @@ export class ToolbarComponent implements OnInit, OnDestroy, AfterViewInit {
}
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()) {
this.unreadMessages++;
}
@ -732,7 +656,7 @@ export class ToolbarComponent implements OnInit, OnDestroy, AfterViewInit {
});
}
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 (this.isCameraEnabled !== p.isCameraEnabled) {
this.onVideoEnabledChanged.emit(p.isCameraEnabled);
@ -756,13 +680,8 @@ export class ToolbarComponent implements OnInit, OnDestroy, AfterViewInit {
}
private subscribeToRecordingStatus() {
this.libService.recordingActivityReadOnly$.pipe(takeUntil(this.destroy$)).subscribe((readOnly: boolean) => {
this.isRecordingReadOnlyMode = readOnly;
this.cd.markForCheck();
});
this.recordingService.recordingStatusObs.pipe(takeUntil(this.destroy$)).subscribe((event: RecordingStatusInfo) => {
const { status, startedAt } = event;
this.recordingSubscription = this.recordingService.recordingStatusObs.subscribe((event: RecordingStatusInfo) => {
const { status, recordingElapsedTime } = event;
this.recordingStatus = status;
if (status === RecordingStatus.STARTED) {
this.startedRecording = event.recordingList.find((rec) => rec.status === RecordingStatus.STARTED);
@ -770,15 +689,15 @@ export class ToolbarComponent implements OnInit, OnDestroy, AfterViewInit {
this.startedRecording = undefined;
}
if (startedAt) {
this.recordingTime = startedAt;
if (recordingElapsedTime) {
this.recordingTime = recordingElapsedTime;
}
this.cd.markForCheck();
});
}
private subscribeToBroadcastingStatus() {
this.broadcastingService.broadcastingStatusObs.pipe(takeUntil(this.destroy$)).subscribe((ev: BroadcastingStatusInfo) => {
this.broadcastingSubscription = this.broadcastingService.broadcastingStatusObs.subscribe((ev: BroadcastingStatusInfo) => {
if (!!ev) {
this.broadcastingStatus = ev.status;
this.broadcastingId = ev.broadcastingId;
@ -788,97 +707,78 @@ export class ToolbarComponent implements OnInit, OnDestroy, AfterViewInit {
}
private subscribeToToolbarDirectives() {
this.libService.minimal$.pipe(takeUntil(this.destroy$)).subscribe((value: boolean) => {
this.minimalSub = this.libService.minimal$.subscribe((value: boolean) => {
this.isMinimal = value;
this.cd.markForCheck();
});
this.libService.brandingLogo$.pipe(takeUntil(this.destroy$)).subscribe((value: string) => {
this.brandingLogoSub = this.libService.brandingLogo$.subscribe((value: string) => {
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.screenshareButtonSub = this.libService.screenshareButton$.subscribe((value: boolean) => {
this.showScreenshareButton = value && !this.platformService.isMobile();
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.checkDisplayMoreOptions();
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.cd.markForCheck();
});
this.libService.recordingButton$.pipe(takeUntil(this.destroy$)).subscribe((value: boolean) => {
this.recordingButtonSub = this.libService.recordingButton$.subscribe((value: boolean) => {
this.showRecordingButton = value;
this.checkDisplayMoreOptions();
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.checkDisplayMoreOptions();
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.checkDisplayMoreOptions();
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.cd.markForCheck();
});
this.libService.participantsPanelButton$.pipe(takeUntil(this.destroy$)).subscribe((value: boolean) => {
this.participantsPanelButtonSub = this.libService.participantsPanelButton$.subscribe((value: boolean) => {
this.showParticipantsPanelButton = value;
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.cd.markForCheck();
});
this.libService.backgroundEffectsButton$.pipe(takeUntil(this.destroy$)).subscribe((value: boolean) => {
this.backgroundEffectsButtonSub = this.libService.backgroundEffectsButton$.subscribe((value: boolean) => {
this.showBackgroundEffectsButton = value;
this.checkDisplayMoreOptions();
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.cd.markForCheck();
});
this.libService.displayRoomName$.pipe(takeUntil(this.destroy$)).subscribe((value: boolean) => {
this.showRoomName = value;
this.displayRoomNameSub = this.libService.displayRoomName$.subscribe((value: boolean) => {
this.showSessionName = value;
this.cd.markForCheck();
});
this.captionsSubs = this.libService.captionsButton$.subscribe((value: boolean) => {
this.showCaptionsButton = value;
this.cd.markForCheck();
});
this.libService.roomName$.pipe(takeUntil(this.destroy$)).subscribe((value: string) => {
this.evalAndSetRoomName(value);
this.cd.markForCheck();
});
// this.libService.captionsButton$.pipe(takeUntil(this.destroy$)).subscribe((value: boolean) => {
// this.showCaptionsButton = value;
// this.cd.markForCheck();
// });
this.libService.toolbarAdditionalButtonsPosition$
.pipe(takeUntil(this.destroy$))
.subscribe((value: ToolbarAdditionalButtonsPosition) => {
this.additionalButtonsPositionSub = this.libService.toolbarAdditionalButtonsPosition$.subscribe(
(value: ToolbarAdditionalButtonsPosition) => {
// Using Promise.resolve() to defer change detection until the next microtask.
// This ensures that Angular's change detection has the latest value before updating the view.
// Without this, Angular's OnPush strategy might not immediately reflect the change,
@ -888,11 +788,12 @@ export class ToolbarComponent implements OnInit, OnDestroy, AfterViewInit {
this.additionalButtonsPosition = value;
this.cd.markForCheck();
});
});
}
);
}
private subscribeToCaptionsToggling() {
this.layoutService.captionsTogglingObs.pipe(takeUntil(this.destroy$)).subscribe((value: boolean) => {
this.captionsSubs = this.layoutService.captionsTogglingObs.subscribe((value: boolean) => {
this.captionsEnabled = value;
this.cd.markForCheck();
});
@ -906,14 +807,4 @@ export class ToolbarComponent implements OnInit, OnDestroy, AfterViewInit {
this.showBroadcastingButton ||
this.showSettingsButton;
}
private evalAndSetRoomName(value: string) {
if (!!value) {
this.roomName = value;
} else if (!!this.room && this.room.name) {
this.roomName = this.room.name;
} else {
this.roomName = '';
}
}
}

View File

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

View File

@ -23,35 +23,32 @@ With the following directives you can modify the default User Interface with the
<!-- start-dynamic-api-directives-content -->
| **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) |
| **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) |
| **tokenError** | `any` | [TokenErrorDirective](../directives/TokenErrorDirective.html) |
| **toolbarActivitiesPanelButton** | `boolean` | [ToolbarActivitiesPanelButtonDirective](../directives/ToolbarActivitiesPanelButtonDirective.html) |
| **toolbarBackgroundEffectsButton** | `boolean` | [ToolbarBackgroundEffectsButtonDirective](../directives/ToolbarBackgroundEffectsButtonDirective.html) |
| **toolbarBroadcastingButton** | `boolean` | [ToolbarBroadcastingButtonDirective](../directives/ToolbarBroadcastingButtonDirective.html) |
| **toolbarCameraButton** | `boolean` | [ToolbarCameraButtonDirective](../directives/ToolbarCameraButtonDirective.html) |
| **toolbarChatPanelButton** | `boolean` | [ToolbarChatPanelButtonDirective](../directives/ToolbarChatPanelButtonDirective.html) |
| **toolbarDisplayLogo** | `boolean` | [ToolbarDisplayLogoDirective](../directives/ToolbarDisplayLogoDirective.html) |
| **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) |
| **minimal** | `boolean` | [MinimalDirective](../directives/MinimalDirective.html) |
| **lang** | `string` | [LangDirective](../directives/LangDirective.html) |
| **langOptions** | `LangOption` | [LangOptionsDirective](../directives/LangOptionsDirective.html) |
| **participantName** | `string` | [ParticipantNameDirective](../directives/ParticipantNameDirective.html) |
| **prejoin** | `boolean` | [PrejoinDirective](../directives/PrejoinDirective.html) |
| **videoEnabled** | `boolean` | [VideoEnabledDirective](../directives/VideoEnabledDirective.html) |
| **audioEnabled** | `boolean` | [AudioEnabledDirective](../directives/AudioEnabledDirective.html) |
| **toolbarScreenshareButton** | `boolean` | [ToolbarScreenshareButtonDirective](../directives/ToolbarScreenshareButtonDirective.html) |
| **toolbarRecordingButton** | `boolean` | [ToolbarRecordingButtonDirective](../directives/ToolbarRecordingButtonDirective.html) |
| **toolbarBroadcastingButton** | `boolean` | [ToolbarBroadcastingButtonDirective](../directives/ToolbarBroadcastingButtonDirective.html) |
| **toolbarFullscreenButton** | `boolean` | [ToolbarFullscreenButtonDirective](../directives/ToolbarFullscreenButtonDirective.html) |
| **toolbarBackgroundEffectsButton** | `boolean` | [ToolbarBackgroundEffectsButtonDirective](../directives/ToolbarBackgroundEffectsButtonDirective.html) |
| **toolbarSettingsButton** | `boolean` | [ToolbarSettingsButtonDirective](../directives/ToolbarSettingsButtonDirective.html) |
| **toolbarLeaveButton** | `boolean` | [ToolbarLeaveButtonDirective](../directives/ToolbarLeaveButtonDirective.html) |
| **toolbarParticipantsPanelButton** | `boolean` | [ToolbarParticipantsPanelButtonDirective](../directives/ToolbarParticipantsPanelButtonDirective.html) |
| **toolbarChatPanelButton** | `boolean` | [ToolbarChatPanelButtonDirective](../directives/ToolbarChatPanelButtonDirective.html) |
| **toolbarActivitiesPanelButton** | `boolean` | [ToolbarActivitiesPanelButtonDirective](../directives/ToolbarActivitiesPanelButtonDirective.html) |
| **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 -->

View File

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

View File

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

View File

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

View File

@ -12,21 +12,18 @@ import { OpenViduComponentsConfigService } from '../../services/config/directive
*
*/
@Directive({
selector: 'ov-admin-dashboard[recordingsList]',
standalone: false
selector: 'ov-admin-dashboard[recordingsList]'
})
export class AdminDashboardRecordingsListDirective implements AfterViewInit, OnDestroy {
@Input() set recordingsList(value: RecordingInfo[]) {
this.recordingsValue = value;
this.update(this.recordingsValue);
}
recordingsValue: RecordingInfo[] = [];
recordingsValue: RecordingInfo [] = [];
constructor(
public elementRef: ElementRef,
private libService: OpenViduComponentsConfigService
) {}
constructor(public elementRef: ElementRef, private libService: OpenViduComponentsConfigService) {}
ngAfterViewInit() {
this.update(this.recordingsValue);
@ -40,7 +37,9 @@ export class AdminDashboardRecordingsListDirective implements AfterViewInit, OnD
}
update(value: RecordingInfo[]) {
this.libService.updateAdminConfig({ recordingsList: value });
if (this.libService.getAdminRecordingsList() !== value) {
this.libService.setAdminRecordingsList(value);
}
}
}
@ -54,52 +53,10 @@ export class AdminDashboardRecordingsListDirective implements AfterViewInit, OnD
*
*/
@Directive({
selector: 'ov-admin-dashboard[navbarTitle]',
standalone: false
selector: 'ov-admin-dashboard[navbarTitle]'
})
export class AdminDashboardTitleDirective implements AfterViewInit, OnDestroy {
@Input() set navbarTitle(value: string) {
this.navbarTitleValue = value;
this.update(this.navbarTitleValue);
}
navbarTitleValue: string = 'OpenVidu Dashboard';
constructor(
public elementRef: ElementRef,
private libService: OpenViduComponentsConfigService
) {}
ngAfterViewInit() {
this.update(this.navbarTitleValue);
}
ngOnDestroy(): void {
this.clear();
}
clear() {
this.navbarTitleValue = 'OpenVidu Dashboard';
this.update(null);
}
update(value: any) {
this.libService.updateAdminConfig({ dashboardTitle: value });
}
}
/**
* The **navbarTitle** directive allows customize the title of the navbar in {@link AdminLoginComponent}.
*
* Default: `'OpenVidu Call Dashboard'`
*
* @example
* <ov-admin-login [navbarTitle]="'My login'"></ov-admin-login>
*
*/
@Directive({
selector: 'ov-admin-login[navbarTitle]',
standalone: false
})
export class AdminLoginTitleDirective implements AfterViewInit, OnDestroy {
@Input() set navbarTitle(value: any) {
this.navbarTitleValue = value;
this.update(this.navbarTitleValue);
@ -107,10 +64,7 @@ export class AdminLoginTitleDirective implements AfterViewInit, OnDestroy {
navbarTitleValue: any = null;
constructor(
public elementRef: ElementRef,
private libService: OpenViduComponentsConfigService
) {}
constructor(public elementRef: ElementRef, private libService: OpenViduComponentsConfigService) {}
ngAfterViewInit() {
this.update(this.navbarTitleValue);
@ -124,10 +78,56 @@ export class AdminLoginTitleDirective implements AfterViewInit, OnDestroy {
}
update(value: any) {
this.libService.updateAdminConfig({ loginTitle: value });
if (this.libService.getAdminDashboardTitle() !== value) {
this.libService.setAdminDashboardTitle(value);
}
}
}
/**
* The **navbarTitle** directive allows customize the title of the navbar in {@link AdminLoginComponent}.
*
* Default: `'OpenVidu Call Dashboard'`
*
* @example
* <ov-admin-login [navbarTitle]="'My login'"></ov-admin-login>
*
*/
@Directive({
selector: 'ov-admin-login[navbarTitle]'
})
export class AdminLoginTitleDirective implements AfterViewInit, OnDestroy {
@Input() set navbarTitle(value: any) {
this.navbarTitleValue = value;
this.update(this.navbarTitleValue);
}
navbarTitleValue: any = null;
constructor(public elementRef: ElementRef, private libService: OpenViduComponentsConfigService) {}
ngAfterViewInit() {
this.update(this.navbarTitleValue);
}
ngOnDestroy(): void {
this.clear();
}
clear() {
this.navbarTitleValue = null;
this.update(null);
}
update(value: any) {
if (this.libService.getAdminLoginTitle() !== value) {
this.libService.setAdminLoginTitle(value);
}
}
}
/**
* The **error** directive allows show the authentication error in {@link AdminLoginComponent}.
*
@ -137,11 +137,11 @@ export class AdminLoginTitleDirective implements AfterViewInit, OnDestroy {
* <ov-admin-login [error]="error"></ov-admin-login>
*
*/
@Directive({
selector: 'ov-admin-login[error]',
standalone: false
@Directive({
selector: 'ov-admin-login[error]'
})
export class AdminLoginErrorDirective implements AfterViewInit, OnDestroy {
@Input() set error(value: any) {
this.errorValue = value;
this.update(this.errorValue);
@ -149,10 +149,7 @@ export class AdminLoginErrorDirective implements AfterViewInit, OnDestroy {
errorValue: any = null;
constructor(
public elementRef: ElementRef,
private libService: OpenViduComponentsConfigService
) {}
constructor(public elementRef: ElementRef, private libService: OpenViduComponentsConfigService) {}
ngAfterViewInit() {
this.update(this.errorValue);
@ -166,6 +163,9 @@ export class AdminLoginErrorDirective implements AfterViewInit, OnDestroy {
}
update(value: any) {
this.libService.updateAdminConfig({ loginError: value });
if (this.libService.getAdminLoginError() !== value) {
this.libService.setAdminLoginError(value);
}
}
}

View File

@ -1,24 +1,7 @@
import { NgModule } from '@angular/core';
import { ActivitiesPanelBroadcastingActivityDirective, ActivitiesPanelRecordingActivityDirective } from './activities-panel.directive';
import {
AdminDashboardRecordingsListDirective,
AdminDashboardTitleDirective,
AdminLoginErrorDirective,
AdminLoginTitleDirective
} from './admin.directive';
import {
FallbackLogoDirective,
LayoutRemoteParticipantsDirective,
PrejoinDisplayParticipantName,
ToolbarBrandingLogoDirective,
ToolbarViewRecordingsButtonDirective,
RecordingActivityReadOnlyDirective,
RecordingActivityShowControlsDirective,
StartStopRecordingButtonsDirective,
RecordingActivityViewRecordingsButtonDirective,
RecordingActivityShowRecordingsListDirective,
ToolbarRoomNameDirective
} from './internals.directive';
import { AdminLoginErrorDirective, AdminDashboardRecordingsListDirective, AdminLoginTitleDirective, AdminDashboardTitleDirective } from './admin.directive';
import { LayoutRemoteParticipantsDirective, FallbackLogoDirective, ToolbarBrandingLogoDirective } from './internals.directive';
import { ParticipantPanelItemMuteButtonDirective } from './participant-panel-item.directive';
import {
StreamDisplayAudioDetectionDirective,
@ -27,21 +10,19 @@ import {
} from './stream.directive';
import {
ToolbarActivitiesPanelButtonDirective,
ToolbarAdditionalButtonsPossitionDirective,
ToolbarBackgroundEffectsButtonDirective,
ToolbarBroadcastingButtonDirective,
ToolbarCameraButtonDirective,
// ToolbarCaptionsButtonDirective,
ToolbarChatPanelButtonDirective,
ToolbarDisplayLogoDirective,
ToolbarDisplayRoomNameDirective,
ToolbarFullscreenButtonDirective,
ToolbarLeaveButtonDirective,
ToolbarMicrophoneButtonDirective,
ToolbarParticipantsPanelButtonDirective,
ToolbarRecordingButtonDirective,
ToolbarScreenshareButtonDirective,
ToolbarSettingsButtonDirective
ToolbarSettingsButtonDirective,
ToolbarAdditionalButtonsPossitionDirective
} from './toolbar.directive';
import {
AudioEnabledDirective,
@ -53,14 +34,14 @@ import {
MinimalDirective,
ParticipantNameDirective,
PrejoinDirective,
RecordingStreamBaseUrlDirective,
ShowDisconnectionDialogDirective,
TokenDirective,
TokenErrorDirective,
VideoEnabledDirective
} from './videoconference.directive';
const directives = [
@NgModule({
declarations: [
LivekitUrlDirective,
TokenDirective,
TokenErrorDirective,
@ -70,15 +51,8 @@ const directives = [
// CaptionsLangOptionsDirective,
// CaptionsLangDirective,
PrejoinDirective,
PrejoinDisplayParticipantName,
VideoEnabledDirective,
RecordingActivityReadOnlyDirective,
RecordingActivityShowControlsDirective,
AudioEnabledDirective,
ShowDisconnectionDialogDirective,
RecordingStreamBaseUrlDirective,
ToolbarCameraButtonDirective,
ToolbarMicrophoneButtonDirective,
ToolbarScreenshareButtonDirective,
ToolbarFullscreenButtonDirective,
ToolbarBackgroundEffectsButtonDirective,
@ -93,7 +67,6 @@ const directives = [
ToolbarDisplayLogoDirective,
ToolbarSettingsButtonDirective,
ToolbarAdditionalButtonsPossitionDirective,
ToolbarViewRecordingsButtonDirective,
StreamDisplayParticipantNameDirective,
StreamDisplayAudioDetectionDirective,
StreamVideoControlsDirective,
@ -107,15 +80,48 @@ const directives = [
AdminLoginTitleDirective,
AdminLoginErrorDirective,
AdminDashboardTitleDirective,
LayoutRemoteParticipantsDirective,
StartStopRecordingButtonsDirective,
RecordingActivityViewRecordingsButtonDirective,
RecordingActivityShowRecordingsListDirective,
ToolbarRoomNameDirective
];
@NgModule({
declarations: [...directives],
exports: [...directives]
LayoutRemoteParticipantsDirective
],
exports: [
LivekitUrlDirective,
TokenDirective,
TokenErrorDirective,
MinimalDirective,
LangDirective,
LangOptionsDirective,
// CaptionsLangOptionsDirective,
// CaptionsLangDirective,
PrejoinDirective,
VideoEnabledDirective,
AudioEnabledDirective,
ToolbarScreenshareButtonDirective,
ToolbarFullscreenButtonDirective,
ToolbarBackgroundEffectsButtonDirective,
// ToolbarCaptionsButtonDirective,
ToolbarLeaveButtonDirective,
ToolbarRecordingButtonDirective,
ToolbarBroadcastingButtonDirective,
ToolbarParticipantsPanelButtonDirective,
ToolbarChatPanelButtonDirective,
ToolbarActivitiesPanelButtonDirective,
ToolbarDisplayRoomNameDirective,
ToolbarDisplayLogoDirective,
ToolbarSettingsButtonDirective,
ToolbarAdditionalButtonsPossitionDirective,
StreamDisplayParticipantNameDirective,
StreamDisplayAudioDetectionDirective,
StreamVideoControlsDirective,
FallbackLogoDirective,
ToolbarBrandingLogoDirective,
ParticipantPanelItemMuteButtonDirective,
ParticipantNameDirective,
ActivitiesPanelRecordingActivityDirective,
ActivitiesPanelBroadcastingActivityDirective,
AdminDashboardRecordingsListDirective,
AdminLoginTitleDirective,
AdminLoginErrorDirective,
AdminDashboardTitleDirective,
LayoutRemoteParticipantsDirective
]
})
export class ApiDirectiveModule {}

View File

@ -9,8 +9,7 @@ import { OpenViduComponentsConfigService } from '../../services/config/directive
* @internal
*/
@Directive({
selector: 'img[ovLogo]',
standalone: false
selector: 'img[ovLogo]'
})
export class FallbackLogoDirective implements OnInit {
defaultLogo =
@ -54,8 +53,7 @@ export class FallbackLogoDirective implements OnInit {
* @internal
*/
@Directive({
selector: 'ov-layout[ovRemoteParticipants]',
standalone: false
selector: 'ov-layout[ovRemoteParticipants]'
})
export class LayoutRemoteParticipantsDirective {
@Input() set ovRemoteParticipants(value: ParticipantModel[] | undefined) {
@ -87,8 +85,7 @@ export class LayoutRemoteParticipantsDirective {
* @internal
*/
@Directive({
selector: 'ov-videoconference[brandingLogo], ov-toolbar[brandingLogo]',
standalone: false
selector: 'ov-videoconference[brandingLogo], ov-toolbar[brandingLogo]'
})
export class ToolbarBrandingLogoDirective implements AfterViewInit, OnDestroy {
/**
@ -122,405 +119,6 @@ export class ToolbarBrandingLogoDirective implements AfterViewInit, OnDestroy {
}
private update(value: string) {
this.libService.updateToolbarConfig({ brandingLogo: value });
}
}
/**
* @internal
*/
@Directive({
selector: 'ov-videoconference[prejoinDisplayParticipantName]',
standalone: false
})
export class PrejoinDisplayParticipantName implements OnDestroy {
/**
* @ignore
*/
@Input() set prejoinDisplayParticipantName(value: boolean) {
this.update(value);
}
/**
* @ignore
*/
constructor(
public elementRef: ElementRef,
private libService: OpenViduComponentsConfigService
) {}
ngOnDestroy(): void {
this.clear();
}
private clear() {
this.update(true);
}
private update(value: boolean) {
this.libService.updateGeneralConfig({ prejoinDisplayParticipantName: value });
}
}
/**
* @internal
*
* The **recordingActivityReadOnly** directive sets the recording activity panel to read-only mode.
* In this mode, users can only view recordings without the ability to start, stop, or delete them.
*
* It is only available for {@link VideoconferenceComponent}.
*
* Default: `false`
*
* @example
* <ov-videoconference [recordingActivityReadOnly]="true"></ov-videoconference>
*/
@Directive({
selector: 'ov-videoconference[recordingActivityReadOnly]',
standalone: false
})
export class RecordingActivityReadOnlyDirective implements OnDestroy {
/**
* @ignore
*/
@Input() set recordingActivityReadOnly(value: boolean) {
this.update(value);
}
/**
* @ignore
*/
constructor(
public elementRef: ElementRef,
private libService: OpenViduComponentsConfigService
) {}
/**
* @ignore
*/
ngOnDestroy(): void {
this.clear();
}
/**
* @ignore
*/
clear() {
this.update(false);
}
/**
* @ignore
*/
update(value: boolean) {
this.libService.updateRecordingActivityConfig({ readOnly: value });
}
}
/**
*
* @internal
*
* The **recordingActivityShowControls** directive allows to show/hide specific recording controls (play, download, delete, externalView).
* You can pass an object with boolean properties to control which buttons are shown.
*
* It is only available for {@link VideoconferenceComponent}.
*
* Default: `{ play: true, download: true, delete: true, externalView: false }`
*
* @example
* <ov-videoconference [recordingActivityShowControls]="{ play: false, download: true, delete: false }"></ov-videoconference>
*/
@Directive({
selector: 'ov-videoconference[recordingActivityShowControls]',
standalone: false
})
export class RecordingActivityShowControlsDirective implements OnDestroy {
/**
* @ignore
*/
@Input() set recordingActivityShowControls(value: { play: boolean; download: boolean; delete: boolean; externalView: boolean }) {
this.update(value);
}
/**
* @ignore
*/
constructor(
public elementRef: ElementRef,
private libService: OpenViduComponentsConfigService
) {}
/**
* @ignore
*/
ngOnDestroy(): void {
this.clear();
}
/**
* @ignore
*/
clear() {
this.update({ play: true, download: true, delete: true, externalView: false });
}
/**
* @ignore
*/
update(value: { play: boolean; download: boolean; delete: boolean; externalView: boolean }) {
this.libService.updateRecordingActivityConfig({ showControls: value });
}
}
/**
* @internal
* The **viewRecordingsButton** directive allows show/hide the view recordings toolbar button.
*
* Default: `false`
*
* It can be used in the parent element {@link VideoconferenceComponent} specifying the name of the `toolbar` component:
*
* @example
* <ov-videoconference [toolbarViewRecordingsButton]="true"></ov-videoconference>
*
* \
* And it also can be used in the {@link ToolbarComponent}.
* @example
* <ov-toolbar [viewRecordingsButton]="true"></ov-toolbar>
*
* When the button is clicked, it will fire the `onViewRecordingsClicked` event.
*/
@Directive({
selector: 'ov-videoconference[toolbarViewRecordingsButton], ov-toolbar[viewRecordingsButton]',
standalone: false
})
export class ToolbarViewRecordingsButtonDirective implements AfterViewInit, OnDestroy {
/**
* @ignore
*/
@Input() set toolbarViewRecordingsButton(value: boolean) {
this.viewRecordingsValue = value;
this.update(this.viewRecordingsValue);
}
/**
* @ignore
*/
@Input() set viewRecordingsButton(value: boolean) {
this.viewRecordingsValue = value;
this.update(this.viewRecordingsValue);
}
private viewRecordingsValue: boolean = false;
/**
* @ignore
*/
constructor(
public elementRef: ElementRef,
private libService: OpenViduComponentsConfigService
) {}
ngAfterViewInit() {
this.update(this.viewRecordingsValue);
}
ngOnDestroy(): void {
this.clear();
}
private clear() {
this.viewRecordingsValue = false;
this.update(true);
}
private update(value: boolean) {
this.libService.updateToolbarConfig({ viewRecordings: value });
}
}
/**
* @internal
*
* The **recordingActivityStartStopRecordingButton** directive allows to show or hide the start/stop recording buttons in recording activity.
*
* Default: `true`
*
* It is only available for {@link VideoconferenceComponent}.
*
* @example
* <ov-videoconference [recordingActivityStartStopRecordingButton]="false"></ov-videoconference>
*/
@Directive({
selector: 'ov-videoconference[recordingActivityStartStopRecordingButton]',
standalone: false
})
export class StartStopRecordingButtonsDirective implements OnDestroy {
/**
* @ignore
*/
@Input() set recordingActivityStartStopRecordingButton(value: boolean) {
this.update(value);
}
/**
* @ignore
*/
constructor(
public elementRef: ElementRef,
private libService: OpenViduComponentsConfigService
) {}
ngOnDestroy(): void {
this.clear();
}
private clear() {
this.update(true);
}
private update(value: boolean) {
this.libService.updateRecordingActivityConfig({ startStopButton: value });
}
}
/**
* @internal
* The **recordingActivityViewRecordingsButton** directive allows to show/hide the view recordings button in the recording activity panel.
*
* Default: `false`
*
* Can be used in {@link VideoconferenceComponent}.
*
* @example
* <ov-videoconference [recordingActivityViewRecordingsButton]="true"></ov-videoconference>
*/
@Directive({
selector: 'ov-videoconference[recordingActivityViewRecordingsButton]',
standalone: false
})
export class RecordingActivityViewRecordingsButtonDirective implements AfterViewInit, OnDestroy {
@Input() set recordingActivityViewRecordingsButton(value: boolean) {
this._value = value;
this.update(this._value);
}
private _value: boolean = false;
constructor(
public elementRef: ElementRef,
private libService: OpenViduComponentsConfigService
) {}
ngAfterViewInit() {
this.update(this._value);
}
ngOnDestroy(): void {
this.clear();
}
private clear() {
this._value = false;
this.update(this._value);
}
private update(value: boolean) {
this.libService.updateRecordingActivityConfig({ viewRecordingsButton: value });
}
}
/**
* @internal
* The **recordingActivityShowRecordingsList** directive allows to show or hide the recordings list in the recording activity panel.
*
* Default: `true`
*
* Can be used in {@link VideoconferenceComponent}.
*
* @example
* <ov-videoconference [recordingActivityShowRecordingsList]="false"></ov-videoconference>
*/
@Directive({
selector: 'ov-videoconference[recordingActivityShowRecordingsList]',
standalone: false
})
export class RecordingActivityShowRecordingsListDirective implements AfterViewInit, OnDestroy {
@Input() set recordingActivityShowRecordingsList(value: boolean) {
this._value = value;
this.update(this._value);
}
private _value: boolean = true;
constructor(
public elementRef: ElementRef,
private libService: OpenViduComponentsConfigService
) {}
ngAfterViewInit() {
this.update(this._value);
}
ngOnDestroy(): void {
this.clear();
}
private clear() {
this._value = true;
this.update(this._value);
}
private update(value: boolean) {
this.libService.updateRecordingActivityConfig({ showRecordingsList: value });
}
}
/**
* @internal
* The **toolbarRoomName** directive allows to display a specific room name in the toolbar.
* If the room name is not set, it will display the room ID instead.
*
* Can be used in {@link ToolbarComponent}.
*
* @example
* <ov-videoconference [toolbarRoomName]="roomName"></ov-videoconference>
*/
@Directive({
selector: 'ov-videoconference[toolbarRoomName], ov-toolbar[roomName]',
standalone: false
})
export class ToolbarRoomNameDirective implements AfterViewInit, OnDestroy {
@Input() set toolbarRoomName(value: string | undefined) {
this._roomName = value;
this.updateRoomName();
}
@Input() set roomName(value: string | undefined) {
this._roomName = value;
this.updateRoomName();
}
private _roomName?: string;
constructor(
public elementRef: ElementRef,
private libService: OpenViduComponentsConfigService
) {}
ngAfterViewInit() {
this.updateRoomName();
}
ngOnDestroy(): void {
this.clear();
}
private clear() {
this._roomName = undefined;
this.updateRoomName();
}
private updateRoomName() {
this.libService.updateToolbarConfig({ roomName: this._roomName || '' });
this.libService.setBrandingLogo(value);
}
}

View File

@ -17,8 +17,7 @@ import { OpenViduComponentsConfigService } from '../../services/config/directive
* <ov-participant-panel-item [muteButton]="false"></ov-participant-panel-item>
*/
@Directive({
selector: 'ov-videoconference[participantPanelItemMuteButton], ov-participant-panel-item[muteButton]',
standalone: false
selector: 'ov-videoconference[participantPanelItemMuteButton], ov-participant-panel-item[muteButton]'
})
export class ParticipantPanelItemMuteButtonDirective implements AfterViewInit, OnDestroy {
@Input() set participantPanelItemMuteButton(value: boolean) {
@ -32,10 +31,7 @@ export class ParticipantPanelItemMuteButtonDirective implements AfterViewInit, O
muteValue: boolean = true;
constructor(
public elementRef: ElementRef,
private libService: OpenViduComponentsConfigService
) {}
constructor(public elementRef: ElementRef, private libService: OpenViduComponentsConfigService) {}
ngAfterViewInit() {
this.update(this.muteValue);
@ -49,6 +45,8 @@ export class ParticipantPanelItemMuteButtonDirective implements AfterViewInit, O
}
update(value: boolean) {
this.libService.updateStreamConfig({ participantItemMuteButton: value });
if (this.libService.showParticipantItemMuteButton() !== value) {
this.libService.setParticipantItemMuteButton(value);
}
}
}

View File

@ -17,8 +17,7 @@ import { OpenViduComponentsConfigService } from '../../services/config/directive
* <ov-stream [displayParticipantName]="false"></ov-stream>
*/
@Directive({
selector: 'ov-videoconference[streamDisplayParticipantName], ov-stream[displayParticipantName]',
standalone: false
selector: 'ov-videoconference[streamDisplayParticipantName], ov-stream[displayParticipantName]'
})
export class StreamDisplayParticipantNameDirective implements AfterViewInit, OnDestroy {
@Input() set streamDisplayParticipantName(value: boolean) {
@ -46,7 +45,9 @@ export class StreamDisplayParticipantNameDirective implements AfterViewInit, OnD
}
update(value: boolean) {
this.libService.updateStreamConfig({ displayParticipantName: value });
if (this.libService.isParticipantNameDisplayed() !== value) {
this.libService.setDisplayParticipantName(value);
}
}
clear() {
@ -70,8 +71,7 @@ export class StreamDisplayParticipantNameDirective implements AfterViewInit, OnD
* <ov-stream [displayAudioDetection]="false"></ov-stream>
*/
@Directive({
selector: 'ov-videoconference[streamDisplayAudioDetection], ov-stream[displayAudioDetection]',
standalone: false
selector: 'ov-videoconference[streamDisplayAudioDetection], ov-stream[displayAudioDetection]'
})
export class StreamDisplayAudioDetectionDirective implements AfterViewInit, OnDestroy {
@Input() set streamDisplayAudioDetection(value: boolean) {
@ -98,7 +98,9 @@ export class StreamDisplayAudioDetectionDirective implements AfterViewInit, OnDe
}
update(value: boolean) {
this.libService.updateStreamConfig({ displayAudioDetection: value });
if (this.libService.isAudioDetectionDisplayed() !== value) {
this.libService.setDisplayAudioDetection(value);
}
}
clear() {
this.update(true);
@ -121,8 +123,7 @@ export class StreamDisplayAudioDetectionDirective implements AfterViewInit, OnDe
* <ov-stream [videoControls]="false"></ov-stream>
*/
@Directive({
selector: 'ov-videoconference[streamVideoControls], ov-stream[videoControls]',
standalone: false
selector: 'ov-videoconference[streamVideoControls], ov-stream[videoControls]'
})
export class StreamVideoControlsDirective implements AfterViewInit, OnDestroy {
@Input() set streamVideoControls(value: boolean) {
@ -150,7 +151,9 @@ export class StreamVideoControlsDirective implements AfterViewInit, OnDestroy {
}
update(value: boolean) {
this.libService.updateStreamConfig({ videoControls: value });
if (this.libService.showStreamVideoControls() !== value) {
this.libService.setStreamVideoControls(value);
}
}
clear() {

View File

@ -2,134 +2,6 @@ import { AfterViewInit, Directive, ElementRef, Input, OnDestroy } from '@angular
import { OpenViduComponentsConfigService } from '../../services/config/directive-config.service';
import { ToolbarAdditionalButtonsPosition } from '../../models/toolbar.model';
/**
* The **cameraButton** directive allows show/hide the camera toolbar button.
*
* Default: `true`
*
* It can be used in the parent element {@link VideoconferenceComponent} specifying the name of the `toolbar` component:
*
* @example
* <ov-videoconference [toolbarCameraButton]="false"></ov-videoconference>
*
* \
* And it also can be used in the {@link ToolbarComponent}.
* @example
* <ov-toolbar [cameraButton]="false"></ov-toolbar>
*/
@Directive({
selector: 'ov-videoconference[toolbarCameraButton], ov-toolbar[cameraButton]',
standalone: false
})
export class ToolbarCameraButtonDirective implements AfterViewInit, OnDestroy {
/**
* @ignore
*/
@Input() set toolbarCameraButton(value: boolean) {
this.cameraValue = value;
this.update(this.cameraValue);
}
/**
* @ignore
*/
@Input() set cameraButton(value: boolean) {
this.cameraValue = value;
this.update(this.cameraValue);
}
private cameraValue: boolean = true;
/**
* @ignore
*/
constructor(
public elementRef: ElementRef,
private libService: OpenViduComponentsConfigService
) {}
ngAfterViewInit() {
this.update(this.cameraValue);
}
ngOnDestroy(): void {
this.clear();
}
private clear() {
this.cameraValue = true;
this.update(true);
}
private update(value: boolean) {
this.libService.updateToolbarConfig({ camera: value });
}
}
/**
* The **microphoneButton** directive allows show/hide the microphone toolbar button.
*
* Default: `true`
*
* It can be used in the parent element {@link VideoconferenceComponent} specifying the name of the `toolbar` component:
*
* @example
* <ov-videoconference [toolbarMicrophoneButton]="false"></ov-videoconference>
*
* \
* And it also can be used in the {@link ToolbarComponent}.
* @example
* <ov-toolbar [microphoneButton]="false"></ov-toolbar>
*/
@Directive({
selector: 'ov-videoconference[toolbarMicrophoneButton], ov-toolbar[microphoneButton]',
standalone: false
})
export class ToolbarMicrophoneButtonDirective implements AfterViewInit, OnDestroy {
/**
* @ignore
*/
@Input() set toolbarMicrophoneButton(value: boolean) {
this.microphoneValue = value;
this.update(this.microphoneValue);
}
/**
* @ignore
*/
@Input() set microphoneButton(value: boolean) {
this.microphoneValue = value;
this.update(this.microphoneValue);
}
private microphoneValue: boolean = true;
/**
* @ignore
*/
constructor(
public elementRef: ElementRef,
private libService: OpenViduComponentsConfigService
) {}
ngAfterViewInit() {
this.update(this.microphoneValue);
}
ngOnDestroy(): void {
this.clear();
}
private clear() {
this.microphoneValue = true;
this.update(true);
}
private update(value: boolean) {
this.libService.updateToolbarConfig({ microphone: value });
}
}
/**
* The **screenshareButton** directive allows show/hide the screenshare toolbar button.
*
@ -146,8 +18,7 @@ export class ToolbarMicrophoneButtonDirective implements AfterViewInit, OnDestro
* <ov-toolbar [screenshareButton]="false"></ov-toolbar>
*/
@Directive({
selector: 'ov-videoconference[toolbarScreenshareButton], ov-toolbar[screenshareButton]',
standalone: false
selector: 'ov-videoconference[toolbarScreenshareButton], ov-toolbar[screenshareButton]'
})
export class ToolbarScreenshareButtonDirective implements AfterViewInit, OnDestroy {
/**
@ -190,7 +61,9 @@ export class ToolbarScreenshareButtonDirective implements AfterViewInit, OnDestr
}
private update(value: boolean) {
this.libService.updateToolbarConfig({ screenshare: value });
if (this.libService.showScreenshareButton() !== value) {
this.libService.setScreenshareButton(value);
}
}
}
@ -210,8 +83,7 @@ export class ToolbarScreenshareButtonDirective implements AfterViewInit, OnDestr
* <ov-toolbar [recordingButton]="false"></ov-toolbar>
*/
@Directive({
selector: 'ov-videoconference[toolbarRecordingButton], ov-toolbar[recordingButton]',
standalone: false
selector: 'ov-videoconference[toolbarRecordingButton], ov-toolbar[recordingButton]'
})
export class ToolbarRecordingButtonDirective implements AfterViewInit, OnDestroy {
/**
@ -251,7 +123,9 @@ export class ToolbarRecordingButtonDirective implements AfterViewInit, OnDestroy
}
private update(value: boolean) {
this.libService.updateToolbarConfig({ recording: value });
if (this.libService.showRecordingButton() !== value) {
this.libService.setRecordingButton(value);
}
}
}
@ -272,8 +146,7 @@ export class ToolbarRecordingButtonDirective implements AfterViewInit, OnDestroy
*
*/
@Directive({
selector: 'ov-videoconference[toolbarBroadcastingButton], ov-toolbar[broadcastingButton]',
standalone: false
selector: 'ov-videoconference[toolbarBroadcastingButton], ov-toolbar[broadcastingButton]'
})
export class ToolbarBroadcastingButtonDirective implements AfterViewInit, OnDestroy {
/**
@ -313,8 +186,10 @@ export class ToolbarBroadcastingButtonDirective implements AfterViewInit, OnDest
}
private update(value: boolean) {
if (this.libService.showBroadcastingButton() !== value) {
this.libService.setBroadcastingButton(value);
}
}
}
/**
@ -333,8 +208,7 @@ export class ToolbarBroadcastingButtonDirective implements AfterViewInit, OnDest
* <ov-toolbar [fullscreenButton]="false"></ov-toolbar>
*/
@Directive({
selector: 'ov-videoconference[toolbarFullscreenButton], ov-toolbar[fullscreenButton]',
standalone: false
selector: 'ov-videoconference[toolbarFullscreenButton], ov-toolbar[fullscreenButton]'
})
export class ToolbarFullscreenButtonDirective implements AfterViewInit, OnDestroy {
/**
@ -374,7 +248,9 @@ export class ToolbarFullscreenButtonDirective implements AfterViewInit, OnDestro
}
private update(value: boolean) {
this.libService.updateToolbarConfig({ fullscreen: value });
if (this.libService.showFullscreenButton() !== value) {
this.libService.setFullscreenButton(value);
}
}
}
@ -394,8 +270,7 @@ export class ToolbarFullscreenButtonDirective implements AfterViewInit, OnDestro
* <ov-toolbar [backgroundEffectsButton]="false"></ov-toolbar>
*/
@Directive({
selector: 'ov-videoconference[toolbarBackgroundEffectsButton], ov-toolbar[backgroundEffectsButton]',
standalone: false
selector: 'ov-videoconference[toolbarBackgroundEffectsButton], ov-toolbar[backgroundEffectsButton]'
})
export class ToolbarBackgroundEffectsButtonDirective implements AfterViewInit, OnDestroy {
/**
@ -435,7 +310,9 @@ export class ToolbarBackgroundEffectsButtonDirective implements AfterViewInit, O
}
private update(value: boolean) {
this.libService.updateToolbarConfig({ backgroundEffects: value });
if (this.libService.showBackgroundEffectsButton() !== value) {
this.libService.setBackgroundEffectsButton(value);
}
}
}
@ -514,8 +391,7 @@ export class ToolbarBackgroundEffectsButtonDirective implements AfterViewInit, O
* <ov-toolbar [settingsButton]="false"></ov-toolbar>
*/
@Directive({
selector: 'ov-videoconference[toolbarSettingsButton], ov-toolbar[settingsButton]',
standalone: false
selector: 'ov-videoconference[toolbarSettingsButton], ov-toolbar[settingsButton]'
})
export class ToolbarSettingsButtonDirective implements AfterViewInit, OnDestroy {
/**
@ -555,7 +431,9 @@ export class ToolbarSettingsButtonDirective implements AfterViewInit, OnDestroy
}
private update(value: boolean) {
this.libService.updateToolbarConfig({ settings: value });
if (this.libService.showToolbarSettingsButton() !== value) {
this.libService.setToolbarSettingsButton(value);
}
}
}
@ -575,8 +453,7 @@ export class ToolbarSettingsButtonDirective implements AfterViewInit, OnDestroy
* <ov-toolbar [leaveButton]="false"></ov-toolbar>
*/
@Directive({
selector: 'ov-videoconference[toolbarLeaveButton], ov-toolbar[leaveButton]',
standalone: false
selector: 'ov-videoconference[toolbarLeaveButton], ov-toolbar[leaveButton]'
})
export class ToolbarLeaveButtonDirective implements AfterViewInit, OnDestroy {
/**
@ -617,7 +494,9 @@ export class ToolbarLeaveButtonDirective implements AfterViewInit, OnDestroy {
}
private update(value: boolean) {
this.libService.updateToolbarConfig({ leave: value });
if (this.libService.showLeaveButton() !== value) {
this.libService.setLeaveButton(value);
}
}
}
@ -637,8 +516,7 @@ export class ToolbarLeaveButtonDirective implements AfterViewInit, OnDestroy {
* <ov-toolbar [participantsPanelButton]="false"></ov-toolbar>
*/
@Directive({
selector: 'ov-videoconference[toolbarParticipantsPanelButton], ov-toolbar[participantsPanelButton]',
standalone: false
selector: 'ov-videoconference[toolbarParticipantsPanelButton], ov-toolbar[participantsPanelButton]'
})
export class ToolbarParticipantsPanelButtonDirective implements AfterViewInit, OnDestroy {
/**
@ -680,7 +558,9 @@ export class ToolbarParticipantsPanelButtonDirective implements AfterViewInit, O
}
private update(value: boolean) {
this.libService.updateToolbarConfig({ participantsPanel: value });
if (this.libService.showParticipantsPanelButton() !== value) {
this.libService.setParticipantsPanelButton(value);
}
}
}
@ -700,8 +580,7 @@ export class ToolbarParticipantsPanelButtonDirective implements AfterViewInit, O
* <ov-toolbar [chatPanelButton]="false"></ov-toolbar>
*/
@Directive({
selector: 'ov-videoconference[toolbarChatPanelButton], ov-toolbar[chatPanelButton]',
standalone: false
selector: 'ov-videoconference[toolbarChatPanelButton], ov-toolbar[chatPanelButton]'
})
export class ToolbarChatPanelButtonDirective implements AfterViewInit, OnDestroy {
/**
@ -741,7 +620,9 @@ export class ToolbarChatPanelButtonDirective implements AfterViewInit, OnDestroy
}
private update(value: boolean) {
this.libService.updateToolbarConfig({ chatPanel: value });
if (this.libService.showChatPanelButton() !== value) {
this.libService.setChatPanelButton(value);
}
}
}
@ -761,8 +642,7 @@ export class ToolbarChatPanelButtonDirective implements AfterViewInit, OnDestroy
* <ov-toolbar [activitiesPanelButton]="false"></ov-toolbar>
*/
@Directive({
selector: 'ov-videoconference[toolbarActivitiesPanelButton], ov-toolbar[activitiesPanelButton]',
standalone: false
selector: 'ov-videoconference[toolbarActivitiesPanelButton], ov-toolbar[activitiesPanelButton]'
})
export class ToolbarActivitiesPanelButtonDirective implements AfterViewInit, OnDestroy {
/**
@ -802,7 +682,9 @@ export class ToolbarActivitiesPanelButtonDirective implements AfterViewInit, OnD
}
private update(value: boolean) {
this.libService.updateToolbarConfig({ activitiesPanel: value });
if (this.libService.showActivitiesPanelButton() !== value) {
this.libService.setActivitiesPanelButton(value);
}
}
}
@ -822,8 +704,7 @@ export class ToolbarActivitiesPanelButtonDirective implements AfterViewInit, OnD
* <ov-toolbar [displayRoomName]="false"></ov-toolbar>
*/
@Directive({
selector: 'ov-videoconference[toolbarDisplayRoomName], ov-toolbar[displayRoomName]',
standalone: false
selector: 'ov-videoconference[toolbarDisplayRoomName], ov-toolbar[displayRoomName]'
})
export class ToolbarDisplayRoomNameDirective implements AfterViewInit, OnDestroy {
/**
@ -864,7 +745,9 @@ export class ToolbarDisplayRoomNameDirective implements AfterViewInit, OnDestroy
}
private update(value: boolean) {
this.libService.updateToolbarConfig({ displayRoomName: value });
if (this.libService.showRoomName() !== value) {
this.libService.setDisplayRoomName(value);
}
}
}
@ -884,8 +767,7 @@ export class ToolbarDisplayRoomNameDirective implements AfterViewInit, OnDestroy
* <ov-toolbar [displayLogo]="false"></ov-toolbar>
*/
@Directive({
selector: 'ov-videoconference[toolbarDisplayLogo], ov-toolbar[displayLogo]',
standalone: false
selector: 'ov-videoconference[toolbarDisplayLogo], ov-toolbar[displayLogo]'
})
export class ToolbarDisplayLogoDirective implements AfterViewInit, OnDestroy {
/**
@ -926,7 +808,9 @@ export class ToolbarDisplayLogoDirective implements AfterViewInit, OnDestroy {
}
private update(value: boolean) {
this.libService.updateToolbarConfig({ displayLogo: value });
if (this.libService.showLogo() !== value) {
this.libService.setDisplayLogo(value);
}
}
}
@ -943,8 +827,7 @@ export class ToolbarDisplayLogoDirective implements AfterViewInit, OnDestroy {
*
*/
@Directive({
selector: '[ovToolbarAdditionalButtonsPosition]',
standalone: false
selector: '[ovToolbarAdditionalButtonsPosition]'
})
export class ToolbarAdditionalButtonsPossitionDirective implements AfterViewInit, OnDestroy {
/**
@ -981,6 +864,8 @@ export class ToolbarAdditionalButtonsPossitionDirective implements AfterViewInit
}
private update(value: ToolbarAdditionalButtonsPosition) {
this.libService.updateToolbarConfig({ additionalButtonsPosition: value });
if (this.libService.getToolbarAdditionalButtonsPosition() !== value) {
this.libService.setToolbarAdditionalButtonsPosition(value);
}
}
}

View File

@ -1,4 +1,4 @@
import { AfterViewInit, Directive, ElementRef, Input, OnDestroy, OnInit } from '@angular/core';
import { Directive, ElementRef, Input, OnDestroy, OnInit } from '@angular/core';
import { CaptionsLangOption } from '../../models/caption.model';
// import { CaptionService } from '../../services/caption/caption.service';
import { OpenViduComponentsConfigService } from '../../services/config/directive-config.service';
@ -18,8 +18,7 @@ import { StorageService } from '../../services/storage/storage.service';
* <ov-videoconference [livekitUrl]="http://localhost:1234"></ov-videoconference>
*/
@Directive({
selector: 'ov-videoconference[livekitUrl]',
standalone: false
selector: 'ov-videoconference[livekitUrl]'
})
export class LivekitUrlDirective implements OnDestroy {
/**
@ -55,7 +54,7 @@ export class LivekitUrlDirective implements OnDestroy {
* @ignore
*/
update(value: string) {
this.libService.updateGeneralConfig({ livekitUrl: value });
this.libService.setLivekitUrl(value);
}
}
@ -71,8 +70,7 @@ export class LivekitUrlDirective implements OnDestroy {
* <ov-videoconference [token]="token"></ov-videoconference>
*/
@Directive({
selector: 'ov-videoconference[token]',
standalone: false
selector: 'ov-videoconference[token]'
})
export class TokenDirective implements OnDestroy {
/**
@ -108,7 +106,7 @@ export class TokenDirective implements OnDestroy {
* @ignore
*/
update(value: string) {
this.libService.updateGeneralConfig({ token: value });
this.libService.setToken(value);
}
}
@ -123,8 +121,7 @@ export class TokenDirective implements OnDestroy {
* <ov-videoconference [tokenError]="error"></ov-videoconference>
*/
@Directive({
selector: 'ov-videoconference[tokenError]',
standalone: false
selector: 'ov-videoconference[tokenError]'
})
export class TokenErrorDirective implements OnDestroy {
/**
@ -160,7 +157,7 @@ export class TokenErrorDirective implements OnDestroy {
* @ignore
*/
update(value: any) {
this.libService.updateGeneralConfig({ tokenError: value });
this.libService.setTokenError(value);
}
}
@ -175,8 +172,7 @@ export class TokenErrorDirective implements OnDestroy {
* <ov-videoconference [minimal]="true"></ov-videoconference>
*/
@Directive({
selector: 'ov-videoconference[minimal]',
standalone: false
selector: 'ov-videoconference[minimal]'
})
export class MinimalDirective implements OnDestroy {
/**
@ -212,7 +208,9 @@ export class MinimalDirective implements OnDestroy {
* @ignore
*/
update(value: boolean) {
this.libService.updateGeneralConfig({ minimal: value });
if (this.libService.isMinimal() !== value) {
this.libService.setMinimal(value);
}
}
}
@ -223,7 +221,7 @@ export class MinimalDirective implements OnDestroy {
*
* **Default:** English `en`
*
* **Available Langs:**
* **Available:**
*
* * English: `en`
* * Spanish: `es`
@ -240,8 +238,7 @@ export class MinimalDirective implements OnDestroy {
* <ov-videoconference [lang]="'es'"></ov-videoconference>
*/
@Directive({
selector: 'ov-videoconference[lang]',
standalone: false
selector: 'ov-videoconference[lang]'
})
export class LangDirective implements OnDestroy {
/**
@ -310,8 +307,7 @@ export class LangDirective implements OnDestroy {
* <ov-videoconference [langOptions]="[{name:'Spanish', lang: 'es'}]"></ov-videoconference>
*/
@Directive({
selector: 'ov-videoconference[langOptions]',
standalone: false
selector: 'ov-videoconference[langOptions]'
})
export class LangOptionsDirective implements OnDestroy {
/**
@ -492,10 +488,9 @@ export class LangOptionsDirective implements OnDestroy {
* <ov-videoconference [participantName]="'OpenVidu'"></ov-videoconference>
*/
@Directive({
selector: 'ov-videoconference[participantName]',
standalone: false
selector: 'ov-videoconference[participantName]'
})
export class ParticipantNameDirective implements AfterViewInit, OnDestroy {
export class ParticipantNameDirective implements OnInit {
/**
* @ignore
*/
@ -514,7 +509,7 @@ export class ParticipantNameDirective implements AfterViewInit, OnDestroy {
/**
* @ignore
*/
ngAfterViewInit(): void {
ngOnInit(): void {
this.update(this.participantName);
}
@ -536,7 +531,7 @@ export class ParticipantNameDirective implements AfterViewInit, OnDestroy {
* @ignore
*/
update(value: string) {
if (value) this.libService.updateGeneralConfig({ participantName: value });
this.libService.setParticipantName(value);
}
}
@ -551,8 +546,7 @@ export class ParticipantNameDirective implements AfterViewInit, OnDestroy {
* <ov-videoconference [prejoin]="false"></ov-videoconference>
*/
@Directive({
selector: 'ov-videoconference[prejoin]',
standalone: false
selector: 'ov-videoconference[prejoin]'
})
export class PrejoinDirective implements OnDestroy {
/**
@ -588,7 +582,9 @@ export class PrejoinDirective implements OnDestroy {
* @ignore
*/
update(value: boolean) {
this.libService.updateGeneralConfig({ prejoin: value });
if (this.libService.isPrejoin() !== value) {
this.libService.setPrejoin(value);
}
}
}
@ -604,8 +600,7 @@ export class PrejoinDirective implements OnDestroy {
* <ov-videoconference [videoEnabled]="false"></ov-videoconference>
*/
@Directive({
selector: 'ov-videoconference[videoEnabled]',
standalone: false
selector: 'ov-videoconference[videoEnabled]'
})
export class VideoEnabledDirective implements OnDestroy {
/**
@ -659,7 +654,7 @@ export class VideoEnabledDirective implements OnDestroy {
// Ensure libService state is consistent with the final enabled state
if (this.libService.isVideoEnabled() !== finalEnabledState) {
this.libService.updateStreamConfig({ videoEnabled: finalEnabledState });
this.libService.setVideoEnabled(finalEnabledState);
}
}
}
@ -676,8 +671,7 @@ export class VideoEnabledDirective implements OnDestroy {
*/
@Directive({
selector: 'ov-videoconference[audioEnabled]',
standalone: false
selector: 'ov-videoconference[audioEnabled]'
})
export class AudioEnabledDirective implements OnDestroy {
/**
@ -727,132 +721,7 @@ export class AudioEnabledDirective implements OnDestroy {
this.storageService.setMicrophoneEnabled(finalEnabledState);
if (this.libService.isAudioEnabled() !== enabled) {
this.libService.updateStreamConfig({ audioEnabled: enabled });
this.libService.setAudioEnabled(enabled);
}
}
}
/**
* The **showDisconnectionDialog** directive allows to show/hide the disconnection dialog when the local participant is disconnected from the room.
*
* It is only available for {@link VideoconferenceComponent}.
*
* Default: `true`
*
* @example
* <ov-videoconference [showDisconnectionDialog]="false"></ov-videoconference>
*/
@Directive({
selector: 'ov-videoconference[showDisconnectionDialog]',
standalone: false
})
export class ShowDisconnectionDialogDirective implements OnDestroy {
/**
* @ignore
*/
@Input() set showDisconnectionDialog(value: boolean) {
this.update(value);
}
/**
* @ignore
*/
constructor(
public elementRef: ElementRef,
private libService: OpenViduComponentsConfigService
) {}
/**
* @ignore
*/
ngOnDestroy(): void {
this.clear();
}
/**
* @ignore
*/
clear() {
this.update(true);
}
/**
* @ignore
*/
update(value: boolean) {
if (this.libService.getShowDisconnectionDialog() !== value) {
this.libService.updateGeneralConfig({ showDisconnectionDialog: value });
}
}
}
/**
* The **recordingStreamBaseUrl** directive sets the base URL for retrieving recording streams.
* The complete request URL is dynamically constructed by concatenating the supplied URL, the
* internally managed recordingId, and the `/media` segment.
*
* The final URL format will be:
*
* {recordingStreamBaseUrl}/{recordingId}/media
*
* Default: `"call/api/recordings/{recordingId}/stream"`
*
* Example:
* Given a recordingStreamBaseUrl of `api/recordings`, the resulting URL for a recordingId of `12345` would be:
* `api/recordings/12345/media`
*
* It is essential that the resulting route is declared and configured on your backend, as it is
* used for serving and accessing the recording streams.
*
* @example
* <ov-videoconference [recordingStreamBaseUrl]="'https://myserver.com/api/recordings'">
* </ov-videoconference>
*/
@Directive({
selector: 'ov-videoconference[recordingStreamBaseUrl]',
standalone: false
})
export class RecordingStreamBaseUrlDirective implements AfterViewInit, OnDestroy {
/**
* @ignore
*/
@Input() set recordingStreamBaseUrl(url: string) {
this.update(url);
}
/**
* @ignore
*/
constructor(
private elementRef: ElementRef,
private libService: OpenViduComponentsConfigService
) {}
/**
* @ignore
*/
ngAfterViewInit(): void {
this.update(this.recordingStreamBaseUrl);
}
/**
* @ignore
*/
ngOnDestroy(): void {
this.clear();
}
/**
* @ignore
*/
clear() {
this.update('');
}
/**
* @ignore
*/
update(value: string) {
if (value) this.libService.updateGeneralConfig({ recordingStreamBaseUrl: value });
}
}

View File

@ -1,284 +0,0 @@
/**
* The ***ovPreJoin** directive empowers you to substitute the default pre-join component template with a custom one.
* This directive allows you to create a completely custom pre-join experience while maintaining the core functionality.
*
* In the example below, we demonstrate how to replace the pre-join template with a custom one that includes
* device selection and a custom join button.
*
* <!--ovPreJoin-start-tutorial-->
* ```typescript
* import { HttpClient } from '@angular/common/http';
* import { Component } from '@angular/core';
* import { lastValueFrom } from 'rxjs';
* import { FormsModule } from '@angular/forms';
*
* import {
* DeviceService,
* ParticipantService,
* OpenViduComponentsModule,
* } from 'openvidu-components-angular';
*
* @Component({
* selector: 'app-root',
* template: `
* <ov-videoconference
* [token]="token"
* [livekitUrl]="LIVEKIT_URL"
* (onTokenRequested)="onTokenRequested($event)"
* (onReadyToJoin)="onReadyToJoin()"
* >
* <!-- Custom Pre-Join Component -->
* <div *ovPreJoin class="custom-prejoin">
* <h2>Join Meeting</h2>
* <div class="prejoin-form">
* <input
* type="text"
* placeholder="Enter your name"
* [(ngModel)]="participantName"
* class="name-input"
* />
* <button
* (click)="joinMeeting()"
* [disabled]="!participantName"
* class="join-button"
* >
* Join Meeting
* </button>
* </div>
* </div>
* </ov-videoconference>
* `,
* styles: `
* .custom-prejoin {
* display: flex;
* flex-direction: column;
* align-items: center;
* justify-content: center;
* height: 100vh;
* background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
* color: white;
* }
* .prejoin-form {
* display: flex;
* flex-direction: column;
* gap: 20px;
* align-items: center;
* }
* .name-input {
* padding: 12px;
* border: none;
* border-radius: 8px;
* font-size: 16px;
* min-width: 250px;
* }
* .join-button {
* padding: 12px 24px;
* background: #4CAF50;
* color: white;
* border: none;
* border-radius: 8px;
* font-size: 16px;
* cursor: pointer;
* transition: background 0.3s;
* }
* .join-button:hover:not(:disabled) {
* background: #45a049;
* }
* .join-button:disabled {
* background: #cccccc;
* cursor: not-allowed;
* }
* `,
* standalone: true,
* imports: [OpenViduComponentsModule, FormsModule],
* })
* export class AppComponent {
* // For local development, leave these variables empty
* // For production, configure them with correct URLs depending on your deployment
* APPLICATION_SERVER_URL = '';
* LIVEKIT_URL = '';
*
* // Define the name of the room and initialize the token variable
* roomName = 'custom-prejoin';
* token!: string;
* participantName: string = '';
*
* constructor(
* private httpClient: HttpClient,
* private deviceService: DeviceService,
* private participantService: ParticipantService
* ) {
* this.configureUrls();
* }
*
* private configureUrls() {
* // If APPLICATION_SERVER_URL is not configured, use default value from local development
* if (!this.APPLICATION_SERVER_URL) {
* if (window.location.hostname === 'localhost') {
* this.APPLICATION_SERVER_URL = 'http://localhost:6080/';
* } else {
* this.APPLICATION_SERVER_URL =
* 'https://' + window.location.hostname + ':6443/';
* }
* }
*
* // If LIVEKIT_URL is not configured, use default value from local development
* if (!this.LIVEKIT_URL) {
* if (window.location.hostname === 'localhost') {
* this.LIVEKIT_URL = 'ws://localhost:7880/';
* } else {
* this.LIVEKIT_URL = 'wss://' + window.location.hostname + ':7443/';
* }
* }
* }
*
* // Function to request a token when a participant joins the room
* async onTokenRequested(participantName: string) {
* const { token } = await this.getToken(this.roomName, participantName);
* this.token = token;
* }
*
* // Function called when ready to join
* onReadyToJoin() {
* console.log('Ready to join the meeting');
* }
*
* // Function to join the meeting
* async joinMeeting() {
* if (this.participantName.trim()) {
* // Request token with the participant name
* await this.onTokenRequested(this.participantName);
* }
* }
*
* // Function to get a token from the server
* getToken(roomName: string, participantName: string): Promise<any> {
* try {
* // Send a POST request to the server to obtain a token
* return lastValueFrom(
* this.httpClient.post<any>(this.APPLICATION_SERVER_URL + 'token', {
* roomName,
* participantName,
* })
* );
* } catch (error: any) {
* // Handle errors, e.g., if the server is not reachable
* if (error.status === 404) {
* throw {
* status: error.status,
* message:
* 'Cannot connect with the backend. ' + error.url + ' not found',
* };
* }
* throw error;
* }
* }
* }
*
* ```
* <!--ovPreJoin-end-tutorial-->
*
* For a detailed tutorial on customizing the pre-join component, please visit [this link](https://openvidu.io/latest/docs/tutorials/angular-components/openvidu-custom-prejoin/).
*/
import { Directive, TemplateRef, ViewContainerRef } from '@angular/core';
@Directive({
selector: '[ovPreJoin]',
standalone: false
})
export class PreJoinDirective {
constructor(
public template: TemplateRef<any>,
public container: ViewContainerRef
) {}
}
/**
* The ***ovParticipantPanelAfterLocalParticipant** directive allows you to inject custom HTML or Angular templates
* immediately after the local participant item in the participant panel.
* This enables you to extend the participant panel with additional controls, information, or UI elements.
*
* Usage example:
* ```html
* <ov-participant-panel>
* <ng-container *ovParticipantPanelAfterLocalParticipant>
* <div class="custom-content">
* <!-- Your custom HTML here -->
* <span>Custom content after local participant</span>
* </div>
* </ng-container>
* </ov-participant-panel>
* ```
*/
@Directive({
selector: '[ovParticipantPanelAfterLocalParticipant]',
standalone: false
})
export class ParticipantPanelAfterLocalParticipantDirective {
constructor(
public template: TemplateRef<any>,
public container: ViewContainerRef
) {}
}
/**
* The ***ovLayoutAdditionalElements** directive allows you to inject custom HTML or Angular templates
* as additional layout elements within the videoconference UI.
* This enables you to extend the layout with extra controls, banners, or any custom UI.
*
* Usage example:
* ```html
* <ov-videoconference>
* <ng-container *ovLayoutAdditionalElements>
* <div class="my-custom-layout-element">
* <!-- Your custom HTML here -->
* <span>Extra layout element</span>
* </div>
* </ng-container>
* </ov-videoconference>
* ```
*/
@Directive({
selector: '[ovLayoutAdditionalElements]',
standalone: false
})
export class LayoutAdditionalElementsDirective {
constructor(
public template: TemplateRef<any>,
public container: ViewContainerRef
) {}
}
/**
* The ***ovParticipantPanelParticipantBadge** directive allows you to inject custom badges or indicators
* in the participant panel.
* This enables you to add role indicators, status badges, or other visual elements.
*
* Usage example:
* ```html
* <ov-participants-panel>
* <div *ovParticipantPanelItem="let participant">
* <ov-participant-panel-item [participant]="participant">
* <!-- Custom badge for local participant only -->
* <ng-container *ovParticipantPanelParticipantBadge>
* <span class="moderator-badge">
* <mat-icon>admin_panel_settings</mat-icon>
* Moderator
* </span>
* </ng-container>
* </ov-participant-panel-item>
* </div>
* </ov-participants-panel>
* ```
*/
@Directive({
selector: '[ovParticipantPanelParticipantBadge]',
standalone: false
})
export class ParticipantPanelParticipantBadgeDirective {
constructor(
public template: TemplateRef<any>,
public container: ViewContainerRef
) {}
}

View File

@ -14,12 +14,6 @@ import {
ActivitiesPanelDirective,
BackgroundEffectsPanelDirective
} from './openvidu-components-angular.directive';
import {
LayoutAdditionalElementsDirective,
ParticipantPanelAfterLocalParticipantDirective,
ParticipantPanelParticipantBadgeDirective,
PreJoinDirective
} from './internals.directive';
@NgModule({
declarations: [
@ -35,10 +29,6 @@ import {
ToolbarAdditionalPanelButtonsDirective,
ParticipantPanelItemElementsDirective,
ActivitiesPanelDirective,
PreJoinDirective,
ParticipantPanelAfterLocalParticipantDirective,
LayoutAdditionalElementsDirective,
ParticipantPanelParticipantBadgeDirective
// BackgroundEffectsPanelDirective
],
exports: [
@ -54,10 +44,6 @@ import {
ToolbarAdditionalPanelButtonsDirective,
ParticipantPanelItemElementsDirective,
ActivitiesPanelDirective,
PreJoinDirective,
ParticipantPanelAfterLocalParticipantDirective,
LayoutAdditionalElementsDirective,
ParticipantPanelParticipantBadgeDirective
// BackgroundEffectsPanelDirective
]
})

View File

@ -33,7 +33,7 @@
"AUDIO_DEVICE": "音频设备",
"NO_VIDEO_DEVICE": "未找到视频设备",
"NO_AUDIO_DEVICE": "未找到音频设备",
"JOIN": "加入房间",
"JOIN": "加入会话",
"PREPARING": "筹备会议"
},
"TOOLBAR": {
@ -55,9 +55,7 @@
"LEAVE": "离开会议",
"PARTICIPANTS": "参与者",
"CHAT": "聊天",
"ACTIVITIES": "活动",
"NO_TRACKS_PUBLISHED": "请分享音频或视频以开始录制。",
"VIEW_RECORDINGS": "查看录像"
"ACTIVITIES": "活动"
},
"STREAM": {
"SETTINGS": "设置",
@ -91,9 +89,7 @@
"MICROPHONE": "麦克风",
"SCREEN": "屏幕",
"NO_STREAMS": "无",
"YOU": "你",
"MUTE": "静音",
"UNMUTE": "取消静音"
"YOU": "你"
},
"SETTINGS": {
"TITLE": "设置",
@ -104,7 +100,7 @@
"CAPTIONS": "字幕",
"DISABLED_AUDIO": "没有音频设备",
"DISABLED_VIDEO": "没有视频设备",
"CAPTIONS_LANG_TEXT": "选择房间参与者将使用的语言。字幕将以该语言显示。"
"CAPTIONS_LANG_TEXT": "选择会话参与者将使用的语言。字幕将以该语言显示。"
},
"BACKGROUND": {
"TITLE": "背景效果",
@ -118,10 +114,6 @@
"SUBTITLE": "为后人记录你的会议",
"CONTENT_TITLE": "记录你的视频通话",
"CONTENT_SUBTITLE": "当录音完成后,你将可以轻松地下载它",
"VIEW_ONLY_SUBTITLE": "查看和访问房间录音",
"VIEW_ONLY_CONTENT_TITLE": "视频通话录音",
"VIEW_ONLY_CONTENT_SUBTITLE": "在这里您可以访问所有可用的录音",
"WATCH": "观看",
"STARTING": "开始录音",
"STOPPING": "停止录制",
"IN_PROGRESS": "录音中",
@ -132,10 +124,7 @@
"DELETE_QUESTION": "您确定要删除录音吗",
"DOWNLOAD": "下载",
"RECORDINGS": "录制",
"NO_MODERATOR": "只有主持人可以开始录音",
"NO_TRACKS_PUBLISHED": "请分享音频或视频以开始录制。",
"NO_RECORDINGS_AVAILABLE": "目前没有可用的录音",
"ERROR_STARTING": "开始录音时出错"
"NO_MODERATOR": "只有主持人可以开始录音"
},
"STREAMING": {
"TITLE": "直播",
@ -150,17 +139,9 @@
}
},
"ERRORS": {
"SESSION": "连接到房间时有错误",
"SESSION": "连接到会话时有错误",
"CONNECTION": "连接丢失",
"RECONNECT": "试图重新连接到房间",
"DISCONNECT": "您已断开连接",
"NETWORK_DISCONNECT": "由于网络连接问题,您已断开连接",
"SIGNAL_CLOSE": "与服务器的连接意外关闭",
"SERVER_SHUTDOWN": "服务器当前已关闭或正在维护中",
"PARTICIPANT_REMOVED": "您已被移出此房间",
"ROOM_DELETED": "此房间已被删除",
"DUPLICATE_IDENTITY": "由于您的昵称已被分配给另一位参与者,您已被断开连接",
"UNKNOWN_DISCONNECT": "您已从房间断开连接",
"RECONNECT": "试图重新连接到会话",
"TOGGLE_CAMERA": "切换相机时出现错误",
"TOGGLE_MICROPHONE": "切换麦克风时出现错误",
"SCREEN_SHARING": "分享屏幕出错",

View File

@ -33,8 +33,8 @@
"AUDIO_DEVICE": "Audiogerät",
"NO_VIDEO_DEVICE": "Video-Gerät nicht gefunden",
"NO_AUDIO_DEVICE": "Audio-Gerät nicht gefunden",
"JOIN": "Raum beitreten",
"PREPARING": "Raum vorbereiten..."
"JOIN": "Sitzung beitreten",
"PREPARING": "Sitzung vorbereiten..."
},
"TOOLBAR": {
"MUTE_AUDIO": "Stummschalten des Audios",
@ -52,11 +52,10 @@
"START_RECORDING": "Aufzeichnung starten",
"STOP_RECORDING": "Aufzeichnung stoppen",
"SETTINGS": "Einstellungen",
"LEAVE": "Die Raum verlassen",
"LEAVE": "Die Sitzung verlassen",
"PARTICIPANTS": "Teilnehmer",
"CHAT": "Chat",
"ACTIVITIES": "Aktivitäten",
"NO_TRACKS_PUBLISHED": "Teile Audio oder Video, um mit der Aufnahme zu beginnen."
"ACTIVITIES": "Aktivitäten"
},
"STREAM": {
"SETTINGS": "Einstellungen",
@ -78,7 +77,7 @@
"CHAT": {
"TITLE": "Chat",
"YOU": "Sie",
"SUBTITLE": "Nachrichten werden am Ende der Raum entfernt",
"SUBTITLE": "Nachrichten werden am Ende der Sitzung entfernt",
"PLACEHOLDER": "Eine Nachricht senden...",
"SEND": "Senden",
"MESSAGE_SENT_NOTIFICATION": "Nachricht gesendet",
@ -90,9 +89,7 @@
"MICROPHONE": "MIKROFON",
"SCREEN": "BILDSCHIRM",
"NO_STREAMS": "KEINE",
"YOU": "Sie",
"MUTE": "Stummschalten",
"UNMUTE": "Stummschaltung aufheben"
"YOU": "Sie"
},
"SETTINGS": {
"TITLE": "Einstellungen",
@ -103,7 +100,7 @@
"CAPTIONS": "Untertitel",
"DISABLED_AUDIO": "Audio deaktiviert",
"DISABLED_VIDEO": "Video deaktiviert",
"CAPTIONS_LANG_TEXT": "Wählen Sie die Sprache, die die Teilnehmer der Raum verwenden. Die Untertitel werden in dieser Sprache angezeigt."
"CAPTIONS_LANG_TEXT": "Wählen Sie die Sprache, die die Teilnehmer der Sitzung verwenden. Die Untertitel werden in dieser Sprache angezeigt."
},
"BACKGROUND": {
"TITLE": "Hintergrund-Effekte",
@ -127,9 +124,7 @@
"DELETE_QUESTION": "Möchten Sie die Aufzeichnung wirklich löschen?",
"DOWNLOAD": "Download",
"RECORDINGS": "AUFZEICHNUNGEN",
"NO_MODERATOR": "Nur der MODERATOR kann die Aufzeichnung starten",
"NO_TRACKS_PUBLISHED": "Teile Audio oder Video, um mit der Aufnahme zu beginnen.",
"ERROR_STARTING": "Fehler beim Starten der Aufnahme"
"NO_MODERATOR": "Nur der MODERATOR kann die Aufzeichnung starten"
},
"STREAMING": {
"TITLE": "Streaming",
@ -144,17 +139,9 @@
}
},
"ERRORS": {
"SESSION": "Es ist ein Fehler beim Verbinden mit der Raum aufgetreten",
"SESSION": "Es ist ein Fehler beim Verbinden mit der Sitzung aufgetreten",
"CONNECTION": "Verbindung verloren",
"RECONNECT": "Ich versuche, die Verbindung zur Raum wiederherzustellen...",
"DISCONNECT": "Sie wurden getrennt",
"NETWORK_DISCONNECT": "Sie wurden aufgrund eines Netzwerkproblems getrennt",
"SIGNAL_CLOSE": "Die Verbindung zum Server wurde unerwartet geschlossen",
"SERVER_SHUTDOWN": "Der Server ist derzeit nicht verfügbar oder wird gewartet",
"PARTICIPANT_REMOVED": "Sie wurden aus diesem Raum entfernt",
"ROOM_DELETED": "Dieser Raum wurde gelöscht",
"DUPLICATE_IDENTITY": "Sie wurden getrennt, da Ihr Spitzname einem anderen Teilnehmer zugewiesen wurde",
"UNKNOWN_DISCONNECT": "Sie wurden vom Raum getrennt",
"RECONNECT": "Ich versuche, die Verbindung zur Sitzung wiederherzustellen...",
"TOGGLE_CAMERA": "Es gab einen Fehler beim Umschalten der Kamera",
"TOGGLE_MICROPHONE": "Es ist ein Fehler beim Umschalten des Mikrofons aufgetreten",
"SCREEN_SHARING": "Fehler beim Teilen des Bildschirms",

View File

@ -30,8 +30,8 @@
"AUDIO_DEVICE": "Audio device",
"NO_VIDEO_DEVICE": "Video device not found",
"NO_AUDIO_DEVICE": "Audio device not found",
"JOIN": "Join room",
"PREPARING": "Preparing room..."
"JOIN": "Join session",
"PREPARING": "Preparing session..."
},
"ROOM": {
"JOINING": "Joining room..."
@ -52,12 +52,10 @@
"START_RECORDING": "Start recording",
"STOP_RECORDING": "Stop recording",
"SETTINGS": "Settings",
"LEAVE": "Leave the room",
"LEAVE": "Leave the session",
"PARTICIPANTS": "Participants",
"CHAT": "Chat",
"ACTIVITIES": "Activities",
"NO_TRACKS_PUBLISHED": "Share audio or video to start recording.",
"VIEW_RECORDINGS": "View recordings"
"ACTIVITIES": "Activities"
},
"STREAM": {
"SETTINGS": "Settings",
@ -79,7 +77,7 @@
"CHAT": {
"TITLE": "Chat",
"YOU": "You",
"SUBTITLE": "Messages will be removed at the end of the room",
"SUBTITLE": "Messages will be removed at the end of the session",
"PLACEHOLDER": "Send a message...",
"SEND": "Send",
"MESSAGE_SENT_NOTIFICATION": "message sent",
@ -91,9 +89,7 @@
"MICROPHONE": "MICROPHONE",
"SCREEN": "SCREEN",
"NO_STREAMS": "NONE",
"YOU": "You",
"MUTE": "Mute",
"UNMUTE": "Unmute"
"YOU": "You"
},
"SETTINGS": {
"TITLE": "Settings",
@ -104,7 +100,7 @@
"CAPTIONS": "Captions",
"DISABLED_AUDIO": "Audio disabled",
"DISABLED_VIDEO": "Video disabled",
"CAPTIONS_LANG_TEXT": "Select the language that the participants of the room will use. The captions will appear in that language."
"CAPTIONS_LANG_TEXT": "Select the language that the participants of the session will use. The captions will appear in that language."
},
"BACKGROUND": {
"TITLE": "Background effects",
@ -118,13 +114,6 @@
"SUBTITLE": "Record your meeting for posterity",
"CONTENT_TITLE": "Record your video call",
"CONTENT_SUBTITLE": "When recording has finished you will be able to download it with ease",
"VIEW_ONLY_TITLE": "Available recordings",
"VIEW_ONLY_SUBTITLE": "View and access room recordings",
"VIEW_ONLY_CONTENT_TITLE": "Video call recordings",
"VIEW_ONLY_CONTENT_SUBTITLE": "Here you can access all available recordings",
"VIEW": "View",
"WATCH": "Watch",
"ACCESS": "Access",
"STARTING": "Starting recording",
"STOPPING": "Stopping recording",
"IN_PROGRESS": "Recording in progress ...",
@ -135,11 +124,7 @@
"DELETE_QUESTION": "Are you sure you want to delete the recording?",
"DOWNLOAD": "Download",
"RECORDINGS": "RECORDINGS",
"NO_MODERATOR": "Only the MODERATOR can start the recording",
"NO_TRACKS_PUBLISHED": "Share audio or video to start recording.",
"NO_RECORDINGS_AVAILABLE": "No recordings available at this time",
"BROWSE_RECORDINGS": "Browse saved recordings",
"ERROR_STARTING": "Error starting recording"
"NO_MODERATOR": "Only the MODERATOR can start the recording"
},
"STREAMING": {
"TITLE": "Streaming",
@ -154,17 +139,9 @@
}
},
"ERRORS": {
"SESSION": "There was an error connecting to the room",
"SESSION": "There was an error connecting to the session",
"CONNECTION": "Connection lost",
"RECONNECT": "Oops! Trying to reconnect to the room...",
"DISCONNECT": "You have been disconnected",
"NETWORK_DISCONNECT": "You were disconnected due to a network connectivity issue",
"SIGNAL_CLOSE": "The connection to the server was unexpectedly closed",
"SERVER_SHUTDOWN": "The server is currently down or under maintenance",
"PARTICIPANT_REMOVED": "You have been removed from this room",
"ROOM_DELETED": "This room has been deleted",
"DUPLICATE_IDENTITY": "You have been disconnected because your alias was assigned to another participant",
"UNKNOWN_DISCONNECT": "You have been disconnected from the room",
"RECONNECT": "Oops! Trying to reconnect to the session...",
"TOGGLE_CAMERA": "There was an error toggling camera",
"TOGGLE_MICROPHONE": "There was an error toggling microhpone",
"SCREEN_SHARING": "Error sharing screen",

View File

@ -33,7 +33,7 @@
"AUDIO_DEVICE": "Dispositivo de audio",
"NO_VIDEO_DEVICE": "Dispositivo de vídeo no encontrado",
"NO_AUDIO_DEVICE": "Dispositivo de audio no encontrado",
"PREPARING": "Preparando la sala ...",
"PREPARING": "Preparando la session ...",
"JOIN": "Unirme ahora"
},
"TOOLBAR": {
@ -52,12 +52,10 @@
"START_RECORDING": "Iniciar grabación",
"STOP_RECORDING": "Detener grabación",
"SETTINGS": "Configuración",
"LEAVE": "Salir de la sala",
"LEAVE": "Salir de la sesión",
"PARTICIPANTS": "Participantes",
"CHAT": "Chat",
"ACTIVITIES": "Actividades",
"NO_TRACKS_PUBLISHED": "Comparte audio o video para poder empezar a grabar.",
"VIEW_RECORDINGS": "Ver grabaciones"
"ACTIVITIES": "Actividades"
},
"STREAM": {
"SETTINGS": "Ajustes",
@ -79,7 +77,7 @@
"CHAT": {
"TITLE": "Chat",
"YOU": "Tú",
"SUBTITLE": "Los mensajes se borrarán al salir de la sala",
"SUBTITLE": "Los mensajes se borrarán al finalizar la sesión",
"PLACEHOLDER": "Enviar mensaje...",
"SEND": "Enviar",
"MESSAGE_SENT_NOTIFICATION": "mensaje enviado",
@ -91,9 +89,7 @@
"MICROPHONE": "MICRÓFONO",
"SCREEN": "PANTALLA",
"NO_STREAMS": "NINGUNO",
"YOU": "Tú",
"MUTE": "Silenciar",
"UNMUTE": "Activar audio"
"YOU": "Tú"
},
"SETTINGS": {
"TITLE": "Configuración",
@ -104,7 +100,7 @@
"CAPTIONS": "Subtítulos",
"DISABLED_AUDIO": "Audio desactivado",
"DISABLED_VIDEO": "Video desactivado",
"CAPTIONS_LANG_TEXT": "Selecciona el idioma que usarán los participantes de la sala. Los subtítulos aparecerán en ese idioma."
"CAPTIONS_LANG_TEXT": "Selecciona el idioma que usarán los participantes de la sesión. Los subtítulos aparecerán en ese idioma."
},
"BACKGROUND": {
"TITLE": "Efectos de fondo",
@ -118,10 +114,6 @@
"SUBTITLE": "Graba tus llamadas para la posteridad",
"CONTENT_TITLE": "Graba tu video conferencia",
"CONTENT_SUBTITLE": "Cuando la grabación haya finalizado, podrás descargarla con facilidad",
"VIEW_ONLY_SUBTITLE": "Visualiza y accede a las grabaciones de la sala",
"VIEW_ONLY_CONTENT_TITLE": "Grabaciones de la video conferencia",
"VIEW_ONLY_CONTENT_SUBTITLE": "Aquí puedes acceder a todas las grabaciones disponibles",
"WATCH": "Visualizar",
"STARTING": "Iniciando grabación...",
"STOPPING": "Parando grabación",
"IN_PROGRESS": "Grabación en curso",
@ -132,10 +124,7 @@
"DELETE_QUESTION": "¿Estás seguro/a de que deseas borrar la grabación?",
"DOWNLOAD": "Descargar",
"RECORDINGS": "GRABACIONES",
"NO_MODERATOR": "Sólo el MODERADOR puede iniciar la grabación",
"NO_TRACKS_PUBLISHED": "Comparte audio o video para poder empezar a grabar.",
"NO_RECORDINGS_AVAILABLE": "No hay grabaciones disponibles en este momento",
"ERROR_STARTING": "Error iniciando la grabación"
"NO_MODERATOR": "Sólo el MODERADOR puede iniciar la grabación"
},
"STREAMING": {
"TITLE": "Streaming",
@ -149,17 +138,9 @@
}
},
"ERRORS": {
"SESSION": "Hubo un error al conectar a la sala",
"SESSION": "Hubo un error al conectar a la sesión",
"CONNECTION": "Sin conexión",
"RECONNECT": "Intentando reconectar a la sala...",
"DISCONNECT": "Te has desconectado",
"NETWORK_DISCONNECT": "Te desconectaste debido a un problema de conectividad de red",
"SIGNAL_CLOSE": "La conexión con el servidor se cerró inesperadamente",
"SERVER_SHUTDOWN": "El servidor está actualmente fuera de servicio o en mantenimiento",
"PARTICIPANT_REMOVED": "Has sido eliminado de esta sala",
"ROOM_DELETED": "Esta sala ha sido eliminada",
"DUPLICATE_IDENTITY": "Te has desconectado porque tu alias ha sido asignado a otro participante",
"UNKNOWN_DISCONNECT": "Te has desconectado de la sala",
"RECONNECT": "Intentando reconectar a la sesión...",
"TOGGLE_CAMERA": "Hubo un error cambiando la cámara",
"TOGGLE_MICROPHONE": "Hubo un error cambiando el micrófono",
"SCREEN_SHARING": "Hubo un error compartiendo pantalla",

View File

@ -33,8 +33,8 @@
"AUDIO_DEVICE": "Périphérique audio",
"NO_VIDEO_DEVICE": "Appareil vidéo introuvable",
"NO_AUDIO_DEVICE": "Appareil audio introuvable",
"JOIN": "Joindre une salle",
"PREPARING": "Préparation de la salle ..."
"JOIN": "Joindre une session",
"PREPARING": "Préparation de la session ..."
},
"TOOLBAR": {
"MUTE_AUDIO": "Mettez votre audio en sourdine",
@ -52,12 +52,10 @@
"START_RECORDING": "démarrer l'enregistrement",
"STOP_RECORDING": "Arrêter l'enregistrement",
"SETTINGS": "Paramètres",
"LEAVE": "Quitter la salle",
"LEAVE": "Quitter la session",
"PARTICIPANTS": "Participants",
"CHAT": "Chat",
"ACTIVITES": "Activités",
"NO_TRACKS_PUBLISHED": "Partagez de laudio ou de la vidéo pour commencer lenregistrement.",
"VIEW_RECORDINGS": "Voir les enregistrements"
"ACTIVITES": "Activités"
},
"STREAM": {
"SETTINGS": "Paramètres",
@ -79,7 +77,7 @@
"CHAT": {
"TITLE": "Chat",
"YOU": "Vous",
"SUBTITLE": "Les messages seront supprimés à la fin de la salle",
"SUBTITLE": "Les messages seront supprimés à la fin de la session",
"PLACEHOLDER": "Envoyer un message...",
"SEND": "Envoyer",
"MESSAGE_SENT_NOTIFICATION": "message envoyé",
@ -91,9 +89,7 @@
"MICROPHONE": "MICROPHONE",
"SCREEN": "ÉCRAN",
"NO_STREAMS": "PAS_DE_FLUX",
"YOU": "Vous",
"MUTE": "Couper le son",
"UNMUTE": "Désactiver le son"
"YOU": "Vous"
},
"SETTINGS": {
"TITLE": "Paramètres",
@ -104,7 +100,7 @@
"CAPTIONS": "Les sous-titres",
"DISABLED_AUDIO": "Désactiver l'audio",
"DISABLED_VIDEO": "Désactiver la vidéo",
"CAPTIONS_LANG_TEXT": "Sélectionnez la langue que les participants de la salle utiliseront. Les sous-titres apparaîtront dans cette langue."
"CAPTIONS_LANG_TEXT": "Sélectionnez la langue que les participants de la session utiliseront. Les sous-titres apparaîtront dans cette langue."
},
"BACKGROUND": {
"TITLE": "Effets de fond",
@ -118,10 +114,6 @@
"SUBTITLE": "Enregistrez votre réunion pour la postérité",
"CONTENT_TITLE": "Enregistrez votre appel vidéo",
"CONTENT_SUBTITLE": "Une fois l'enregistrement terminé, vous pourrez le télécharger facilement",
"VIEW_ONLY_SUBTITLE": "Visualisez et accédez aux enregistrements de la salle",
"VIEW_ONLY_CONTENT_TITLE": "Enregistrements d'appel vidéo",
"VIEW_ONLY_CONTENT_SUBTITLE": "Ici vous pouvez accéder à tous les enregistrements disponibles",
"WATCH": "Regarder",
"STARTING": "Début de l'enregistrement",
"STOPPING": "Arrêt de l'enregistrement",
"IN_PROGRESS": "Enregistrement en cours",
@ -132,10 +124,7 @@
"DELETE_QUESTION": "Voulez-vous vraiment supprimer l'enregistrement ?",
"DOWNLOAD": "Télécharger",
"RECORDINGS": "ENREGISTREMENTS",
"NO_MODERATOR": "Seul le MODERATEUR peut lancer l'enregistrement",
"NO_TRACKS_PUBLISHED": "Partagez de laudio ou de la vidéo pour commencer lenregistrement.",
"NO_RECORDINGS_AVAILABLE": "Aucun enregistrement disponible pour le moment",
"ERROR_STARTING": "Erreur de démarrage"
"NO_MODERATOR": "Seul le MODERATEUR peut lancer l'enregistrement"
},
"STREAMING": {
"TITLE": "Streaming",
@ -150,17 +139,9 @@
}
},
"ERRORS": {
"SESSION": "There was an error connecting to the salle",
"SESSION": "There was an error connecting to the session",
"CONNECTION": "Connexion perdue",
"RECONNECT": "Oups ! Tentative de reconnexion à la salle...",
"DISCONNECT": "Vous avez été déconnecté",
"NETWORK_DISCONNECT": "Vous avez été déconnecté en raison d'un problème de connexion réseau",
"SIGNAL_CLOSE": "La connexion au serveur a été interrompue de manière inattendue",
"SERVER_SHUTDOWN": "Le serveur est actuellement hors service ou en maintenance",
"PARTICIPANT_REMOVED": "Vous avez été retiré de cette salle",
"ROOM_DELETED": "Cette salle a été supprimée",
"DUPLICATE_IDENTITY": "Vous avez été déconnecté car votre pseudo a été attribué à un autre participant",
"UNKNOWN_DISCONNECT": "Vous avez été déconnecté de la salle",
"RECONNECT": "Oups ! Tentative de reconnexion à la session...",
"TOGGLE_CAMERA": "There was an error toggle camera",
"TOGGLE_MICROPHONE": "There was an error toggling microhpone",
"SCREEN_SHARING": "Erreur de partage d'écran",

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