mirror of https://github.com/OpenVidu/openvidu.git
Compare commits
No commits in common. "master" and "v3.3.0" have entirely different histories.
|
@ -63,7 +63,7 @@ jobs:
|
|||
# - 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
|
||||
|
@ -100,7 +100,7 @@ jobs:
|
|||
# - 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
|
||||
|
@ -133,7 +133,7 @@ jobs:
|
|||
# - 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
|
||||
|
@ -166,7 +166,7 @@ jobs:
|
|||
# - 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
|
||||
|
@ -180,38 +180,6 @@ jobs:
|
|||
- 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
|
||||
|
@ -231,7 +199,7 @@ jobs:
|
|||
# - 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
|
||||
|
@ -264,7 +232,7 @@ jobs:
|
|||
# - 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
|
||||
|
@ -297,7 +265,7 @@ jobs:
|
|||
# - 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
|
||||
|
@ -330,7 +298,7 @@ jobs:
|
|||
# - 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
|
||||
|
@ -363,7 +331,7 @@ jobs:
|
|||
# - 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
|
||||
|
@ -396,7 +364,7 @@ jobs:
|
|||
# - 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
|
||||
run: docker run --network=host -d -v $(pwd)/openvidu-components-angular/e2e/assets:/e2e-assets selenium/standalone-chrome:127.0
|
||||
- name: Run openvidu-local-deployment
|
||||
uses: OpenVidu/actions/start-openvidu-local-deployment@main
|
||||
- name: Start OpenVidu Call backend
|
||||
|
@ -429,7 +397,7 @@ jobs:
|
|||
# - 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
|
||||
|
@ -443,36 +411,3 @@ jobs:
|
|||
- name: Cleanup
|
||||
if: always()
|
||||
uses: OpenVidu/actions/cleanup@main
|
||||
|
||||
e2e_virtual_backgrounds:
|
||||
needs: test_setup
|
||||
name: Virtual Backgrounds E2E
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout Repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ inputs.commit_sha || github.sha }}
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '20'
|
||||
- name: Install wait-on package
|
||||
run: npm install -g wait-on
|
||||
# - name: Run Browserless Chrome
|
||||
# run: docker run -d -p 3000:3000 --network host browserless/chrome:1.57-chrome-stable
|
||||
- name: Run Chrome
|
||||
run: docker run --network=host -d -p 4444:4444 selenium/standalone-chrome:138.0
|
||||
- name: Run openvidu-local-deployment
|
||||
uses: OpenVidu/actions/start-openvidu-local-deployment@main
|
||||
- name: Start OpenVidu Call backend
|
||||
uses: OpenVidu/actions/start-openvidu-call@main
|
||||
- name: Build and Serve openvidu-components-angular Testapp
|
||||
uses: OpenVidu/actions/start-openvidu-components-testapp@main
|
||||
- name: Run Webcomponent E2E
|
||||
env:
|
||||
LAUNCH_MODE: CI
|
||||
run: npm run e2e:lib-virtual-backgrounds --prefix openvidu-components-angular
|
||||
- name: Cleanup
|
||||
if: always()
|
||||
uses: OpenVidu/actions/cleanup@main
|
||||
|
|
|
@ -89,7 +89,7 @@ describe('Testing API Directives', () => {
|
|||
|
||||
await utils.checkPrejoinIsPresent();
|
||||
|
||||
await utils.waitForElement('.language-selector');
|
||||
await utils.waitForElement('#lang-btn-compact');
|
||||
|
||||
const element = await utils.waitForElement('#join-button');
|
||||
expect(await element.getText()).toEqual('Unirme ahora');
|
||||
|
@ -108,20 +108,20 @@ describe('Testing API Directives', () => {
|
|||
const panelTitle = await utils.waitForElement('.panel-title');
|
||||
expect(await panelTitle.getText()).toEqual('Configuración');
|
||||
|
||||
const element = await utils.waitForElement('.lang-name');
|
||||
expect(await element.getAttribute('innerText')).toEqual('Español expand_more');
|
||||
const element = await utils.waitForElement('#lang-selected-name');
|
||||
expect(await element.getAttribute('innerText')).toEqual('Español');
|
||||
});
|
||||
|
||||
it('should override the LANG OPTIONS', async () => {
|
||||
await browser.get(`${url}&prejoin=true&langOptions=true`);
|
||||
|
||||
await utils.checkPrejoinIsPresent();
|
||||
await utils.waitForElement('.language-selector');
|
||||
await utils.clickOn('.language-selector');
|
||||
await utils.waitForElement('#lang-btn-compact');
|
||||
await utils.clickOn('#lang-btn-compact');
|
||||
await browser.sleep(500);
|
||||
expect(await utils.getNumberOfElements('.language-option')).toEqual(2);
|
||||
expect(await utils.getNumberOfElements('.lang-menu-opt')).toEqual(2);
|
||||
|
||||
await utils.clickOn('.language-option');
|
||||
await utils.clickOn('.lang-menu-opt');
|
||||
await browser.sleep(500);
|
||||
|
||||
await utils.clickOn('#join-button');
|
||||
|
@ -136,12 +136,12 @@ describe('Testing API Directives', () => {
|
|||
await browser.sleep(500);
|
||||
|
||||
await utils.waitForElement('#settings-container');
|
||||
await utils.waitForElement('.full-lang-button');
|
||||
await utils.clickOn('.full-lang-button');
|
||||
await utils.waitForElement('.lang-button');
|
||||
await utils.clickOn('.lang-button');
|
||||
|
||||
await browser.sleep(500);
|
||||
|
||||
expect(await utils.getNumberOfElements('.language-option')).toEqual(2);
|
||||
expect(await utils.getNumberOfElements('.lang-menu-opt')).toEqual(2);
|
||||
});
|
||||
|
||||
it('should show the PREJOIN page', async () => {
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
export const LAUNCH_MODE = process.env.LAUNCH_MODE || 'DEV';
|
||||
|
||||
jasmine.DEFAULT_TIMEOUT_INTERVAL = 60000;
|
||||
jasmine.DEFAULT_TIMEOUT_INTERVAL = 30000;
|
||||
|
|
|
@ -92,12 +92,35 @@ describe('Testing videoconference EVENTS', () => {
|
|||
expect(await utils.isPresent('#onVideoEnabledChanged-true')).toBeTrue();
|
||||
});
|
||||
|
||||
it('should receive the onVideoEnabledChanged event when clicking on the settings panel', async () => {
|
||||
await browser.get(`${url}&prejoin=false`);
|
||||
|
||||
await utils.checkSessionIsPresent();
|
||||
|
||||
await utils.checkToolbarIsPresent();
|
||||
await utils.togglePanel('settings');
|
||||
await browser.sleep(500);
|
||||
|
||||
await utils.waitForElement('#settings-container');
|
||||
await utils.clickOn('#video-opt');
|
||||
|
||||
await utils.waitForElement('ov-video-devices-select');
|
||||
await utils.clickOn('ov-video-devices-select #camera-button');
|
||||
// Checking if onVideoEnabledChanged has been received
|
||||
await utils.waitForElement('#onVideoEnabledChanged-false');
|
||||
expect(await utils.isPresent('#onVideoEnabledChanged-false')).toBeTrue();
|
||||
|
||||
await utils.clickOn('ov-video-devices-select #camera-button');
|
||||
await utils.waitForElement('#onVideoEnabledChanged-true');
|
||||
expect(await utils.isPresent('#onVideoEnabledChanged-true')).toBeTrue();
|
||||
});
|
||||
|
||||
it('should receive the onVideoDeviceChanged event on prejoin', async () => {
|
||||
await browser.get(`${url}&fakeDevices=true`);
|
||||
await utils.checkPrejoinIsPresent();
|
||||
|
||||
await utils.waitForElement('#video-dropdown');
|
||||
await utils.clickOn('#video-dropdown');
|
||||
await utils.waitForElement('#video-devices-form');
|
||||
await utils.clickOn('#video-devices-form');
|
||||
|
||||
await utils.waitForElement('#option-custom_fake_video_1');
|
||||
await utils.clickOn('#option-custom_fake_video_1');
|
||||
|
@ -119,8 +142,8 @@ describe('Testing videoconference EVENTS', () => {
|
|||
await utils.clickOn('#video-opt');
|
||||
|
||||
await utils.waitForElement('ov-video-devices-select');
|
||||
await utils.waitForElement('#video-dropdown');
|
||||
await utils.clickOn('#video-dropdown');
|
||||
await utils.waitForElement('#video-devices-form');
|
||||
await utils.clickOn('#video-devices-form');
|
||||
|
||||
await utils.waitForElement('#option-custom_fake_video_1');
|
||||
await utils.clickOn('#option-custom_fake_video_1');
|
||||
|
@ -161,12 +184,35 @@ describe('Testing videoconference EVENTS', () => {
|
|||
expect(await utils.isPresent('#onAudioEnabledChanged-true')).toBeTrue();
|
||||
});
|
||||
|
||||
it('should receive the onAudioEnabledChanged event when clicking on the settings panel', async () => {
|
||||
await browser.get(`${url}&prejoin=false`);
|
||||
|
||||
await utils.checkSessionIsPresent();
|
||||
|
||||
await utils.checkToolbarIsPresent();
|
||||
await utils.togglePanel('settings');
|
||||
await browser.sleep(500);
|
||||
|
||||
await utils.waitForElement('#settings-container');
|
||||
await utils.clickOn('#audio-opt');
|
||||
|
||||
await utils.waitForElement('ov-audio-devices-select');
|
||||
await utils.clickOn('ov-audio-devices-select #microphone-button');
|
||||
// Checking if onAudioEnabledChanged has been received
|
||||
await utils.waitForElement('#onAudioEnabledChanged-false');
|
||||
expect(await utils.isPresent('#onAudioEnabledChanged-false')).toBeTrue();
|
||||
|
||||
await utils.clickOn('ov-audio-devices-select #microphone-button');
|
||||
await utils.waitForElement('#onAudioEnabledChanged-true');
|
||||
expect(await utils.isPresent('#onAudioEnabledChanged-true')).toBeTrue();
|
||||
});
|
||||
|
||||
it('should receive the onAudioDeviceChanged event on prejoin', async () => {
|
||||
await browser.get(`${url}&fakeDevices=true`);
|
||||
await utils.checkPrejoinIsPresent();
|
||||
|
||||
await utils.waitForElement('#audio-dropdown');
|
||||
await utils.clickOn('#audio-dropdown');
|
||||
await utils.waitForElement('#audio-devices-form');
|
||||
await utils.clickOn('#audio-devices-form');
|
||||
|
||||
await utils.waitForElement('#option-custom_fake_audio_1');
|
||||
await utils.clickOn('#option-custom_fake_audio_1');
|
||||
|
@ -188,8 +234,8 @@ describe('Testing videoconference EVENTS', () => {
|
|||
await utils.clickOn('#audio-opt');
|
||||
|
||||
await utils.waitForElement('ov-audio-devices-select');
|
||||
await utils.waitForElement('#audio-dropdown');
|
||||
await utils.clickOn('#audio-dropdown');
|
||||
await utils.waitForElement('#audio-devices-form');
|
||||
await utils.clickOn('#audio-devices-form');
|
||||
|
||||
await utils.waitForElement('#option-custom_fake_audio_1');
|
||||
await utils.clickOn('#option-custom_fake_audio_1');
|
||||
|
@ -202,8 +248,8 @@ describe('Testing videoconference EVENTS', () => {
|
|||
await browser.get(`${url}`);
|
||||
await utils.checkPrejoinIsPresent();
|
||||
|
||||
await utils.waitForElement('.language-selector');
|
||||
await utils.clickOn('.language-selector');
|
||||
await utils.waitForElement('#lang-btn-compact');
|
||||
await utils.clickOn('#lang-btn-compact');
|
||||
|
||||
await browser.sleep(500);
|
||||
await utils.clickOn('#lang-opt-es');
|
||||
|
@ -223,8 +269,8 @@ describe('Testing videoconference EVENTS', () => {
|
|||
await browser.sleep(500);
|
||||
|
||||
await utils.waitForElement('#settings-container');
|
||||
await utils.waitForElement('.full-lang-button');
|
||||
await utils.clickOn('.full-lang-button');
|
||||
await utils.waitForElement('.lang-button');
|
||||
await utils.clickOn('.lang-button');
|
||||
|
||||
await browser.sleep(500);
|
||||
await utils.clickOn('#lang-opt-es');
|
||||
|
@ -352,7 +398,7 @@ describe('Testing videoconference EVENTS', () => {
|
|||
expect(await utils.isPresent('#onSettingsPanelStatusChanged-false')).toBeTrue();
|
||||
});
|
||||
|
||||
it('should receive the onRecordingStartRequested and onRecordingStopRequested event when clicking toolbar button', async () => {
|
||||
it('should receive the onRecordingStartRequested event when clicking toolbar button', async () => {
|
||||
const roomName = 'recordingToolbarEvent';
|
||||
await browser.get(`${url}&prejoin=false&roomName=${roomName}`);
|
||||
|
||||
|
@ -364,16 +410,10 @@ describe('Testing videoconference EVENTS', () => {
|
|||
// Checking if onRecordingStartRequested has been received
|
||||
await utils.waitForElement(`#onRecordingStartRequested-${roomName}`);
|
||||
expect(await utils.isPresent(`#onRecordingStartRequested-${roomName}`)).toBeTrue();
|
||||
|
||||
await utils.waitForElement('.activity-status.started');
|
||||
|
||||
await utils.toggleRecordingFromToolbar();
|
||||
|
||||
// Checking if onRecordingStopRequested has been received
|
||||
await utils.waitForElement(`#onRecordingStopRequested-${roomName}`);
|
||||
expect(await utils.isPresent(`#onRecordingStopRequested-${roomName}`)).toBeTrue();
|
||||
});
|
||||
|
||||
xit('should receive the onRecordingStopRequested event when clicking toolbar button', async () => {});
|
||||
|
||||
xit('should receive the onBroadcastingStopRequested event when clicking toolbar button', async () => {
|
||||
await browser.get(`${url}&prejoin=false`);
|
||||
|
||||
|
@ -406,7 +446,7 @@ describe('Testing videoconference EVENTS', () => {
|
|||
expect(await utils.isPresent('#onBroadcastingStopRequested')).toBeTrue();
|
||||
});
|
||||
|
||||
it('should receive the onRecordingStartRequested and onRecordingStopRequested when clicking from activities panel', async () => {
|
||||
it('should receive the onRecordingStartRequested when clicking from activities panel', async () => {
|
||||
const roomName = 'recordingActivitiesEvent';
|
||||
await browser.get(`${url}&prejoin=false&roomName=${roomName}`);
|
||||
|
||||
|
@ -432,6 +472,8 @@ describe('Testing videoconference EVENTS', () => {
|
|||
expect(await utils.isPresent(`#onRecordingStartRequested-${roomName}`)).toBeTrue();
|
||||
});
|
||||
|
||||
xit('should receive the onRecordingStopRequested when clicking from activities panel', async () => {});
|
||||
|
||||
xit('should receive the onRecordingDeleteRequested event', async () => {
|
||||
let element;
|
||||
const roomName = 'deleteRecordingEvent';
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
});
|
|
@ -33,7 +33,7 @@ describe('Media Devices: Virtual Device Replacement and Permissions Handling', (
|
|||
|
||||
await browser.get(`${url}&fakeDevices=true`);
|
||||
|
||||
let videoDevices = await utils.waitForElement('#video-dropdown');
|
||||
let videoDevices = await utils.waitForElement('#video-devices-form');
|
||||
await videoDevices.click();
|
||||
let element = await utils.waitForElement('#option-custom_fake_video_1');
|
||||
await element.click();
|
||||
|
@ -63,7 +63,7 @@ describe('Media Devices: Virtual Device Replacement and Permissions Handling', (
|
|||
await browser.sleep(500);
|
||||
await utils.clickOn('#video-opt');
|
||||
expect(await utils.isPresent('ov-video-devices-select')).toBeTrue();
|
||||
let videoDevices = await utils.waitForElement('#video-dropdown');
|
||||
let videoDevices = await utils.waitForElement('#video-devices-form');
|
||||
await videoDevices.click();
|
||||
let element = await utils.waitForElement('#option-custom_fake_video_1');
|
||||
await element.click();
|
||||
|
@ -130,15 +130,16 @@ describe('Media Devices: UI Behavior Without Media Device Permissions', () => {
|
|||
await browser.quit();
|
||||
});
|
||||
|
||||
it('should camera and microphone buttons be disabled in the prejoin page when permissions are denied', async () => {
|
||||
it('should disable camera and microphone buttons in the prejoin page when permissions are denied', async () => {
|
||||
await browser.get(`${url}`);
|
||||
await utils.checkPrejoinIsPresent();
|
||||
await utils.waitForElement('#no-video-device-message');
|
||||
await utils.waitForElement('#no-audio-device-message');
|
||||
expect(await utils.isPresent('#backgrounds-button')).toBeFalse();
|
||||
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 camera and microphone buttons be disabled in the room page when permissions are denied', async () => {
|
||||
it('should disable camera and microphone buttons in the room page when permissions are denied', async () => {
|
||||
await browser.get(`${url}`);
|
||||
await utils.checkPrejoinIsPresent();
|
||||
await utils.clickOn('#join-button');
|
||||
|
@ -150,7 +151,7 @@ describe('Media Devices: UI Behavior Without Media Device Permissions', () => {
|
|||
expect(await button.isEnabled()).toBeFalse();
|
||||
});
|
||||
|
||||
it('should camera and microphone buttons be disabled in the room page without prejoin when permissions are denied', async () => {
|
||||
it('should disable camera and microphone buttons in the room page without prejoin when permissions are denied', async () => {
|
||||
await browser.get(`${url}&prejoin=false`);
|
||||
await utils.checkSessionIsPresent();
|
||||
await utils.checkToolbarIsPresent();
|
||||
|
@ -160,7 +161,7 @@ describe('Media Devices: UI Behavior Without Media Device Permissions', () => {
|
|||
expect(await button.isEnabled()).toBeFalse();
|
||||
});
|
||||
|
||||
it('should show an audio and video device warning in settings when permissions are denied', async () => {
|
||||
it('should disable camera and microphone device selection buttons in settings when permissions are denied', async () => {
|
||||
await browser.get(`${url}&prejoin=false`);
|
||||
await utils.checkToolbarIsPresent();
|
||||
await utils.togglePanel('settings');
|
||||
|
@ -169,9 +170,11 @@ describe('Media Devices: UI Behavior Without Media Device Permissions', () => {
|
|||
expect(await utils.isPresent('.settings-container')).toBeTrue();
|
||||
await utils.clickOn('#video-opt');
|
||||
expect(await utils.isPresent('ov-video-devices-select')).toBeTrue();
|
||||
await utils.waitForElement('#no-video-device-message');
|
||||
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();
|
||||
await utils.waitForElement('#no-audio-device-message');
|
||||
button = await utils.waitForElement('#microphone-button');
|
||||
expect(await button.isEnabled()).toBeFalse();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -29,6 +29,40 @@ describe('Panels: UI Navigation and Section Switching', () => {
|
|||
await browser.quit();
|
||||
});
|
||||
|
||||
/**
|
||||
* TODO
|
||||
* It only works with OpenVidu PRO because this is a PRO feature
|
||||
*/
|
||||
// it('should toggle BACKGROUND panel on prejoin page when VIDEO is MUTED', async () => {
|
||||
// let element;
|
||||
// await browser.get(`${url}`);
|
||||
// element = await utils.waitForElement('#pre-join-container');
|
||||
// expect(await utils.isPresent('#pre-join-container')).toBeTrue();
|
||||
|
||||
// const backgroundButton = await utils.waitForElement('#background-effects-btn');
|
||||
// expect(await utils.isPresent('#background-effects-btn')).toBeTrue();
|
||||
// expect(await backgroundButton.isEnabled()).toBeTrue();
|
||||
// await backgroundButton.click();
|
||||
// await browser.sleep(500);
|
||||
|
||||
// await utils.waitForElement('#background-effects-container');
|
||||
// expect(await utils.isPresent('#background-effects-container')).toBeTrue();
|
||||
|
||||
// element = await utils.waitForElement('#camera-button');
|
||||
// expect(await utils.isPresent('#camera-button')).toBeTrue();
|
||||
// expect(await element.isEnabled()).toBeTrue();
|
||||
// await element.click();
|
||||
|
||||
// await browser.sleep(500);
|
||||
// element = await utils.waitForElement('#video-poster');
|
||||
// expect(await utils.isPresent('#video-poster')).toBeTrue();
|
||||
|
||||
// expect(await backgroundButton.isDisplayed()).toBeTrue();
|
||||
// expect(await backgroundButton.isEnabled()).toBeFalse();
|
||||
|
||||
// expect(await utils.isPresent('#background-effects-container')).toBeFalse();
|
||||
// });
|
||||
|
||||
it('should open and close the CHAT panel and verify its content', async () => {
|
||||
await browser.get(`${url}&prejoin=false`);
|
||||
await utils.checkLayoutPresent();
|
||||
|
|
|
@ -679,13 +679,12 @@ describe('Stream UI controls and interaction features', () => {
|
|||
await browser.sleep(1000);
|
||||
const tabs = await browser.getAllWindowHandles();
|
||||
|
||||
await browser.switchTo().window(tabs[1]);
|
||||
await utils.clickOn('#mic-btn');
|
||||
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);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,8 +1,4 @@
|
|||
import { By, until, WebDriver, WebElement } from 'selenium-webdriver';
|
||||
import * as fs from 'fs';
|
||||
import { PNG } from 'pngjs';
|
||||
import pixelmatch from 'pixelmatch';
|
||||
type PNGWithMetadata = PNG & { data: Buffer };
|
||||
|
||||
export class OpenViduComponentsPO {
|
||||
private TIMEOUT = 10 * 1000;
|
||||
|
@ -192,16 +188,6 @@ export class OpenViduComponentsPO {
|
|||
await this.waitForElement('#participants-panel-btn');
|
||||
await this.clickOn('#participants-panel-btn');
|
||||
break;
|
||||
case 'backgrounds':
|
||||
await this.waitForElement('#more-options-btn');
|
||||
await this.clickOn('#more-options-btn');
|
||||
|
||||
await this.browser.sleep(500);
|
||||
await this.waitForElement('#virtual-bg-btn');
|
||||
await this.clickOn('#virtual-bg-btn');
|
||||
|
||||
await this.browser.sleep(1000);
|
||||
break;
|
||||
|
||||
case 'settings':
|
||||
await this.toggleToolbarMoreOptions();
|
||||
|
@ -209,74 +195,5 @@ export class OpenViduComponentsPO {
|
|||
await this.clickOn('#toolbar-settings-btn');
|
||||
break;
|
||||
}
|
||||
|
||||
await this.browser.sleep(500);
|
||||
}
|
||||
|
||||
async applyBackground(bgId: string) {
|
||||
await this.waitForElement('ov-background-effects-panel');
|
||||
await this.browser.sleep(1000);
|
||||
await this.waitForElement(`#effect-${bgId}`);
|
||||
|
||||
await this.clickOn(`#effect-${bgId}`);
|
||||
await this.browser.sleep(2000);
|
||||
}
|
||||
|
||||
async applyVirtualBackgroundFromPrejoin(bgId: string): Promise<void> {
|
||||
await this.waitForElement('#backgrounds-button');
|
||||
await this.clickOn('#backgrounds-button');
|
||||
|
||||
await this.applyBackground(bgId);
|
||||
await this.clickOn('#backgrounds-button');
|
||||
}
|
||||
|
||||
async saveScreenshot(filename: string, element: WebElement) {
|
||||
const image = await element.takeScreenshot();
|
||||
fs.writeFileSync(filename, image, 'base64');
|
||||
}
|
||||
|
||||
async expectVirtualBackgroundApplied(
|
||||
img1Name: string,
|
||||
img2Name: string,
|
||||
{
|
||||
threshold = 0.4,
|
||||
minDiffPixels = 500,
|
||||
debug = false
|
||||
}: {
|
||||
threshold?: number;
|
||||
minDiffPixels?: number;
|
||||
debug?: boolean;
|
||||
} = {}
|
||||
): Promise<void> {
|
||||
const beforeImg = PNG.sync.read(fs.readFileSync(img1Name));
|
||||
const afterImg = PNG.sync.read(fs.readFileSync(img2Name));
|
||||
const { width, height } = beforeImg;
|
||||
const diff = new PNG({ width, height });
|
||||
|
||||
// const numDiffPixels = pixelmatch(img1.data, img2.data, diff.data, width, height, {
|
||||
// threshold: 0.4
|
||||
// // alpha: 0.5,
|
||||
// // includeAA: false,
|
||||
// // diffColor: [255, 0, 0]
|
||||
// });
|
||||
|
||||
const numDiffPixels = pixelmatch(beforeImg.data, afterImg.data, diff.data, width, height, {
|
||||
threshold
|
||||
// includeAA: true
|
||||
});
|
||||
|
||||
if (numDiffPixels <= minDiffPixels) {
|
||||
// Sólo guardar los archivos de debug si falla la prueba
|
||||
if (debug) {
|
||||
fs.writeFileSync('before.png', PNG.sync.write(beforeImg));
|
||||
fs.writeFileSync('after.png', PNG.sync.write(afterImg));
|
||||
fs.writeFileSync('diff.png', PNG.sync.write(diff));
|
||||
}
|
||||
}
|
||||
|
||||
expect(numDiffPixels).toBeGreaterThan(minDiffPixels, 'The virtual background was not applied correctly');
|
||||
|
||||
// fs.writeFileSync('diff.png', PNG.sync.write(diff));
|
||||
// expect(numDiffPixels).to.be.greaterThan(500, 'The virtual background was not applied correctly');
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,155 +0,0 @@
|
|||
import { Builder, WebDriver } from 'selenium-webdriver';
|
||||
import { TestAppConfig } from './selenium.conf';
|
||||
import { OpenViduComponentsPO } from './utils.po.test';
|
||||
|
||||
const url = TestAppConfig.appUrl;
|
||||
|
||||
describe('Prejoin: Virtual Backgrounds', () => {
|
||||
let browser: WebDriver;
|
||||
let utils: OpenViduComponentsPO;
|
||||
|
||||
async function createChromeBrowser(): Promise<WebDriver> {
|
||||
return await new Builder()
|
||||
.forBrowser(TestAppConfig.browserName)
|
||||
.withCapabilities(TestAppConfig.browserCapabilities)
|
||||
.setChromeOptions(TestAppConfig.browserOptions)
|
||||
.usingServer(TestAppConfig.seleniumAddress)
|
||||
.build();
|
||||
}
|
||||
|
||||
beforeEach(async () => {
|
||||
browser = await createChromeBrowser();
|
||||
utils = new OpenViduComponentsPO(browser);
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
try {
|
||||
await utils.leaveRoom();
|
||||
} catch (error) {}
|
||||
await browser.quit();
|
||||
});
|
||||
|
||||
it('should close BACKGROUNDS on prejoin page when VIDEO is disabled', async () => {
|
||||
let element;
|
||||
await browser.get(`${url}`);
|
||||
await utils.checkPrejoinIsPresent();
|
||||
|
||||
const backgroundButton = await utils.waitForElement('#backgrounds-button');
|
||||
expect(await utils.isPresent('#backgrounds-button')).toBeTrue();
|
||||
expect(await backgroundButton.isEnabled()).toBeTrue();
|
||||
await utils.clickOn('#backgrounds-button');
|
||||
await browser.sleep(500);
|
||||
|
||||
await utils.waitForElement('#background-effects-container');
|
||||
expect(await utils.isPresent('#background-effects-container')).toBeTrue();
|
||||
|
||||
await utils.clickOn('#camera-button');
|
||||
|
||||
await browser.sleep(500);
|
||||
element = await utils.waitForElement('#video-poster');
|
||||
expect(await utils.isPresent('#video-poster')).toBeTrue();
|
||||
|
||||
expect(await backgroundButton.isDisplayed()).toBeTrue();
|
||||
expect(await backgroundButton.isEnabled()).toBeFalse();
|
||||
|
||||
await browser.sleep(1000);
|
||||
expect(await utils.getNumberOfElements('#background-effects-container')).toBe(0);
|
||||
});
|
||||
|
||||
it('should open and close BACKGROUNDS panel on prejoin page', async () => {
|
||||
await browser.get(`${url}`);
|
||||
await utils.checkPrejoinIsPresent();
|
||||
|
||||
const backgroundButton = await utils.waitForElement('#backgrounds-button');
|
||||
expect(await utils.isPresent('#backgrounds-button')).toBeTrue();
|
||||
expect(await backgroundButton.isEnabled()).toBeTrue();
|
||||
await backgroundButton.click();
|
||||
await browser.sleep(500);
|
||||
|
||||
await utils.waitForElement('#background-effects-container');
|
||||
expect(await utils.isPresent('#background-effects-container')).toBeTrue();
|
||||
|
||||
await utils.clickOn('#backgrounds-button');
|
||||
await browser.sleep(1000);
|
||||
expect(await utils.getNumberOfElements('#background-effects-container')).toBe(0);
|
||||
});
|
||||
|
||||
it('should apply a background effect on prejoin page', async () => {
|
||||
await browser.get(`${url}`);
|
||||
await utils.checkPrejoinIsPresent();
|
||||
|
||||
let videoElement = await utils.waitForElement('.OV_video-element');
|
||||
await utils.saveScreenshot('before.png', videoElement);
|
||||
|
||||
await utils.applyVirtualBackgroundFromPrejoin('1');
|
||||
|
||||
await browser.sleep(1000);
|
||||
|
||||
videoElement = await utils.waitForElement('.OV_video-element');
|
||||
await utils.saveScreenshot('after.png', videoElement);
|
||||
|
||||
await utils.expectVirtualBackgroundApplied('before.png', 'after.png');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Room: Virtual Backgrounds', () => {
|
||||
let browser: WebDriver;
|
||||
let utils: OpenViduComponentsPO;
|
||||
|
||||
async function createChromeBrowser(): Promise<WebDriver> {
|
||||
return await new Builder()
|
||||
.forBrowser(TestAppConfig.browserName)
|
||||
.withCapabilities(TestAppConfig.browserCapabilities)
|
||||
.setChromeOptions(TestAppConfig.browserOptions)
|
||||
.usingServer(TestAppConfig.seleniumAddress)
|
||||
.build();
|
||||
}
|
||||
|
||||
beforeEach(async () => {
|
||||
browser = await createChromeBrowser();
|
||||
utils = new OpenViduComponentsPO(browser);
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
try {
|
||||
await utils.leaveRoom();
|
||||
} catch (error) {}
|
||||
await browser.quit();
|
||||
});
|
||||
|
||||
it('should open and close BACKGROUNDS panel in the room', async () => {
|
||||
await browser.get(`${url}&prejoin=false`);
|
||||
await utils.checkLayoutPresent();
|
||||
await utils.checkToolbarIsPresent();
|
||||
await utils.togglePanel('backgrounds');
|
||||
|
||||
await utils.waitForElement('#background-effects-container');
|
||||
expect(await utils.isPresent('#background-effects-container')).toBeTrue();
|
||||
|
||||
await utils.togglePanel('backgrounds');
|
||||
await browser.sleep(1000);
|
||||
expect(await utils.getNumberOfElements('#background-effects-container')).toBe(0);
|
||||
});
|
||||
|
||||
it('should apply a background effect in the room', async () => {
|
||||
await browser.get(`${url}&prejoin=false`);
|
||||
await utils.checkLayoutPresent();
|
||||
|
||||
await utils.togglePanel('backgrounds');
|
||||
|
||||
await utils.waitForElement('#background-effects-container');
|
||||
expect(await utils.isPresent('#background-effects-container')).toBeTrue();
|
||||
|
||||
let videoElement = await utils.waitForElement('.OV_video-element');
|
||||
await utils.saveScreenshot('before.png', videoElement);
|
||||
|
||||
await utils.applyBackground('1');
|
||||
|
||||
await browser.sleep(1000);
|
||||
|
||||
videoElement = await utils.waitForElement('.OV_video-element');
|
||||
await utils.saveScreenshot('after.png', videoElement);
|
||||
|
||||
await utils.expectVirtualBackgroundApplied('before.png', 'after.png');
|
||||
});
|
||||
});
|
|
@ -33,10 +33,9 @@
|
|||
"@compodoc/compodoc": "^1.1.25",
|
||||
"@types/jasmine": "^5.1.4",
|
||||
"@types/node": "20.12.14",
|
||||
"@types/pngjs": "^6.0.5",
|
||||
"@types/selenium-webdriver": "4.1.16",
|
||||
"@types/ws": "^8.5.12",
|
||||
"chromedriver": "138.0.0",
|
||||
"chromedriver": "136.0.2",
|
||||
"concat": "^1.0.3",
|
||||
"cross-env": "^7.0.3",
|
||||
"eslint-config-prettier": "^9.1.0",
|
||||
|
@ -58,8 +57,6 @@
|
|||
"lint-staged": "^15.2.10",
|
||||
"ng-packagr": "19.2.2",
|
||||
"npm-watch": "^0.13.0",
|
||||
"pixelmatch": "^7.1.0",
|
||||
"pngjs": "^7.0.0",
|
||||
"prettier": "3.3.3",
|
||||
"selenium-webdriver": "4.32.0",
|
||||
"ts-node": "10.9.2",
|
||||
|
@ -2940,6 +2937,33 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@compodoc/compodoc/node_modules/@angular-devkit/schematics/node_modules/chokidar": {
|
||||
"version": "3.6.0",
|
||||
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz",
|
||||
"integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"anymatch": "~3.1.2",
|
||||
"braces": "~3.0.2",
|
||||
"glob-parent": "~5.1.2",
|
||||
"is-binary-path": "~2.1.0",
|
||||
"is-glob": "~4.0.1",
|
||||
"normalize-path": "~3.0.0",
|
||||
"readdirp": "~3.6.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 8.10.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://paulmillr.com/funding/"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"fsevents": "~2.3.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@compodoc/compodoc/node_modules/@babel/core": {
|
||||
"version": "7.25.8",
|
||||
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.25.8.tgz",
|
||||
|
@ -3112,6 +3136,21 @@
|
|||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@compodoc/compodoc/node_modules/glob-parent": {
|
||||
"version": "5.1.2",
|
||||
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
|
||||
"integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"is-glob": "^4.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 6"
|
||||
}
|
||||
},
|
||||
"node_modules/@compodoc/compodoc/node_modules/magic-string": {
|
||||
"version": "0.30.11",
|
||||
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.11.tgz",
|
||||
|
@ -3122,6 +3161,36 @@
|
|||
"@jridgewell/sourcemap-codec": "^1.5.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@compodoc/compodoc/node_modules/readdirp": {
|
||||
"version": "3.6.0",
|
||||
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
|
||||
"integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"picomatch": "^2.2.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@compodoc/compodoc/node_modules/readdirp/node_modules/picomatch": {
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
|
||||
"integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=8.6"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/jonschlinkert"
|
||||
}
|
||||
},
|
||||
"node_modules/@compodoc/live-server": {
|
||||
"version": "1.2.3",
|
||||
"resolved": "https://registry.npmjs.org/@compodoc/live-server/-/live-server-1.2.3.tgz",
|
||||
|
@ -6590,16 +6659,6 @@
|
|||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/pngjs": {
|
||||
"version": "6.0.5",
|
||||
"resolved": "https://registry.npmjs.org/@types/pngjs/-/pngjs-6.0.5.tgz",
|
||||
"integrity": "sha512-0k5eKfrA83JOZPppLtS2C7OUtyNAl2wKNxfyYl9Q5g9lPkgBl/9hNyAu6HuEH2J4XmIv2znEpkDd0SaZVxW6iQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/qs": {
|
||||
"version": "6.14.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.14.0.tgz",
|
||||
|
@ -8067,9 +8126,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/chromedriver": {
|
||||
"version": "138.0.0",
|
||||
"resolved": "https://registry.npmjs.org/chromedriver/-/chromedriver-138.0.0.tgz",
|
||||
"integrity": "sha512-bJ/DNm5Y0TbqM71ARaAohTWVwcQ2SsWciYC5Q9Ul7DC/oTxm6B1vI2h6WscFCOOi49ul4tXZVjA/LOruljjmjA==",
|
||||
"version": "136.0.2",
|
||||
"resolved": "https://registry.npmjs.org/chromedriver/-/chromedriver-136.0.2.tgz",
|
||||
"integrity": "sha512-yJ52GN01edLYWYK/OspYBv3plzF08Ucdq4ukYigJGOX8dWr/tP5PXSZPWFPVarmbmcO57pNLP9Im8hsYljMEjw==",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"license": "Apache-2.0",
|
||||
|
@ -8086,7 +8145,7 @@
|
|||
"chromedriver": "bin/chromedriver"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20"
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/cli-cursor": {
|
||||
|
@ -15762,19 +15821,6 @@
|
|||
"@napi-rs/nice": "^1.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/pixelmatch": {
|
||||
"version": "7.1.0",
|
||||
"resolved": "https://registry.npmjs.org/pixelmatch/-/pixelmatch-7.1.0.tgz",
|
||||
"integrity": "sha512-1wrVzJ2STrpmONHKBy228LM1b84msXDUoAzVEl0R8Mz4Ce6EPr+IVtxm8+yvrqLYMHswREkjYFaMxnyGnaY3Ng==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"pngjs": "^7.0.0"
|
||||
},
|
||||
"bin": {
|
||||
"pixelmatch": "bin/pixelmatch"
|
||||
}
|
||||
},
|
||||
"node_modules/pkg-dir": {
|
||||
"version": "7.0.0",
|
||||
"resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-7.0.0.tgz",
|
||||
|
@ -15879,16 +15925,6 @@
|
|||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/pngjs": {
|
||||
"version": "7.0.0",
|
||||
"resolved": "https://registry.npmjs.org/pngjs/-/pngjs-7.0.0.tgz",
|
||||
"integrity": "sha512-LKWqWJRhstyYo9pGvgor/ivk2w94eSjE3RGVuzLGlr3NmD8bf7RcYGze1mNdEHRP6TRP6rMuDHk5t44hnTRyow==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=14.19.0"
|
||||
}
|
||||
},
|
||||
"node_modules/portfinder": {
|
||||
"version": "1.0.37",
|
||||
"resolved": "https://registry.npmjs.org/portfinder/-/portfinder-1.0.37.tgz",
|
||||
|
|
|
@ -25,10 +25,9 @@
|
|||
"@compodoc/compodoc": "^1.1.25",
|
||||
"@types/jasmine": "^5.1.4",
|
||||
"@types/node": "20.12.14",
|
||||
"@types/pngjs": "^6.0.5",
|
||||
"@types/selenium-webdriver": "4.1.16",
|
||||
"@types/ws": "^8.5.12",
|
||||
"chromedriver": "138.0.0",
|
||||
"chromedriver": "136.0.2",
|
||||
"concat": "^1.0.3",
|
||||
"cross-env": "^7.0.3",
|
||||
"eslint-config-prettier": "^9.1.0",
|
||||
|
@ -50,8 +49,6 @@
|
|||
"lint-staged": "^15.2.10",
|
||||
"ng-packagr": "19.2.2",
|
||||
"npm-watch": "^0.13.0",
|
||||
"pixelmatch": "^7.1.0",
|
||||
"pngjs": "^7.0.0",
|
||||
"prettier": "3.3.3",
|
||||
"selenium-webdriver": "4.32.0",
|
||||
"ts-node": "10.9.2",
|
||||
|
@ -92,7 +89,6 @@
|
|||
"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",
|
||||
|
@ -100,7 +96,6 @@
|
|||
"e2e:lib-screensharing": "tsc --project ./e2e && npx jasmine --fail-fast ./e2e/dist/screensharing.test.js",
|
||||
"e2e:lib-stream": "tsc --project ./e2e && npx jasmine --fail-fast ./e2e/dist/stream.test.js",
|
||||
"e2e:lib-toolbar": "tsc --project ./e2e && npx jasmine --fail-fast ./e2e/dist/toolbar.test.js",
|
||||
"e2e:lib-virtual-backgrounds": "tsc --project ./e2e && npx jasmine --fail-fast ./e2e/dist/virtual-backgrounds.test.js",
|
||||
"simulate:multiparty": "livekit-cli load-test --url ws://localhost:7880 --api-key devkey --api-secret secret --room daily-call --publishers 8 --audio-publishers 8 --identity-prefix Participant --identity publisher",
|
||||
"husky": "cd .. && husky install"
|
||||
},
|
||||
|
|
|
@ -8,11 +8,9 @@ import { Component, Input } from '@angular/core';
|
|||
selector: 'ov-avatar-profile',
|
||||
template: `
|
||||
<div class="poster" id="video-poster">
|
||||
@if (letter) {
|
||||
<div class="initial" [ngStyle]="{ 'background-color': color }">
|
||||
<span id="poster-text">{{ letter }}</span>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
`,
|
||||
styleUrls: ['./avatar-profile.component.scss'],
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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';
|
||||
|
@ -22,7 +20,6 @@ import { CdkDrag } from '@angular/cdk/drag-drop';
|
|||
import { PanelService } from '../../services/panel/panel.service';
|
||||
import { GlobalConfigService } from '../../services/config/global-config.service';
|
||||
import { OpenViduComponentsConfigService } from '../../services/config/directive-config.service';
|
||||
import { LayoutTemplateConfiguration, TemplateManagerService } from '../../services/template/template-manager.service';
|
||||
|
||||
/**
|
||||
*
|
||||
|
@ -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,11 +72,11 @@ 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;
|
||||
|
@ -116,13 +90,10 @@ export class LayoutComponent implements OnInit, OnDestroy, AfterViewInit {
|
|||
private participantService: ParticipantService,
|
||||
private globalService: GlobalConfigService,
|
||||
private directiveService: OpenViduComponentsConfigService,
|
||||
private cd: ChangeDetectorRef,
|
||||
private templateManagerService: TemplateManagerService
|
||||
private cd: ChangeDetectorRef
|
||||
) {}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.setupTemplates();
|
||||
|
||||
this.subscribeToParticipants();
|
||||
this.subscribeToCaptions();
|
||||
}
|
||||
|
@ -136,11 +107,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 +126,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 +135,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 +146,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 +218,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
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { animate, style, transition, trigger } from '@angular/animations';
|
||||
import { AfterViewInit, Component, ElementRef, Input, OnDestroy, ViewChild } from '@angular/core';
|
||||
import { AfterViewInit, Component, ElementRef, Input, ViewChild } from '@angular/core';
|
||||
import { Track } from 'livekit-client';
|
||||
|
||||
/**
|
||||
|
@ -21,13 +21,12 @@ import { Track } from 'livekit-client';
|
|||
],
|
||||
standalone: false
|
||||
})
|
||||
export class MediaElementComponent implements AfterViewInit, OnDestroy {
|
||||
export class MediaElementComponent implements AfterViewInit {
|
||||
_track: Track;
|
||||
_videoElement: ElementRef;
|
||||
_audioElement: ElementRef;
|
||||
type: Track.Source = Track.Source.Camera;
|
||||
private _muted: boolean = false;
|
||||
private previousTrack: Track | null = null;
|
||||
|
||||
@Input() showAvatar: boolean;
|
||||
@Input() avatarColor: string;
|
||||
|
@ -38,25 +37,20 @@ export class MediaElementComponent implements AfterViewInit, OnDestroy {
|
|||
set videoElement(element: ElementRef) {
|
||||
this._videoElement = element;
|
||||
this.attachTracks();
|
||||
|
||||
}
|
||||
|
||||
@ViewChild('audioElement', { static: false })
|
||||
set audioElement(element: ElementRef) {
|
||||
this._audioElement = element;
|
||||
this.attachTracks();
|
||||
|
||||
}
|
||||
|
||||
@Input()
|
||||
set track(track: Track) {
|
||||
if (!track) return;
|
||||
|
||||
// Detach previous track if it's different
|
||||
if (this.previousTrack && this.previousTrack !== track) {
|
||||
this.detachPreviousTrack();
|
||||
}
|
||||
|
||||
this._track = track;
|
||||
this.previousTrack = track;
|
||||
this.attachTracks();
|
||||
}
|
||||
|
||||
|
@ -75,23 +69,6 @@ export class MediaElementComponent implements AfterViewInit, OnDestroy {
|
|||
});
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
this.detachPreviousTrack();
|
||||
}
|
||||
|
||||
private detachPreviousTrack() {
|
||||
if (this.previousTrack) {
|
||||
// Detach from video element
|
||||
if (this.isVideoTrack() && this._videoElement?.nativeElement) {
|
||||
this.previousTrack.detach(this._videoElement.nativeElement);
|
||||
}
|
||||
// Detach from audio element
|
||||
if (this.isAudioTrack() && this._audioElement?.nativeElement) {
|
||||
this.previousTrack.detach(this._audioElement.nativeElement);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private updateVideoStyles() {
|
||||
this.type = this._track.source;
|
||||
if (this.type === Track.Source.ScreenShare) {
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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';
|
||||
|
@ -54,21 +54,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 +80,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 +105,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 +118,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 +126,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();
|
||||
});
|
||||
|
|
|
@ -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,
|
||||
|
@ -76,7 +76,7 @@ export class BroadcastingActivityComponent implements OnInit {
|
|||
*/
|
||||
isPanelOpened: boolean = false;
|
||||
|
||||
private destroy$ = new Subject<void>();
|
||||
private broadcastingSub: Subscription;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
|
@ -99,8 +99,7 @@ export class BroadcastingActivityComponent implements OnInit {
|
|||
* @internal
|
||||
*/
|
||||
ngOnDestroy() {
|
||||
this.destroy$.next();
|
||||
this.destroy$.complete();
|
||||
if (this.broadcastingSub) this.broadcastingSub.unsubscribe();
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -148,7 +147,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;
|
||||
|
|
File diff suppressed because one or more lines are too long
|
@ -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 {
|
||||
|
|
|
@ -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.
|
||||
|
@ -32,7 +31,7 @@ import { OpenViduComponentsConfigService } from '../../../../services/config/dir
|
|||
// 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 +67,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 +99,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 +115,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 +125,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,97 +226,8 @@ 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;
|
||||
this.recordingList = recordingList;
|
||||
|
@ -395,24 +239,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();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,17 +1,10 @@
|
|||
<div class="panel-container" id="background-effects-container" [class.prejoin-mode]="mode === 'prejoin'">
|
||||
@if (mode === 'meeting') {
|
||||
<div class="panel-container" id="background-effects-container">
|
||||
<div class="panel-header-container">
|
||||
<h3 class="panel-title">{{ 'PANEL.BACKGROUND.TITLE' | translate }}</h3>
|
||||
|
||||
<button class="panel-close-button" mat-icon-button matTooltip="{{ 'PANEL.CLOSE' | translate }}" (click)="close()">
|
||||
<mat-icon>close</mat-icon>
|
||||
</button>
|
||||
</div>
|
||||
} @else {
|
||||
<button class="pansel-close-button" mat-icon-button matTooltip="{{ 'PANEL.CLOSE' | translate }}" (click)="close()">
|
||||
<mat-icon>arrow_back</mat-icon>
|
||||
</button>
|
||||
}
|
||||
|
||||
<div class="effects-container" fxFlex="100%" fxLayoutAlign="space-evenly none">
|
||||
<div>
|
||||
|
|
|
@ -1,11 +1,5 @@
|
|||
.prejoin-mode {
|
||||
margin: 0 10px 0px 10px;
|
||||
max-height: 100%;
|
||||
min-height: 100%;
|
||||
}
|
||||
.background-title {
|
||||
color: var(--ov-text-surface-color);
|
||||
margin: 10px 0;
|
||||
}
|
||||
.effects-container {
|
||||
display: block !important;
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
|
||||
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, OnInit } from '@angular/core';
|
||||
import { Subscription } from 'rxjs';
|
||||
import { BackgroundEffect, EffectType } from '../../../models/background-effect.model';
|
||||
import { PanelType } from '../../../models/panel.model';
|
||||
|
@ -16,9 +16,6 @@ import { VirtualBackgroundService } from '../../../services/virtual-background/v
|
|||
standalone: false
|
||||
})
|
||||
export class BackgroundEffectsPanelComponent implements OnInit {
|
||||
@Input() mode: 'prejoin' | 'meeting' = 'meeting';
|
||||
@Output() onClose = new EventEmitter<void>();
|
||||
|
||||
backgroundSelectedId: string;
|
||||
effectType = EffectType;
|
||||
backgroundImages: BackgroundEffect[] = [];
|
||||
|
@ -56,12 +53,8 @@ export class BackgroundEffectsPanelComponent implements OnInit {
|
|||
}
|
||||
|
||||
close() {
|
||||
if (this.mode === 'prejoin') {
|
||||
this.onClose.emit();
|
||||
} else {
|
||||
this.panelService.togglePanel(PanelType.BACKGROUND_EFFECTS);
|
||||
}
|
||||
}
|
||||
|
||||
async applyBackground(effect: BackgroundEffect) {
|
||||
await this.backgroundService.applyBackground(effect);
|
||||
|
|
|
@ -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';
|
||||
|
@ -34,7 +34,7 @@ export class ChatPanelComponent implements OnInit, AfterViewInit {
|
|||
*/
|
||||
messageList: ChatMessage[] = [];
|
||||
|
||||
private destroy$ = new Subject<void>();
|
||||
private chatMessageSubscription: Subscription;
|
||||
|
||||
/**
|
||||
* @ignore
|
||||
|
@ -66,8 +66,7 @@ export class ChatPanelComponent implements OnInit, AfterViewInit {
|
|||
* @ignore
|
||||
*/
|
||||
ngOnDestroy(): void {
|
||||
this.destroy$.next();
|
||||
this.destroy$.complete();
|
||||
if (this.chatMessageSubscription) this.chatMessageSubscription.unsubscribe();
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -110,7 +109,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();
|
||||
|
|
|
@ -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';
|
||||
|
||||
/**
|
||||
*
|
||||
|
@ -76,20 +75,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 +119,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 +131,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 +195,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 +207,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;
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,17 +1,16 @@
|
|||
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',
|
||||
|
@ -36,69 +35,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 +80,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;
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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}.
|
||||
|
@ -50,33 +48,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 +69,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 +109,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
|
||||
*/
|
||||
|
|
|
@ -54,7 +54,7 @@
|
|||
|
||||
::ng-deep .lang-selector .expand-more-icon,
|
||||
::ng-deep .lang-selector mat-icon {
|
||||
color: var(--ov-text-surface-color) !important;
|
||||
color: var(--ov-secondary-action-color) !important;
|
||||
}
|
||||
|
||||
::ng-deep .lang-selector div,
|
||||
|
|
|
@ -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';
|
||||
|
@ -27,8 +27,11 @@ export class SettingsPanelComponent implements OnInit {
|
|||
showCameraButton: boolean = true;
|
||||
showMicrophoneButton: boolean = true;
|
||||
showCaptions: boolean = true;
|
||||
panelSubscription: Subscription;
|
||||
isMobile: boolean = false;
|
||||
private destroy$ = new Subject<void>();
|
||||
private cameraButtonSub: Subscription;
|
||||
private microphoneButtonSub: Subscription;
|
||||
private captionsSubs: Subscription;
|
||||
constructor(
|
||||
private panelService: PanelService,
|
||||
private platformService: PlatformService,
|
||||
|
@ -41,8 +44,10 @@ export class SettingsPanelComponent implements OnInit {
|
|||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
this.destroy$.next();
|
||||
this.destroy$.complete();
|
||||
if (this.panelSubscription) this.panelSubscription.unsubscribe();
|
||||
if (this.cameraButtonSub) this.cameraButtonSub.unsubscribe();
|
||||
if (this.microphoneButtonSub) this.microphoneButtonSub.unsubscribe();
|
||||
if (this.captionsSubs) this.captionsSubs.unsubscribe();
|
||||
}
|
||||
|
||||
close() {
|
||||
|
@ -53,13 +58,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.cameraButtonSub = this.libService.cameraButton$.subscribe((value: boolean) => (this.showCameraButton = value));
|
||||
this.microphoneButtonSub = this.libService.microphoneButton$.subscribe((value: boolean) => (this.showMicrophoneButton = 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;
|
||||
}
|
||||
|
|
|
@ -1,123 +1,64 @@
|
|||
<div class="prejoin-container" id="prejoin-container" [class.name-error]="!!_error">
|
||||
<!-- Top Language Toolbar -->
|
||||
<div class="top-toolbar" *ngIf="!isMinimal">
|
||||
<ov-lang-selector [compact]="false" class="language-selector" (onLangChanged)="onLangChanged.emit($event)"> </ov-lang-selector>
|
||||
<div class="container" id="prejoin-container">
|
||||
|
||||
<div *ngIf="isLoading" id="loading-container">
|
||||
<mat-spinner [diameter]="50"></mat-spinner>
|
||||
<span>{{ 'PREJOIN.PREPARING' | translate }}</span>
|
||||
</div>
|
||||
|
||||
<!-- Loading State -->
|
||||
@if (isLoading) {
|
||||
<div class="loading-overlay">
|
||||
<div class="loading-content">
|
||||
<mat-spinner [diameter]="40"></mat-spinner>
|
||||
<span class="loading-text">{{ 'PREJOIN.PREPARING' | translate }}</span>
|
||||
</div>
|
||||
</div>
|
||||
} @else {
|
||||
<!-- Main Content -->
|
||||
<div class="prejoin-content">
|
||||
<!-- Main Card -->
|
||||
<div class="prejoin-main">
|
||||
<!-- Video Preview Section -->
|
||||
<div class="video-preview-section">
|
||||
<div class="video-preview-container" [@containerResize]="showBackgroundPanel ? 'compact' : 'normal'">
|
||||
<div class="video-frame">
|
||||
<div *ngIf="!isLoading" id="prejoin-card">
|
||||
<ov-lang-selector *ngIf="!isMinimal" [compact]="true" class="lang-btn" (onLangChanged)="onLangChanged.emit($event)">
|
||||
</ov-lang-selector>
|
||||
|
||||
<div>
|
||||
<div class="video-container">
|
||||
<div id="video-poster">
|
||||
<ov-media-element
|
||||
[track]="videoTrack"
|
||||
[showAvatar]="!isVideoEnabled"
|
||||
[showAvatar]="!videoTrack || videoTrack.isMuted"
|
||||
[avatarName]="participantName"
|
||||
[avatarColor]="'hsl(48, 100%, 50%)'"
|
||||
[isLocal]="true"
|
||||
class="video-element"
|
||||
[id]="videoTrack?.id || 'no-video'"
|
||||
>
|
||||
</ov-media-element>
|
||||
|
||||
<!-- Video Controls Overlay -->
|
||||
<div class="video-overlay">
|
||||
<div class="device-controls">
|
||||
<div class="control-group" *ngIf="showCameraButton">
|
||||
<ov-video-devices-select
|
||||
[compact]="true"
|
||||
(onVideoDeviceChanged)="videoDeviceChanged($event)"
|
||||
(onVideoEnabledChanged)="videoEnabledChanged($event)"
|
||||
(onVideoDevicesLoaded)="onVideoDevicesLoaded($event)"
|
||||
class="device-selector"
|
||||
>
|
||||
</ov-video-devices-select>
|
||||
></ov-media-element>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="control-group" *ngIf="showMicrophoneButton">
|
||||
<div class="media-controls-container">
|
||||
<!-- Camera -->
|
||||
<div class="video-controls-container" *ngIf="showCameraButton">
|
||||
<ov-video-devices-select
|
||||
(onVideoDeviceChanged)="onVideoDeviceChanged.emit($event)"
|
||||
(onVideoEnabledChanged)="videoEnabledChanged($event)"
|
||||
></ov-video-devices-select>
|
||||
</div>
|
||||
|
||||
<!-- Microphone -->
|
||||
<div class="audio-controls-container" *ngIf="showMicrophoneButton">
|
||||
<ov-audio-devices-select
|
||||
[compact]="true"
|
||||
(onAudioDeviceChanged)="onAudioDeviceChanged.emit($event)"
|
||||
(onAudioEnabledChanged)="audioEnabledChanged($event)"
|
||||
(onDeviceSelectorClicked)="onDeviceSelectorClicked()"
|
||||
class="device-selector"
|
||||
>
|
||||
</ov-audio-devices-select>
|
||||
</div>
|
||||
></ov-audio-devices-select>
|
||||
</div>
|
||||
|
||||
<!-- Virtual Background Button -->
|
||||
@if (backgroundEffectEnabled && hasVideoDevices) {
|
||||
<div class="background-control">
|
||||
<button
|
||||
mat-icon-button
|
||||
class="background-button"
|
||||
(click)="toggleBackgroundPanel()"
|
||||
[matTooltip]="'Virtual Backgrounds'"
|
||||
[disabled]="!isVideoEnabled"
|
||||
id="backgrounds-button"
|
||||
>
|
||||
<mat-icon class="material-symbols-outlined">background_replace</mat-icon>
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if (showBackgroundPanel) {
|
||||
<div class="vb-container" [@slideInOut]>
|
||||
<ov-background-effects-panel [mode]="'prejoin'" (onClose)="closeBackgroundPanel()"> </ov-background-effects-panel>
|
||||
</div>
|
||||
} @else {
|
||||
<!-- Configuration Section -->
|
||||
<div class="configuration-section">
|
||||
<!-- Participant Name Input -->
|
||||
<div class="participant-name-container input-section" *ngIf="showParticipantName">
|
||||
<div class="participant-name-container" *ngIf="showParticipantName">
|
||||
<ov-participant-name-input
|
||||
[isPrejoinPage]="true"
|
||||
[error]="!!_error"
|
||||
(onNameUpdated)="onParticipantNameChanged($event)"
|
||||
(onEnterPressed)="onEnterPressed()"
|
||||
class="name-input"
|
||||
>
|
||||
</ov-participant-name-input>
|
||||
></ov-participant-name-input>
|
||||
</div>
|
||||
|
||||
<!-- Error Message -->
|
||||
<div *ngIf="!!_error" class="error-message" id="token-error">
|
||||
<mat-icon class="error-icon">error_outline</mat-icon>
|
||||
<span class="error-text">{{ _error }}</span>
|
||||
<div *ngIf="!!_error" id="token-error">
|
||||
<span class="error">{{ _error }}</span>
|
||||
</div>
|
||||
|
||||
<!-- Join Button -->
|
||||
<div class="join-section">
|
||||
<button
|
||||
mat-flat-button
|
||||
(click)="join()"
|
||||
class="join-button"
|
||||
id="join-button"
|
||||
[disabled]="showParticipantName && !participantName"
|
||||
>
|
||||
<div class="join-btn-container">
|
||||
<button mat-flat-button (click)="joinSession()" id="join-button">
|
||||
{{ 'PREJOIN.JOIN' | translate }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
|
|
@ -1,417 +1,159 @@
|
|||
:host {
|
||||
display: block;
|
||||
width: 100%;
|
||||
.container {
|
||||
height: 100%;
|
||||
|
||||
.prejoin-container {
|
||||
min-height: 100vh;
|
||||
background: var(--ov-background-color);
|
||||
background-color: var(--ov-background-color);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 20px;
|
||||
box-sizing: border-box;
|
||||
position: relative;
|
||||
transition: all 0.3s ease;
|
||||
|
||||
&.name-error {
|
||||
.prejoin-main {
|
||||
min-height: fit-content;
|
||||
}
|
||||
}
|
||||
|
||||
.prejoin-content {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
|
||||
.prejoin-main {
|
||||
max-width: 480px;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes slideInFromRight {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateX(20px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateX(0);
|
||||
}
|
||||
}
|
||||
|
||||
// Top Language Toolbar
|
||||
.top-toolbar {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 1000;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
padding: 20px 24px;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
// Loading State
|
||||
.loading-overlay {
|
||||
#loading-container {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
top: 40%;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background-color: var(--ov-background-color, #f5f5f5);
|
||||
z-index: 1000;
|
||||
|
||||
.loading-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
|
||||
.loading-text {
|
||||
color: var(--ov-text-primary-color, #333);
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
text-align: center;
|
||||
color: var(--ov-text-primary-color);
|
||||
.mat-mdc-progress-spinner {
|
||||
--mdc-circular-progress-active-indicator-color: var(--ov-primary-action-color, #4285f4);
|
||||
}
|
||||
margin: auto;
|
||||
}
|
||||
}
|
||||
|
||||
// Main Content
|
||||
.prejoin-main {
|
||||
width: 100%;
|
||||
max-width: 520px;
|
||||
max-height: 544px;
|
||||
background: var(--ov-surface-color, #ffffff);
|
||||
border-radius: var(--ov-surface-radius);
|
||||
overflow: hidden;
|
||||
#prejoin-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.12);
|
||||
}
|
||||
|
||||
// Video Preview Section
|
||||
.video-preview-section {
|
||||
padding: 0;
|
||||
.video-preview-container {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
aspect-ratio: 4/3;
|
||||
border-radius: var(--ov-surface-radius) var(--ov-surface-radius) 0 0;
|
||||
overflow: hidden;
|
||||
background: #000;
|
||||
.video-frame {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: relative;
|
||||
|
||||
::ng-deep .video-element {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
display: block;
|
||||
|
||||
video {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
border-radius: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.video-overlay {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
padding: 16px;
|
||||
z-index: 9999;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: flex-end;
|
||||
|
||||
.device-controls {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.background-control {
|
||||
position: absolute;
|
||||
bottom: 16px;
|
||||
left: 16px;
|
||||
|
||||
.background-button {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
background: rgba(255, 255, 255, 0.7);
|
||||
backdrop-filter: blur(20px);
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
border-radius: 16px;
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
color: #333333;
|
||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1);
|
||||
transform: translateZ(0);
|
||||
|
||||
&:active {
|
||||
transform: translateY(-1px);
|
||||
transition: all 0.15s ease;
|
||||
}
|
||||
|
||||
&.mat-mdc-button-disabled {
|
||||
background: rgba(255, 255, 255, 0.137);
|
||||
color: rgba(233, 233, 233, 0.5);
|
||||
cursor: not-allowed;
|
||||
|
||||
&:hover {
|
||||
transform: none;
|
||||
}
|
||||
}
|
||||
|
||||
mat-icon {
|
||||
font-size: 22px;
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
opacity: 0.9;
|
||||
transition: opacity 0.2s ease;
|
||||
}
|
||||
|
||||
&:hover mat-icon {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.vb-container {
|
||||
height: fit-content;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
// Configuration Section
|
||||
.configuration-section {
|
||||
padding: 24px 24px 24px; // Added top padding since video has no padding
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
|
||||
.input-section {
|
||||
::ng-deep .name-input {
|
||||
.mat-mdc-form-field {
|
||||
width: 100%;
|
||||
|
||||
.mat-mdc-text-field-wrapper {
|
||||
border-radius: var(--ov-surface-radius);
|
||||
background-color: var(--ov-input-background, #f8f9fa);
|
||||
border: 1px solid var(--ov-border-color, #e0e0e0);
|
||||
transition: all 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
border-color: var(--ov-primary-action-color, #4285f4);
|
||||
}
|
||||
|
||||
&.mdc-text-field--focused {
|
||||
border-color: var(--ov-primary-action-color, #4285f4);
|
||||
box-shadow: 0 0 0 3px rgba(66, 133, 244, 0.1);
|
||||
}
|
||||
}
|
||||
|
||||
.mat-mdc-form-field-subscript-wrapper {
|
||||
display: none;
|
||||
}
|
||||
|
||||
input {
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
color: var(--ov-text-primary-color, #333);
|
||||
padding: 16px;
|
||||
|
||||
&::placeholder {
|
||||
color: var(--ov-text-secondary-color, #666);
|
||||
font-weight: 400;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.error-message {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 12px 16px;
|
||||
background-color: rgba(244, 67, 54, 0.08);
|
||||
border: 1px solid rgba(244, 67, 54, 0.2);
|
||||
border-radius: var(--ov-surface-radius);
|
||||
color: var(--ov-error-color, #d32f2f);
|
||||
|
||||
.error-icon {
|
||||
font-size: 18px;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
}
|
||||
|
||||
.error-text {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
|
||||
.join-section {
|
||||
.join-button {
|
||||
width: 100%;
|
||||
height: 56px;
|
||||
background: var(--ov-primary-action-color, #4285f4);
|
||||
color: white;
|
||||
border-radius: var(--ov-surface-radius);
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.5px;
|
||||
transition: all 0.2s ease;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
box-shadow: 0 2px 8px rgba(66, 133, 244, 0.2);
|
||||
|
||||
&:hover:not([disabled]) {
|
||||
background: var(--ov-primary-action-hover, #3367d6);
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 4px 16px rgba(66, 133, 244, 0.3);
|
||||
margin: auto;
|
||||
border-radius: var(--ov-surface-radius);
|
||||
width: 90%;
|
||||
max-width: 370px;
|
||||
// max-height: 650px;
|
||||
height: min-content;
|
||||
padding: 55px 30px;
|
||||
background-color: var(--ov-surface-color);
|
||||
box-shadow: 6px 4px 20px rgba(0, 0, 0, 0.3);
|
||||
position: relative;
|
||||
}
|
||||
|
||||
&:active:not([disabled]) {
|
||||
transform: translateY(0);
|
||||
::ng-deep .lang-btn {
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
right: 10px;
|
||||
height: 25px !important;
|
||||
font-size: 14px !important;
|
||||
}
|
||||
|
||||
&[disabled] {
|
||||
background: var(--ov-disabled-color, #ccc);
|
||||
color: var(--ov-disabled-text-color, #999);
|
||||
cursor: not-allowed;
|
||||
box-shadow: none;
|
||||
::ng-deep .lang-btn mat-icon {
|
||||
color: var(--ov-text-surface-color) !important;
|
||||
}
|
||||
|
||||
.join-icon {
|
||||
font-size: 20px;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Responsive Design
|
||||
@media (max-width: 640px) {
|
||||
.prejoin-container {
|
||||
padding: 16px;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.prejoin-main {
|
||||
.video-container {
|
||||
margin: auto;
|
||||
height: 35vh;
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
#video-poster {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
position: relative;
|
||||
border-radius: var(--ov-surface-radius);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.video-preview-section {
|
||||
padding: 0px 0px 12px;
|
||||
.media-controls-container {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
margin-top: 15px;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.video-preview-container {
|
||||
aspect-ratio: 4/3;
|
||||
.participant-name-container {
|
||||
display: block !important;
|
||||
width: 100%;
|
||||
margin: 10px 0;
|
||||
}
|
||||
|
||||
.video-controls-container,
|
||||
.audio-controls-container {
|
||||
width: calc(50% - 10px);
|
||||
margin: 5px 0;
|
||||
}
|
||||
|
||||
.join-btn-container {
|
||||
width: 100%;
|
||||
margin-top: 15px;
|
||||
}
|
||||
|
||||
#join-button {
|
||||
background-color: var(--ov-primary-action-color);
|
||||
color: var(--ov-secondary-action-color);
|
||||
font-weight: bold;
|
||||
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;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
#prejoin-card {
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.video-container {
|
||||
height: 40vh;
|
||||
}
|
||||
|
||||
.media-controls-container {
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.video-controls-container,
|
||||
.audio-controls-container {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.configuration-section {
|
||||
padding: 0 20px 20px;
|
||||
gap: 16px;
|
||||
@media (max-width: 800px) and (orientation: landscape) {
|
||||
.media-controls-container {
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.top-toolbar {
|
||||
padding: 16px 20px;
|
||||
.video-controls-container,
|
||||
.audio-controls-container {
|
||||
width: 48%;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.prejoin-container {
|
||||
padding: 12px;
|
||||
@media (max-height: 630px) {
|
||||
.video-container {
|
||||
height: 30vh;
|
||||
}
|
||||
|
||||
.configuration-section {
|
||||
padding: 0 16px 16px;
|
||||
.media-controls-container {
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.video-overlay .device-controls {
|
||||
gap: 8px;
|
||||
|
||||
::ng-deep .device-selector .mat-mdc-icon-button {
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
|
||||
mat-icon {
|
||||
font-size: 18px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.top-toolbar {
|
||||
padding: 12px 16px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-height: 640px) {
|
||||
.prejoin-container {
|
||||
align-items: flex-start;
|
||||
padding-top: 60px; // Add space for top toolbar
|
||||
}
|
||||
|
||||
.video-preview-section .video-preview-container {
|
||||
aspect-ratio: 4/3; // Keep the taller aspect ratio even on small screens
|
||||
}
|
||||
}
|
||||
|
||||
// Dark theme support
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.prejoin-container {
|
||||
background: linear-gradient(135deg, #1a1a1a 0%, #2d2d2d 100%);
|
||||
}
|
||||
|
||||
.prejoin-main {
|
||||
background: #2d2d2d;
|
||||
box-shadow:
|
||||
0 8px 32px rgba(0, 0, 0, 0.3),
|
||||
0 2px 8px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.configuration-section .input-section ::ng-deep .name-input .participant-name-input-container .input-wrapper {
|
||||
background-color: #3a3a3a;
|
||||
border-color: #555;
|
||||
}
|
||||
}
|
||||
|
||||
// Animation keyframes
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(20px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
.prejoin-main {
|
||||
animation: fadeIn 0.3s ease-out;
|
||||
transform: translateZ(0);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,17 +9,17 @@ import {
|
|||
OnInit,
|
||||
Output
|
||||
} from '@angular/core';
|
||||
import { animate, state, style, transition, trigger } from '@angular/animations';
|
||||
import { filter, Subject, take, takeUntil } from 'rxjs';
|
||||
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';
|
||||
import { LoggerService } from '../../services/logger/logger.service';
|
||||
import { OpenViduService } from '../../services/openvidu/openvidu.service';
|
||||
import { TranslateService } from '../../services/translate/translate.service';
|
||||
import { LocalTrack, Track } from 'livekit-client';
|
||||
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
|
||||
|
@ -29,38 +29,7 @@ import { LangOption } from '../../models/lang.model';
|
|||
templateUrl: './pre-join.component.html',
|
||||
styleUrls: ['./pre-join.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
standalone: false,
|
||||
animations: [
|
||||
trigger('containerResize', [
|
||||
state(
|
||||
'normal',
|
||||
style({
|
||||
height: '*'
|
||||
})
|
||||
),
|
||||
state(
|
||||
'compact',
|
||||
style({
|
||||
height: '300px'
|
||||
})
|
||||
),
|
||||
transition('normal => compact', [animate('250ms cubic-bezier(0.25, 0.8, 0.25, 1)')]),
|
||||
transition('compact => normal', [animate('350ms cubic-bezier(0.25, 0.8, 0.25, 1)')])
|
||||
]),
|
||||
trigger('slideInOut', [
|
||||
transition(':enter', [
|
||||
style({
|
||||
opacity: 0
|
||||
}),
|
||||
animate(
|
||||
'300ms cubic-bezier(0.34, 1.56, 0.64, 1)',
|
||||
style({
|
||||
opacity: 1
|
||||
})
|
||||
)
|
||||
])
|
||||
])
|
||||
]
|
||||
standalone: false
|
||||
})
|
||||
export class PreJoinComponent implements OnInit, OnDestroy {
|
||||
@Input() set error(error: { name: string; message: string } | undefined) {
|
||||
|
@ -74,6 +43,7 @@ export class PreJoinComponent implements OnInit, OnDestroy {
|
|||
@Output() onReadyToJoin = new EventEmitter<any>();
|
||||
|
||||
_error: string | undefined;
|
||||
|
||||
windowSize: number;
|
||||
isLoading = true;
|
||||
participantName: string | undefined = '';
|
||||
|
@ -87,17 +57,15 @@ export class PreJoinComponent implements OnInit, OnDestroy {
|
|||
showLogo: boolean = true;
|
||||
showParticipantName: boolean = true;
|
||||
|
||||
// Future feature preparation
|
||||
backgroundEffectEnabled: boolean = true; // Enable virtual backgrounds by default
|
||||
showBackgroundPanel: boolean = false;
|
||||
|
||||
videoTrack: LocalTrack | undefined;
|
||||
audioTrack: LocalTrack | undefined;
|
||||
isVideoEnabled: boolean = false;
|
||||
hasVideoDevices: boolean = true;
|
||||
private tracks: LocalTrack[];
|
||||
private log: ILogger;
|
||||
private destroy$ = new Subject<void>();
|
||||
private cameraButtonSub: Subscription;
|
||||
private microphoneButtonSub: Subscription;
|
||||
private minimalSub: Subscription;
|
||||
private displayLogoSub: Subscription;
|
||||
private displayParticipantNameSub: Subscription;
|
||||
private shouldRemoveTracksWhenComponentIsDestroyed: boolean = true;
|
||||
|
||||
@HostListener('window:resize')
|
||||
|
@ -110,6 +78,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
|
||||
) {
|
||||
|
@ -118,7 +87,7 @@ export class PreJoinComponent implements OnInit, OnDestroy {
|
|||
|
||||
async ngOnInit() {
|
||||
this.subscribeToPrejoinDirectives();
|
||||
await this.initializeDevicesWithRetry();
|
||||
await this.initializeDevices();
|
||||
this.windowSize = window.innerWidth;
|
||||
this.isLoading = false;
|
||||
this.changeDetector.markForCheck();
|
||||
|
@ -130,136 +99,99 @@ export class PreJoinComponent implements OnInit, OnDestroy {
|
|||
// }
|
||||
|
||||
async ngOnDestroy() {
|
||||
this.destroy$.next();
|
||||
this.destroy$.complete();
|
||||
this.cdkSrv.setSelector('body');
|
||||
if (this.minimalSub) this.minimalSub.unsubscribe();
|
||||
if (this.cameraButtonSub) this.cameraButtonSub.unsubscribe();
|
||||
if (this.microphoneButtonSub) this.microphoneButtonSub.unsubscribe();
|
||||
if (this.displayLogoSub) this.displayLogoSub.unsubscribe();
|
||||
if (this.displayParticipantNameSub) this.displayParticipantNameSub.unsubscribe();
|
||||
|
||||
if (this.shouldRemoveTracksWhenComponentIsDestroyed) {
|
||||
this.tracks?.forEach((track) => {
|
||||
this.tracks.forEach((track) => {
|
||||
track.stop();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private async initializeDevices() {
|
||||
try {
|
||||
this.tracks = await this.openviduService.createLocalTracks();
|
||||
this.openviduService.setLocalTracks(this.tracks);
|
||||
this.videoTrack = this.tracks.find((track) => track.kind === 'video');
|
||||
this.audioTrack = this.tracks.find((track) => track.kind === 'audio');
|
||||
} catch (error) {
|
||||
this.log.e('Error creating local tracks:', error);
|
||||
}
|
||||
}
|
||||
|
||||
onDeviceSelectorClicked() {
|
||||
// Some devices as iPhone do not show the menu panels correctly
|
||||
// Updating the container where the panel is added fix the problem.
|
||||
this.cdkSrv.setSelector('#prejoin-container');
|
||||
}
|
||||
|
||||
join() {
|
||||
if (this.showParticipantName && !this.participantName?.trim()) {
|
||||
joinSession() {
|
||||
if (this.showParticipantName && !this.participantName) {
|
||||
this._error = this.translateService.translate('PREJOIN.NICKNAME_REQUIRED');
|
||||
return;
|
||||
}
|
||||
|
||||
// Clear any previous errors
|
||||
this._error = undefined;
|
||||
|
||||
// Mark tracks as permanent for avoiding to be removed in ngOnDestroy
|
||||
this.shouldRemoveTracksWhenComponentIsDestroyed = false;
|
||||
|
||||
// Assign participant name to the observable if it is defined
|
||||
if (this.participantName?.trim()) {
|
||||
this.libService.updateGeneralConfig({ participantName: this.participantName.trim() });
|
||||
if(this.participantName) this.libService.setParticipantName(this.participantName);
|
||||
|
||||
this.libService.participantName$
|
||||
.pipe(
|
||||
filter((name) => name === this.participantName?.trim()),
|
||||
take(1)
|
||||
)
|
||||
.subscribe(() => this.onReadyToJoin.emit());
|
||||
} else {
|
||||
// No participant name to set, emit immediately
|
||||
this.onReadyToJoin.emit();
|
||||
}
|
||||
}
|
||||
|
||||
onParticipantNameChanged(name: string) {
|
||||
this.participantName = name?.trim() || '';
|
||||
// Clear error when user starts typing
|
||||
if (this._error && this.participantName) {
|
||||
this._error = undefined;
|
||||
}
|
||||
if (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.libService.cameraButton$.pipe(takeUntil(this.destroy$)).subscribe((value: boolean) => {
|
||||
this.cameraButtonSub = this.libService.cameraButton$.subscribe((value: boolean) => {
|
||||
this.showCameraButton = value;
|
||||
this.changeDetector.markForCheck();
|
||||
});
|
||||
|
||||
this.libService.microphoneButton$.pipe(takeUntil(this.destroy$)).subscribe((value: boolean) => {
|
||||
this.microphoneButtonSub = this.libService.microphoneButton$.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.libService.participantName$.pipe(takeUntil(this.destroy$)).subscribe((value: string) => {
|
||||
this.libService.participantName$.subscribe((value: string) => {
|
||||
if (value) {
|
||||
this.participantName = value;
|
||||
this.changeDetector.markForCheck();
|
||||
}
|
||||
});
|
||||
|
||||
this.libService.prejoinDisplayParticipantName$.pipe(takeUntil(this.destroy$)).subscribe((value: boolean) => {
|
||||
this.displayParticipantNameSub = this.libService.prejoinDisplayParticipantName$.subscribe((value: boolean) => {
|
||||
this.showParticipantName = value;
|
||||
this.changeDetector.markForCheck();
|
||||
});
|
||||
}
|
||||
|
||||
async videoEnabledChanged(enabled: boolean) {
|
||||
this.isVideoEnabled = enabled;
|
||||
if (!enabled) {
|
||||
this.closeBackgroundPanel();
|
||||
} else if (!this.videoTrack) {
|
||||
if (enabled && !this.videoTrack) {
|
||||
const newVideoTrack = await this.openviduService.createLocalTracks(true, false);
|
||||
this.videoTrack = newVideoTrack[0];
|
||||
this.tracks.push(this.videoTrack);
|
||||
this.openviduService.setLocalTracks(this.tracks);
|
||||
}
|
||||
|
||||
this.onVideoEnabledChanged.emit(enabled);
|
||||
}
|
||||
|
||||
async videoDeviceChanged(device: CustomDevice) {
|
||||
try {
|
||||
this.log.d('Video device changed to:', device);
|
||||
|
||||
// Get the updated tracks from the service
|
||||
const updatedTracks = this.openviduService.getLocalTracks();
|
||||
|
||||
// Find the new video track
|
||||
const newVideoTrack = updatedTracks.find((track) => track.kind === 'video');
|
||||
|
||||
// if (newVideoTrack && newVideoTrack !== this.videoTrack) {
|
||||
this.tracks = updatedTracks;
|
||||
this.videoTrack = newVideoTrack;
|
||||
|
||||
this.onVideoDeviceChanged.emit(device);
|
||||
} catch (error) {
|
||||
this.log.e('Error handling video device change:', error);
|
||||
this.handleError(error);
|
||||
}
|
||||
}
|
||||
|
||||
onVideoDevicesLoaded(devices: CustomDevice[]) {
|
||||
this.hasVideoDevices = devices.length > 0;
|
||||
}
|
||||
|
||||
async audioEnabledChanged(enabled: boolean) {
|
||||
if (enabled && !this.audioTrack) {
|
||||
const newAudioTrack = await this.openviduService.createLocalTracks(false, true);
|
||||
|
@ -269,68 +201,4 @@ export class PreJoinComponent implements OnInit, OnDestroy {
|
|||
}
|
||||
this.onAudioEnabledChanged.emit(enabled);
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle virtual background panel visibility with smooth animation
|
||||
*/
|
||||
toggleBackgroundPanel() {
|
||||
// Add a small delay to ensure smooth transition
|
||||
if (!this.showBackgroundPanel) {
|
||||
// Opening panel
|
||||
this.showBackgroundPanel = true;
|
||||
this.changeDetector.markForCheck();
|
||||
} else {
|
||||
// Closing panel - add slight delay for smooth animation
|
||||
setTimeout(() => {
|
||||
this.showBackgroundPanel = false;
|
||||
this.changeDetector.markForCheck();
|
||||
}, 50);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Close virtual background panel with smooth animation
|
||||
*/
|
||||
closeBackgroundPanel() {
|
||||
// Add animation delay for smooth closing
|
||||
setTimeout(() => {
|
||||
this.showBackgroundPanel = false;
|
||||
this.changeDetector.markForCheck();
|
||||
}, 100);
|
||||
}
|
||||
|
||||
/**
|
||||
* Enhanced error handling with better UX
|
||||
*/
|
||||
private handleError(error: any) {
|
||||
this.log.e('PreJoin component error:', error);
|
||||
this._error = error.message || 'An unexpected error occurred';
|
||||
this.changeDetector.markForCheck();
|
||||
}
|
||||
|
||||
/**
|
||||
* Improved device initialization with error handling
|
||||
*/
|
||||
private async initializeDevicesWithRetry(maxRetries: number = 3): Promise<void> {
|
||||
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
||||
try {
|
||||
this.tracks = await this.openviduService.createLocalTracks();
|
||||
this.openviduService.setLocalTracks(this.tracks);
|
||||
this.videoTrack = this.tracks.find((track) => track.kind === 'video');
|
||||
this.audioTrack = this.tracks.find((track) => track.kind === 'audio');
|
||||
this.isVideoEnabled = this.openviduService.isVideoTrackEnabled();
|
||||
|
||||
return; // Success, exit retry loop
|
||||
} catch (error) {
|
||||
this.log.w(`Device initialization attempt ${attempt} failed:`, error);
|
||||
|
||||
if (attempt === maxRetries) {
|
||||
this.handleError(error);
|
||||
} else {
|
||||
// Wait before retrying
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000 * attempt));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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';
|
||||
|
@ -48,7 +48,6 @@ import {
|
|||
} 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';
|
||||
|
||||
/**
|
||||
* @internal
|
||||
|
@ -83,7 +82,7 @@ export class SessionComponent implements OnInit, OnDestroy {
|
|||
|
||||
/**
|
||||
* Provides event notifications that fire when participant is disconnected from Room.
|
||||
* @deprecated Use {@link SessionComponent.onParticipantLeft} instead.
|
||||
* @deprecated Use {@link onParticipantLeft} instead.
|
||||
*/
|
||||
@Output() onRoomDisconnected: EventEmitter<void> = new EventEmitter<void>();
|
||||
|
||||
|
@ -104,16 +103,12 @@ 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;
|
||||
|
||||
constructor(
|
||||
|
@ -130,11 +125,9 @@ 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();
|
||||
}
|
||||
|
||||
@HostListener('window:beforeunload')
|
||||
|
@ -198,29 +191,7 @@ export class SessionComponent implements OnInit, OnDestroy {
|
|||
|
||||
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.subscribeToCaptionLanguage();
|
||||
this.subcribeToActiveSpeakersChanged();
|
||||
|
@ -233,15 +204,14 @@ 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
|
||||
|
@ -260,18 +230,6 @@ 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);
|
||||
|
@ -279,8 +237,8 @@ export class SessionComponent implements OnInit, OnDestroy {
|
|||
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();
|
||||
}
|
||||
|
||||
|
@ -290,8 +248,7 @@ export class SessionComponent implements OnInit, OnDestroy {
|
|||
await this.openviduService.disconnectRoom(() => {
|
||||
this.onParticipantLeft.emit({
|
||||
roomName: this.openviduService.getRoomName(),
|
||||
participantName: this.participantService.getLocalParticipant()?.name || '',
|
||||
identity: this.participantService.getLocalParticipant()?.identity || '',
|
||||
participantName: this.participantService.getLocalParticipant()?.identity || '',
|
||||
reason
|
||||
});
|
||||
}, false);
|
||||
|
@ -311,7 +268,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 +287,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 +405,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,9 +457,7 @@ 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);
|
||||
|
@ -535,11 +490,9 @@ 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 || '',
|
||||
participantName: this.participantService.getLocalParticipant()?.identity || '',
|
||||
reason: ParticipantLeftReason.NETWORK_DISCONNECT
|
||||
};
|
||||
const messageErrorKey = 'ERRORS.DISCONNECT';
|
||||
|
@ -579,7 +532,7 @@ export class SessionComponent implements OnInit, OnDestroy {
|
|||
this.log.d('Participant disconnected', participantLeftEvent);
|
||||
this.onParticipantLeft.emit(participantLeftEvent);
|
||||
this.onRoomDisconnected.emit();
|
||||
if (this.libService.getShowDisconnectionDialog() && descriptionErrorKey) {
|
||||
if (descriptionErrorKey) {
|
||||
this.actionService.openDialog(
|
||||
this.translateService.translate(messageErrorKey),
|
||||
this.translateService.translate(descriptionErrorKey)
|
||||
|
@ -588,17 +541,6 @@ export class SessionComponent implements OnInit, OnDestroy {
|
|||
});
|
||||
}
|
||||
|
||||
private subscribeToVirtualBackground() {
|
||||
this.libService.backgroundEffectsButton$.subscribe(async (enable) => {
|
||||
if (!enable && this.backgroundService.isBackgroundApplied()) {
|
||||
await this.backgroundService.removeBackground();
|
||||
if (this.panelService.isBackgroundEffectsPanelOpened()) {
|
||||
this.panelService.closePanel();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private startUpdateLayoutInterval() {
|
||||
this.updateLayoutInterval = setInterval(() => {
|
||||
this.layoutService.update();
|
||||
|
|
|
@ -1,99 +1,55 @@
|
|||
<div class="audio-device-selector" [class.compact]="compact">
|
||||
<!-- Unified Device Button (Compact Mode) -->
|
||||
@if (compact) {
|
||||
@if (hasAudioDevices) {
|
||||
<div class="unified-device-button">
|
||||
<!-- Main toggle button -->
|
||||
<div class="device-container-element" [class.mute-btn]="!isMicrophoneEnabled">
|
||||
<!-- <button mat-stroked-button [matMenuTriggerFor]="menu" #menuTrigger="matMenuTrigger" id="audio-devices-menu">
|
||||
<mat-icon class="audio-icon">mic</mat-icon>
|
||||
<span class="device-label"> {{ microphoneSelected.label }} </span>
|
||||
<mat-icon iconPositionEnd class="chevron-icon">
|
||||
{{ menuTrigger.menuOpen ? 'expand_less' : 'expand_more' }}
|
||||
</mat-icon>
|
||||
</button>
|
||||
<mat-menu #menu="matMenu">
|
||||
<button mat-menu-item *ngFor="let microphone of microphones">{{ microphone.label }}</button>
|
||||
</mat-menu> -->
|
||||
<mat-form-field id="audio-devices-form" *ngIf="microphones.length > 0">
|
||||
<mat-select
|
||||
[disabled]="!hasAudioDevices"
|
||||
[compareWith]="compareObjectDevices"
|
||||
[value]="microphoneSelected"
|
||||
(selectionChange)="onMicrophoneSelected($event)"
|
||||
>
|
||||
<mat-select-trigger>
|
||||
<button
|
||||
mat-flat-button
|
||||
class="toggle-section"
|
||||
id="microphone-button"
|
||||
[disableRipple]="true"
|
||||
[disabled]="!hasAudioDevices || microphoneStatusChanging"
|
||||
[class.device-enabled]="isMicrophoneEnabled"
|
||||
[class.device-disabled]="!isMicrophoneEnabled"
|
||||
[class.mute-btn]="!isMicrophoneEnabled"
|
||||
(click)="toggleMic($event)"
|
||||
[matTooltip]="isMicrophoneEnabled ? ('TOOLBAR.MUTE_AUDIO' | translate) : ('TOOLBAR.UNMUTE_AUDIO' | translate)"
|
||||
[matTooltipDisabled]="!hasAudioDevices"
|
||||
id="microphone-button"
|
||||
>
|
||||
<mat-icon [id]="isMicrophoneEnabled ? 'mic' : 'mic_off'">{{ isMicrophoneEnabled ? 'mic' : 'mic_off' }}</mat-icon>
|
||||
<mat-icon *ngIf="isMicrophoneEnabled" id="mic"> mic </mat-icon>
|
||||
<mat-icon *ngIf="!isMicrophoneEnabled" id="mic_off"> mic_off </mat-icon>
|
||||
</button>
|
||||
|
||||
<!-- Dropdown section -->
|
||||
@if (isMicrophoneEnabled) {
|
||||
<button
|
||||
mat-flat-button
|
||||
id="audio-dropdown"
|
||||
class="dropdown-section"
|
||||
[matMenuTriggerFor]="microphoneMenu"
|
||||
[disabled]="microphoneStatusChanging"
|
||||
>
|
||||
<mat-icon>expand_more</mat-icon>
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
} @else {
|
||||
<!-- No Microphone Available -->
|
||||
<div id="no-audio-device-message" class="no-device-message">
|
||||
<mat-icon class="warning-icon">warning</mat-icon>
|
||||
<span>{{ 'PREJOIN.NO_AUDIO_DEVICE' | translate }}</span>
|
||||
</div>
|
||||
}
|
||||
} @else {
|
||||
<!-- Normal Mode - Input Style Selector -->
|
||||
<div class="normal-device-selector">
|
||||
<!-- Input-style Device Selector -->
|
||||
<div class="device-input-selector" [class.disabled]="!hasAudioDevices || !isMicrophoneEnabled">
|
||||
<!-- When microphone is enabled -->
|
||||
@if (isMicrophoneEnabled) {
|
||||
<div class="device-input-selector">
|
||||
<button
|
||||
mat-flat-button
|
||||
id="audio-dropdown"
|
||||
class="selector-button"
|
||||
[disabled]="microphoneStatusChanging || microphones.length <= 1"
|
||||
[matMenuTriggerFor]="microphoneMenu"
|
||||
[attr.aria-expanded]="false"
|
||||
>
|
||||
<mat-icon class="device-icon">mic</mat-icon>
|
||||
<span class="selected-device-name">{{ microphoneSelected?.label || 'No microphone selected' }}</span>
|
||||
<mat-icon class="dropdown-icon" *ngIf="microphones.length > 1">expand_more</mat-icon>
|
||||
</button>
|
||||
</div>
|
||||
} @else {
|
||||
@if (hasAudioDevices) {
|
||||
<!-- When microphone is disabled -->
|
||||
<div class="device-input-selector disabled">
|
||||
<div class="selector-button disabled">
|
||||
<mat-icon class="device-icon">mic_off</mat-icon>
|
||||
<span class="selected-device-name">
|
||||
{{ !hasAudioDevices ? ('PREJOIN.NO_AUDIO_DEVICE' | translate) : 'Microphone disabled' }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
} @else {
|
||||
<!-- No Microphone Available -->
|
||||
<div id="no-audio-device-message" class="no-device-message">
|
||||
<mat-icon class="warning-icon">warning</mat-icon>
|
||||
<span>{{ 'PREJOIN.NO_AUDIO_DEVICE' | translate }}</span>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
<!-- Device Selection Menu (Shared) -->
|
||||
<mat-menu #microphoneMenu="matMenu" class="device-menu">
|
||||
@for (microphone of microphones; track microphone.device) {
|
||||
<button
|
||||
mat-menu-item
|
||||
<span class="selected-text" *ngIf="!isMicrophoneEnabled">{{ 'PANEL.SETTINGS.DISABLED_AUDIO' | translate }}</span>
|
||||
<span class="selected-text" *ngIf="isMicrophoneEnabled"> {{ microphoneSelected.label }} </span>
|
||||
</mat-select-trigger>
|
||||
<mat-option
|
||||
*ngFor="let microphone of microphones"
|
||||
[disabled]="!isMicrophoneEnabled"
|
||||
[value]="microphone"
|
||||
id="option-{{ microphone.label }}"
|
||||
(click)="onMicrophoneSelected({ value: microphone })"
|
||||
[class.selected]="microphone.device === microphoneSelected.device"
|
||||
>
|
||||
<mat-icon *ngIf="microphone.device === microphoneSelected.device">check</mat-icon>
|
||||
<span>{{ microphone.label }}</span>
|
||||
{{ microphone.label }}
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
|
||||
<div id="audio-devices-form" *ngIf="microphones.length === 0">
|
||||
<div id="mat-select-trigger">
|
||||
<button mat-icon-button id="microphone-button" class="mute-btn" [disabled]="true">
|
||||
<mat-icon id="mic_off"> mic_off </mat-icon>
|
||||
</button>
|
||||
}
|
||||
</mat-menu>
|
||||
<span id="audio-devices-not-found"> {{ 'PREJOIN.NO_AUDIO_DEVICE' | translate }} </span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -1,29 +1,103 @@
|
|||
@use '../device-selector-shared' as shared;
|
||||
$ov-selection-color-btn: #afafaf;
|
||||
$ov-selection-color: #cccccc;
|
||||
|
||||
:host {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
.device-container-element {
|
||||
border-radius: var(--ov-surface-radius);
|
||||
border: 1px solid $ov-selection-color-btn;
|
||||
}
|
||||
.device-container-element.mute-btn {
|
||||
border: 1px solid var(--ov-error-color);
|
||||
}
|
||||
#audio-devices-form {
|
||||
width: 100%;
|
||||
height: 50px;
|
||||
}
|
||||
|
||||
.audio-device-selector {
|
||||
@include shared.device-selector-base();
|
||||
#audio-devices-not-found {
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
// Audio-specific overrides for normal mode
|
||||
&:not(.compact) {
|
||||
.normal-device-selector {
|
||||
.device-input-selector {
|
||||
&:not(.disabled) {
|
||||
.selector-button {
|
||||
// Audio-specific hover effect (simpler than video)
|
||||
&:hover:not([disabled]) {
|
||||
border-color: var(--ov-primary-action-color, #4285f4);
|
||||
#microphone-button {
|
||||
color:#000000
|
||||
}
|
||||
|
||||
::ng-deep .mat-mdc-text-field-wrapper,
|
||||
::ng-deep .mat-mdc-form-field-flex,
|
||||
::ng-deep .mat-mdc-select-trigger {
|
||||
height: 50px !important;
|
||||
}
|
||||
|
||||
::ng-deep .mat-mdc-form-field-subscript-wrapper {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
::ng-deep .mat-mdc-text-field-wrapper {
|
||||
padding-left: 0px;
|
||||
padding-right: 10px;
|
||||
background-color: $ov-selection-color !important;
|
||||
border-radius: var(--ov-surface-radius);
|
||||
}
|
||||
::ng-deep .mdc-button--unelevated {
|
||||
border-top-left-radius: var(--ov-surface-radius);
|
||||
border-bottom-left-radius: var(--ov-surface-radius);
|
||||
border-bottom-right-radius: 0px !important;
|
||||
border-top-right-radius: 0px !important;
|
||||
background-color: $ov-selection-color-btn !important;
|
||||
width: 48px !important;
|
||||
min-width: 48px !important;
|
||||
padding: 0;
|
||||
height: 50px;
|
||||
}
|
||||
|
||||
::ng-deep .mat-mdc-unelevated-button > .mat-icon {
|
||||
height: 24px;
|
||||
width: 24px;
|
||||
font-size: 24px !important;
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
::ng-deep .mat-mdc-form-field-infix {
|
||||
padding: 0px !important;
|
||||
min-height: 100%;
|
||||
}
|
||||
|
||||
.selected-text {
|
||||
padding-left: 5px;
|
||||
}
|
||||
|
||||
.mat-icon {
|
||||
vertical-align: middle;
|
||||
display: inline-flex;
|
||||
}
|
||||
|
||||
::ng-deep .mdc-text-field--filled:not(.mdc-text-field--disabled) .mdc-line-ripple::before {
|
||||
border: 0px !important;
|
||||
}
|
||||
|
||||
::ng-deep .mat-mdc-button-touch-target {
|
||||
border-radius: var(--ov-surface-radius) !important;
|
||||
}
|
||||
|
||||
.mute-btn {
|
||||
color: #ffffff !important;
|
||||
background-color: var(--ov-error-color) !important;
|
||||
}
|
||||
}
|
||||
::ng-deep .mat-mdc-select-panel {
|
||||
background-color: #ffffff !important;
|
||||
}
|
||||
|
||||
// Include shared device menu styles
|
||||
@include shared.device-menu-styles();
|
||||
::ng-deep .mat-mdc-option {
|
||||
padding: 10px 10px !important;
|
||||
}
|
||||
|
||||
::ng-deep .mat-mdc-form-field.mat-focused .mat-mdc-select-arrow {
|
||||
color: var(--ov-primary-action-color) !important;
|
||||
}
|
||||
::ng-deep .mdc-text-field--filled:not(.mdc-text-field--disabled) .mdc-line-ripple::after {
|
||||
border-bottom-color: var(--ov-primary-action-color) !important;
|
||||
}
|
||||
::ng-deep .mat-mdc-option.mdc-list-item--selected:not(.mdc-list-item--disabled):not(.mat-mdc-option-multiple) {
|
||||
background-color: $ov-selection-color !important;
|
||||
}
|
||||
|
|
|
@ -1,12 +1,10 @@
|
|||
import { Component, EventEmitter, Input, OnDestroy, OnInit, Output } from '@angular/core';
|
||||
import { Component, EventEmitter, OnDestroy, OnInit, Output } from '@angular/core';
|
||||
import { Subscription } from 'rxjs';
|
||||
import { CustomDevice } from '../../../models/device.model';
|
||||
import { DeviceService } from '../../../services/device/device.service';
|
||||
import { ParticipantService } from '../../../services/participant/participant.service';
|
||||
import { StorageService } from '../../../services/storage/storage.service';
|
||||
import { ParticipantModel } from '../../../models/participant.model';
|
||||
import { LoggerService } from '../../../services/logger/logger.service';
|
||||
import { ILogger } from '../../../models/logger.model';
|
||||
|
||||
/**
|
||||
* @internal
|
||||
|
@ -18,7 +16,6 @@ import { ILogger } from '../../../models/logger.model';
|
|||
standalone: false
|
||||
})
|
||||
export class AudioDevicesComponent implements OnInit, OnDestroy {
|
||||
@Input() compact: boolean = false;
|
||||
@Output() onAudioDeviceChanged = new EventEmitter<CustomDevice>();
|
||||
@Output() onAudioEnabledChanged = new EventEmitter<boolean>();
|
||||
|
||||
|
@ -28,16 +25,12 @@ export class AudioDevicesComponent implements OnInit, OnDestroy {
|
|||
microphoneSelected: CustomDevice | undefined;
|
||||
microphones: CustomDevice[] = [];
|
||||
private localParticipantSubscription: Subscription;
|
||||
private log: ILogger;
|
||||
|
||||
constructor(
|
||||
private deviceSrv: DeviceService,
|
||||
private storageSrv: StorageService,
|
||||
private participantService: ParticipantService,
|
||||
private loggerSrv: LoggerService
|
||||
) {
|
||||
this.log = this.loggerSrv.get('AudioDevicesComponent');
|
||||
}
|
||||
private participantService: ParticipantService
|
||||
) {}
|
||||
|
||||
async ngOnInit() {
|
||||
this.subscribeToParticipantMediaProperties();
|
||||
|
@ -66,19 +59,14 @@ export class AudioDevicesComponent implements OnInit, OnDestroy {
|
|||
}
|
||||
|
||||
async onMicrophoneSelected(event: any) {
|
||||
try {
|
||||
const device: CustomDevice = event?.value;
|
||||
if (this.deviceSrv.needUpdateAudioTrack(device)) {
|
||||
this.microphoneStatusChanging = true;
|
||||
await this.participantService.switchMicrophone(device.device);
|
||||
this.deviceSrv.setMicSelected(device.device);
|
||||
this.microphoneSelected = this.deviceSrv.getMicrophoneSelected();
|
||||
this.onAudioDeviceChanged.emit(this.microphoneSelected);
|
||||
}
|
||||
} catch (error) {
|
||||
this.log.e('Error switching microphone', error);
|
||||
} finally {
|
||||
this.microphoneStatusChanging = false;
|
||||
this.onAudioDeviceChanged.emit(this.microphoneSelected);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,246 +0,0 @@
|
|||
// Shared styles for device selectors (video and audio)
|
||||
// This file contains common styling for both video-devices and audio-devices components
|
||||
|
||||
@mixin device-selector-base() {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
|
||||
// Compact Mode - Unified Button
|
||||
&.compact {
|
||||
.unified-device-button {
|
||||
display: flex;
|
||||
background: rgba(255, 255, 255, 0.7);
|
||||
backdrop-filter: blur(20px);
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
transition: all 0.2s ease;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
|
||||
.toggle-section {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-width: 50px;
|
||||
width: 50px;
|
||||
height: 48px;
|
||||
border: none;
|
||||
background: transparent;
|
||||
border-radius: 0;
|
||||
padding: 0;
|
||||
transition: all 0.2s ease;
|
||||
|
||||
&.device-enabled {
|
||||
color: var(--ov-primary-action-color, #4285f4);
|
||||
|
||||
mat-icon {
|
||||
color: var(--ov-primary-action-color, #4285f4);
|
||||
}
|
||||
}
|
||||
|
||||
&.device-disabled {
|
||||
background: rgba(244, 67, 54, 0.9);
|
||||
color: white;
|
||||
|
||||
mat-icon {
|
||||
color: white;
|
||||
}
|
||||
}
|
||||
|
||||
&[disabled] {
|
||||
background: rgba(150, 150, 150, 0.5);
|
||||
color: rgba(150, 150, 150, 0.8);
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
mat-icon {
|
||||
font-size: 20px;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.dropdown-section {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-width: 30px;
|
||||
width: 30px;
|
||||
height: 48px;
|
||||
border: none;
|
||||
background: transparent;
|
||||
border-left: 1px solid rgba(0, 0, 0, 0.1);
|
||||
border-radius: 0;
|
||||
padding: 0;
|
||||
color: var(--ov-text-secondary-color, #666);
|
||||
transition: all 0.2s ease;
|
||||
|
||||
mat-icon {
|
||||
font-size: 16px;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Normal Mode - Input Style Selector
|
||||
&:not(.compact) {
|
||||
.normal-device-selector {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
width: 100%;
|
||||
|
||||
.device-input-selector {
|
||||
flex: 1;
|
||||
|
||||
&:not(.disabled) {
|
||||
.selector-button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
width: 100%;
|
||||
height: 48px;
|
||||
padding: 0 16px;
|
||||
background: var(--ov-surface-color, #ffffff);
|
||||
border: 2px solid var(--ov-border-color, #e0e0e0);
|
||||
border-radius: 8px;
|
||||
color: var(--ov-text-surface-color);
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
transition: all 0.2s ease;
|
||||
text-align: left;
|
||||
justify-content: flex-start;
|
||||
|
||||
&[disabled] {
|
||||
background: var(--ov-disabled-background, #f5f5f5);
|
||||
color: var(--ov-disabled-text-color, #999);
|
||||
cursor: not-allowed;
|
||||
border-color: var(--ov-disabled-border-color, #ddd);
|
||||
}
|
||||
|
||||
.device-icon {
|
||||
font-size: 18px;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
color: var(--ov-text-secondary-color, #666);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.selected-device-name {
|
||||
flex: 1;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.dropdown-icon {
|
||||
font-size: 18px;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
color: var(--ov-text-secondary-color, #666);
|
||||
flex-shrink: 0;
|
||||
transition: transform 0.2s ease;
|
||||
}
|
||||
|
||||
&[aria-expanded='true'] .dropdown-icon {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.disabled {
|
||||
.selector-button.disabled {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
height: 48px;
|
||||
padding: 0 16px;
|
||||
background: var(--ov-disabled-background, #f5f5f5);
|
||||
border: 2px solid var(--ov-disabled-border-color, #ddd);
|
||||
border-radius: 8px;
|
||||
color: var(--ov-disabled-text-color, #999);
|
||||
font-size: 14px;
|
||||
cursor: not-allowed;
|
||||
|
||||
.device-icon {
|
||||
font-size: 18px;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
color: var(--ov-error-color, #d32f2f);
|
||||
}
|
||||
|
||||
.selected-device-name {
|
||||
flex: 1;
|
||||
font-style: italic;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.no-device-message {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 8px 12px;
|
||||
background: rgba(255, 193, 7, 0.1);
|
||||
border: 1px solid rgba(255, 193, 7, 0.3);
|
||||
border-radius: 8px;
|
||||
color: var(--ov-warning-color, #ff9800);
|
||||
font-size: 12px;
|
||||
|
||||
.warning-icon {
|
||||
font-size: 16px;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Shared device menu styles
|
||||
@mixin device-menu-styles() {
|
||||
::ng-deep .device-menu.mat-mdc-menu-panel {
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.12);
|
||||
border: 1px solid var(--ov-border-color, #e0e0e0);
|
||||
overflow: hidden;
|
||||
background-color: var(--ov-surface-color);
|
||||
|
||||
.mat-mdc-menu-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 12px 16px;
|
||||
transition: background-color 0.2s ease;
|
||||
font-size: 14px;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--ov-hover-color, #f5f5f5);
|
||||
}
|
||||
|
||||
&.selected {
|
||||
background-color: rgba(66, 133, 244, 0.08);
|
||||
color: var(--ov-primary-action-color, #4285f4);
|
||||
|
||||
mat-icon {
|
||||
color: var(--ov-primary-action-color, #4285f4);
|
||||
}
|
||||
}
|
||||
|
||||
mat-icon {
|
||||
font-size: 18px;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
}
|
||||
|
||||
span {
|
||||
flex: 1;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,33 +1,18 @@
|
|||
<div class="language-selector-container">
|
||||
@if (compact) {
|
||||
<!-- Compact version (icon only) -->
|
||||
<button mat-icon-button [matMenuTriggerFor]="langMenu" class="compact-lang-button" [matTooltip]="'Change language'" disableRipple>
|
||||
<button id="lang-btn-compact" *ngIf="compact" mat-icon-button [matMenuTriggerFor]="menu">
|
||||
<mat-icon>translate</mat-icon>
|
||||
</button>
|
||||
} @else {
|
||||
<!-- Full version (with text) -->
|
||||
<button mat-flat-button [matMenuTriggerFor]="langMenu" class="full-lang-button">
|
||||
<!-- <mat-icon class="lang-icon">translate</mat-icon> -->
|
||||
<span class="lang-name">
|
||||
{{ langSelected?.name }}
|
||||
<mat-icon class="expand-icon">expand_more</mat-icon>
|
||||
</span>
|
||||
</button>
|
||||
}
|
||||
|
||||
<!-- Language Menu -->
|
||||
<mat-menu #langMenu="matMenu" class="language-menu">
|
||||
@for (lang of languages; track lang.lang) {
|
||||
</button>
|
||||
<button *ngIf="!compact" mat-flat-button [matMenuTriggerFor]="menu" class="lang-button" id="lang-btn">
|
||||
<span id="lang-selected-name">{{ langSelected?.name }}</span>
|
||||
<mat-icon class="expand-more-icon">expand_more</mat-icon>
|
||||
</button>
|
||||
<mat-menu #menu="matMenu">
|
||||
<button
|
||||
mat-menu-item
|
||||
*ngFor="let lang of languages"
|
||||
(click)="onLangSelected(lang.lang)"
|
||||
[attr.id]="'lang-opt-' + lang.lang"
|
||||
[class.selected]="langSelected?.lang === lang.lang"
|
||||
class="language-option"
|
||||
class="lang-menu-opt"
|
||||
>
|
||||
<mat-icon *ngIf="langSelected?.lang === lang.lang" class="check-icon">check</mat-icon>
|
||||
<span class="lang-option-name">{{ lang.name }}</span>
|
||||
<span>{{ lang.name }}</span>
|
||||
</button>
|
||||
}
|
||||
</mat-menu>
|
||||
</div>
|
||||
</mat-menu>
|
||||
|
|
|
@ -1,113 +1,21 @@
|
|||
:host {
|
||||
display: inline-block;
|
||||
$ov-surface-color-lighter: color-mix(in srgb, var(--ov-surface-color), #fff 5%);
|
||||
|
||||
.language-selector-container {
|
||||
.compact-lang-button {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
background: rgba(255, 255, 255, 0.9);
|
||||
backdrop-filter: blur(10px);
|
||||
border: 1px solid var(--ov-border-color, #e0e0e0);
|
||||
border-radius: 10px;
|
||||
transition: all 0.2s ease;
|
||||
color: var(--ov-text-secondary-color, #666);
|
||||
.lang-button {
|
||||
background-color: var(--ov-primary-action-color) !important;
|
||||
color: var(--ov-secondary-action-color) !important;
|
||||
}
|
||||
.lang-button .mat-icon {
|
||||
color: var(--ov-secondary-action-color);
|
||||
|
||||
mat-icon {
|
||||
font-size: 18px;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
}
|
||||
}
|
||||
}
|
||||
::ng-deep .mat-mdc-menu-panel {
|
||||
border-radius: var(--ov-surface-radius) !important;
|
||||
background-color: $ov-surface-color-lighter !important;
|
||||
box-shadow: 1px 1px 5px 0px rgba(0, 0, 0, 0.2) !important;
|
||||
}
|
||||
|
||||
.full-lang-button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 12px 16px;
|
||||
background: var(--ov-surface-color, #ffffff);
|
||||
border: 2px solid var(--ov-border-color, #e0e0e0);
|
||||
border-radius: 12px;
|
||||
transition: all 0.2s ease;
|
||||
color: var(--ov-text-primary-color, #333);
|
||||
font-weight: 500;
|
||||
|
||||
&:hover {
|
||||
border-color: var(--ov-primary-action-color, #4285f4);
|
||||
box-shadow: 0 2px 8px rgba(66, 133, 244, 0.1);
|
||||
}
|
||||
|
||||
.lang-icon {
|
||||
font-size: 18px;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
color: var(--ov-text-surface-color, #666);
|
||||
}
|
||||
|
||||
.lang-name {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
display: inline-block !important;
|
||||
::ng-deep .mat-mdc-menu-item,
|
||||
.mat-mdc-menu-item:visited,
|
||||
.mat-mdc-menu-item:link {
|
||||
color: var(--ov-text-surface-color) !important;
|
||||
}
|
||||
|
||||
.expand-icon {
|
||||
font-size: 16px;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
color: var(--ov-text-secondary-color, #666);
|
||||
transition: transform 0.2s ease;
|
||||
}
|
||||
|
||||
&[aria-expanded='true'] .expand-icon {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
::ng-deep .language-menu.mat-mdc-menu-panel {
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.12);
|
||||
border: 1px solid var(--ov-border-color, #e0e0e0);
|
||||
overflow: hidden;
|
||||
background: var(--ov-surface-color, #ffffff);
|
||||
|
||||
.language-option {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 12px 16px;
|
||||
transition: background-color 0.2s ease;
|
||||
font-size: 14px;
|
||||
min-height: 48px;
|
||||
color: var(--ov-text-surface-color);
|
||||
|
||||
&:hover {
|
||||
background-color: var(--ov-hover-color, #f5f5f5);
|
||||
}
|
||||
|
||||
&.selected {
|
||||
background-color: rgba(66, 133, 244, 0.08);
|
||||
color: var(--ov-primary-action-color, #4285f4);
|
||||
|
||||
.check-icon {
|
||||
color: var(--ov-primary-action-color, #4285f4);
|
||||
}
|
||||
}
|
||||
|
||||
.check-icon {
|
||||
font-size: 18px;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
}
|
||||
|
||||
.lang-option-name {
|
||||
flex: 1;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
&.selected .lang-option-name {
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,17 +1,20 @@
|
|||
<div class="participant-name-input-container" [class.error]="error">
|
||||
<div class="input-wrapper">
|
||||
<mat-icon class="input-icon">person</mat-icon>
|
||||
<div id="name-input-container" [ngClass]="{ warn: !name }">
|
||||
<mat-form-field id="name-form" [ngClass]="{ error: error }">
|
||||
<mat-select-trigger>
|
||||
<button mat-flat-button disabled>
|
||||
<mat-icon>person</mat-icon>
|
||||
</button>
|
||||
</mat-select-trigger>
|
||||
<input
|
||||
id="name-input"
|
||||
matInput
|
||||
(change)="updateName()"
|
||||
type="text"
|
||||
maxlength="20"
|
||||
[(ngModel)]="name"
|
||||
autocomplete="off"
|
||||
[disabled]="!isPrejoinPage"
|
||||
(input)="updateName()"
|
||||
(keypress)="eventKeyPress($event)"
|
||||
[placeholder]="'PREJOIN.NICKNAME' | translate"
|
||||
class="name-input-field"
|
||||
/>
|
||||
</div>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
|
|
|
@ -1,71 +1,67 @@
|
|||
$ov-selection-color-btn: #afafaf;
|
||||
$ov-selection-color: #cccccc;
|
||||
:host {
|
||||
display: block;
|
||||
width: 100%;
|
||||
#name-input-container {
|
||||
height: 70px;
|
||||
border-radius: var(--ov-surface-radius);
|
||||
}
|
||||
|
||||
.participant-name-input-container {
|
||||
#name-input-container mat-form-field {
|
||||
width: 100%;
|
||||
color: var(--ov-secondary-action-color);
|
||||
}
|
||||
|
||||
.input-wrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background: var(--ov-input-background, #f8f9fa);
|
||||
border: 2px solid var(--ov-border-color, #e0e0e0);
|
||||
border-radius: 12px;
|
||||
::ng-deep .mat-mdc-form-field-infix {
|
||||
display: inline-flex;
|
||||
padding: 0px !important;
|
||||
}
|
||||
::ng-deep .mat-mdc-text-field-wrapper {
|
||||
padding: 0;
|
||||
transition: all 0.2s ease;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
|
||||
&:focus-within {
|
||||
border-color: var(--ov-primary-action-color, #4285f4);
|
||||
box-shadow: 0 0 0 3px rgba(66, 133, 244, 0.1);
|
||||
height: 70px;
|
||||
background-color: $ov-selection-color !important;
|
||||
border-radius: var(--ov-surface-radius);
|
||||
}
|
||||
::ng-deep .mdc-text-field--filled:not(.mdc-text-field--disabled) .mdc-line-ripple::before {
|
||||
border: 0px !important;
|
||||
}
|
||||
::ng-deep .mdc-button--unelevated {
|
||||
border-top-left-radius: var(--ov-surface-radius) !important;
|
||||
border-bottom-left-radius: var(--ov-surface-radius) !important;
|
||||
border-bottom-right-radius: 0px !important;
|
||||
border-top-right-radius: 0px !important;
|
||||
background-color: $ov-selection-color-btn !important;
|
||||
width: 48px !important;
|
||||
min-width: 48px !important;
|
||||
padding: 0;
|
||||
height: 70px;
|
||||
}
|
||||
|
||||
.input-icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 48px;
|
||||
height: 56px;
|
||||
background: var(--ov-surface-secondary, #f0f0f0);
|
||||
color: var(--ov-text-secondary-color, #666);
|
||||
font-size: 20px;
|
||||
border-top-left-radius: 10px;
|
||||
border-bottom-left-radius: 10px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.name-input-field {
|
||||
flex: 1;
|
||||
height: 56px;
|
||||
padding: 0 16px;
|
||||
border: none;
|
||||
outline: none;
|
||||
background: transparent;
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
|
||||
&::placeholder {
|
||||
color: var(--ov-text-secondary-color, #666);
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
background: var(--ov-disabled-background, #f5f5f5);
|
||||
color: var(--ov-disabled-text-color, #999);
|
||||
cursor: not-allowed;
|
||||
}
|
||||
.error {
|
||||
::ng-deep .mdc-button--unelevated {
|
||||
background-color: var(--ov-error-color) !important;
|
||||
}
|
||||
}
|
||||
|
||||
&.error .input-wrapper {
|
||||
border-color: var(--ov-error-color, #d32f2f);
|
||||
box-shadow: 0 0 0 3px rgba(211, 47, 47, 0.1);
|
||||
::ng-deep .mat-mdc-unelevated-button > .mat-icon {
|
||||
height: 24px;
|
||||
width: 24px;
|
||||
font-size: 24px !important;
|
||||
margin: auto;
|
||||
color: #000000 !important;
|
||||
}
|
||||
|
||||
.input-icon {
|
||||
background: rgba(211, 47, 47, 0.1);
|
||||
color: var(--ov-error-color, #d32f2f);
|
||||
}
|
||||
}
|
||||
input {
|
||||
padding-left: 10px !important;
|
||||
border-top-right-radius: var(--ov-surface-radius) !important;
|
||||
border-bottom-right-radius: var(--ov-surface-radius) !important;
|
||||
border-bottom-left-radius: 0px !important;
|
||||
border-top-left-radius: 0px !important;
|
||||
border: 1px solid $ov-selection-color-btn;
|
||||
color: #000000;
|
||||
caret-color: #000000 !important;
|
||||
}
|
||||
}
|
||||
|
||||
::ng-deep .mat-mdc-form-field.mat-focused .mat-mdc-select-arrow {
|
||||
color: var(--ov-primary-action-color) !important;
|
||||
}
|
||||
|
|
|
@ -1,95 +1,40 @@
|
|||
<div class="video-device-selector" [class.compact]="compact">
|
||||
<!-- Unified Device Button (Compact Mode) -->
|
||||
@if (compact) {
|
||||
@if (hasVideoDevices) {
|
||||
<div class="unified-device-button">
|
||||
<!-- Main toggle button -->
|
||||
<div class="device-container-element" [class.mute-btn]="!isCameraEnabled">
|
||||
<mat-form-field id="video-devices-form" *ngIf="cameras.length > 0">
|
||||
<mat-select
|
||||
[disabled]="!hasVideoDevices"
|
||||
[compareWith]="compareObjectDevices"
|
||||
[value]="cameraSelected"
|
||||
(selectionChange)="onCameraSelected($event)"
|
||||
>
|
||||
<mat-select-trigger id="mat-select-trigger">
|
||||
<button
|
||||
mat-flat-button
|
||||
class="toggle-section"
|
||||
id="camera-button"
|
||||
[disableRipple]="true"
|
||||
[disabled]="!hasVideoDevices || cameraStatusChanging"
|
||||
[class.device-enabled]="isCameraEnabled"
|
||||
[class.device-disabled]="!isCameraEnabled"
|
||||
[class.mute-btn]="!isCameraEnabled"
|
||||
(click)="toggleCam($event)"
|
||||
[matTooltip]="isCameraEnabled ? ('TOOLBAR.STOP_VIDEO' | translate) : ('TOOLBAR.START_VIDEO' | translate)"
|
||||
[matTooltipDisabled]="!hasVideoDevices"
|
||||
id="camera-button"
|
||||
>
|
||||
<mat-icon [id]="isCameraEnabled ? 'videocam' : 'videocam_off'">
|
||||
{{ isCameraEnabled ? 'videocam' : 'videocam_off' }}
|
||||
</mat-icon>
|
||||
<mat-icon *ngIf="isCameraEnabled" id="videocam"> videocam </mat-icon>
|
||||
<mat-icon *ngIf="!isCameraEnabled" id="videocam_off"> videocam_off </mat-icon>
|
||||
</button>
|
||||
<span class="selected-text" *ngIf="!isCameraEnabled"> {{ 'PANEL.SETTINGS.DISABLED_VIDEO' | translate }} </span>
|
||||
<span class="selected-text" *ngIf="isCameraEnabled"> {{ cameraSelected.label }} </span>
|
||||
</mat-select-trigger>
|
||||
<mat-option *ngFor="let camera of cameras" [disabled]="!isCameraEnabled" [value]="camera" id="option-{{ camera.label }}">
|
||||
{{ camera.label }}
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
|
||||
<!-- Dropdown section -->
|
||||
@if (isCameraEnabled) {
|
||||
<button
|
||||
mat-flat-button
|
||||
id="video-dropdown"
|
||||
class="dropdown-section"
|
||||
[matMenuTriggerFor]="cameraMenu"
|
||||
[disabled]="cameraStatusChanging"
|
||||
>
|
||||
<mat-icon>expand_more</mat-icon>
|
||||
<div id="video-devices-form" *ngIf="cameras.length === 0">
|
||||
<div id="mat-select-trigger">
|
||||
<button mat-icon-button id="camera-button" class="mute-btn" [disabled]="true">
|
||||
<mat-icon id="videocam_off"> videocam_off </mat-icon>
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
} @else {
|
||||
<!-- No Camera Available -->
|
||||
<div id="no-video-device-message" class="no-device-message">
|
||||
<mat-icon class="warning-icon">warning</mat-icon>
|
||||
<span>{{ 'PREJOIN.NO_VIDEO_DEVICE' | translate }}</span>
|
||||
</div>
|
||||
}
|
||||
} @else {
|
||||
<!-- Normal Mode - Input-style Selector -->
|
||||
<div class="normal-device-selector">
|
||||
<!-- Device Selector (Input Style) -->
|
||||
@if (isCameraEnabled) {
|
||||
<div class="device-input-selector">
|
||||
<button
|
||||
mat-flat-button
|
||||
id="video-dropdown"
|
||||
class="selector-button"
|
||||
[matMenuTriggerFor]="cameraMenu"
|
||||
[disabled]="cameraStatusChanging || cameras.length <= 1"
|
||||
>
|
||||
<mat-icon class="device-icon">videocam</mat-icon>
|
||||
<span class="selected-device-name">{{ cameraSelected?.label || 'No camera selected' }}</span>
|
||||
<mat-icon class="dropdown-icon" *ngIf="cameras.length > 1">expand_more</mat-icon>
|
||||
</button>
|
||||
</div>
|
||||
} @else {
|
||||
@if (hasVideoDevices) {
|
||||
<!-- Disabled state message -->
|
||||
<div class="device-input-selector disabled">
|
||||
<div class="selector-button disabled">
|
||||
<mat-icon class="device-icon">videocam_off</mat-icon>
|
||||
<span class="selected-device-name">{{ 'PANEL.SETTINGS.DISABLED_VIDEO' | translate }}</span>
|
||||
<span id="video-devices-not-found"> {{ 'PREJOIN.NO_VIDEO_DEVICE' | translate }} </span>
|
||||
</div>
|
||||
</div>
|
||||
} @else {
|
||||
<!-- No Camera Available -->
|
||||
<div id="no-video-device-message" class="no-device-message">
|
||||
<mat-icon class="warning-icon">warning</mat-icon>
|
||||
<span>{{ 'PREJOIN.NO_VIDEO_DEVICE' | translate }}</span>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
}
|
||||
|
||||
<!-- Device Selection Menu (Shared) -->
|
||||
<mat-menu #cameraMenu="matMenu" class="device-menu">
|
||||
@for (camera of cameras; track camera.device) {
|
||||
<button
|
||||
mat-menu-item
|
||||
id="option-{{ camera.label }}"
|
||||
(click)="onCameraSelected({ value: camera })"
|
||||
[class.selected]="camera.device === cameraSelected?.device"
|
||||
>
|
||||
<mat-icon *ngIf="camera.device === cameraSelected?.device" class="check-icon">check</mat-icon>
|
||||
<span>{{ camera.label }}</span>
|
||||
</button>
|
||||
}
|
||||
</mat-menu>
|
||||
</div>
|
||||
|
|
|
@ -1,51 +1,106 @@
|
|||
@use '../device-selector-shared' as shared;
|
||||
|
||||
$ov-selection-color-btn: #afafaf;
|
||||
$ov-selection-color: #cccccc;
|
||||
|
||||
:host {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.video-device-selector {
|
||||
@include shared.device-selector-base();
|
||||
|
||||
// Video-specific overrides for compact mode
|
||||
&.compact {
|
||||
.unified-device-button {
|
||||
.toggle-section {
|
||||
display: flex-end; // Video-specific styling
|
||||
.device-container-element {
|
||||
border-radius: var(--ov-surface-radius);
|
||||
border: 1px solid $ov-selection-color-btn;
|
||||
}
|
||||
.device-container-element.mute-btn {
|
||||
border: 1px solid var(--ov-error-color);
|
||||
}
|
||||
#video-devices-form {
|
||||
width: 100%;
|
||||
height: 50px;
|
||||
}
|
||||
|
||||
// Video-specific overrides for normal mode
|
||||
&:not(.compact) {
|
||||
.normal-device-selector {
|
||||
.device-input-selector {
|
||||
&:not(.disabled) {
|
||||
.selector-button {
|
||||
// Video-specific hover effect with box-shadow
|
||||
&:hover:not([disabled]) {
|
||||
background-color: white !important;
|
||||
border-color: var(--ov-primary-action-color);
|
||||
#video-devices-not-found {
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
#camera-button {
|
||||
color: #000000;
|
||||
}
|
||||
|
||||
::ng-deep .mat-mdc-text-field-wrapper,
|
||||
::ng-deep .mat-mdc-form-field-flex,
|
||||
::ng-deep .mat-mdc-select-trigger {
|
||||
height: 50px !important;
|
||||
}
|
||||
|
||||
::ng-deep .mat-mdc-form-field-subscript-wrapper {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
::ng-deep .mat-mdc-text-field-wrapper {
|
||||
padding-left: 0px;
|
||||
padding-right: 10px;
|
||||
background-color: $ov-selection-color !important;
|
||||
border-radius: var(--ov-surface-radius);
|
||||
}
|
||||
::ng-deep .mdc-button--unelevated {
|
||||
border-top-left-radius: var(--ov-surface-radius);
|
||||
border-bottom-left-radius: var(--ov-surface-radius);
|
||||
border-bottom-right-radius: 0px !important;
|
||||
border-top-right-radius: 0px !important;
|
||||
background-color: $ov-selection-color-btn !important;
|
||||
width: 48px !important;
|
||||
min-width: 48px !important;
|
||||
padding: 0;
|
||||
height: 50px;
|
||||
}
|
||||
|
||||
::ng-deep .mat-mdc-unelevated-button > .mat-icon {
|
||||
height: 24px;
|
||||
width: 24px;
|
||||
font-size: 24px !important;
|
||||
margin: auto;
|
||||
}
|
||||
::ng-deep .mat-mdc-form-field-infix {
|
||||
padding: 0px !important;
|
||||
min-height: 100%;
|
||||
}
|
||||
|
||||
.selected-text {
|
||||
padding-left: 5px;
|
||||
}
|
||||
|
||||
.mat-icon {
|
||||
vertical-align: middle;
|
||||
display: inline-flex;
|
||||
}
|
||||
|
||||
::ng-deep .mdc-text-field--filled:not(.mdc-text-field--disabled) .mdc-line-ripple::before {
|
||||
border: 0px !important;
|
||||
}
|
||||
|
||||
::ng-deep .mat-mdc-button-touch-target {
|
||||
border-radius: var(--ov-surface-radius) !important;
|
||||
}
|
||||
|
||||
.mute-btn {
|
||||
color: #ffffff !important;
|
||||
background-color: var(--ov-error-color) !important;
|
||||
}
|
||||
}
|
||||
::ng-deep .mat-mdc-select-panel {
|
||||
background-color: var(--ov-surface-color) !important;
|
||||
}
|
||||
|
||||
// Include shared device menu styles
|
||||
@include shared.device-menu-styles();
|
||||
::ng-deep .mat-mdc-select-panel {
|
||||
background-color: #e2e2e2 !important;
|
||||
}
|
||||
|
||||
// Video-specific additional styles
|
||||
::ng-deep .mat-mdc-option {
|
||||
padding: 10px 10px !important;
|
||||
}
|
||||
|
||||
::ng-deep .mat-mdc-form-field.mat-focused .mat-mdc-select-arrow {
|
||||
color: var(--ov-primary-action-color-lighter) !important;
|
||||
}
|
||||
::ng-deep .mdc-text-field--filled:not(.mdc-text-field--disabled) .mdc-line-ripple::after {
|
||||
border-bottom-color: var(--ov-primary-action-color-lighter) !important;
|
||||
}
|
||||
|
||||
::ng-deep .mat-mdc-option.mdc-list-item--selected:not(.mdc-list-item--disabled):not(.mat-mdc-option-multiple) {
|
||||
background-color: $ov-selection-color !important;
|
||||
}
|
||||
|
|
|
@ -1,12 +1,10 @@
|
|||
import { Component, EventEmitter, Input, OnDestroy, OnInit, Output } from '@angular/core';
|
||||
import { Component, EventEmitter, OnDestroy, OnInit, Output } from '@angular/core';
|
||||
import { Subscription } from 'rxjs';
|
||||
import { CustomDevice } from '../../../models/device.model';
|
||||
import { DeviceService } from '../../../services/device/device.service';
|
||||
import { ParticipantService } from '../../../services/participant/participant.service';
|
||||
import { StorageService } from '../../../services/storage/storage.service';
|
||||
import { ParticipantModel } from '../../../models/participant.model';
|
||||
import { LoggerService } from '../../../services/logger/logger.service';
|
||||
import { ILogger } from '../../../models/logger.model';
|
||||
|
||||
/**
|
||||
* @internal
|
||||
|
@ -18,10 +16,8 @@ import { ILogger } from '../../../models/logger.model';
|
|||
standalone: false
|
||||
})
|
||||
export class VideoDevicesComponent implements OnInit, OnDestroy {
|
||||
@Input() compact: boolean = false;
|
||||
@Output() onVideoDeviceChanged = new EventEmitter<CustomDevice>();
|
||||
@Output() onVideoEnabledChanged = new EventEmitter<boolean>();
|
||||
@Output() onVideoDevicesLoaded = new EventEmitter<CustomDevice[]>();
|
||||
|
||||
cameraStatusChanging: boolean;
|
||||
isCameraEnabled: boolean;
|
||||
|
@ -30,16 +26,11 @@ export class VideoDevicesComponent implements OnInit, OnDestroy {
|
|||
cameras: CustomDevice[] = [];
|
||||
localParticipantSubscription: Subscription;
|
||||
|
||||
private log: ILogger;
|
||||
|
||||
constructor(
|
||||
private storageSrv: StorageService,
|
||||
private deviceSrv: DeviceService,
|
||||
private participantService: ParticipantService,
|
||||
private loggerSrv: LoggerService
|
||||
) {
|
||||
this.log = this.loggerSrv.get('VideoDevicesComponent');
|
||||
}
|
||||
private participantService: ParticipantService
|
||||
) {}
|
||||
|
||||
async ngOnInit() {
|
||||
this.subscribeToParticipantMediaProperties();
|
||||
|
@ -50,7 +41,6 @@ export class VideoDevicesComponent implements OnInit, OnDestroy {
|
|||
this.cameraSelected = this.deviceSrv.getCameraSelected();
|
||||
}
|
||||
|
||||
this.onVideoDevicesLoaded.emit(this.cameras);
|
||||
this.isCameraEnabled = this.participantService.isMyCameraEnabled();
|
||||
}
|
||||
|
||||
|
@ -70,24 +60,37 @@ export class VideoDevicesComponent implements OnInit, OnDestroy {
|
|||
}
|
||||
|
||||
async onCameraSelected(event: any) {
|
||||
try {
|
||||
const device: CustomDevice = event?.value;
|
||||
|
||||
// Is New deviceId different from the old one?
|
||||
if (this.deviceSrv.needUpdateVideoTrack(device)) {
|
||||
// const mirror = this.deviceSrv.cameraNeedsMirror(device.device);
|
||||
// Reapply Virtual Background to new Publisher if necessary
|
||||
// const backgroundSelected = this.backgroundService.backgroundSelected.getValue();
|
||||
// const isBackgroundApplied = this.backgroundService.isBackgroundApplied();
|
||||
|
||||
// if (isBackgroundApplied) {
|
||||
// await this.backgroundService.removeBackground();
|
||||
// }
|
||||
// const pp: PublisherProperties = { videoSource: device.device, audioSource: false, mirror };
|
||||
// const publisher = this.participantService.getMyCameraPublisher();
|
||||
// await this.openviduService.replaceCameraTrack(publisher, pp);
|
||||
|
||||
this.cameraStatusChanging = true;
|
||||
|
||||
await this.participantService.switchCamera(device.device);
|
||||
|
||||
// if (isBackgroundApplied) {
|
||||
// const bgSelected = this.backgroundService.backgrounds.find((b) => b.id === backgroundSelected);
|
||||
// if (bgSelected) {
|
||||
// await this.backgroundService.applyBackground(bgSelected);
|
||||
// }
|
||||
// }
|
||||
|
||||
this.deviceSrv.setCameraSelected(device.device);
|
||||
this.cameraSelected = device;
|
||||
this.onVideoDeviceChanged.emit(this.cameraSelected);
|
||||
}
|
||||
} catch (error) {
|
||||
this.log.e('Error switching camera', error);
|
||||
} finally {
|
||||
this.cameraSelected = this.deviceSrv.getCameraSelected();
|
||||
this.cameraStatusChanging = false;
|
||||
this.onVideoDeviceChanged.emit(this.cameraSelected);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
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';
|
||||
|
@ -92,7 +92,10 @@ 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;
|
||||
|
||||
/**
|
||||
|
@ -110,9 +113,11 @@ export class StreamComponent implements OnInit, OnDestroy {
|
|||
}
|
||||
|
||||
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 +183,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();
|
||||
});
|
||||
|
|
|
@ -6,26 +6,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>
|
||||
|
@ -126,36 +120,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"
|
||||
|
|
|
@ -126,7 +126,6 @@ $ov-recording-blinking-color: #eb5144;
|
|||
text-align: center;
|
||||
line-height: 20px;
|
||||
margin: auto;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.recording-tag {
|
||||
|
|
|
@ -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,7 +44,6 @@ 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';
|
||||
|
@ -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,15 +89,16 @@ 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.
|
||||
* @deprecated Use {@link onParticipantLeft} instead.
|
||||
*/
|
||||
@Output() onRoomDisconnected: EventEmitter<void> = new EventEmitter<void>();
|
||||
|
||||
|
@ -144,12 +145,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
|
||||
*/
|
||||
|
@ -245,11 +240,6 @@ export class ToolbarComponent implements OnInit, OnDestroy, AfterViewInit {
|
|||
*/
|
||||
showRecordingButton: boolean = true;
|
||||
|
||||
/**
|
||||
* @ignore
|
||||
*/
|
||||
showViewRecordingsButton: boolean = false;
|
||||
|
||||
/**
|
||||
* @ignore
|
||||
*/
|
||||
|
@ -290,12 +280,7 @@ export class ToolbarComponent implements OnInit, OnDestroy, AfterViewInit {
|
|||
/**
|
||||
* @ignore
|
||||
*/
|
||||
showRoomName: boolean = true;
|
||||
|
||||
/**
|
||||
* @ignore
|
||||
*/
|
||||
roomName: string = '';
|
||||
showSessionName: boolean = true;
|
||||
|
||||
/**
|
||||
* @ignore
|
||||
|
@ -327,11 +312,6 @@ export class ToolbarComponent implements OnInit, OnDestroy, AfterViewInit {
|
|||
*/
|
||||
recordingStatus: RecordingStatus = RecordingStatus.STOPPED;
|
||||
|
||||
/**
|
||||
* @ignore
|
||||
*/
|
||||
isRecordingReadOnlyMode: boolean = false;
|
||||
|
||||
/**
|
||||
* @ignore
|
||||
*/
|
||||
|
@ -359,18 +339,31 @@ 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 cameraButtonSub: Subscription;
|
||||
private microphoneButtonSub: Subscription;
|
||||
private screenshareButtonSub: Subscription;
|
||||
private fullscreenButtonSub: Subscription;
|
||||
private backgroundEffectsButtonSub: Subscription;
|
||||
private leaveButtonSub: Subscription;
|
||||
private recordingButtonSub: Subscription;
|
||||
private broadcastingButtonSub: Subscription;
|
||||
private recordingSubscription: Subscription;
|
||||
private broadcastingSubscription: Subscription;
|
||||
private activitiesPanelButtonSub: Subscription;
|
||||
private participantsPanelButtonSub: Subscription;
|
||||
private chatPanelButtonSub: Subscription;
|
||||
private displayLogoSub: Subscription;
|
||||
private brandingLogoSub: Subscription;
|
||||
private displayRoomNameSub: Subscription;
|
||||
private settingsButtonSub: Subscription;
|
||||
private captionsSubs: Subscription;
|
||||
private additionalButtonsPositionSub: Subscription;
|
||||
private fullscreenChangeSubscription: Subscription;
|
||||
private currentWindowHeight = window.innerHeight;
|
||||
|
||||
/**
|
||||
|
@ -393,8 +386,7 @@ 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');
|
||||
}
|
||||
|
@ -424,12 +416,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 +437,34 @@ 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.cameraButtonSub) this.cameraButtonSub.unsubscribe();
|
||||
if (this.microphoneButtonSub) this.microphoneButtonSub.unsubscribe();
|
||||
if (this.screenshareButtonSub) this.screenshareButtonSub.unsubscribe();
|
||||
if (this.fullscreenButtonSub) this.fullscreenButtonSub.unsubscribe();
|
||||
if (this.backgroundEffectsButtonSub) this.backgroundEffectsButtonSub.unsubscribe();
|
||||
if (this.leaveButtonSub) this.leaveButtonSub.unsubscribe();
|
||||
if (this.recordingButtonSub) this.recordingButtonSub.unsubscribe();
|
||||
if (this.broadcastingButtonSub) this.broadcastingButtonSub.unsubscribe();
|
||||
if (this.participantsPanelButtonSub) this.participantsPanelButtonSub.unsubscribe();
|
||||
if (this.chatPanelButtonSub) this.chatPanelButtonSub.unsubscribe();
|
||||
if (this.displayLogoSub) this.displayLogoSub.unsubscribe();
|
||||
if (this.brandingLogoSub) this.brandingLogoSub.unsubscribe();
|
||||
if (this.displayRoomNameSub) this.displayRoomNameSub.unsubscribe();
|
||||
if (this.minimalSub) this.minimalSub.unsubscribe();
|
||||
if (this.activitiesPanelButtonSub) this.activitiesPanelButtonSub.unsubscribe();
|
||||
if (this.recordingSubscription) this.recordingSubscription.unsubscribe();
|
||||
if (this.broadcastingSubscription) this.broadcastingSubscription.unsubscribe();
|
||||
if (this.settingsButtonSub) this.settingsButtonSub.unsubscribe();
|
||||
if (this.captionsSubs) this.captionsSubs.unsubscribe();
|
||||
if (this.fullscreenChangeSubscription) this.fullscreenChangeSubscription.unsubscribe();
|
||||
if (this.additionalButtonsPositionSub) this.additionalButtonsPositionSub.unsubscribe();
|
||||
this.isFullscreenActive = false;
|
||||
this.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
|
||||
*/
|
||||
|
@ -558,8 +527,7 @@ export class ToolbarComponent implements OnInit, OnDestroy, AfterViewInit {
|
|||
await this.openviduService.disconnectRoom(() => {
|
||||
this.onParticipantLeft.emit({
|
||||
roomName: this.openviduService.getRoomName(),
|
||||
participantName: this.participantService.getLocalParticipant()?.name || '',
|
||||
identity: this.participantService.getLocalParticipant()?.identity || '',
|
||||
participantName: this.participantService.getLocalParticipant()?.identity || '',
|
||||
reason: ParticipantLeftReason.LEAVE
|
||||
});
|
||||
this.onRoomDisconnected.emit();
|
||||
|
@ -570,33 +538,10 @@ export class ToolbarComponent implements OnInit, OnDestroy, AfterViewInit {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @ignore
|
||||
*/
|
||||
openRecordingActivityPanel() {
|
||||
if (this.showActivitiesPanelButton && !this.isActivitiesOpened) {
|
||||
this.panelService.togglePanel(PanelType.ACTIVITIES, 'recording');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @ignore
|
||||
*/
|
||||
openBroadcastingActivityPanel() {
|
||||
if (this.showActivitiesPanelButton && !this.isActivitiesOpened) {
|
||||
this.panelService.togglePanel(PanelType.ACTIVITIES, 'broadcasting');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @ignore
|
||||
*/
|
||||
toggleRecording() {
|
||||
if (this.recordingStatus === RecordingStatus.FAILED) {
|
||||
this.openRecordingActivityPanel();
|
||||
return;
|
||||
}
|
||||
|
||||
const payload: RecordingStartRequestedEvent = {
|
||||
roomName: this.openviduService.getRoomName()
|
||||
};
|
||||
|
@ -606,7 +551,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 +570,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 +625,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 +640,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 +654,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 +666,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 +675,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 +699,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 +708,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 +726,86 @@ 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.cameraButtonSub = this.libService.cameraButton$.subscribe((value: boolean) => {
|
||||
this.showCameraButton = value;
|
||||
this.cd.markForCheck();
|
||||
});
|
||||
this.libService.microphoneButton$.pipe(takeUntil(this.destroy$)).subscribe((value: boolean) => {
|
||||
this.microphoneButtonSub = this.libService.microphoneButton$.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 +815,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 +834,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 = '';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,19 +1,12 @@
|
|||
<div id="call-container">
|
||||
<!-- Loading spinner -->
|
||||
@if (componentState.isLoading) {
|
||||
<div id="spinner">
|
||||
<mat-spinner [diameter]="spinnerDiameter"></mat-spinner>
|
||||
<div id="spinner" *ngIf="loading">
|
||||
<mat-spinner [diameter]="50"></mat-spinner>
|
||||
<span>{{ 'PREJOIN.PREPARING' | translate }}</span>
|
||||
</div>
|
||||
} @else if (componentState.showPrejoin) {
|
||||
<!-- Prejoin -->
|
||||
<div [@inOutAnimation] id="pre-join-container">
|
||||
<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)"
|
||||
|
@ -21,19 +14,14 @@
|
|||
(onAudioEnabledChanged)="onAudioEnabledChanged.emit($event)"
|
||||
(onLangChanged)="onLangChanged.emit($event)"
|
||||
></ov-pre-join>
|
||||
</ng-template>
|
||||
</div>
|
||||
} @else if (componentState.error?.hasError) {
|
||||
<!-- Error -->
|
||||
|
||||
<div id="spinner">
|
||||
<div id="spinner" *ngIf="!loading && error">
|
||||
<mat-icon class="error-icon">error</mat-icon>
|
||||
<span>{{ componentState.error?.message }}</span>
|
||||
<span>{{ errorMessage }}</span>
|
||||
</div>
|
||||
} @else if (componentState.isRoomReady) {
|
||||
<!-- VideoConference -->
|
||||
|
||||
<div [@inOutAnimation] id="vc-container">
|
||||
<div [@inOutAnimation] id="vc-container" *ngIf="isRoomReady && !showPrejoin && !loading && !error">
|
||||
<ov-session
|
||||
(onRoomCreated)="onRoomCreated.emit($event)"
|
||||
(onRoomReconnecting)="onRoomDisconnected.emit()"
|
||||
|
@ -62,13 +50,6 @@
|
|||
</ng-template>
|
||||
</ov-session>
|
||||
</div>
|
||||
} @else {
|
||||
<!-- Fallback / unknown state -->
|
||||
<div id="unknown-state">
|
||||
<mat-icon class="warning-icon">help_outline</mat-icon>
|
||||
<span>An error occurred in the video conference</span>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
<ng-template #defaultToolbar>
|
||||
|
@ -83,7 +64,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>
|
||||
|
@ -148,8 +128,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>
|
||||
|
@ -162,9 +140,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>
|
||||
|
||||
|
@ -183,10 +158,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>
|
||||
|
||||
|
|
|
@ -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, filter, skip, take } 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,285 +51,68 @@ 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
|
||||
|
@ -422,10 +182,6 @@ export class VideoconferenceComponent implements OnDestroy, AfterViewInit {
|
|||
* @internal
|
||||
*/
|
||||
openviduAngularAdditionalPanelsTemplate: TemplateRef<any>;
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
openviduAngularParticipantPanelAfterLocalParticipantTemplate: TemplateRef<any>;
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
|
@ -442,20 +198,6 @@ 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.
|
||||
|
@ -471,7 +213,7 @@ export class VideoconferenceComponent implements OnDestroy, AfterViewInit {
|
|||
|
||||
/**
|
||||
* Provides event notifications that fire when Room is disconnected for the local participant.
|
||||
* @deprecated Use {@link VideoconferenceComponent.onParticipantLeft} instead
|
||||
* @deprecated Use {@link onParticipantLeft} instead
|
||||
*/
|
||||
@Output() onRoomDisconnected: EventEmitter<void> = new EventEmitter<void>();
|
||||
|
||||
|
@ -573,13 +315,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.
|
||||
|
@ -600,12 +335,6 @@ export class VideoconferenceComponent implements 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>();
|
||||
|
||||
/**
|
||||
* Provides event notifications that fire when Room is created for the local participant.
|
||||
* It provides the {@link https://openvidu.io/latest/docs/getting-started/#room Room} payload as event data.
|
||||
|
@ -626,55 +355,37 @@ export class VideoconferenceComponent implements OnDestroy, AfterViewInit {
|
|||
|
||||
/**
|
||||
* @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
|
||||
}
|
||||
};
|
||||
error: boolean = false;
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
errorMessage: string = '';
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
showPrejoin: boolean = true;
|
||||
|
||||
private destroy$ = new Subject<void>();
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
isRoomReady: boolean = false;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
loading = true;
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
_tokenError: any;
|
||||
private prejoinSub: Subscription;
|
||||
private tokenSub: Subscription;
|
||||
private tokenErrorSub: Subscription;
|
||||
private participantNameSub: Subscription;
|
||||
private log: ILogger;
|
||||
private 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
|
||||
*/
|
||||
|
@ -684,29 +395,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');
|
||||
|
||||
this.addMaterialIconsIfNeeded();
|
||||
|
||||
// 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();
|
||||
}
|
||||
|
||||
|
@ -714,202 +413,116 @@ export class VideoconferenceComponent implements OnDestroy, AfterViewInit {
|
|||
* @internal
|
||||
*/
|
||||
ngAfterViewInit() {
|
||||
this.setupTemplates();
|
||||
this.deviceSrv.initializeDevices().then(() => {
|
||||
this.updateComponentState({
|
||||
isLoading: false
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
private addMaterialIconsIfNeeded(): void {
|
||||
//Add material icons to the page if not already present
|
||||
const existingLink = document.querySelector(VideoconferenceComponent.MATERIAL_ICONS_SELECTOR);
|
||||
const existingLink = document.querySelector('link[href*="Material+Symbols+Outlined"]');
|
||||
if (!existingLink) {
|
||||
const link = document.createElement('link');
|
||||
link.href = VideoconferenceComponent.MATERIAL_ICONS_URL;
|
||||
link.href = 'https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined&icon_names=background_replace,keep_off';
|
||||
link.rel = 'stylesheet';
|
||||
document.head.appendChild(link);
|
||||
}
|
||||
if (this.externalToolbar) {
|
||||
this.log.d('Setting EXTERNAL TOOLBAR');
|
||||
this.openviduAngularToolbarTemplate = this.externalToolbar.template;
|
||||
} else {
|
||||
this.log.d('Setting DEFAULT TOOLBAR');
|
||||
if (this.externalToolbarAdditionalButtons) {
|
||||
this.log.d('Setting EXTERNAL TOOLBAR ADDITIONAL BUTTONS');
|
||||
this.openviduAngularToolbarAdditionalButtonsTemplate = this.externalToolbarAdditionalButtons.template;
|
||||
}
|
||||
if (this.externalToolbarAdditionalPanelButtons) {
|
||||
this.log.d('Setting EXTERNAL TOOLBAR ADDITIONAL PANEL BUTTONS');
|
||||
this.openviduAngularToolbarAdditionalPanelButtonsTemplate = this.externalToolbarAdditionalPanelButtons.template;
|
||||
}
|
||||
this.openviduAngularToolbarTemplate = this.defaultToolbarTemplate;
|
||||
}
|
||||
|
||||
if (this.externalPanel) {
|
||||
this.log.d('Setting EXTERNAL PANEL');
|
||||
this.openviduAngularPanelTemplate = this.externalPanel.template;
|
||||
} else {
|
||||
this.log.d('Setting DEFAULT PANEL');
|
||||
|
||||
if (this.externalParticipantsPanel) {
|
||||
this.openviduAngularParticipantsPanelTemplate = this.externalParticipantsPanel.template;
|
||||
this.log.d('Setting EXTERNAL PARTICIPANTS PANEL');
|
||||
} else {
|
||||
this.log.d('Setting DEFAULT PARTICIPANTS PANEL');
|
||||
if (this.externalParticipantPanelItem) {
|
||||
this.log.d('Setting EXTERNAL P ITEM');
|
||||
this.openviduAngularParticipantPanelItemTemplate = this.externalParticipantPanelItem.template;
|
||||
} else {
|
||||
if (this.externalParticipantPanelItemElements) {
|
||||
this.log.d('Setting EXTERNAL PARTICIPANT PANEL ITEM ELEMENT');
|
||||
this.openviduAngularParticipantPanelItemElementsTemplate = this.externalParticipantPanelItemElements.template;
|
||||
}
|
||||
this.openviduAngularParticipantPanelItemTemplate = this.defaultParticipantPanelItemTemplate;
|
||||
this.log.d('Setting DEFAULT P ITEM');
|
||||
}
|
||||
this.openviduAngularParticipantsPanelTemplate = this.defaultParticipantsPanelTemplate;
|
||||
}
|
||||
|
||||
if (this.externalChatPanel) {
|
||||
this.log.d('Setting EXTERNAL CHAT PANEL');
|
||||
this.openviduAngularChatPanelTemplate = this.externalChatPanel.template;
|
||||
} else {
|
||||
this.log.d('Setting DEFAULT CHAT PANEL');
|
||||
this.openviduAngularChatPanelTemplate = this.defaultChatPanelTemplate;
|
||||
}
|
||||
|
||||
if (this.externalActivitiesPanel) {
|
||||
this.log.d('Setting EXTERNAL ACTIVITIES PANEL');
|
||||
this.openviduAngularActivitiesPanelTemplate = this.externalActivitiesPanel.template;
|
||||
} else {
|
||||
this.log.d('Setting DEFAULT ACTIVITIES PANEL');
|
||||
this.openviduAngularActivitiesPanelTemplate = this.defaultActivitiesPanelTemplate;
|
||||
}
|
||||
|
||||
if (this.externalAdditionalPanels) {
|
||||
this.log.d('Setting EXTERNAL ADDITIONAL PANELS');
|
||||
this.openviduAngularAdditionalPanelsTemplate = this.externalAdditionalPanels.template;
|
||||
}
|
||||
this.openviduAngularPanelTemplate = this.defaultPanelTemplate;
|
||||
}
|
||||
|
||||
if (this.externalLayout) {
|
||||
this.log.d('Setting EXTERNAL LAYOUT');
|
||||
this.openviduAngularLayoutTemplate = this.externalLayout.template;
|
||||
} else {
|
||||
this.log.d('Setting DEAFULT LAYOUT');
|
||||
|
||||
if (this.externalStream) {
|
||||
this.log.d('Setting EXTERNAL STREAM');
|
||||
this.openviduAngularStreamTemplate = this.externalStream.template;
|
||||
} else {
|
||||
this.log.d('Setting DEFAULT STREAM');
|
||||
this.openviduAngularStreamTemplate = this.defaultStreamTemplate;
|
||||
}
|
||||
this.openviduAngularLayoutTemplate = this.defaultLayoutTemplate;
|
||||
}
|
||||
this.deviceSrv.initializeDevices().then(() => (this.loading = false));
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
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.latestParticipantName;
|
||||
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');
|
||||
|
@ -919,68 +532,27 @@ 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');
|
||||
|
||||
if (this.hasUserInitiatedJoin()) {
|
||||
// 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
|
||||
});
|
||||
console.log(this.componentState);
|
||||
console.warn(
|
||||
this.componentState.isRoomReady &&
|
||||
!this.componentState.showPrejoin &&
|
||||
!this.componentState.isLoading &&
|
||||
!this.componentState.error?.hasError
|
||||
);
|
||||
} else {
|
||||
// Only update showPrejoin if user hasn't initiated join process yet
|
||||
// This prevents prejoin from showing again after user clicked join
|
||||
this.updateComponentState({
|
||||
state: VideoconferenceState.PREJOIN_SHOWN,
|
||||
isRoomReady: true,
|
||||
showPrejoin: this.libService.showPrejoin()
|
||||
});
|
||||
}
|
||||
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
|
||||
|
@ -991,11 +563,10 @@ export class VideoconferenceComponent implements OnDestroy, AfterViewInit {
|
|||
this._onReadyToJoin();
|
||||
} else {
|
||||
// No name yet - set up a one-time subscription to wait for it
|
||||
this.libService.participantName$
|
||||
const waitForNameSub = this.libService.participantName$
|
||||
.pipe(
|
||||
filter((name) => !!name),
|
||||
take(1),
|
||||
takeUntil(this.destroy$)
|
||||
take(1)
|
||||
)
|
||||
.subscribe(() => {
|
||||
// Now we have the name in latestParticipantName
|
||||
|
@ -1005,35 +576,24 @@ export class VideoconferenceComponent implements OnDestroy, AfterViewInit {
|
|||
setTimeout(() => {
|
||||
if (!this.latestParticipantName) {
|
||||
this.log.w('No participant name received after timeout, proceeding anyway');
|
||||
waitForNameSub.unsubscribe();
|
||||
const storedName = this.storageSrv.getParticipantName();
|
||||
if (storedName) {
|
||||
this.latestParticipantName = storedName;
|
||||
this.libService.updateGeneralConfig({ participantName: storedName });
|
||||
this.libService.setParticipantName(storedName);
|
||||
}
|
||||
this._onReadyToJoin();
|
||||
}
|
||||
}, VideoconferenceComponent.PARTICIPANT_NAME_TIMEOUT_MS);
|
||||
}, 1000);
|
||||
}
|
||||
}
|
||||
// this.cd.markForCheck();
|
||||
});
|
||||
|
||||
this.libService.participantName$.pipe(takeUntil(this.destroy$)).subscribe((name: string) => {
|
||||
this.participantNameSub = this.libService.participantName$.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
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
@ -49,7 +49,9 @@ export class ActivitiesPanelRecordingActivityDirective implements AfterViewInit,
|
|||
}
|
||||
|
||||
update(value: boolean) {
|
||||
this.libService.updateRecordingActivityConfig({ enabled: value });
|
||||
if (this.libService.showRecordingActivity() !== value) {
|
||||
this.libService.setRecordingActivity(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -101,6 +103,8 @@ export class ActivitiesPanelBroadcastingActivityDirective implements AfterViewIn
|
|||
}
|
||||
|
||||
update(value: boolean) {
|
||||
if (this.libService.showBroadcastingActivity() !== value) {
|
||||
this.libService.setBroadcastingActivity(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -16,17 +16,15 @@ import { OpenViduComponentsConfigService } from '../../services/config/directive
|
|||
standalone: false
|
||||
})
|
||||
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 +38,9 @@ export class AdminDashboardRecordingsListDirective implements AfterViewInit, OnD
|
|||
}
|
||||
|
||||
update(value: RecordingInfo[]) {
|
||||
this.libService.updateAdminConfig({ recordingsList: value });
|
||||
if (this.libService.getAdminRecordingsList() !== value) {
|
||||
this.libService.setAdminRecordingsList(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -58,6 +58,7 @@ export class AdminDashboardRecordingsListDirective implements AfterViewInit, OnD
|
|||
standalone: false
|
||||
})
|
||||
export class AdminDashboardTitleDirective implements AfterViewInit, OnDestroy {
|
||||
|
||||
@Input() set navbarTitle(value: string) {
|
||||
this.navbarTitleValue = value;
|
||||
this.update(this.navbarTitleValue);
|
||||
|
@ -65,10 +66,7 @@ export class AdminDashboardTitleDirective implements AfterViewInit, OnDestroy {
|
|||
|
||||
navbarTitleValue: string = 'OpenVidu Dashboard';
|
||||
|
||||
constructor(
|
||||
public elementRef: ElementRef,
|
||||
private libService: OpenViduComponentsConfigService
|
||||
) {}
|
||||
constructor(public elementRef: ElementRef, private libService: OpenViduComponentsConfigService) {}
|
||||
|
||||
ngAfterViewInit() {
|
||||
this.update(this.navbarTitleValue);
|
||||
|
@ -82,10 +80,13 @@ export class AdminDashboardTitleDirective implements AfterViewInit, OnDestroy {
|
|||
}
|
||||
|
||||
update(value: any) {
|
||||
this.libService.updateAdminConfig({ dashboardTitle: value });
|
||||
if (this.libService.getAdminDashboardTitle() !== value) {
|
||||
this.libService.setAdminDashboardTitle(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* The **navbarTitle** directive allows customize the title of the navbar in {@link AdminLoginComponent}.
|
||||
*
|
||||
|
@ -100,6 +101,7 @@ export class AdminDashboardTitleDirective implements AfterViewInit, OnDestroy {
|
|||
standalone: false
|
||||
})
|
||||
export class AdminLoginTitleDirective implements AfterViewInit, OnDestroy {
|
||||
|
||||
@Input() set navbarTitle(value: any) {
|
||||
this.navbarTitleValue = value;
|
||||
this.update(this.navbarTitleValue);
|
||||
|
@ -107,10 +109,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 +123,14 @@ export class AdminLoginTitleDirective implements AfterViewInit, OnDestroy {
|
|||
}
|
||||
|
||||
update(value: any) {
|
||||
this.libService.updateAdminConfig({ loginTitle: value });
|
||||
if (this.libService.getAdminLoginTitle() !== value) {
|
||||
this.libService.setAdminLoginTitle(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* The **error** directive allows show the authentication error in {@link AdminLoginComponent}.
|
||||
*
|
||||
|
@ -137,11 +140,12 @@ export class AdminLoginTitleDirective implements AfterViewInit, OnDestroy {
|
|||
* <ov-admin-login [error]="error"></ov-admin-login>
|
||||
*
|
||||
*/
|
||||
@Directive({
|
||||
@Directive({
|
||||
selector: 'ov-admin-login[error]',
|
||||
standalone: false
|
||||
})
|
||||
export class AdminLoginErrorDirective implements AfterViewInit, OnDestroy {
|
||||
|
||||
@Input() set error(value: any) {
|
||||
this.errorValue = value;
|
||||
this.update(this.errorValue);
|
||||
|
@ -149,10 +153,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 +167,9 @@ export class AdminLoginErrorDirective implements AfterViewInit, OnDestroy {
|
|||
}
|
||||
|
||||
update(value: any) {
|
||||
this.libService.updateAdminConfig({ loginError: value });
|
||||
if (this.libService.getAdminLoginError() !== value) {
|
||||
this.libService.setAdminLoginError(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,23 +1,16 @@
|
|||
import { NgModule } from '@angular/core';
|
||||
import { ActivitiesPanelBroadcastingActivityDirective, ActivitiesPanelRecordingActivityDirective } from './activities-panel.directive';
|
||||
import {
|
||||
AdminDashboardRecordingsListDirective,
|
||||
AdminDashboardTitleDirective,
|
||||
AdminLoginErrorDirective,
|
||||
AdminLoginTitleDirective
|
||||
AdminDashboardRecordingsListDirective,
|
||||
AdminLoginTitleDirective,
|
||||
AdminDashboardTitleDirective
|
||||
} from './admin.directive';
|
||||
import {
|
||||
FallbackLogoDirective,
|
||||
LayoutRemoteParticipantsDirective,
|
||||
PrejoinDisplayParticipantName,
|
||||
FallbackLogoDirective,
|
||||
ToolbarBrandingLogoDirective,
|
||||
ToolbarViewRecordingsButtonDirective,
|
||||
RecordingActivityReadOnlyDirective,
|
||||
RecordingActivityShowControlsDirective,
|
||||
StartStopRecordingButtonsDirective,
|
||||
RecordingActivityViewRecordingsButtonDirective,
|
||||
RecordingActivityShowRecordingsListDirective,
|
||||
ToolbarRoomNameDirective
|
||||
PrejoinDisplayParticipantName
|
||||
} from './internals.directive';
|
||||
import { ParticipantPanelItemMuteButtonDirective } from './participant-panel-item.directive';
|
||||
import {
|
||||
|
@ -27,21 +20,21 @@ import {
|
|||
} from './stream.directive';
|
||||
import {
|
||||
ToolbarActivitiesPanelButtonDirective,
|
||||
ToolbarAdditionalButtonsPossitionDirective,
|
||||
ToolbarBackgroundEffectsButtonDirective,
|
||||
ToolbarBroadcastingButtonDirective,
|
||||
ToolbarCameraButtonDirective,
|
||||
// ToolbarCaptionsButtonDirective,
|
||||
ToolbarChatPanelButtonDirective,
|
||||
ToolbarDisplayLogoDirective,
|
||||
ToolbarDisplayRoomNameDirective,
|
||||
ToolbarFullscreenButtonDirective,
|
||||
ToolbarLeaveButtonDirective,
|
||||
ToolbarMicrophoneButtonDirective,
|
||||
ToolbarParticipantsPanelButtonDirective,
|
||||
ToolbarRecordingButtonDirective,
|
||||
ToolbarScreenshareButtonDirective,
|
||||
ToolbarSettingsButtonDirective
|
||||
ToolbarSettingsButtonDirective,
|
||||
ToolbarAdditionalButtonsPossitionDirective,
|
||||
ToolbarCameraButtonDirective,
|
||||
ToolbarMicrophoneButtonDirective
|
||||
} from './toolbar.directive';
|
||||
import {
|
||||
AudioEnabledDirective,
|
||||
|
@ -54,7 +47,6 @@ import {
|
|||
ParticipantNameDirective,
|
||||
PrejoinDirective,
|
||||
RecordingStreamBaseUrlDirective,
|
||||
ShowDisconnectionDialogDirective,
|
||||
TokenDirective,
|
||||
TokenErrorDirective,
|
||||
VideoEnabledDirective
|
||||
|
@ -72,10 +64,7 @@ const directives = [
|
|||
PrejoinDirective,
|
||||
PrejoinDisplayParticipantName,
|
||||
VideoEnabledDirective,
|
||||
RecordingActivityReadOnlyDirective,
|
||||
RecordingActivityShowControlsDirective,
|
||||
AudioEnabledDirective,
|
||||
ShowDisconnectionDialogDirective,
|
||||
RecordingStreamBaseUrlDirective,
|
||||
ToolbarCameraButtonDirective,
|
||||
ToolbarMicrophoneButtonDirective,
|
||||
|
@ -93,7 +82,6 @@ const directives = [
|
|||
ToolbarDisplayLogoDirective,
|
||||
ToolbarSettingsButtonDirective,
|
||||
ToolbarAdditionalButtonsPossitionDirective,
|
||||
ToolbarViewRecordingsButtonDirective,
|
||||
StreamDisplayParticipantNameDirective,
|
||||
StreamDisplayAudioDetectionDirective,
|
||||
StreamVideoControlsDirective,
|
||||
|
@ -107,11 +95,7 @@ const directives = [
|
|||
AdminLoginTitleDirective,
|
||||
AdminLoginErrorDirective,
|
||||
AdminDashboardTitleDirective,
|
||||
LayoutRemoteParticipantsDirective,
|
||||
StartStopRecordingButtonsDirective,
|
||||
RecordingActivityViewRecordingsButtonDirective,
|
||||
RecordingActivityShowRecordingsListDirective,
|
||||
ToolbarRoomNameDirective
|
||||
LayoutRemoteParticipantsDirective
|
||||
];
|
||||
|
||||
@NgModule({
|
||||
|
|
|
@ -122,7 +122,7 @@ export class ToolbarBrandingLogoDirective implements AfterViewInit, OnDestroy {
|
|||
}
|
||||
|
||||
private update(value: string) {
|
||||
this.libService.updateToolbarConfig({ brandingLogo: value });
|
||||
this.libService.setBrandingLogo(value);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -158,369 +158,6 @@ export class PrejoinDisplayParticipantName implements OnDestroy {
|
|||
}
|
||||
|
||||
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.setPrejoinDisplayParticipantName(value);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -32,10 +32,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 +46,8 @@ export class ParticipantPanelItemMuteButtonDirective implements AfterViewInit, O
|
|||
}
|
||||
|
||||
update(value: boolean) {
|
||||
this.libService.updateStreamConfig({ participantItemMuteButton: value });
|
||||
if (this.libService.showParticipantItemMuteButton() !== value) {
|
||||
this.libService.setParticipantItemMuteButton(value);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -46,7 +46,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() {
|
||||
|
@ -98,7 +100,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);
|
||||
|
@ -150,7 +154,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() {
|
||||
|
|
|
@ -62,7 +62,9 @@ export class ToolbarCameraButtonDirective implements AfterViewInit, OnDestroy {
|
|||
}
|
||||
|
||||
private update(value: boolean) {
|
||||
this.libService.updateToolbarConfig({ camera: value });
|
||||
if (this.libService.showCameraButton() !== value) {
|
||||
this.libService.setCameraButton(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -126,7 +128,9 @@ export class ToolbarMicrophoneButtonDirective implements AfterViewInit, OnDestro
|
|||
}
|
||||
|
||||
private update(value: boolean) {
|
||||
this.libService.updateToolbarConfig({ microphone: value });
|
||||
if (this.libService.showMicrophoneButton() !== value) {
|
||||
this.libService.setMicrophoneButton(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -190,7 +194,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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -251,7 +257,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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -313,8 +321,10 @@ export class ToolbarBroadcastingButtonDirective implements AfterViewInit, OnDest
|
|||
}
|
||||
|
||||
private update(value: boolean) {
|
||||
if (this.libService.showBroadcastingButton() !== value) {
|
||||
this.libService.setBroadcastingButton(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -374,7 +384,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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -435,7 +447,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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -555,7 +569,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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -617,7 +633,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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -680,7 +698,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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -741,7 +761,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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -802,7 +824,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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -864,7 +888,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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -926,7 +952,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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -981,6 +1009,8 @@ export class ToolbarAdditionalButtonsPossitionDirective implements AfterViewInit
|
|||
}
|
||||
|
||||
private update(value: ToolbarAdditionalButtonsPosition) {
|
||||
this.libService.updateToolbarConfig({ additionalButtonsPosition: value });
|
||||
if (this.libService.getToolbarAdditionalButtonsPosition() !== value) {
|
||||
this.libService.setToolbarAdditionalButtonsPosition(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -55,7 +55,7 @@ export class LivekitUrlDirective implements OnDestroy {
|
|||
* @ignore
|
||||
*/
|
||||
update(value: string) {
|
||||
this.libService.updateGeneralConfig({ livekitUrl: value });
|
||||
this.libService.setLivekitUrl(value);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -108,7 +108,7 @@ export class TokenDirective implements OnDestroy {
|
|||
* @ignore
|
||||
*/
|
||||
update(value: string) {
|
||||
this.libService.updateGeneralConfig({ token: value });
|
||||
this.libService.setToken(value);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -160,7 +160,7 @@ export class TokenErrorDirective implements OnDestroy {
|
|||
* @ignore
|
||||
*/
|
||||
update(value: any) {
|
||||
this.libService.updateGeneralConfig({ tokenError: value });
|
||||
this.libService.setTokenError(value);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -212,7 +212,9 @@ export class MinimalDirective implements OnDestroy {
|
|||
* @ignore
|
||||
*/
|
||||
update(value: boolean) {
|
||||
this.libService.updateGeneralConfig({ minimal: value });
|
||||
if (this.libService.isMinimal() !== value) {
|
||||
this.libService.setMinimal(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -536,7 +538,7 @@ export class ParticipantNameDirective implements AfterViewInit, OnDestroy {
|
|||
* @ignore
|
||||
*/
|
||||
update(value: string) {
|
||||
if (value) this.libService.updateGeneralConfig({ participantName: value });
|
||||
if (value) this.libService.setParticipantName(value);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -588,7 +590,9 @@ export class PrejoinDirective implements OnDestroy {
|
|||
* @ignore
|
||||
*/
|
||||
update(value: boolean) {
|
||||
this.libService.updateGeneralConfig({ prejoin: value });
|
||||
if (this.libService.isPrejoin() !== value) {
|
||||
this.libService.setPrejoin(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -659,7 +663,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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -727,61 +731,7 @@ export class AudioEnabledDirective implements OnDestroy {
|
|||
this.storageService.setMicrophoneEnabled(finalEnabledState);
|
||||
|
||||
if (this.libService.isAudioEnabled() !== enabled) {
|
||||
this.libService.updateStreamConfig({ audioEnabled: enabled });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The **showDisconnectionDialog** directive allows to show/hide the disconnection dialog when the local participant is disconnected from the room.
|
||||
*
|
||||
* It is only available for {@link VideoconferenceComponent}.
|
||||
*
|
||||
* Default: `true`
|
||||
*
|
||||
* @example
|
||||
* <ov-videoconference [showDisconnectionDialog]="false"></ov-videoconference>
|
||||
*/
|
||||
@Directive({
|
||||
selector: 'ov-videoconference[showDisconnectionDialog]',
|
||||
standalone: false
|
||||
})
|
||||
export class ShowDisconnectionDialogDirective implements OnDestroy {
|
||||
/**
|
||||
* @ignore
|
||||
*/
|
||||
@Input() set showDisconnectionDialog(value: boolean) {
|
||||
this.update(value);
|
||||
}
|
||||
|
||||
/**
|
||||
* @ignore
|
||||
*/
|
||||
constructor(
|
||||
public elementRef: ElementRef,
|
||||
private libService: OpenViduComponentsConfigService
|
||||
) {}
|
||||
|
||||
/**
|
||||
* @ignore
|
||||
*/
|
||||
ngOnDestroy(): void {
|
||||
this.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* @ignore
|
||||
*/
|
||||
clear() {
|
||||
this.update(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* @ignore
|
||||
*/
|
||||
update(value: boolean) {
|
||||
if (this.libService.getShowDisconnectionDialog() !== value) {
|
||||
this.libService.updateGeneralConfig({ showDisconnectionDialog: value });
|
||||
this.libService.setAudioEnabled(enabled);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -853,6 +803,6 @@ export class RecordingStreamBaseUrlDirective implements AfterViewInit, OnDestroy
|
|||
* @ignore
|
||||
*/
|
||||
update(value: string) {
|
||||
if (value) this.libService.updateGeneralConfig({ recordingStreamBaseUrl: value });
|
||||
if (value) this.libService.setRecordingStreamBaseUrl(value);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
) {}
|
||||
}
|
|
@ -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
|
||||
]
|
||||
})
|
||||
|
|
|
@ -1814,4 +1814,3 @@ export class StreamDirective {
|
|||
public container: ViewContainerRef
|
||||
) {}
|
||||
}
|
||||
|
||||
|
|
|
@ -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": "设置",
|
||||
|
@ -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": "直播",
|
||||
|
|
|
@ -55,8 +55,7 @@
|
|||
"LEAVE": "Die Raum 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",
|
||||
|
@ -90,9 +89,7 @@
|
|||
"MICROPHONE": "MIKROFON",
|
||||
"SCREEN": "BILDSCHIRM",
|
||||
"NO_STREAMS": "KEINE",
|
||||
"YOU": "Sie",
|
||||
"MUTE": "Stummschalten",
|
||||
"UNMUTE": "Stummschaltung aufheben"
|
||||
"YOU": "Sie"
|
||||
},
|
||||
"SETTINGS": {
|
||||
"TITLE": "Einstellungen",
|
||||
|
@ -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",
|
||||
|
|
|
@ -55,9 +55,7 @@
|
|||
"LEAVE": "Leave the room",
|
||||
"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",
|
||||
|
@ -91,9 +89,7 @@
|
|||
"MICROPHONE": "MICROPHONE",
|
||||
"SCREEN": "SCREEN",
|
||||
"NO_STREAMS": "NONE",
|
||||
"YOU": "You",
|
||||
"MUTE": "Mute",
|
||||
"UNMUTE": "Unmute"
|
||||
"YOU": "You"
|
||||
},
|
||||
"SETTINGS": {
|
||||
"TITLE": "Settings",
|
||||
|
@ -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",
|
||||
|
|
|
@ -55,9 +55,7 @@
|
|||
"LEAVE": "Salir de la sala",
|
||||
"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",
|
||||
|
@ -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",
|
||||
|
@ -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",
|
||||
|
|
|
@ -55,9 +55,7 @@
|
|||
"LEAVE": "Quitter la salle",
|
||||
"PARTICIPANTS": "Participants",
|
||||
"CHAT": "Chat",
|
||||
"ACTIVITES": "Activités",
|
||||
"NO_TRACKS_PUBLISHED": "Partagez de l’audio ou de la vidéo pour commencer l’enregistrement.",
|
||||
"VIEW_RECORDINGS": "Voir les enregistrements"
|
||||
"ACTIVITES": "Activités"
|
||||
},
|
||||
"STREAM": {
|
||||
"SETTINGS": "Paramètres",
|
||||
|
@ -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",
|
||||
|
@ -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 l’audio ou de la vidéo pour commencer l’enregistrement.",
|
||||
"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",
|
||||
|
|
|
@ -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": "सेटिंग्स",
|
||||
|
@ -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": "स्ट्रीमिंग",
|
||||
|
|
|
@ -55,9 +55,7 @@
|
|||
"LEAVE": "Abbandona la stanza",
|
||||
"PARTICIPANTS": "Partecipanti",
|
||||
"CHAT": "Chat",
|
||||
"ACTIVITIES": "Attività",
|
||||
"NO_TRACKS_PUBLISHED": "Condividi audio o video per iniziare la registrazione.",
|
||||
"VIEW_RECORDINGS": "Visualizza registrazioni"
|
||||
"ACTIVITIES": "Attività"
|
||||
},
|
||||
"STREAM": {
|
||||
"SETTINGS": "Impostazioni",
|
||||
|
@ -91,9 +89,7 @@
|
|||
"MICROPHONE": "MICROFONO",
|
||||
"SCREEN": "SCREEN",
|
||||
"NO_STREAMS": "NESSUNO",
|
||||
"YOU": "Tu",
|
||||
"MUTE": "Disattiva l'audio",
|
||||
"UNMUTE": "Attiva l'audio"
|
||||
"YOU": "Tu"
|
||||
},
|
||||
"SETTINGS": {
|
||||
"TITLE": "Impostazioni",
|
||||
|
@ -118,10 +114,6 @@
|
|||
"SUBTITLE": "Registra la tua riunione per i posteri",
|
||||
"CONTENT_TITLE": "Registra la tua videochiamata",
|
||||
"CONTENT_SUBTITLE": "Al termine della registrazione potrete scaricarla con facilità",
|
||||
"VIEW_ONLY_SUBTITLE": "Visualizza e accedi alle registrazioni della sala",
|
||||
"VIEW_ONLY_CONTENT_TITLE": "Registrazioni di videochiamate",
|
||||
"VIEW_ONLY_CONTENT_SUBTITLE": "Qui puoi accedere a tutte le registrazioni disponibili",
|
||||
"WATCH": "Guardare",
|
||||
"STARTING": "Avvio della registrazione",
|
||||
"STOPPING": "Interruzione della registrazione",
|
||||
"IN_PROGRESS": "Registrazione in corso",
|
||||
|
@ -132,10 +124,7 @@
|
|||
"DELETE_QUESTION": "Sei sicuro di voler eliminare la registrazione?",
|
||||
"DOWNLOAD": "Scarica",
|
||||
"RECORDINGS": "REGISTRAZIONI",
|
||||
"NO_MODERATOR": "Solo il MODERATORE può avviare la registrazione",
|
||||
"NO_TRACKS_PUBLISHED": "Condividi audio o video per iniziare la registrazione.",
|
||||
"NO_RECORDINGS_AVAILABLE": "Nessuna registrazione disponibile al momento",
|
||||
"ERROR_STARTING": "Errore di avvio"
|
||||
"NO_MODERATOR": "Solo il MODERATORE può avviare la registrazione"
|
||||
},
|
||||
"STREAMING": {
|
||||
"TITLE": "Streaming",
|
||||
|
|
|
@ -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": "設定",
|
||||
|
@ -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": "ストリーミング",
|
||||
|
|
|
@ -55,9 +55,7 @@
|
|||
"LEAVE": "Verlaat de kamer",
|
||||
"PARTICIPANTS": "Deelnemers",
|
||||
"CHAT": "Chat",
|
||||
"ACTIVITIES": "Activiteiten",
|
||||
"NO_TRACKS_PUBLISHED": "Deel audio of video om met opnemen te beginnen.",
|
||||
"VIEW_RECORDINGS": "Opnames bekijken"
|
||||
"ACTIVITIES": "Activiteiten"
|
||||
},
|
||||
"STREAM": {
|
||||
"SETTINGS": "Instellingen",
|
||||
|
@ -91,9 +89,7 @@
|
|||
"MICROPHONE": "MICROFOON",
|
||||
"SCREEN": "SCHERM",
|
||||
"NO_STREAMS": "GEEN",
|
||||
"YOU": "Jij",
|
||||
"MUTE": "Dempen",
|
||||
"UNMUTE": "Dempen opheffen"
|
||||
"YOU": "Jij"
|
||||
},
|
||||
"SETTINGS": {
|
||||
"TITLE": "Instellingen",
|
||||
|
@ -118,10 +114,6 @@
|
|||
"SUBTITLE": "Neem uw vergadering op voor het nageslacht",
|
||||
"CONTENT_TITLE": "Neem uw videogesprek op",
|
||||
"CONTENT_SUBTITLE": "Als de opname klaar is kunt u deze met gemak downloaden",
|
||||
"VIEW_ONLY_SUBTITLE": "Bekijk en toegang tot kameropnames",
|
||||
"VIEW_ONLY_CONTENT_TITLE": "Videogesprek opnames",
|
||||
"VIEW_ONLY_CONTENT_SUBTITLE": "Hier heeft u toegang tot alle beschikbare opnames",
|
||||
"WATCH": "Bekijken",
|
||||
"STARTING": "Beginnen met opnemen",
|
||||
"STOPPING": "Opname stoppen",
|
||||
"IN_PROGRESS": "Opname in uitvoering",
|
||||
|
@ -132,10 +124,7 @@
|
|||
"DELETE_QUESTION": "Weet je zeker dat je de opname wilt verwijderen?",
|
||||
"DOWNLOAD": "Downloaden",
|
||||
"RECORDINGS": "OPNAME",
|
||||
"NO_MODERATOR": "Alleen de MOEDERATOR kan de opname starten",
|
||||
"NO_TRACKS_PUBLISHED": "Deel audio of video om met opnemen te beginnen.",
|
||||
"NO_RECORDINGS_AVAILABLE": "Momenteel zijn er geen opnames beschikbaar",
|
||||
"ERROR_STARTING": "Fout bij starten opname"
|
||||
"NO_MODERATOR": "Alleen de MOEDERATOR kan de opname starten"
|
||||
},
|
||||
"STREAMING": {
|
||||
"TITLE": "Streaming",
|
||||
|
|
|
@ -55,9 +55,7 @@
|
|||
"LEAVE": "Sair da sala",
|
||||
"PARTICIPANTS": "Participantes",
|
||||
"CHAT": "Chat",
|
||||
"ACTIVITIES": "Actividades",
|
||||
"NO_TRACKS_PUBLISHED": "Compartilhe áudio ou vídeo para começar a gravar.",
|
||||
"VIEW_RECORDINGS": "Ver gravações"
|
||||
"ACTIVITIES": "Actividades"
|
||||
},
|
||||
"STREAM": {
|
||||
"SETTINGS": "Configurações",
|
||||
|
@ -91,9 +89,7 @@
|
|||
"MICROPHONE": "MICROFONE",
|
||||
"SCREEN": "TELA",
|
||||
"NO_STREAMS": "NENHUM",
|
||||
"YOU": "Você (eu)",
|
||||
"MUTE": "Silenciar",
|
||||
"UNMUTE": "Ativar som"
|
||||
"YOU": "Você (eu)"
|
||||
},
|
||||
"SETTINGS": {
|
||||
"TITLE": "Configurações",
|
||||
|
@ -118,10 +114,6 @@
|
|||
"SUBTITLE": "Grave a sua reunião para a posteridade",
|
||||
"CONTENT_TITLE": "Grave a sua videochamada",
|
||||
"CONTENT_SUBTITLE": "Quando a gravação tiver terminado, poderá descarregá-la com facilidade",
|
||||
"VIEW_ONLY_SUBTITLE": "Visualize e acesse gravações da sala",
|
||||
"VIEW_ONLY_CONTENT_TITLE": "Gravações de videochamada",
|
||||
"VIEW_ONLY_CONTENT_SUBTITLE": "Aqui você pode acessar todas as gravações disponíveis",
|
||||
"WATCH": "Assistir",
|
||||
"STARTING": "Começar a gravação",
|
||||
"STOPPING": "Parando a gravação",
|
||||
"IN_PROGRESS": "Gravação em andamento",
|
||||
|
@ -132,10 +124,7 @@
|
|||
"DELETE_QUESTION": "Tem certeza de que deseja excluir a gravação?",
|
||||
"DOWNLOAD": "Download",
|
||||
"RECORDINGS": "GRAVAÇÕES",
|
||||
"NO_MODERATOR": "Só o MODERADOR pode iniciar a gravação",
|
||||
"NO_TRACKS_PUBLISHED": "Compartilhe áudio ou vídeo para começar a gravar.",
|
||||
"NO_RECORDINGS_AVAILABLE": "Nenhuma gravação disponível no momento",
|
||||
"ERROR_STARTING": "Erro ao iniciar gravação"
|
||||
"NO_MODERATOR": "Só o MODERADOR pode iniciar a gravação"
|
||||
},
|
||||
"STREAMING": {
|
||||
"TITLE": "Streaming",
|
||||
|
|
|
@ -3,7 +3,6 @@
|
|||
*/
|
||||
export interface ILogger {
|
||||
d(...args: any[]): void;
|
||||
v(...args: any[]): void;
|
||||
w(...args: any[]): void;
|
||||
e(...args: any[]): void;
|
||||
}
|
||||
|
|
|
@ -18,7 +18,6 @@ import {
|
|||
export interface ParticipantLeftEvent {
|
||||
roomName: string;
|
||||
participantName: string;
|
||||
identity: string;
|
||||
reason: ParticipantLeftReason;
|
||||
}
|
||||
|
||||
|
|
|
@ -21,7 +21,7 @@ export enum RecordingOutputMode {
|
|||
export interface RecordingStatusInfo {
|
||||
status: RecordingStatus;
|
||||
recordingList: RecordingInfo[];
|
||||
startedAt?: Date;
|
||||
recordingElapsedTime?: Date;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
|
|
|
@ -9,36 +9,7 @@ export enum StorageKeys {
|
|||
CAMERA_ENABLED = 'cameraEnabled',
|
||||
LANG = 'lang',
|
||||
CAPTION_LANG = 'captionLang',
|
||||
BACKGROUND = 'virtualBg',
|
||||
TAB_ID = 'tabId',
|
||||
ACTIVE_TABS = 'activeTabs'
|
||||
BACKGROUND = "virtualBg"
|
||||
}
|
||||
|
||||
export const PERSISTENT_KEYS: StorageKeys[] = [
|
||||
StorageKeys.VIDEO_DEVICE,
|
||||
StorageKeys.AUDIO_DEVICE,
|
||||
StorageKeys.LANG,
|
||||
StorageKeys.CAPTION_LANG,
|
||||
StorageKeys.BACKGROUND
|
||||
];
|
||||
|
||||
export const SESSION_KEYS: StorageKeys[] = [StorageKeys.TAB_ID];
|
||||
|
||||
export const TAB_MANAGEMENT_KEYS: StorageKeys[] = [StorageKeys.TAB_ID, StorageKeys.ACTIVE_TABS];
|
||||
|
||||
// Data that should be unique per tab (stored in localStorage with tabId prefix)
|
||||
export const TAB_SPECIFIC_KEYS: StorageKeys[] = [
|
||||
StorageKeys.PARTICIPANT_NAME,
|
||||
StorageKeys.MICROPHONE_ENABLED,
|
||||
StorageKeys.CAMERA_ENABLED,
|
||||
StorageKeys.LANG,
|
||||
StorageKeys.CAPTION_LANG,
|
||||
StorageKeys.BACKGROUND,
|
||||
StorageKeys.VIDEO_DEVICE,
|
||||
StorageKeys.AUDIO_DEVICE
|
||||
];
|
||||
|
||||
// Data that should be truly persistent and shared between tabs
|
||||
export const SHARED_PERSISTENT_KEYS: StorageKeys[] = [];
|
||||
|
||||
export const STORAGE_PREFIX = 'ovComponents-';
|
||||
|
|
|
@ -1,98 +0,0 @@
|
|||
/**
|
||||
* Enum representing the possible states of the videoconference component
|
||||
*/
|
||||
export enum VideoconferenceState {
|
||||
/**
|
||||
* Initial state when the component is loading
|
||||
*/
|
||||
INITIALIZING = 'INITIALIZING',
|
||||
|
||||
/**
|
||||
* Prejoin page is being shown to the user
|
||||
*/
|
||||
PREJOIN_SHOWN = 'PREJOIN_SHOWN',
|
||||
|
||||
/**
|
||||
* User has initiated the join process, waiting for token
|
||||
*/
|
||||
JOINING = 'JOINING',
|
||||
|
||||
/**
|
||||
* Token received and room is ready to connect
|
||||
*/
|
||||
READY_TO_CONNECT = 'READY_TO_CONNECT',
|
||||
|
||||
/**
|
||||
* Successfully connected to the room
|
||||
*/
|
||||
CONNECTED = 'CONNECTED',
|
||||
|
||||
/**
|
||||
* Disconnected from the room
|
||||
*/
|
||||
DISCONNECTED = 'DISCONNECTED',
|
||||
|
||||
/**
|
||||
* Error state
|
||||
*/
|
||||
ERROR = 'ERROR'
|
||||
}
|
||||
|
||||
/**
|
||||
* Interface representing the state information of the videoconference component
|
||||
*/
|
||||
export interface VideoconferenceStateInfo {
|
||||
/**
|
||||
* Current state of the videoconference
|
||||
*/
|
||||
state: VideoconferenceState;
|
||||
|
||||
/**
|
||||
* Whether prejoin page should be visible
|
||||
*/
|
||||
showPrejoin: boolean;
|
||||
|
||||
/**
|
||||
* Whether room is ready for connection
|
||||
*/
|
||||
isRoomReady: boolean;
|
||||
|
||||
/**
|
||||
* Whether user is connected to the room
|
||||
*/
|
||||
isConnected: boolean;
|
||||
|
||||
/**
|
||||
* Whether audio devices are available
|
||||
*/
|
||||
hasAudioDevices: boolean;
|
||||
|
||||
/**
|
||||
* Whether video devices are available
|
||||
*/
|
||||
hasVideoDevices: boolean;
|
||||
|
||||
/**
|
||||
* Whether user has initiated the join process
|
||||
*/
|
||||
hasUserInitiatedJoin: boolean;
|
||||
|
||||
/**
|
||||
* Whether prejoin was shown to the user at least once
|
||||
*/
|
||||
wasPrejoinShown: boolean;
|
||||
|
||||
/**
|
||||
* Whether the component is in loading state
|
||||
*/
|
||||
isLoading: boolean;
|
||||
|
||||
/**
|
||||
* Error information if any
|
||||
*/
|
||||
error?: {
|
||||
hasError: boolean;
|
||||
message?: string;
|
||||
tokenError?: any;
|
||||
};
|
||||
}
|
File diff suppressed because it is too large
Load Diff
|
@ -180,59 +180,23 @@ export class DeviceService {
|
|||
*/
|
||||
private async getLocalDevices(): Promise<MediaDeviceInfo[]> {
|
||||
// Forcing media permissions request.
|
||||
const strategies = [
|
||||
{ audio: true, video: true },
|
||||
{ audio: true, video: false },
|
||||
{ audio: false, video: true }
|
||||
];
|
||||
|
||||
for (const strategy of strategies) {
|
||||
let localTracks: LocalTrack[] = [];
|
||||
try {
|
||||
this.log.d(`Trying strategy: audio=${strategy.audio}, video=${strategy.video}`);
|
||||
const localTracks = await createLocalTracks(strategy);
|
||||
localTracks = await createLocalTracks({ audio: true, video: true });
|
||||
localTracks.forEach((track) => track.stop());
|
||||
|
||||
// Permission granted
|
||||
const devices = this.platformSrv.isFirefox() ? await this.getMediaDevicesFirefox() : await Room.getLocalDevices();
|
||||
|
||||
return devices.filter((d: MediaDeviceInfo) => d.label && d.deviceId && d.deviceId !== 'default');
|
||||
} catch (error: any) {
|
||||
this.log.w(`Strategy failed: audio=${strategy.audio}, video=${strategy.video}`, error);
|
||||
|
||||
// If it's the last attempt and failed, we handle the error
|
||||
if (strategy === strategies[strategies.length - 1]) {
|
||||
return await this.handleFinalFallback(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
this.log.e('Error getting local devices', error);
|
||||
this.deviceAccessDeniedError = true;
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
private async getMediaDevicesFirefox(): Promise<MediaDeviceInfo[]> {
|
||||
// Firefox requires to get user media to get the devices
|
||||
await navigator.mediaDevices.getUserMedia({ audio: true, video: true });
|
||||
return navigator.mediaDevices.enumerateDevices();
|
||||
}
|
||||
|
||||
private async handleFinalFallback(error: any): Promise<MediaDeviceInfo[]> {
|
||||
this.log.w('All permission strategies failed, trying device enumeration without permissions');
|
||||
|
||||
try {
|
||||
if (error?.name === 'NotReadableError' || error?.name === 'AbortError') {
|
||||
this.log.w('Device busy, using enumerateDevices() instead');
|
||||
const devices = await navigator.mediaDevices.enumerateDevices();
|
||||
return devices.filter((d) => d.deviceId && d.deviceId !== 'default');
|
||||
}
|
||||
if (error?.name === 'NotAllowedError' || error?.name === 'SecurityError') {
|
||||
this.log.w('Permission denied to access devices');
|
||||
this.deviceAccessDeniedError = true;
|
||||
}
|
||||
return [];
|
||||
} catch (error) {
|
||||
this.log.e('Complete failure getting devices', error);
|
||||
this.deviceAccessDeniedError = true;
|
||||
return [];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import { Injectable } from '@angular/core';
|
||||
import { ILogService, ILogger } from '../../models/logger.model';
|
||||
import { ILogService } from '../../models/logger.model';
|
||||
|
||||
import { GlobalConfigService } from '../config/global-config.service';
|
||||
|
||||
/**
|
||||
|
@ -9,87 +10,42 @@ import { GlobalConfigService } from '../config/global-config.service';
|
|||
providedIn: 'root'
|
||||
})
|
||||
export class LoggerService implements ILogService {
|
||||
private log: Console;
|
||||
private LOG_FNS: Function[] = [];
|
||||
private MSG_PREFIXES: string[][] = [
|
||||
['[', '] DEBUG: '],
|
||||
['[', '] VERBOSE: '],
|
||||
public log;
|
||||
public LOG_FNS = [];
|
||||
public MSG_PREFIXES = [
|
||||
['[', ']'],
|
||||
['[', '] WARN: '],
|
||||
['[', '] ERROR: ']
|
||||
];
|
||||
private loggerCache: Map<string, ILogger> = new Map();
|
||||
|
||||
constructor(private globalService: GlobalConfigService) {
|
||||
this.initializeLogger();
|
||||
}
|
||||
|
||||
private initializeLogger(): void {
|
||||
private getLoggerFns(prefix: string) {
|
||||
this.log = window.console;
|
||||
this.LOG_FNS = [
|
||||
this.log.log.bind(this.log),
|
||||
this.log.debug.bind(this.log),
|
||||
this.log.warn.bind(this.log),
|
||||
this.log.error.bind(this.log)
|
||||
];
|
||||
this.LOG_FNS = [this.log.log, this.log.warn, this.log.error];
|
||||
const loggerFns = this.LOG_FNS.map((logTemplFn, i) => {
|
||||
return logTemplFn.bind(this.log, this.MSG_PREFIXES[i][0] + prefix + this.MSG_PREFIXES[i][1]);
|
||||
});
|
||||
return loggerFns;
|
||||
}
|
||||
|
||||
private createLoggerFunctions(
|
||||
prefix: string
|
||||
): [(...args: any[]) => void, (...args: any[]) => void, (...args: any[]) => void, (...args: any[]) => void] {
|
||||
public get(prefix: string) {
|
||||
const prodMode = this.globalService.isProduction();
|
||||
|
||||
const debugFn = (...args: any[]): void => {
|
||||
const loggerService = this;
|
||||
return {
|
||||
d: function(...args: any[]) {
|
||||
if (!prodMode) {
|
||||
// Only log debug messages in non-production mode
|
||||
this.LOG_FNS[0](this.MSG_PREFIXES[0][0] + prefix + this.MSG_PREFIXES[0][1], ...args);
|
||||
loggerService.getLoggerFns(prefix)[0].apply(this.log, arguments);
|
||||
}
|
||||
},
|
||||
w: function(...args: any[]) {
|
||||
loggerService.getLoggerFns(prefix)[1].apply(this.log, arguments);
|
||||
|
||||
},
|
||||
e: function(...args: any[]) {
|
||||
loggerService.getLoggerFns(prefix)[2].apply(this.log, arguments);
|
||||
}
|
||||
};
|
||||
|
||||
const verboseFn = (...args: any[]): void => {
|
||||
if (!prodMode) {
|
||||
// Only log verbose messages in non-production mode and when verbose is enabled
|
||||
this.LOG_FNS[1](this.MSG_PREFIXES[1][0] + prefix + this.MSG_PREFIXES[1][1], ...args);
|
||||
}
|
||||
};
|
||||
|
||||
const warnFn = (...args: any[]): void => {
|
||||
this.LOG_FNS[2](this.MSG_PREFIXES[2][0] + prefix + this.MSG_PREFIXES[2][1], ...args);
|
||||
};
|
||||
|
||||
const errorFn = (...args: any[]): void => {
|
||||
this.LOG_FNS[3](this.MSG_PREFIXES[3][0] + prefix + this.MSG_PREFIXES[3][1], ...args);
|
||||
};
|
||||
|
||||
return [debugFn, verboseFn, warnFn, errorFn];
|
||||
}
|
||||
|
||||
public get(prefix: string): ILogger {
|
||||
// Check cache first
|
||||
if (this.loggerCache.has(prefix)) {
|
||||
return this.loggerCache.get(prefix)!;
|
||||
}
|
||||
|
||||
// Create new logger functions
|
||||
const [debugFn, verboseFn, warnFn, errorFn] = this.createLoggerFunctions(prefix);
|
||||
|
||||
const logger: ILogger = {
|
||||
d: debugFn,
|
||||
v: verboseFn,
|
||||
w: warnFn,
|
||||
e: errorFn
|
||||
};
|
||||
|
||||
// Cache the logger
|
||||
this.loggerCache.set(prefix, logger);
|
||||
|
||||
return logger;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears the logger cache. Useful for testing or when configuration changes.
|
||||
* @internal
|
||||
*/
|
||||
public clearCache(): void {
|
||||
this.loggerCache.clear();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -64,12 +64,6 @@ export class OpenViduService {
|
|||
* @internal
|
||||
*/
|
||||
initRoom(): void {
|
||||
// If room already exists, don't recreate it
|
||||
if (this.room) {
|
||||
this.log.d('Room already initialized, skipping re-initialization');
|
||||
return;
|
||||
}
|
||||
|
||||
const videoDeviceId = this.deviceService.getCameraSelected()?.device ?? undefined;
|
||||
const audioDeviceId = this.deviceService.getMicrophoneSelected()?.device ?? undefined;
|
||||
const roomOptions: RoomOptions = {
|
||||
|
@ -94,7 +88,6 @@ export class OpenViduService {
|
|||
disconnectOnPageLeave: true
|
||||
};
|
||||
this.room = new Room(roomOptions);
|
||||
this.log.d('Room initialized successfully');
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -137,20 +130,12 @@ export class OpenViduService {
|
|||
*/
|
||||
getRoom(): Room {
|
||||
if (!this.room) {
|
||||
this.log.e('Room is not initialized. Make sure token is set before accessing the room.');
|
||||
throw new Error('Room is not initialized. Make sure token is set before accessing the room.');
|
||||
this.log.e('Room is not initialized');
|
||||
throw new Error('Room is not initialized');
|
||||
}
|
||||
return this.room;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if room is initialized without throwing an error
|
||||
* @returns true if room is initialized, false otherwise
|
||||
*/
|
||||
isRoomInitialized(): boolean {
|
||||
return !!this.room;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the room name
|
||||
*/
|
||||
|
@ -166,37 +151,18 @@ export class OpenViduService {
|
|||
return this.room?.state === ConnectionState.Connected;
|
||||
}
|
||||
|
||||
hasRoomTracksPublished(): boolean {
|
||||
const { localParticipant, remoteParticipants } = this.getRoom();
|
||||
const localTracks = localParticipant.getTrackPublications();
|
||||
const remoteTracks = Array.from(remoteParticipants.values()).flatMap((p) => p.getTrackPublications());
|
||||
|
||||
return localTracks.length > 0 || remoteTracks.length > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
initializeAndSetToken(token: string, livekitUrl?: string): void {
|
||||
const { livekitUrl: urlFromToken } = this.extractLivekitData(token);
|
||||
|
||||
initializeAndSetToken(token: string, livekitUrl: string): void {
|
||||
const livekitData = this.extractLivekitData(token, livekitUrl);
|
||||
this.livekitToken = token;
|
||||
const url = livekitUrl || urlFromToken;
|
||||
this.livekitUrl = livekitData.livekitUrl;
|
||||
|
||||
if (!url) {
|
||||
if (!this.livekitUrl) {
|
||||
this.log.e('LiveKit URL is not defined. Please, check the livekitUrl parameter of the VideoConferenceComponent');
|
||||
throw new Error('Livekit URL is not defined');
|
||||
}
|
||||
|
||||
this.livekitUrl = url;
|
||||
// this.livekitRoomAdmin = !!livekitRoomAdmin;
|
||||
|
||||
// Initialize room if it doesn't exist yet
|
||||
// This ensures that getRoom() won't fail if token is set before onTokenRequested
|
||||
if (!this.room) {
|
||||
this.log.d('Room not initialized yet, initializing room due to token assignment');
|
||||
this.initRoom();
|
||||
}
|
||||
// return this.room.prepareConnection(this.livekitUrl, this.livekitToken);
|
||||
}
|
||||
|
||||
|
@ -208,7 +174,7 @@ export class OpenViduService {
|
|||
* @internal
|
||||
*/
|
||||
setLocalTracks(tracks: LocalTrack[]): void {
|
||||
this.localTracks = tracks.filter((track) => track !== undefined) as LocalTrack[];
|
||||
this.localTracks = tracks;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -235,29 +201,30 @@ export class OpenViduService {
|
|||
*
|
||||
* @param videoDeviceId - The ID of the video device to use. If not provided, the default video device will be used.
|
||||
* @param audioDeviceId - The ID of the audio device to use. If not provided, the default audio device will be used.
|
||||
* @param allowPartialCreation - If true, allows creating tracks even if some devices fail
|
||||
* @returns A promise that resolves to an array of LocalTrack objects representing the created tracks.
|
||||
* @internal
|
||||
*/
|
||||
async createLocalTracks(
|
||||
videoDeviceId: string | boolean | undefined = undefined,
|
||||
audioDeviceId: string | boolean | undefined = undefined,
|
||||
allowPartialCreation: boolean = true
|
||||
audioDeviceId: string | boolean | undefined = undefined
|
||||
): Promise<LocalTrack[]> {
|
||||
// Default values: true if device is enabled, false otherwise
|
||||
videoDeviceId ??= this.deviceService.isCameraEnabled();
|
||||
audioDeviceId ??= this.deviceService.isMicrophoneEnabled();
|
||||
// If video and audio device IDs are not provided, check if they are enabled and use the default devices
|
||||
if (videoDeviceId === undefined) videoDeviceId = this.deviceService.isCameraEnabled();
|
||||
if (audioDeviceId === undefined) audioDeviceId = this.deviceService.isMicrophoneEnabled();
|
||||
|
||||
const options: CreateLocalTracksOptions = {
|
||||
let options: CreateLocalTracksOptions = {
|
||||
audio: { echoCancellation: true, noiseSuppression: true },
|
||||
video: {}
|
||||
};
|
||||
|
||||
// Video device
|
||||
if (videoDeviceId === true) {
|
||||
options.video = this.deviceService.hasVideoDeviceAvailable()
|
||||
? { deviceId: this.deviceService.getCameraSelected()?.device || 'default' }
|
||||
: false;
|
||||
if (this.deviceService.hasVideoDeviceAvailable()) {
|
||||
videoDeviceId = this.deviceService.getCameraSelected()?.device || 'default';
|
||||
(options.video as VideoCaptureOptions).deviceId = videoDeviceId;
|
||||
} else {
|
||||
options.video = false;
|
||||
}
|
||||
} else if (videoDeviceId === false) {
|
||||
options.video = false;
|
||||
} else {
|
||||
|
@ -279,64 +246,21 @@ export class OpenViduService {
|
|||
}
|
||||
|
||||
let newLocalTracks: LocalTrack[] = [];
|
||||
|
||||
if (options.audio || options.video) {
|
||||
this.log.d('Creating local tracks with options', options);
|
||||
|
||||
if (allowPartialCreation) {
|
||||
// Try to create tracks separately to handle device conflicts gracefully
|
||||
newLocalTracks = await this.createTracksWithFallback(options);
|
||||
} else {
|
||||
// Original behavior - all or nothing
|
||||
newLocalTracks = await createLocalTracks(options);
|
||||
}
|
||||
|
||||
// Mute tracks if devices are disabled
|
||||
if (!this.deviceService.isCameraEnabled()) {
|
||||
newLocalTracks.find((t) => t.kind === Track.Kind.Video)?.mute();
|
||||
const videoTrack = newLocalTracks.find((track) => track.kind === Track.Kind.Video);
|
||||
if (videoTrack) videoTrack.mute();
|
||||
}
|
||||
if (!this.deviceService.isMicrophoneEnabled()) {
|
||||
newLocalTracks.find((t) => t.kind === Track.Kind.Audio)?.mute();
|
||||
const audioTrack = newLocalTracks.find((track) => track.kind === Track.Kind.Audio);
|
||||
if (audioTrack) audioTrack.mute();
|
||||
}
|
||||
}
|
||||
return newLocalTracks;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates tracks with fallback strategy to handle device conflicts
|
||||
* @param options - The track creation options
|
||||
* @returns Array of successfully created tracks
|
||||
* @internal
|
||||
*/
|
||||
private async createTracksWithFallback(options: CreateLocalTracksOptions): Promise<LocalTrack[]> {
|
||||
const tracks: LocalTrack[] = [];
|
||||
|
||||
// Try to create video track separately
|
||||
if (options.video) {
|
||||
try {
|
||||
const videoTracks = await createLocalTracks({ video: options.video });
|
||||
tracks.push(...videoTracks);
|
||||
this.log.d('Video track created successfully');
|
||||
} catch (error) {
|
||||
this.log.w('Failed to create video track, device may be busy:', error);
|
||||
// Still continue to try audio track
|
||||
}
|
||||
}
|
||||
|
||||
// Try to create audio track separately
|
||||
if (options.audio) {
|
||||
try {
|
||||
const audioTracks = await createLocalTracks({ audio: options.audio });
|
||||
tracks.push(...audioTracks);
|
||||
this.log.d('Audio track created successfully');
|
||||
} catch (error) {
|
||||
this.log.w('Failed to create audio track, device may be busy:', error);
|
||||
}
|
||||
}
|
||||
|
||||
return tracks;
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
* As the Room is not created yet, we need to handle the media tracks with a temporary array of tracks.
|
||||
|
@ -377,7 +301,7 @@ export class OpenViduService {
|
|||
return this.deviceService.isCameraEnabled();
|
||||
}
|
||||
const videoTrack = this.localTracks.find((track) => track.kind === Track.Kind.Video);
|
||||
return !!videoTrack && !videoTrack.isMuted && videoTrack?.mediaStreamTrack?.enabled;
|
||||
return !!videoTrack && !videoTrack.isMuted;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -390,134 +314,25 @@ export class OpenViduService {
|
|||
return this.deviceService.isMicrophoneEnabled();
|
||||
}
|
||||
const audioTrack = this.localTracks.find((track) => track.kind === Track.Kind.Audio);
|
||||
return !!audioTrack && !audioTrack.isMuted && audioTrack?.mediaStreamTrack?.enabled;
|
||||
return !!audioTrack && !audioTrack.isMuted;
|
||||
}
|
||||
|
||||
/**
|
||||
* Switch the camera device when the room is not connected (prejoin page)
|
||||
* Switch the microphone device when the room is not connected (prejoin page)
|
||||
* @param deviceId new video device to use
|
||||
* @internal
|
||||
*/
|
||||
async switchCamera(deviceId: string): Promise<void> {
|
||||
const existingTrack = this.localTracks.find((track) => track.kind === Track.Kind.Video) as LocalVideoTrack;
|
||||
|
||||
if (existingTrack) {
|
||||
//TODO: SHould use replace track using restartTrack
|
||||
// Try to restart existing track
|
||||
this.removeVideoTrack();
|
||||
// try {
|
||||
// await existingTrack.restartTrack({ deviceId: deviceId });
|
||||
// this.log.d('Camera switched successfully using existing track');
|
||||
// return;
|
||||
// } catch (error) {
|
||||
// this.log.w('Failed to restart video track, trying to create new one:', error);
|
||||
// // Remove the failed track
|
||||
// this.removeVideoTrack();
|
||||
// }
|
||||
}
|
||||
|
||||
// Create new video track if no existing track or restart failed
|
||||
try {
|
||||
const newVideoTracks = await createLocalTracks({
|
||||
video: { deviceId: deviceId }
|
||||
});
|
||||
|
||||
const videoTrack = newVideoTracks.find((t) => t.kind === Track.Kind.Video);
|
||||
if (videoTrack) {
|
||||
|
||||
// Mute if camera is disabled in settings
|
||||
if (!this.deviceService.isCameraEnabled()) {
|
||||
await videoTrack.mute();
|
||||
}
|
||||
|
||||
this.localTracks.push(videoTrack);
|
||||
this.log.d('New camera track created and added');
|
||||
}
|
||||
} catch (error) {
|
||||
this.log.e('Failed to create new video track:', error);
|
||||
throw new Error(`Failed to switch camera: ${error.message}`);
|
||||
}
|
||||
switchCamera(deviceId: string): Promise<void> {
|
||||
return (this.localTracks?.find((track) => track.kind === Track.Kind.Video) as LocalVideoTrack).restartTrack({ deviceId: deviceId });
|
||||
}
|
||||
|
||||
/**
|
||||
* Switches the microphone device when the room is not connected (prejoin page)
|
||||
* @param deviceId new audio device to use
|
||||
* @param deviceId new video device to use
|
||||
* @internal
|
||||
*/
|
||||
async switchMicrophone(deviceId: string): Promise<void> {
|
||||
const existingTrack = this.localTracks?.find((track) => track.kind === Track.Kind.Audio) as LocalAudioTrack;
|
||||
|
||||
if (existingTrack) {
|
||||
this.removeAudioTrack();
|
||||
//TODO: SHould use replace track using restartTrack
|
||||
// Try to restart existing track
|
||||
// try {
|
||||
// await existingTrack.restartTrack({ deviceId: deviceId });
|
||||
// this.log.d('Microphone switched successfully using existing track');
|
||||
// return;
|
||||
// } catch (error) {
|
||||
// this.log.w('Failed to restart audio track, trying to create new one:', error);
|
||||
// // Remove the failed track
|
||||
// this.removeAudioTrack();
|
||||
// }
|
||||
}
|
||||
|
||||
// Create new audio track if no existing track or restart failed
|
||||
try {
|
||||
const newAudioTracks = await createLocalTracks({
|
||||
audio: {
|
||||
deviceId: deviceId,
|
||||
echoCancellation: true,
|
||||
noiseSuppression: true,
|
||||
autoGainControl: true
|
||||
}
|
||||
});
|
||||
|
||||
const audioTrack = newAudioTracks.find((t) => t.kind === Track.Kind.Audio);
|
||||
if (audioTrack) {
|
||||
this.localTracks.push(audioTrack);
|
||||
|
||||
// Mute if microphone is disabled in settings
|
||||
if (!this.deviceService.isMicrophoneEnabled()) {
|
||||
await audioTrack.mute();
|
||||
}
|
||||
|
||||
this.log.d('New microphone track created and added');
|
||||
}
|
||||
} catch (error) {
|
||||
this.log.e('Failed to create new audio track:', error);
|
||||
throw new Error(`Failed to switch microphone: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes video track from local tracks
|
||||
* @internal
|
||||
*/
|
||||
private removeVideoTrack(): void {
|
||||
const videoTrackIndex = this.localTracks.findIndex((track) => track.kind === Track.Kind.Video);
|
||||
if (videoTrackIndex !== -1) {
|
||||
const videoTrack = this.localTracks[videoTrackIndex];
|
||||
videoTrack.stop();
|
||||
videoTrack.detach();
|
||||
this.localTracks.splice(videoTrackIndex, 1);
|
||||
this.log.d('Video track removed');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes audio track from local tracks
|
||||
* @internal
|
||||
*/
|
||||
private removeAudioTrack(): void {
|
||||
const audioTrackIndex = this.localTracks.findIndex((track) => track.kind === Track.Kind.Audio);
|
||||
if (audioTrackIndex !== -1) {
|
||||
const audioTrack = this.localTracks[audioTrackIndex];
|
||||
audioTrack.stop();
|
||||
audioTrack.detach();
|
||||
this.localTracks.splice(audioTrackIndex, 1);
|
||||
this.log.d('Audio track removed');
|
||||
}
|
||||
switchMicrophone(deviceId: string): Promise<void> {
|
||||
return (this.localTracks?.find((track) => track.kind === Track.Kind.Audio) as LocalAudioTrack).restartTrack({ deviceId: deviceId });
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -528,8 +343,9 @@ export class OpenViduService {
|
|||
* @throws Error if there is an error decoding and parsing the token.
|
||||
* @internal
|
||||
*/
|
||||
private extractLivekitData(token: string): { livekitUrl?: string; livekitRoomAdmin: boolean } {
|
||||
private extractLivekitData(token: string, livekitUrl: string): { livekitUrl: string; livekitRoomAdmin: boolean } {
|
||||
try {
|
||||
const response = { livekitUrl, livekitRoomAdmin: false };
|
||||
const base64Url = token.split('.')[1];
|
||||
const base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/');
|
||||
const jsonPayload = decodeURIComponent(
|
||||
|
@ -545,13 +361,13 @@ export class OpenViduService {
|
|||
const payload = JSON.parse(jsonPayload);
|
||||
if (payload?.metadata) {
|
||||
const tokenMetadata = JSON.parse(payload.metadata);
|
||||
return {
|
||||
livekitUrl: tokenMetadata.livekitUrl,
|
||||
livekitRoomAdmin: !!tokenMetadata.roomAdmin
|
||||
};
|
||||
if (tokenMetadata.livekitUrl) {
|
||||
response.livekitUrl = tokenMetadata.livekitUrl;
|
||||
}
|
||||
response.livekitRoomAdmin = tokenMetadata.roomAdmin;
|
||||
}
|
||||
|
||||
return { livekitRoomAdmin: false };
|
||||
return response;
|
||||
} catch (error) {
|
||||
throw new Error('Error decoding and parsing token: ' + error);
|
||||
}
|
||||
|
|
|
@ -20,7 +20,7 @@ export class RecordingService {
|
|||
private recordingStatus = <BehaviorSubject<RecordingStatusInfo>>new BehaviorSubject({
|
||||
status: RecordingStatus.STOPPED,
|
||||
recordingList: [] as RecordingInfo[],
|
||||
startedAt: new Date(0, 0, 0, 0, 0, 0)
|
||||
recordingElapsedTime: new Date(0, 0, 0, 0, 0, 0)
|
||||
});
|
||||
private log: ILogger;
|
||||
|
||||
|
@ -41,9 +41,12 @@ export class RecordingService {
|
|||
* @internal
|
||||
*/
|
||||
setRecordingStarted(recordingInfo?: RecordingInfo, startTimestamp?: number) {
|
||||
// Determine the actual start timestamp of the recording
|
||||
// Priority: startTimestamp parameter > recordingInfo.startedAt > current time
|
||||
this.recordingStartTimestamp = startTimestamp || recordingInfo?.startedAt || Date.now();
|
||||
// Register the start timestamp of the recording
|
||||
// to calculate the elapsed time
|
||||
this.recordingStartTimestamp = recordingInfo?.startedAt || Date.now();
|
||||
|
||||
// Initialize the recording elapsed time
|
||||
this.startRecordingTimer();
|
||||
|
||||
const { recordingList } = this.recordingStatus.getValue();
|
||||
let updatedRecordingList = [...recordingList];
|
||||
|
@ -58,22 +61,17 @@ export class RecordingService {
|
|||
updatedRecordingList = [recordingInfo, ...updatedRecordingList];
|
||||
}
|
||||
}
|
||||
|
||||
// Calculate the elapsed time based on the actual start timestamp
|
||||
const recordingElapsedTime = new Date(0, 0, 0, 0, 0, 0);
|
||||
if (this.recordingStartTimestamp) {
|
||||
const elapsedSeconds = Math.floor((Date.now() - this.recordingStartTimestamp) / 1000);
|
||||
recordingElapsedTime.setSeconds(Math.max(0, elapsedSeconds)); // Ensure non-negative
|
||||
if (startTimestamp) {
|
||||
const elapsedSeconds = Math.floor((Date.now() - startTimestamp) / 1000);
|
||||
recordingElapsedTime.setSeconds(elapsedSeconds);
|
||||
}
|
||||
|
||||
this.updateStatus({
|
||||
status: RecordingStatus.STARTED,
|
||||
recordingList: updatedRecordingList,
|
||||
startedAt: recordingElapsedTime
|
||||
recordingElapsedTime
|
||||
});
|
||||
|
||||
// Start the timer after updating the initial state
|
||||
this.startRecordingTimer();
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -98,7 +96,7 @@ export class RecordingService {
|
|||
this.updateStatus({
|
||||
status: RecordingStatus.STOPPED,
|
||||
recordingList: updatedRecordingList,
|
||||
startedAt: new Date(0, 0, 0, 0, 0, 0)
|
||||
recordingElapsedTime: new Date(0, 0, 0, 0, 0, 0)
|
||||
});
|
||||
|
||||
this.recordingStartTimestamp = null;
|
||||
|
@ -109,11 +107,11 @@ export class RecordingService {
|
|||
* The `started` stastus will be updated automatically when the recording is actually started.
|
||||
*/
|
||||
setRecordingStarting() {
|
||||
const { recordingList, startedAt } = this.recordingStatus.getValue();
|
||||
const { recordingList, recordingElapsedTime } = this.recordingStatus.getValue();
|
||||
this.updateStatus({
|
||||
status: RecordingStatus.STARTING,
|
||||
recordingList,
|
||||
startedAt
|
||||
recordingElapsedTime
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -123,11 +121,11 @@ export class RecordingService {
|
|||
*/
|
||||
setRecordingFailed(error: string) {
|
||||
this.stopRecordingTimer();
|
||||
const { startedAt, recordingList } = this.recordingStatus.getValue();
|
||||
const { recordingElapsedTime, recordingList } = this.recordingStatus.getValue();
|
||||
const statusInfo: RecordingStatusInfo = {
|
||||
status: RecordingStatus.FAILED,
|
||||
recordingList,
|
||||
startedAt,
|
||||
recordingElapsedTime,
|
||||
error
|
||||
};
|
||||
this.updateStatus(statusInfo);
|
||||
|
@ -138,12 +136,12 @@ export class RecordingService {
|
|||
* The `stopped` stastus will be updated automatically when the recording is actually stopped.
|
||||
*/
|
||||
setRecordingStopping() {
|
||||
const { startedAt, recordingList } = this.recordingStatus.getValue();
|
||||
const { recordingElapsedTime, recordingList } = this.recordingStatus.getValue();
|
||||
|
||||
this.updateStatus({
|
||||
status: RecordingStatus.STOPPING,
|
||||
recordingList,
|
||||
startedAt
|
||||
recordingElapsedTime
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -201,14 +199,14 @@ export class RecordingService {
|
|||
* @internal
|
||||
*/
|
||||
deleteRecording(recording: RecordingInfo) {
|
||||
const { recordingList, status, startedAt } = this.recordingStatus.getValue();
|
||||
const { recordingList, status, recordingElapsedTime } = this.recordingStatus.getValue();
|
||||
const updatedList = recordingList.filter((item) => item.id !== recording.id);
|
||||
|
||||
if (updatedList.length !== recordingList.length) {
|
||||
this.updateStatus({
|
||||
status,
|
||||
recordingList: updatedList,
|
||||
startedAt
|
||||
recordingElapsedTime
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
@ -221,11 +219,11 @@ export class RecordingService {
|
|||
* @internal
|
||||
*/
|
||||
setRecordingList(recordings: RecordingInfo[]) {
|
||||
const { status, startedAt, error } = this.recordingStatus.getValue();
|
||||
const { status, recordingElapsedTime, error } = this.recordingStatus.getValue();
|
||||
this.updateStatus({
|
||||
status,
|
||||
recordingList: recordings,
|
||||
startedAt,
|
||||
recordingElapsedTime,
|
||||
error
|
||||
});
|
||||
}
|
||||
|
@ -235,21 +233,19 @@ export class RecordingService {
|
|||
* @param status {@link RecordingStatus}
|
||||
*/
|
||||
private updateStatus(statusInfo: RecordingStatusInfo) {
|
||||
const { status, recordingList, error, startedAt } = statusInfo;
|
||||
const { status, recordingList, error, recordingElapsedTime } = statusInfo;
|
||||
this.recordingStatus.next({
|
||||
status,
|
||||
recordingList,
|
||||
startedAt,
|
||||
recordingElapsedTime,
|
||||
error
|
||||
});
|
||||
}
|
||||
|
||||
private startRecordingTimer() {
|
||||
// Don't override the timestamp if it's already set correctly
|
||||
if (this.recordingStartTimestamp === null) {
|
||||
this.recordingStartTimestamp = Date.now();
|
||||
}
|
||||
|
||||
if (this.recordingTimeInterval) {
|
||||
clearInterval(this.recordingTimeInterval);
|
||||
}
|
||||
|
@ -257,29 +253,29 @@ export class RecordingService {
|
|||
this.recordingTimeInterval = setInterval(() => {
|
||||
if (!this.recordingStartTimestamp) return;
|
||||
|
||||
// Calculate elapsed time based on the actual recording start timestamp
|
||||
let { recordingElapsedTime } = this.recordingStatus.getValue();
|
||||
if (recordingElapsedTime) {
|
||||
// Calculamos con precisión el tiempo transcurrido
|
||||
const elapsedSeconds = Math.floor((Date.now() - this.recordingStartTimestamp) / 1000);
|
||||
const startedAt = new Date(0, 0, 0, 0, 0, 0);
|
||||
startedAt.setSeconds(Math.max(0, elapsedSeconds)); // Ensure non-negative
|
||||
const updatedElapsedTime = new Date(0, 0, 0, 0, 0, 0);
|
||||
updatedElapsedTime.setSeconds(elapsedSeconds);
|
||||
|
||||
const { recordingList, status } = this.recordingStatus.getValue();
|
||||
this.updateStatus({
|
||||
status,
|
||||
recordingList,
|
||||
startedAt
|
||||
recordingElapsedTime: updatedElapsedTime
|
||||
});
|
||||
}
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
private stopRecordingTimer() {
|
||||
if (this.recordingTimeInterval) {
|
||||
clearInterval(this.recordingTimeInterval);
|
||||
}
|
||||
const { recordingList, status, error } = this.recordingStatus.getValue();
|
||||
const statusInfo: RecordingStatusInfo = {
|
||||
status,
|
||||
recordingList,
|
||||
startedAt: new Date(0, 0, 0, 0, 0, 0), // Reset elapsed time when stopped
|
||||
error
|
||||
};
|
||||
this.updateStatus(statusInfo);
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { Injectable, OnDestroy } from '@angular/core';
|
||||
import { Injectable } from '@angular/core';
|
||||
import { ILogger } from '../../models/logger.model';
|
||||
import { STORAGE_PREFIX, StorageKeys, SESSION_KEYS, TAB_MANAGEMENT_KEYS, TAB_SPECIFIC_KEYS, SHARED_PERSISTENT_KEYS } from '../../models/storage.model';
|
||||
import { STORAGE_PREFIX, StorageKeys } from '../../models/storage.model';
|
||||
import { LoggerService } from '../logger/logger.service';
|
||||
import { CustomDevice } from '../../models/device.model';
|
||||
|
||||
|
@ -10,125 +10,13 @@ import { CustomDevice } from '../../models/device.model';
|
|||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class StorageService implements OnDestroy {
|
||||
public localStorage = window.localStorage;
|
||||
public sessionStorage = window.sessionStorage;
|
||||
export class StorageService {
|
||||
public storage = window.localStorage;
|
||||
public log: ILogger;
|
||||
protected PREFIX_KEY = STORAGE_PREFIX;
|
||||
private tabId: string;
|
||||
private readonly TAB_CLEANUP_INTERVAL = 30000; // 30 seconds
|
||||
private cleanupInterval: any;
|
||||
|
||||
constructor(protected loggerSrv: LoggerService) {
|
||||
this.log = this.loggerSrv.get('StorageService');
|
||||
this.initializeTabManagement();
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes tab management system
|
||||
* Creates unique tab ID and sets up cleanup mechanism
|
||||
*/
|
||||
private initializeTabManagement(): void {
|
||||
// Generate unique tab ID
|
||||
this.tabId = this.generateTabId();
|
||||
this.setSessionValue(StorageKeys.TAB_ID, this.tabId);
|
||||
|
||||
// Register this tab as active
|
||||
this.registerActiveTab();
|
||||
|
||||
// Set up periodic cleanup of inactive tabs
|
||||
this.setupTabCleanup();
|
||||
|
||||
// Listen for page unload to clean up this tab
|
||||
window.addEventListener('beforeunload', () => {
|
||||
this.unregisterActiveTab();
|
||||
});
|
||||
|
||||
this.log.d(`Tab initialized with ID: ${this.tabId}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a unique tab identifier
|
||||
*/
|
||||
private generateTabId(): string {
|
||||
return `tab_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers current tab as active
|
||||
*/
|
||||
private registerActiveTab(): void {
|
||||
const activeTabs = this.getActiveTabsFromStorage() || {};
|
||||
activeTabs[this.tabId] = Date.now();
|
||||
this.setLocalValue(StorageKeys.ACTIVE_TABS, activeTabs);
|
||||
}
|
||||
|
||||
/**
|
||||
* Unregisters current tab from active tabs
|
||||
*/
|
||||
private unregisterActiveTab(): void {
|
||||
const activeTabs = this.getActiveTabsFromStorage() || {};
|
||||
delete activeTabs[this.tabId];
|
||||
this.setLocalValue(StorageKeys.ACTIVE_TABS, activeTabs);
|
||||
this.cleanupTabData(this.tabId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets up periodic cleanup of inactive tabs
|
||||
*/
|
||||
private setupTabCleanup(): void {
|
||||
this.cleanupInterval = setInterval(() => {
|
||||
this.cleanupInactiveTabs();
|
||||
}, this.TAB_CLEANUP_INTERVAL);
|
||||
}
|
||||
|
||||
/**
|
||||
* Cleans up data from inactive tabs
|
||||
*/
|
||||
private cleanupInactiveTabs(): void {
|
||||
const activeTabs = this.getActiveTabsFromStorage() || {};
|
||||
const currentTime = Date.now();
|
||||
const timeoutThreshold = this.TAB_CLEANUP_INTERVAL * 2; // 60 seconds
|
||||
|
||||
Object.keys(activeTabs).forEach(tabId => {
|
||||
const lastActivity = activeTabs[tabId];
|
||||
if (currentTime - lastActivity > timeoutThreshold) {
|
||||
this.log.d(`Cleaning up inactive tab: ${tabId}`);
|
||||
delete activeTabs[tabId];
|
||||
this.cleanupTabData(tabId);
|
||||
}
|
||||
});
|
||||
|
||||
// Update heartbeat for current tab
|
||||
activeTabs[this.tabId] = currentTime;
|
||||
this.setLocalValue(StorageKeys.ACTIVE_TABS, activeTabs);
|
||||
}
|
||||
|
||||
/**
|
||||
* Cleans up data associated with a specific tab
|
||||
*/
|
||||
private cleanupTabData(tabId: string): void {
|
||||
// Clean up tab-specific data from localStorage
|
||||
TAB_SPECIFIC_KEYS.forEach(key => {
|
||||
const storageKey = `${this.PREFIX_KEY}${tabId}_${key}`;
|
||||
this.localStorage.removeItem(storageKey);
|
||||
});
|
||||
|
||||
this.log.d(`Cleaned up data for tab: ${tabId}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets active tabs from localStorage
|
||||
*/
|
||||
private getActiveTabsFromStorage(): { [key: string]: number } | null {
|
||||
return this.getLocalValue(StorageKeys.ACTIVE_TABS);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the current tab ID
|
||||
*/
|
||||
public getTabId(): string {
|
||||
return this.tabId;
|
||||
}
|
||||
|
||||
getParticipantName(): string | null {
|
||||
|
@ -218,164 +106,24 @@ export class StorageService implements OnDestroy {
|
|||
}
|
||||
|
||||
protected set(key: string, item: any) {
|
||||
if (SESSION_KEYS.includes(key as StorageKeys)) {
|
||||
this.setSessionValue(key, item);
|
||||
} else {
|
||||
this.setLocalValue(key, item);
|
||||
}
|
||||
const value = JSON.stringify({ item: item });
|
||||
this.storage.setItem(this.PREFIX_KEY + key, value);
|
||||
}
|
||||
|
||||
protected get(key: string): any {
|
||||
if (SESSION_KEYS.includes(key as StorageKeys)) {
|
||||
return this.getSessionValue(key);
|
||||
} else {
|
||||
return this.getLocalValue(key);
|
||||
const str = this.storage.getItem(this.PREFIX_KEY + key);
|
||||
if (!!str) {
|
||||
return JSON.parse(str).item;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
protected remove(key: string) {
|
||||
if (SESSION_KEYS.includes(key as StorageKeys)) {
|
||||
this.removeSessionValue(key);
|
||||
} else {
|
||||
this.removeLocalValue(key);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if a key should use tab-specific storage in localStorage
|
||||
*/
|
||||
private shouldUseTabSpecificKey(key: string): boolean {
|
||||
return TAB_SPECIFIC_KEYS.includes(key as StorageKeys);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets value in localStorage with tab-specific key if needed
|
||||
*/
|
||||
private setLocalValue(key: string, item: any): void {
|
||||
const value = JSON.stringify({ item: item });
|
||||
const storageKey = this.shouldUseTabSpecificKey(key)
|
||||
? `${this.PREFIX_KEY}${this.tabId}_${key}`
|
||||
: `${this.PREFIX_KEY}${key}`;
|
||||
this.localStorage.setItem(storageKey, value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets value from localStorage with tab-specific key if needed
|
||||
*/
|
||||
private getLocalValue(key: string): any {
|
||||
const storageKey = this.shouldUseTabSpecificKey(key)
|
||||
? `${this.PREFIX_KEY}${this.tabId}_${key}`
|
||||
: `${this.PREFIX_KEY}${key}`;
|
||||
const str = this.localStorage.getItem(storageKey);
|
||||
if (!!str) {
|
||||
return JSON.parse(str).item;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes value from localStorage with tab-specific key if needed
|
||||
*/
|
||||
private removeLocalValue(key: string): void {
|
||||
const storageKey = this.shouldUseTabSpecificKey(key)
|
||||
? `${this.PREFIX_KEY}${this.tabId}_${key}`
|
||||
: `${this.PREFIX_KEY}${key}`;
|
||||
this.localStorage.removeItem(storageKey);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets value in sessionStorage
|
||||
*/
|
||||
private setSessionValue(key: string, item: any): void {
|
||||
const value = JSON.stringify({ item: item });
|
||||
this.sessionStorage.setItem(this.PREFIX_KEY + key, value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets value from sessionStorage
|
||||
*/
|
||||
private getSessionValue(key: string): any {
|
||||
const str = this.sessionStorage.getItem(this.PREFIX_KEY + key);
|
||||
if (!!str) {
|
||||
return JSON.parse(str).item;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes value from sessionStorage
|
||||
*/
|
||||
private removeSessionValue(key: string): void {
|
||||
this.sessionStorage.removeItem(this.PREFIX_KEY + key);
|
||||
this.storage.removeItem(this.PREFIX_KEY + key);
|
||||
}
|
||||
|
||||
public clear() {
|
||||
this.log.d('Clearing localStorage and sessionStorage');
|
||||
|
||||
// Clear only our prefixed keys from localStorage
|
||||
Object.keys(this.localStorage).forEach(key => {
|
||||
if (key.startsWith(this.PREFIX_KEY)) {
|
||||
this.localStorage.removeItem(key);
|
||||
}
|
||||
});
|
||||
|
||||
// Clear only our prefixed keys from sessionStorage
|
||||
Object.keys(this.sessionStorage).forEach(key => {
|
||||
if (key.startsWith(this.PREFIX_KEY)) {
|
||||
this.sessionStorage.removeItem(key);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears only session data (tab-specific data)
|
||||
*/
|
||||
public clearSessionData(): void {
|
||||
this.log.d('Clearing session data');
|
||||
Object.keys(this.sessionStorage).forEach(key => {
|
||||
if (key.startsWith(this.PREFIX_KEY)) {
|
||||
this.sessionStorage.removeItem(key);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears only tab-specific data for current tab
|
||||
*/
|
||||
public clearTabSpecificData(): void {
|
||||
this.log.d('Clearing tab-specific data');
|
||||
TAB_SPECIFIC_KEYS.forEach(key => {
|
||||
this.removeLocalValue(key);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears only persistent data
|
||||
*/
|
||||
public clearPersistentData(): void {
|
||||
this.log.d('Clearing persistent data');
|
||||
SHARED_PERSISTENT_KEYS.forEach(key => {
|
||||
this.removeLocalValue(key);
|
||||
});
|
||||
TAB_MANAGEMENT_KEYS.forEach(key => {
|
||||
this.removeLocalValue(key);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Cleanup method to be called when service is destroyed
|
||||
*/
|
||||
public destroy(): void {
|
||||
if (this.cleanupInterval) {
|
||||
clearInterval(this.cleanupInterval);
|
||||
}
|
||||
this.unregisterActiveTab();
|
||||
}
|
||||
|
||||
/**
|
||||
* Angular lifecycle hook - called when service is destroyed
|
||||
*/
|
||||
ngOnDestroy(): void {
|
||||
this.destroy();
|
||||
this.log.d('Clearing localStorage');
|
||||
this.storage.clear();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,431 +0,0 @@
|
|||
import { Injectable, TemplateRef } from '@angular/core';
|
||||
import { ILogger } from '../../models/logger.model';
|
||||
import { LoggerService } from '../logger/logger.service';
|
||||
import {
|
||||
ActivitiesPanelDirective,
|
||||
AdditionalPanelsDirective,
|
||||
ChatPanelDirective,
|
||||
LayoutDirective,
|
||||
PanelDirective,
|
||||
ParticipantPanelItemDirective,
|
||||
ParticipantPanelItemElementsDirective,
|
||||
ParticipantsPanelDirective,
|
||||
StreamDirective,
|
||||
ToolbarAdditionalButtonsDirective,
|
||||
ToolbarAdditionalPanelButtonsDirective,
|
||||
ToolbarDirective
|
||||
} from '../../directives/template/openvidu-components-angular.directive';
|
||||
import {
|
||||
PreJoinDirective,
|
||||
ParticipantPanelAfterLocalParticipantDirective,
|
||||
LayoutAdditionalElementsDirective
|
||||
} from '../../directives/template/internals.directive';
|
||||
|
||||
/**
|
||||
* Configuration object for all templates in the videoconference component
|
||||
*/
|
||||
export interface TemplateConfiguration {
|
||||
// Toolbar templates
|
||||
toolbarTemplate: TemplateRef<any>;
|
||||
toolbarAdditionalButtonsTemplate?: TemplateRef<any>;
|
||||
toolbarAdditionalPanelButtonsTemplate?: TemplateRef<any>;
|
||||
|
||||
// Panel templates
|
||||
panelTemplate: TemplateRef<any>;
|
||||
chatPanelTemplate: TemplateRef<any>;
|
||||
participantsPanelTemplate: TemplateRef<any>;
|
||||
activitiesPanelTemplate: TemplateRef<any>;
|
||||
additionalPanelsTemplate?: TemplateRef<any>;
|
||||
|
||||
// Participant templates
|
||||
participantPanelAfterLocalParticipantTemplate?: TemplateRef<any>;
|
||||
participantPanelItemTemplate: TemplateRef<any>;
|
||||
participantPanelItemElementsTemplate?: TemplateRef<any>;
|
||||
|
||||
// Layout templates
|
||||
layoutTemplate: TemplateRef<any>;
|
||||
streamTemplate: TemplateRef<any>;
|
||||
layoutAdditionalElementsTemplate?: TemplateRef<any>;
|
||||
|
||||
// PreJoin template
|
||||
preJoinTemplate?: TemplateRef<any>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configuration object for panel component templates
|
||||
*/
|
||||
export interface PanelTemplateConfiguration {
|
||||
participantsPanelTemplate?: TemplateRef<any>;
|
||||
chatPanelTemplate?: TemplateRef<any>;
|
||||
activitiesPanelTemplate?: TemplateRef<any>;
|
||||
additionalPanelsTemplate?: TemplateRef<any>;
|
||||
backgroundEffectsPanelTemplate?: TemplateRef<any>;
|
||||
settingsPanelTemplate?: TemplateRef<any>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configuration object for toolbar component templates
|
||||
*/
|
||||
export interface ToolbarTemplateConfiguration {
|
||||
toolbarAdditionalButtonsTemplate?: TemplateRef<any>;
|
||||
toolbarAdditionalPanelButtonsTemplate?: TemplateRef<any>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configuration object for layout component templates
|
||||
*/
|
||||
export interface LayoutTemplateConfiguration {
|
||||
layoutStreamTemplate?: TemplateRef<any>;
|
||||
layoutAdditionalElementsTemplate?: TemplateRef<any>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configuration object for participants panel component templates
|
||||
*/
|
||||
export interface ParticipantsPanelTemplateConfiguration {
|
||||
participantPanelItemTemplate?: TemplateRef<any>;
|
||||
participantPanelAfterLocalParticipantTemplate?: TemplateRef<any>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configuration object for participant panel item component templates
|
||||
*/
|
||||
export interface ParticipantPanelItemTemplateConfiguration {
|
||||
participantPanelItemElementsTemplate?: TemplateRef<any>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configuration object for session component templates
|
||||
*/
|
||||
export interface SessionTemplateConfiguration {
|
||||
toolbarTemplate?: TemplateRef<any>;
|
||||
panelTemplate?: TemplateRef<any>;
|
||||
layoutTemplate?: TemplateRef<any>;
|
||||
}
|
||||
|
||||
/**
|
||||
* External directives provided by the consumer
|
||||
*/
|
||||
export interface ExternalDirectives {
|
||||
toolbar?: ToolbarDirective;
|
||||
toolbarAdditionalButtons?: ToolbarAdditionalButtonsDirective;
|
||||
toolbarAdditionalPanelButtons?: ToolbarAdditionalPanelButtonsDirective;
|
||||
additionalPanels?: AdditionalPanelsDirective;
|
||||
panel?: PanelDirective;
|
||||
chatPanel?: ChatPanelDirective;
|
||||
activitiesPanel?: ActivitiesPanelDirective;
|
||||
participantsPanel?: ParticipantsPanelDirective;
|
||||
participantPanelAfterLocalParticipant?: ParticipantPanelAfterLocalParticipantDirective;
|
||||
participantPanelItem?: ParticipantPanelItemDirective;
|
||||
participantPanelItemElements?: ParticipantPanelItemElementsDirective;
|
||||
layout?: LayoutDirective;
|
||||
stream?: StreamDirective;
|
||||
preJoin?: PreJoinDirective;
|
||||
layoutAdditionalElements?: LayoutAdditionalElementsDirective;
|
||||
}
|
||||
|
||||
/**
|
||||
* Default templates provided by the component
|
||||
*/
|
||||
export interface DefaultTemplates {
|
||||
toolbar: TemplateRef<any>;
|
||||
panel: TemplateRef<any>;
|
||||
chatPanel: TemplateRef<any>;
|
||||
participantsPanel: TemplateRef<any>;
|
||||
activitiesPanel: TemplateRef<any>;
|
||||
participantPanelItem: TemplateRef<any>;
|
||||
layout: TemplateRef<any>;
|
||||
stream: TemplateRef<any>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Service responsible for managing and configuring templates for the videoconference component.
|
||||
* This service centralizes all template setup logic, making the main component cleaner and more maintainable.
|
||||
*/
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class TemplateManagerService {
|
||||
private log: ILogger;
|
||||
|
||||
constructor(private loggerSrv: LoggerService) {
|
||||
this.log = this.loggerSrv.get('TemplateManagerService');
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets up all templates based on external directives and default templates
|
||||
*/
|
||||
setupTemplates(externalDirectives: ExternalDirectives, defaultTemplates: DefaultTemplates): TemplateConfiguration {
|
||||
this.log.v('Setting up templates...');
|
||||
|
||||
const config: TemplateConfiguration = {
|
||||
toolbarTemplate: this.setupToolbarTemplate(externalDirectives, defaultTemplates),
|
||||
panelTemplate: this.setupPanelTemplate(externalDirectives, defaultTemplates),
|
||||
layoutTemplate: this.setupLayoutTemplate(externalDirectives, defaultTemplates),
|
||||
preJoinTemplate: this.setupPreJoinTemplate(externalDirectives),
|
||||
|
||||
// Individual templates
|
||||
chatPanelTemplate: this.setupChatPanelTemplate(externalDirectives, defaultTemplates),
|
||||
participantsPanelTemplate: this.setupParticipantsPanelTemplate(externalDirectives, defaultTemplates),
|
||||
activitiesPanelTemplate: this.setupActivitiesPanelTemplate(externalDirectives, defaultTemplates),
|
||||
participantPanelItemTemplate: this.setupParticipantPanelItemTemplate(externalDirectives, defaultTemplates),
|
||||
streamTemplate: this.setupStreamTemplate(externalDirectives, defaultTemplates),
|
||||
participantPanelAfterLocalParticipantTemplate: this.setupParticipantPanelAfterLocalParticipantTemplate(externalDirectives)
|
||||
};
|
||||
|
||||
// Optional templates
|
||||
if (externalDirectives.toolbarAdditionalButtons) {
|
||||
config.toolbarAdditionalButtonsTemplate = externalDirectives.toolbarAdditionalButtons.template;
|
||||
this.log.v('Setting EXTERNAL TOOLBAR ADDITIONAL BUTTONS');
|
||||
}
|
||||
|
||||
if (externalDirectives.toolbarAdditionalPanelButtons) {
|
||||
config.toolbarAdditionalPanelButtonsTemplate = externalDirectives.toolbarAdditionalPanelButtons.template;
|
||||
this.log.v('Setting EXTERNAL TOOLBAR ADDITIONAL PANEL BUTTONS');
|
||||
}
|
||||
|
||||
if (externalDirectives.additionalPanels) {
|
||||
config.additionalPanelsTemplate = externalDirectives.additionalPanels.template;
|
||||
this.log.v('Setting EXTERNAL ADDITIONAL PANELS');
|
||||
}
|
||||
|
||||
if (externalDirectives.participantPanelItemElements) {
|
||||
config.participantPanelItemElementsTemplate = externalDirectives.participantPanelItemElements.template;
|
||||
this.log.v('Setting EXTERNAL PARTICIPANT PANEL ITEM ELEMENTS');
|
||||
}
|
||||
|
||||
if (externalDirectives.layoutAdditionalElements) {
|
||||
this.log.v('Setting EXTERNAL ADDITIONAL LAYOUT ELEMENTS');
|
||||
config.layoutAdditionalElementsTemplate = externalDirectives.layoutAdditionalElements.template;
|
||||
}
|
||||
|
||||
this.log.v('Template setup completed', config);
|
||||
return config;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets up the participantPanelAfterLocalParticipant template
|
||||
*/
|
||||
private setupParticipantPanelAfterLocalParticipantTemplate(externalDirectives: ExternalDirectives): TemplateRef<any> | undefined {
|
||||
if (externalDirectives.participantPanelAfterLocalParticipant) {
|
||||
this.log.v('Setting EXTERNAL PARTICIPANT PANEL AFTER LOCAL PARTICIPANT');
|
||||
return (externalDirectives.participantPanelAfterLocalParticipant as any).template;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets up the toolbar template
|
||||
*/
|
||||
private setupToolbarTemplate(externalDirectives: ExternalDirectives, defaultTemplates: DefaultTemplates): TemplateRef<any> {
|
||||
if (externalDirectives.toolbar) {
|
||||
this.log.v('Setting EXTERNAL TOOLBAR');
|
||||
return externalDirectives.toolbar.template;
|
||||
} else {
|
||||
this.log.v('Setting DEFAULT TOOLBAR');
|
||||
return defaultTemplates.toolbar;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets up the panel template
|
||||
*/
|
||||
private setupPanelTemplate(externalDirectives: ExternalDirectives, defaultTemplates: DefaultTemplates): TemplateRef<any> {
|
||||
if (externalDirectives.panel) {
|
||||
this.log.v('Setting EXTERNAL PANEL');
|
||||
return externalDirectives.panel.template;
|
||||
} else {
|
||||
this.log.v('Setting DEFAULT PANEL');
|
||||
return defaultTemplates.panel;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets up the layout template
|
||||
*/
|
||||
private setupLayoutTemplate(externalDirectives: ExternalDirectives, defaultTemplates: DefaultTemplates): TemplateRef<any> {
|
||||
if (externalDirectives.layout) {
|
||||
this.log.v('Setting EXTERNAL LAYOUT');
|
||||
return externalDirectives.layout.template;
|
||||
} else {
|
||||
this.log.v('Setting DEFAULT LAYOUT');
|
||||
return defaultTemplates.layout;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets up the prejoin template
|
||||
*/
|
||||
private setupPreJoinTemplate(externalDirectives: ExternalDirectives): TemplateRef<any> | undefined {
|
||||
if (externalDirectives.preJoin) {
|
||||
this.log.v('Setting EXTERNAL PREJOIN');
|
||||
return externalDirectives.preJoin.template;
|
||||
} else {
|
||||
this.log.v('Setting DEFAULT PREJOIN (none)');
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets up the chat panel template
|
||||
*/
|
||||
private setupChatPanelTemplate(externalDirectives: ExternalDirectives, defaultTemplates: DefaultTemplates): TemplateRef<any> {
|
||||
if (externalDirectives.chatPanel) {
|
||||
this.log.v('Setting EXTERNAL CHAT PANEL');
|
||||
return externalDirectives.chatPanel.template;
|
||||
} else {
|
||||
this.log.v('Setting DEFAULT CHAT PANEL');
|
||||
return defaultTemplates.chatPanel;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets up the participants panel template
|
||||
*/
|
||||
private setupParticipantsPanelTemplate(externalDirectives: ExternalDirectives, defaultTemplates: DefaultTemplates): TemplateRef<any> {
|
||||
if (externalDirectives.participantsPanel) {
|
||||
this.log.v('Setting EXTERNAL PARTICIPANTS PANEL');
|
||||
return externalDirectives.participantsPanel.template;
|
||||
} else {
|
||||
this.log.v('Setting DEFAULT PARTICIPANTS PANEL');
|
||||
return defaultTemplates.participantsPanel;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets up the activities panel template
|
||||
*/
|
||||
private setupActivitiesPanelTemplate(externalDirectives: ExternalDirectives, defaultTemplates: DefaultTemplates): TemplateRef<any> {
|
||||
if (externalDirectives.activitiesPanel) {
|
||||
this.log.v('Setting EXTERNAL ACTIVITIES PANEL');
|
||||
return externalDirectives.activitiesPanel.template;
|
||||
} else {
|
||||
this.log.v('Setting DEFAULT ACTIVITIES PANEL');
|
||||
return defaultTemplates.activitiesPanel;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets up the participant panel item template
|
||||
*/
|
||||
private setupParticipantPanelItemTemplate(
|
||||
externalDirectives: ExternalDirectives,
|
||||
defaultTemplates: DefaultTemplates
|
||||
): TemplateRef<any> {
|
||||
if (externalDirectives.participantPanelItem) {
|
||||
this.log.v('Setting EXTERNAL PARTICIPANT PANEL ITEM');
|
||||
return externalDirectives.participantPanelItem.template;
|
||||
} else {
|
||||
this.log.v('Setting DEFAULT PARTICIPANT PANEL ITEM');
|
||||
return defaultTemplates.participantPanelItem;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets up the stream template
|
||||
*/
|
||||
private setupStreamTemplate(externalDirectives: ExternalDirectives, defaultTemplates: DefaultTemplates): TemplateRef<any> {
|
||||
if (externalDirectives.stream) {
|
||||
this.log.v('Setting EXTERNAL STREAM');
|
||||
return externalDirectives.stream.template;
|
||||
} else {
|
||||
this.log.v('Setting DEFAULT STREAM');
|
||||
return defaultTemplates.stream;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets up templates for the PanelComponent
|
||||
*/
|
||||
setupPanelTemplates(
|
||||
externalParticipantsPanel?: ParticipantsPanelDirective,
|
||||
externalChatPanel?: ChatPanelDirective,
|
||||
externalActivitiesPanel?: ActivitiesPanelDirective,
|
||||
externalAdditionalPanels?: AdditionalPanelsDirective
|
||||
): PanelTemplateConfiguration {
|
||||
this.log.v('Setting up panel templates...');
|
||||
|
||||
return {
|
||||
participantsPanelTemplate: externalParticipantsPanel?.template,
|
||||
chatPanelTemplate: externalChatPanel?.template,
|
||||
activitiesPanelTemplate: externalActivitiesPanel?.template,
|
||||
additionalPanelsTemplate: externalAdditionalPanels?.template
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets up templates for the ToolbarComponent
|
||||
*/
|
||||
setupToolbarTemplates(
|
||||
externalAdditionalButtons?: ToolbarAdditionalButtonsDirective,
|
||||
externalAdditionalPanelButtons?: ToolbarAdditionalPanelButtonsDirective
|
||||
): ToolbarTemplateConfiguration {
|
||||
this.log.v('Setting up toolbar templates...');
|
||||
|
||||
return {
|
||||
toolbarAdditionalButtonsTemplate: externalAdditionalButtons?.template,
|
||||
toolbarAdditionalPanelButtonsTemplate: externalAdditionalPanelButtons?.template
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets up templates for the LayoutComponent
|
||||
*/
|
||||
setupLayoutTemplates(
|
||||
externalStream?: StreamDirective,
|
||||
externalLayoutAdditionalElements?: LayoutAdditionalElementsDirective
|
||||
): LayoutTemplateConfiguration {
|
||||
this.log.v('Setting up layout templates...');
|
||||
|
||||
return {
|
||||
layoutStreamTemplate: externalStream?.template,
|
||||
layoutAdditionalElementsTemplate: externalLayoutAdditionalElements?.template
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets up templates for the ParticipantsPanelComponent
|
||||
*/
|
||||
setupParticipantsPanelTemplates(
|
||||
externalParticipantPanelItem?: ParticipantPanelItemDirective,
|
||||
defaultParticipantPanelItem?: TemplateRef<any>,
|
||||
externalParticipantPanelAfterLocalParticipant?: TemplateRef<any>
|
||||
): ParticipantsPanelTemplateConfiguration {
|
||||
this.log.v('Setting up participants panel templates...');
|
||||
|
||||
return {
|
||||
participantPanelItemTemplate: externalParticipantPanelItem?.template || defaultParticipantPanelItem,
|
||||
participantPanelAfterLocalParticipantTemplate: externalParticipantPanelAfterLocalParticipant
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets up templates for the ParticipantPanelItemComponent
|
||||
*/
|
||||
setupParticipantPanelItemTemplates(
|
||||
externalParticipantPanelItemElements?: ParticipantPanelItemElementsDirective
|
||||
): ParticipantPanelItemTemplateConfiguration {
|
||||
this.log.v('Setting up participant panel item templates...');
|
||||
|
||||
return {
|
||||
participantPanelItemElementsTemplate: externalParticipantPanelItemElements?.template
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets up templates for the SessionComponent
|
||||
*/
|
||||
setupSessionTemplates(
|
||||
toolbarTemplate?: TemplateRef<any>,
|
||||
panelTemplate?: TemplateRef<any>,
|
||||
layoutTemplate?: TemplateRef<any>
|
||||
): SessionTemplateConfiguration {
|
||||
this.log.v('Setting up session templates...');
|
||||
|
||||
return {
|
||||
toolbarTemplate,
|
||||
panelTemplate,
|
||||
layoutTemplate
|
||||
};
|
||||
}
|
||||
}
|
|
@ -2,12 +2,12 @@ import { Injectable } from '@angular/core';
|
|||
import { BehaviorSubject, Observable } from 'rxjs';
|
||||
import { BackgroundEffect, EffectType } from '../../models/background-effect.model';
|
||||
import { ParticipantService } from '../participant/participant.service';
|
||||
import { OpenViduService } from '../openvidu/openvidu.service';
|
||||
import { StorageService } from '../storage/storage.service';
|
||||
import { LocalVideoTrack, Track } from 'livekit-client';
|
||||
import { LocalTrack } from 'livekit-client';
|
||||
import { BackgroundBlur, BackgroundOptions, ProcessorWrapper, VirtualBackground } from '@livekit/track-processors';
|
||||
import { LoggerService } from '../logger/logger.service';
|
||||
import { ILogger } from '../../models/logger.model';
|
||||
import { ParticipantTrackPublication } from '../../models/participant.model';
|
||||
|
||||
/**
|
||||
* @internal
|
||||
|
@ -49,7 +49,6 @@ export class VirtualBackgroundService {
|
|||
private log: ILogger;
|
||||
constructor(
|
||||
private participantService: ParticipantService,
|
||||
private openviduService: OpenViduService,
|
||||
private storageService: StorageService,
|
||||
private loggerSrv: LoggerService
|
||||
) {
|
||||
|
@ -80,12 +79,11 @@ export class VirtualBackgroundService {
|
|||
// If the background is already applied, do nothing
|
||||
if (this.backgroundIsAlreadyApplied(bg.id)) return;
|
||||
|
||||
const cameraTrack = this.getCameraTrack();
|
||||
if (!cameraTrack) {
|
||||
this.log.e('No camera track found. Cannot apply background.');
|
||||
const cameraTracks = this.getCameraTracks();
|
||||
if (!cameraTracks) {
|
||||
this.log.e('No camera tracks found. Cannot apply background.');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// If no effect is selected, remove the background
|
||||
if (bg.type === EffectType.NONE) {
|
||||
|
@ -93,7 +91,8 @@ export class VirtualBackgroundService {
|
|||
return;
|
||||
}
|
||||
|
||||
const currentProcessor = cameraTrack.getProcessor() as ProcessorWrapper<BackgroundOptions>;
|
||||
const localTrack = cameraTracks[0].track as LocalTrack;
|
||||
const currentProcessor = localTrack.getProcessor() as ProcessorWrapper<BackgroundOptions>;
|
||||
|
||||
// Check if the background is the same type as the previous one
|
||||
if (this.hasSameTypeAsPreviousOne(bg.type) && currentProcessor) {
|
||||
|
@ -105,7 +104,7 @@ export class VirtualBackgroundService {
|
|||
this.log.e('No processor found for the background effect.');
|
||||
return;
|
||||
}
|
||||
await this.applyProcessorToCameraTrack(cameraTrack, newProcessor);
|
||||
await this.applyProcessorToCameraTracks(cameraTracks, newProcessor);
|
||||
}
|
||||
|
||||
this.storageService.setBackground(bg.id);
|
||||
|
@ -129,14 +128,15 @@ export class VirtualBackgroundService {
|
|||
async removeBackground() {
|
||||
if (this.isBackgroundApplied()) {
|
||||
this.backgroundIdSelected.next('no_effect');
|
||||
const cameraTrack = this.getCameraTrack();
|
||||
if (cameraTrack) {
|
||||
const tracks = this.participantService.getLocalParticipant()?.tracks;
|
||||
const promises = tracks?.map(async (t) => {
|
||||
try {
|
||||
await cameraTrack.stopProcessor();
|
||||
await (t.track as LocalTrack).stopProcessor();
|
||||
} catch (e) {
|
||||
this.log.w('Error stopping processor:', e);
|
||||
}
|
||||
}
|
||||
});
|
||||
await Promise.all(promises || []);
|
||||
this.storageService.removeBackground();
|
||||
}
|
||||
}
|
||||
|
@ -160,41 +160,26 @@ export class VirtualBackgroundService {
|
|||
return undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the camera track from either the published tracks (if in room) or local tracks (if in prejoin)
|
||||
* @returns The camera LocalTrack or undefined if not found
|
||||
* @private
|
||||
*/
|
||||
private getCameraTrack(): LocalVideoTrack | undefined {
|
||||
// First, try to get from published tracks (when in room)
|
||||
if (this.openviduService.isRoomConnected()) {
|
||||
const localParticipant = this.participantService.getLocalParticipant();
|
||||
const cameraTrackPublication = localParticipant?.cameraTracks?.[0];
|
||||
if (cameraTrackPublication?.track) {
|
||||
return cameraTrackPublication.track as LocalVideoTrack;
|
||||
}
|
||||
}
|
||||
private async applyProcessorToCameraTracks(
|
||||
cameraTracks: ParticipantTrackPublication[],
|
||||
processor: ProcessorWrapper<BackgroundOptions>
|
||||
) {
|
||||
const promises = cameraTracks.map((track) => {
|
||||
return (track.track as LocalTrack).setProcessor(processor);
|
||||
});
|
||||
|
||||
// Fallback to local tracks (when in prejoin or tracks not yet published)
|
||||
const localTracks = this.openviduService.getLocalTracks();
|
||||
const cameraTrack = localTracks.find((track) => track.kind === Track.Kind.Video);
|
||||
return cameraTrack as LocalVideoTrack | undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies a background processor to the camera track
|
||||
* @param cameraTrack The camera track to apply the processor to
|
||||
* @param processor The background processor to apply
|
||||
* @private
|
||||
*/
|
||||
private async applyProcessorToCameraTrack(cameraTrack: LocalVideoTrack, processor: ProcessorWrapper<BackgroundOptions>): Promise<void> {
|
||||
await cameraTrack.setProcessor(processor);
|
||||
await Promise.all(promises || []);
|
||||
}
|
||||
|
||||
private backgroundIsAlreadyApplied(backgroundId: string): boolean {
|
||||
return backgroundId === this.backgroundIdSelected.getValue();
|
||||
}
|
||||
|
||||
private getCameraTracks(): ParticipantTrackPublication[] | undefined {
|
||||
const localParticipant = this.participantService.getLocalParticipant();
|
||||
return localParticipant?.cameraTracks;
|
||||
}
|
||||
|
||||
/**
|
||||
* Replaces the current background effect with a new one by updating the processor options.
|
||||
*
|
||||
|
|
|
@ -18,7 +18,6 @@ export * from './lib/components/toolbar/toolbar.component';
|
|||
export * from './lib/components/videoconference/videoconference.component';
|
||||
export * from './lib/config/openvidu-components-angular.config';
|
||||
// Directives
|
||||
export * from './lib/directives/template/internals.directive';
|
||||
export * from './lib/directives/api/activities-panel.directive';
|
||||
export * from './lib/directives/api/admin.directive';
|
||||
export * from './lib/directives/api/api.directive.module';
|
||||
|
|
|
@ -32,17 +32,6 @@
|
|||
[activitiesPanelRecordingActivity]="activitiesPanelRecordingActivity"
|
||||
[activitiesPanelBroadcastingActivity]="activitiesPanelBroadcastingActivity"
|
||||
[toolbarSettingsButton]="toolbarSettingsButton"
|
||||
[toolbarViewRecordingsButton]="toolbarViewRecordingsButton"
|
||||
[recordingActivityShowControls]="{
|
||||
play: false,
|
||||
download: false,
|
||||
delete: false,
|
||||
externalView: true
|
||||
}"
|
||||
[recordingActivityReadOnly]="false"
|
||||
[recordingActivityStartStopRecordingButton]="recordingActivityStartStopRecordingButton"
|
||||
[recordingActivityViewRecordingsButton]="recordingActivityViewRecordingsButton"
|
||||
[recordingActivityShowRecordingsList]="true"
|
||||
(onTokenRequested)="onTokenRequested($event)"
|
||||
(onReadyToJoin)="onReadyToJoin()"
|
||||
(onRoomCreated)="onRoomCreated($event)"
|
||||
|
@ -65,7 +54,6 @@
|
|||
(onBroadcastingStopRequested)="onBroadcastingStopRequested($event)"
|
||||
(onSettingsPanelStatusChanged)="onSettingsPanelStatusChanged($event)"
|
||||
(onActivitiesPanelStatusChanged)="onActivitiesPanelStatusChanged($event)"
|
||||
(onViewRecordingClicked)="onRoomDisconnected()"
|
||||
>
|
||||
</ov-videoconference>
|
||||
}
|
||||
|
|
|
@ -34,6 +34,7 @@ export class CallComponent implements OnInit {
|
|||
{ name: 'custom', lang: 'cus' }
|
||||
];
|
||||
prejoin: boolean = true;
|
||||
prejoinDisplayParticipantName: boolean = true;
|
||||
participantName: string = `Participant${Math.floor(Math.random() * 1000)}`;
|
||||
videoEnabled: boolean = true;
|
||||
audioEnabled: boolean = true;
|
||||
|
@ -58,13 +59,6 @@ export class CallComponent implements OnInit {
|
|||
activitiesPanelBroadcastingActivity: boolean = true;
|
||||
toolbarSettingsButton: boolean = true;
|
||||
fakeDevices: boolean = false;
|
||||
// Internal directive inputs (public for E2E)
|
||||
|
||||
prejoinDisplayParticipantName: boolean = true;
|
||||
|
||||
public recordingActivityViewRecordingsButton: boolean = false;
|
||||
public recordingActivityStartStopRecordingButton: boolean = true;
|
||||
toolbarViewRecordingsButton: boolean = false;
|
||||
private redirectToHomeOnLeaves: boolean = true;
|
||||
|
||||
private staticVideos = [
|
||||
|
@ -110,6 +104,8 @@ export class CallComponent implements OnInit {
|
|||
} catch {}
|
||||
}
|
||||
if (params['prejoin'] !== undefined) this.prejoin = params['prejoin'] === 'true';
|
||||
if (params['displayParticipantName'] !== undefined)
|
||||
this.prejoinDisplayParticipantName = params['displayParticipantName'] === 'true';
|
||||
if (params['participantName']) this.participantName = params['participantName'];
|
||||
if (params['videoEnabled'] !== undefined) this.videoEnabled = params['videoEnabled'] === 'true';
|
||||
if (params['audioEnabled'] !== undefined) this.audioEnabled = params['audioEnabled'] === 'true';
|
||||
|
@ -145,15 +141,6 @@ export class CallComponent implements OnInit {
|
|||
|
||||
if (params['fakeDevices'] !== undefined) this.fakeDevices = params['fakeDevices'] === 'true';
|
||||
|
||||
// Internal/private directive params
|
||||
if (params['prejoinDisplayParticipantName'] !== undefined)
|
||||
this.prejoinDisplayParticipantName = params['prejoinDisplayParticipantName'] === 'true';
|
||||
if (params['recordingActivityViewRecordingsButton'] !== undefined)
|
||||
this.recordingActivityViewRecordingsButton = params['recordingActivityViewRecordingsButton'] === 'true';
|
||||
if (params['recordingActivityStartStopRecordingButton'] !== undefined)
|
||||
this.recordingActivityStartStopRecordingButton = params['recordingActivityStartStopRecordingButton'] === 'true';
|
||||
if (params['toolbarViewRecordingsButton'] !== undefined)
|
||||
this.toolbarViewRecordingsButton = params['toolbarViewRecordingsButton'] === 'true';
|
||||
if (params['redirectToHome'] === undefined) {
|
||||
this.redirectToHomeOnLeaves = true;
|
||||
} else {
|
||||
|
@ -211,10 +198,8 @@ export class CallComponent implements OnInit {
|
|||
if (publication.videoTrack?.attachedElements) {
|
||||
this.replaceWithStaticVideos(publication.videoTrack?.attachedElements);
|
||||
const firstVideo = this.staticVideos.shift();
|
||||
if (firstVideo) {
|
||||
this.staticVideos.push(firstVideo);
|
||||
}
|
||||
}
|
||||
}, 2000);
|
||||
}
|
||||
});
|
||||
|
@ -332,8 +317,7 @@ export class CallComponent implements OnInit {
|
|||
}
|
||||
}
|
||||
async onRecordingStopRequested(event: RecordingStopRequestedEvent) {
|
||||
this.appendElement('onRecordingStopRequested-' + event.roomName);
|
||||
|
||||
this.appendElement('onRecordingStopRequested');
|
||||
console.warn('STOP RECORDING CLICKED', event);
|
||||
try {
|
||||
await this.restService.stopRecording(event);
|
||||
|
|
|
@ -271,9 +271,10 @@ Resources:
|
|||
"GRAFANA_ADMIN_PASSWORD": "none",
|
||||
"LIVEKIT_API_KEY": "none",
|
||||
"LIVEKIT_API_SECRET": "none",
|
||||
"MEET_ADMIN_USER": "none",
|
||||
"MEET_ADMIN_SECRET": "none",
|
||||
"MEET_API_KEY": "none",
|
||||
"DEFAULT_APP_USERNAME": "none",
|
||||
"DEFAULT_APP_PASSWORD": "none",
|
||||
"DEFAULT_APP_ADMIN_USERNAME": "none",
|
||||
"DEFAULT_APP_ADMIN_PASSWORD": "none",
|
||||
"ENABLED_MODULES": "none"
|
||||
}
|
||||
|
||||
|
@ -366,7 +367,7 @@ Resources:
|
|||
'/usr/local/bin/install.sh':
|
||||
content: !Sub |
|
||||
#!/bin/bash -x
|
||||
OPENVIDU_VERSION=main
|
||||
OPENVIDU_VERSION=3.3.0
|
||||
DOMAIN=
|
||||
YQ_VERSION=v4.44.5
|
||||
|
||||
|
@ -409,10 +410,11 @@ Resources:
|
|||
DASHBOARD_ADMIN_PASSWORD="$(/usr/local/bin/store_secret.sh generate DASHBOARD_ADMIN_PASSWORD)"
|
||||
GRAFANA_ADMIN_USERNAME="$(/usr/local/bin/store_secret.sh save GRAFANA_ADMIN_USERNAME "grafanaadmin")"
|
||||
GRAFANA_ADMIN_PASSWORD="$(/usr/local/bin/store_secret.sh generate GRAFANA_ADMIN_PASSWORD)"
|
||||
MEET_ADMIN_USER="$(/usr/local/bin/store_secret.sh save MEET_ADMIN_USER "meetadmin")"
|
||||
MEET_ADMIN_SECRET="$(/usr/local/bin/store_secret.sh generate MEET_ADMIN_SECRET)"
|
||||
MEET_API_KEY="$(/usr/local/bin/store_secret.sh generate MEET_API_KEY)"
|
||||
ENABLED_MODULES="$(/usr/local/bin/store_secret.sh save ENABLED_MODULES "observability,openviduMeet")"
|
||||
DEFAULT_APP_USERNAME="$(/usr/local/bin/store_secret.sh save DEFAULT_APP_USERNAME "calluser")"
|
||||
DEFAULT_APP_PASSWORD="$(/usr/local/bin/store_secret.sh generate DEFAULT_APP_PASSWORD)"
|
||||
DEFAULT_APP_ADMIN_USERNAME="$(/usr/local/bin/store_secret.sh save DEFAULT_APP_ADMIN_USERNAME "calladmin")"
|
||||
DEFAULT_APP_ADMIN_PASSWORD="$(/usr/local/bin/store_secret.sh generate DEFAULT_APP_ADMIN_PASSWORD)"
|
||||
ENABLED_MODULES="$(/usr/local/bin/store_secret.sh save ENABLED_MODULES "observability,app")"
|
||||
LIVEKIT_API_KEY="$(/usr/local/bin/store_secret.sh generate LIVEKIT_API_KEY "API" 12)"
|
||||
LIVEKIT_API_SECRET="$(/usr/local/bin/store_secret.sh generate LIVEKIT_API_SECRET)"
|
||||
|
||||
|
@ -437,9 +439,10 @@ Resources:
|
|||
"--dashboard-admin-password=$DASHBOARD_ADMIN_PASSWORD"
|
||||
"--grafana-admin-user=$GRAFANA_ADMIN_USERNAME"
|
||||
"--grafana-admin-password=$GRAFANA_ADMIN_PASSWORD"
|
||||
"--meet-admin-user=$MEET_ADMIN_USER"
|
||||
"--meet-admin-password=$MEET_ADMIN_SECRET"
|
||||
"--meet-api-key=$MEET_API_KEY"
|
||||
"--default-app-user=$DEFAULT_APP_USERNAME"
|
||||
"--default-app-password=$DEFAULT_APP_PASSWORD"
|
||||
"--default-app-admin-user=$DEFAULT_APP_ADMIN_USERNAME"
|
||||
"--default-app-admin-password=$DEFAULT_APP_ADMIN_PASSWORD"
|
||||
"--livekit-api-key=$LIVEKIT_API_KEY"
|
||||
"--livekit-api-secret=$LIVEKIT_API_SECRET"
|
||||
)
|
||||
|
@ -635,9 +638,10 @@ Resources:
|
|||
sed -i "s/GRAFANA_ADMIN_PASSWORD=.*/GRAFANA_ADMIN_PASSWORD=$(echo $SHARED_SECRET | jq -r .GRAFANA_ADMIN_PASSWORD)/" "${!CONFIG_DIR}/openvidu.env"
|
||||
sed -i "s/LIVEKIT_API_KEY=.*/LIVEKIT_API_KEY=$(echo $SHARED_SECRET | jq -r .LIVEKIT_API_KEY)/" "${!CONFIG_DIR}/openvidu.env"
|
||||
sed -i "s/LIVEKIT_API_SECRET=.*/LIVEKIT_API_SECRET=$(echo $SHARED_SECRET | jq -r .LIVEKIT_API_SECRET)/" "${!CONFIG_DIR}/openvidu.env"
|
||||
sed -i "s/MEET_ADMIN_USER=.*/MEET_ADMIN_USER=$(echo $SHARED_SECRET | jq -r .MEET_ADMIN_USER)/" "${!CONFIG_DIR}/meet.env"
|
||||
sed -i "s/MEET_ADMIN_SECRET=.*/MEET_ADMIN_SECRET=$(echo $SHARED_SECRET | jq -r .MEET_ADMIN_SECRET)/" "${!CONFIG_DIR}/meet.env"
|
||||
sed -i "s/MEET_API_KEY=.*/MEET_API_KEY=$(echo $SHARED_SECRET | jq -r .MEET_API_KEY)/" "${!CONFIG_DIR}/meet.env"
|
||||
sed -i "s/CALL_USER=.*/CALL_USER=$(echo $SHARED_SECRET | jq -r .DEFAULT_APP_USERNAME)/" "${!CONFIG_DIR}/app.env"
|
||||
sed -i "s/CALL_SECRET=.*/CALL_SECRET=$(echo $SHARED_SECRET | jq -r .DEFAULT_APP_PASSWORD)/" "${!CONFIG_DIR}/app.env"
|
||||
sed -i "s/CALL_ADMIN_USER=.*/CALL_ADMIN_USER=$(echo $SHARED_SECRET | jq -r .DEFAULT_APP_ADMIN_USERNAME)/" "${!CONFIG_DIR}/app.env"
|
||||
sed -i "s/CALL_ADMIN_SECRET=.*/CALL_ADMIN_SECRET=$(echo $SHARED_SECRET | jq -r .DEFAULT_APP_ADMIN_PASSWORD)/" "${!CONFIG_DIR}/app.env"
|
||||
sed -i "s/ENABLED_MODULES=.*/ENABLED_MODULES=$(echo $SHARED_SECRET | jq -r .ENABLED_MODULES)/" "${!CONFIG_DIR}/openvidu.env"
|
||||
|
||||
# Update URLs in secret
|
||||
|
@ -688,9 +692,10 @@ Resources:
|
|||
SHARED_SECRET="$(echo "$SHARED_SECRET" | jq '. + {"GRAFANA_ADMIN_PASSWORD": "'"$(/usr/local/bin/get_value_from_config.sh GRAFANA_ADMIN_PASSWORD "${!CONFIG_DIR}/openvidu.env")"'"}')"
|
||||
SHARED_SECRET="$(echo "$SHARED_SECRET" | jq '. + {"LIVEKIT_API_KEY": "'"$(/usr/local/bin/get_value_from_config.sh LIVEKIT_API_KEY "${!CONFIG_DIR}/openvidu.env")"'"}')"
|
||||
SHARED_SECRET="$(echo "$SHARED_SECRET" | jq '. + {"LIVEKIT_API_SECRET": "'"$(/usr/local/bin/get_value_from_config.sh LIVEKIT_API_SECRET "${!CONFIG_DIR}/openvidu.env")"'"}')"
|
||||
SHARED_SECRET="$(echo "$SHARED_SECRET" | jq '. + {"MEET_ADMIN_USER": "'"$(/usr/local/bin/get_value_from_config.sh MEET_ADMIN_USER "${!CONFIG_DIR}/meet.env")"'"}')"
|
||||
SHARED_SECRET="$(echo "$SHARED_SECRET" | jq '. + {"MEET_ADMIN_SECRET": "'"$(/usr/local/bin/get_value_from_config.sh MEET_ADMIN_SECRET "${!CONFIG_DIR}/meet.env")"'"}')"
|
||||
SHARED_SECRET="$(echo "$SHARED_SECRET" | jq '. + {"MEET_API_KEY": "'"$(/usr/local/bin/get_value_from_config.sh MEET_API_KEY "${!CONFIG_DIR}/meet.env")"'"}')"
|
||||
SHARED_SECRET="$(echo "$SHARED_SECRET" | jq '. + {"DEFAULT_APP_USERNAME": "'"$(/usr/local/bin/get_value_from_config.sh CALL_USER "${!CONFIG_DIR}/app.env")"'"}')"
|
||||
SHARED_SECRET="$(echo "$SHARED_SECRET" | jq '. + {"DEFAULT_APP_PASSWORD": "'"$(/usr/local/bin/get_value_from_config.sh CALL_SECRET "${!CONFIG_DIR}/app.env")"'"}')"
|
||||
SHARED_SECRET="$(echo "$SHARED_SECRET" | jq '. + {"DEFAULT_APP_ADMIN_USERNAME": "'"$(/usr/local/bin/get_value_from_config.sh CALL_ADMIN_USER "${!CONFIG_DIR}/app.env")"'"}')"
|
||||
SHARED_SECRET="$(echo "$SHARED_SECRET" | jq '. + {"DEFAULT_APP_ADMIN_PASSWORD": "'"$(/usr/local/bin/get_value_from_config.sh CALL_ADMIN_SECRET "${!CONFIG_DIR}/app.env")"'"}')"
|
||||
SHARED_SECRET="$(echo "$SHARED_SECRET" | jq '. + {"ENABLED_MODULES": "'"$(/usr/local/bin/get_value_from_config.sh ENABLED_MODULES "${!CONFIG_DIR}/openvidu.env")"'"}')"
|
||||
|
||||
# Update shared secret
|
||||
|
@ -931,14 +936,6 @@ Resources:
|
|||
FromPort: 1935
|
||||
ToPort: 1935
|
||||
CidrIpv6: ::/0
|
||||
- IpProtocol: tcp
|
||||
FromPort: 7881
|
||||
ToPort: 7881
|
||||
CidrIp: 0.0.0.0/0
|
||||
- IpProtocol: tcp
|
||||
FromPort: 7881
|
||||
ToPort: 7881
|
||||
CidrIpv6: ::/0
|
||||
- IpProtocol: udp
|
||||
FromPort: 7885
|
||||
ToPort: 7885
|
||||
|
@ -955,6 +952,14 @@ Resources:
|
|||
FromPort: 50000
|
||||
ToPort: 60000
|
||||
CidrIpv6: ::/0
|
||||
- IpProtocol: tcp
|
||||
FromPort: 50000
|
||||
ToPort: 60000
|
||||
CidrIp: 0.0.0.0/0
|
||||
- IpProtocol: tcp
|
||||
FromPort: 50000
|
||||
ToPort: 60000
|
||||
CidrIpv6: ::/0
|
||||
|
||||
Outputs:
|
||||
ServicesAndCredentials:
|
||||
|
|
|
@ -270,7 +270,7 @@ var stringInterpolationParams = {
|
|||
|
||||
var installScriptTemplate = '''
|
||||
#!/bin/bash -x
|
||||
OPENVIDU_VERSION=main
|
||||
OPENVIDU_VERSION=3.3.0
|
||||
DOMAIN=
|
||||
|
||||
apt-get update && apt-get install -y \
|
||||
|
@ -300,12 +300,13 @@ DASHBOARD_ADMIN_USERNAME="$(/usr/local/bin/store_secret.sh save DASHBOARD-ADMIN-
|
|||
DASHBOARD_ADMIN_PASSWORD="$(/usr/local/bin/store_secret.sh generate DASHBOARD-ADMIN-PASSWORD)"
|
||||
GRAFANA_ADMIN_USERNAME="$(/usr/local/bin/store_secret.sh save GRAFANA-ADMIN-USERNAME "grafanaadmin")"
|
||||
GRAFANA_ADMIN_PASSWORD="$(/usr/local/bin/store_secret.sh generate GRAFANA-ADMIN-PASSWORD)"
|
||||
MEET_ADMIN_USER="$(/usr/local/bin/store_secret.sh save MEET-ADMIN-USER "meetadmin")"
|
||||
MEET_ADMIN_SECRET="$(/usr/local/bin/store_secret.sh generate MEET-ADMIN-SECRET)"
|
||||
MEET_API_KEY="$(/usr/local/bin/store_secret.sh generate MEET-API-KEY)"
|
||||
DEFAULT_APP_USERNAME="$(/usr/local/bin/store_secret.sh save DEFAULT-APP-USERNAME "calluser")"
|
||||
DEFAULT_APP_PASSWORD="$(/usr/local/bin/store_secret.sh generate DEFAULT-APP-PASSWORD)"
|
||||
DEFAULT_APP_ADMIN_USERNAME="$(/usr/local/bin/store_secret.sh save DEFAULT-APP-ADMIN-USERNAME "calladmin")"
|
||||
DEFAULT_APP_ADMIN_PASSWORD="$(/usr/local/bin/store_secret.sh generate DEFAULT-APP-ADMIN-PASSWORD)"
|
||||
LIVEKIT_API_KEY="$(/usr/local/bin/store_secret.sh generate LIVEKIT-API-KEY "API" 12)"
|
||||
LIVEKIT_API_SECRET="$(/usr/local/bin/store_secret.sh generate LIVEKIT-API-SECRET)"
|
||||
ENABLED_MODULES="$(/usr/local/bin/store_secret.sh save ENABLED-MODULES "observability,openviduMeet")"
|
||||
ENABLED_MODULES="$(/usr/local/bin/store_secret.sh save ENABLED-MODULES "observability,app")"
|
||||
|
||||
# Base command
|
||||
INSTALL_COMMAND="sh <(curl -fsSL http://get.openvidu.io/community/singlenode/$OPENVIDU_VERSION/install.sh)"
|
||||
|
@ -328,9 +329,10 @@ COMMON_ARGS=(
|
|||
"--dashboard-admin-password=$DASHBOARD_ADMIN_PASSWORD"
|
||||
"--grafana-admin-user=$GRAFANA_ADMIN_USERNAME"
|
||||
"--grafana-admin-password=$GRAFANA_ADMIN_PASSWORD"
|
||||
"--meet-admin-user=$MEET_ADMIN_USER"
|
||||
"--meet-admin-password=$MEET_ADMIN_SECRET"
|
||||
"--meet-api-key=$MEET_API_KEY"
|
||||
"--default-app-user=$DEFAULT_APP_USERNAME"
|
||||
"--default-app-password=$DEFAULT_APP_PASSWORD"
|
||||
"--default-app-admin-user=$DEFAULT_APP_ADMIN_USERNAME"
|
||||
"--default-app-admin-password=$DEFAULT_APP_ADMIN_PASSWORD"
|
||||
"--livekit-api-key=$LIVEKIT_API_KEY"
|
||||
"--livekit-api-secret=$LIVEKIT_API_SECRET"
|
||||
)
|
||||
|
@ -475,9 +477,10 @@ export GRAFANA_ADMIN_USERNAME=$(az keyvault secret show --vault-name ${keyVaultN
|
|||
export GRAFANA_ADMIN_PASSWORD=$(az keyvault secret show --vault-name ${keyVaultName} --name GRAFANA-ADMIN-PASSWORD --query value -o tsv)
|
||||
export LIVEKIT_API_KEY=$(az keyvault secret show --vault-name ${keyVaultName} --name LIVEKIT-API-KEY --query value -o tsv)
|
||||
export LIVEKIT_API_SECRET=$(az keyvault secret show --vault-name ${keyVaultName} --name LIVEKIT-API-SECRET --query value -o tsv)
|
||||
export MEET_ADMIN_USER=$(az keyvault secret show --vault-name ${keyVaultName} --name MEET-ADMIN-USER --query value -o tsv)
|
||||
export MEET_ADMIN_SECRET=$(az keyvault secret show --vault-name ${keyVaultName} --name MEET-ADMIN-SECRET --query value -o tsv)
|
||||
export MEET_API_KEY=$(az keyvault secret show --vault-name ${keyVaultName} --name MEET-API-KEY --query value -o tsv)
|
||||
export DEFAULT_APP_USERNAME=$(az keyvault secret show --vault-name ${keyVaultName} --name DEFAULT-APP-USERNAME --query value -o tsv)
|
||||
export DEFAULT_APP_PASSWORD=$(az keyvault secret show --vault-name ${keyVaultName} --name DEFAULT-APP-PASSWORD --query value -o tsv)
|
||||
export DEFAULT_APP_ADMIN_USERNAME=$(az keyvault secret show --vault-name ${keyVaultName} --name DEFAULT-APP-ADMIN-USERNAME --query value -o tsv)
|
||||
export DEFAULT_APP_ADMIN_PASSWORD=$(az keyvault secret show --vault-name ${keyVaultName} --name DEFAULT-APP-ADMIN-PASSWORD --query value -o tsv)
|
||||
export ENABLED_MODULES=$(az keyvault secret show --vault-name ${keyVaultName} --name ENABLED-MODULES --query value -o tsv)
|
||||
|
||||
|
||||
|
@ -494,9 +497,10 @@ sed -i "s/GRAFANA_ADMIN_USERNAME=.*/GRAFANA_ADMIN_USERNAME=$GRAFANA_ADMIN_USERNA
|
|||
sed -i "s/GRAFANA_ADMIN_PASSWORD=.*/GRAFANA_ADMIN_PASSWORD=$GRAFANA_ADMIN_PASSWORD/" "${CONFIG_DIR}/openvidu.env"
|
||||
sed -i "s/LIVEKIT_API_KEY=.*/LIVEKIT_API_KEY=$LIVEKIT_API_KEY/" "${CONFIG_DIR}/openvidu.env"
|
||||
sed -i "s/LIVEKIT_API_SECRET=.*/LIVEKIT_API_SECRET=$LIVEKIT_API_SECRET/" "${CONFIG_DIR}/openvidu.env"
|
||||
sed -i "s/MEET_ADMIN_USER=.*/MEET_ADMIN_USER=$MEET_ADMIN_USER/" "${CONFIG_DIR}/meet.env"
|
||||
sed -i "s/MEET_ADMIN_SECRET=.*/MEET_ADMIN_SECRET=$MEET_ADMIN_SECRET/" "${CONFIG_DIR}/meet.env"
|
||||
sed -i "s/MEET_API_KEY=.*/MEET_API_KEY=$MEET_API_KEY/" "${CONFIG_DIR}/meet.env"
|
||||
sed -i "s/CALL_USER=.*/CALL_USER=$DEFAULT_APP_USERNAME/" "${CONFIG_DIR}/app.env"
|
||||
sed -i "s/CALL_SECRET=.*/CALL_SECRET=$DEFAULT_APP_PASSWORD/" "${CONFIG_DIR}/app.env"
|
||||
sed -i "s/CALL_ADMIN_USER=.*/CALL_ADMIN_USER=$DEFAULT_APP_ADMIN_USERNAME/" "${CONFIG_DIR}/app.env"
|
||||
sed -i "s/CALL_ADMIN_SECRET=.*/CALL_ADMIN_SECRET=$DEFAULT_APP_ADMIN_PASSWORD/" "${CONFIG_DIR}/app.env"
|
||||
sed -i "s/ENABLED_MODULES=.*/ENABLED_MODULES=$ENABLED_MODULES/" "${CONFIG_DIR}/openvidu.env"
|
||||
|
||||
|
||||
|
@ -543,9 +547,10 @@ GRAFANA_ADMIN_USERNAME="$(/usr/local/bin/get_value_from_config.sh GRAFANA_ADMIN_
|
|||
GRAFANA_ADMIN_PASSWORD="$(/usr/local/bin/get_value_from_config.sh GRAFANA_ADMIN_PASSWORD "${CONFIG_DIR}/openvidu.env")"
|
||||
LIVEKIT_API_KEY="$(/usr/local/bin/get_value_from_config.sh LIVEKIT_API_KEY "${CONFIG_DIR}/openvidu.env")"
|
||||
LIVEKIT_API_SECRET="$(/usr/local/bin/get_value_from_config.sh LIVEKIT_API_SECRET "${CONFIG_DIR}/openvidu.env")"
|
||||
MEET_ADMIN_USER="$(/usr/local/bin/get_value_from_config.sh MEET_ADMIN_USER "${CONFIG_DIR}/meet.env")"
|
||||
MEET_ADMIN_SECRET="$(/usr/local/bin/get_value_from_config.sh MEET_ADMIN_SECRET "${CONFIG_DIR}/meet.env")"
|
||||
MEET_API_KEY="$(/usr/local/bin/get_value_from_config.sh MEET_API_KEY "${CONFIG_DIR}/meet.env")"
|
||||
DEFAULT_APP_USERNAME="$(/usr/local/bin/get_value_from_config.sh CALL_USER "${CONFIG_DIR}/app.env")"
|
||||
DEFAULT_APP_PASSWORD="$(/usr/local/bin/get_value_from_config.sh CALL_SECRET "${CONFIG_DIR}/app.env")"
|
||||
DEFAULT_APP_ADMIN_USERNAME="$(/usr/local/bin/get_value_from_config.sh CALL_ADMIN_USER "${CONFIG_DIR}/app.env")"
|
||||
DEFAULT_APP_ADMIN_PASSWORD="$(/usr/local/bin/get_value_from_config.sh CALL_ADMIN_SECRET "${CONFIG_DIR}/app.env")"
|
||||
ENABLED_MODULES="$(/usr/local/bin/get_value_from_config.sh ENABLED_MODULES "${CONFIG_DIR}/openvidu.env")"
|
||||
|
||||
|
||||
|
@ -564,9 +569,10 @@ az keyvault secret set --vault-name ${keyVaultName} --name GRAFANA-ADMIN-USERNAM
|
|||
az keyvault secret set --vault-name ${keyVaultName} --name GRAFANA-ADMIN-PASSWORD --value $GRAFANA_ADMIN_PASSWORD
|
||||
az keyvault secret set --vault-name ${keyVaultName} --name LIVEKIT-API-KEY --value $LIVEKIT_API_KEY
|
||||
az keyvault secret set --vault-name ${keyVaultName} --name LIVEKIT-API-SECRET --value $LIVEKIT_API_SECRET
|
||||
az keyvault secret set --vault-name ${keyVaultName} --name MEET-ADMIN-USER --value $MEET_ADMIN_USER
|
||||
az keyvault secret set --vault-name ${keyVaultName} --name MEET-ADMIN-SECRET --value $MEET_ADMIN_SECRET
|
||||
az keyvault secret set --vault-name ${keyVaultName} --name MEET-API-KEY --value $MEET_API_KEY
|
||||
az keyvault secret set --vault-name ${keyVaultName} --name DEFAULT-APP-USERNAME --value $DEFAULT_APP_USERNAME
|
||||
az keyvault secret set --vault-name ${keyVaultName} --name DEFAULT-APP-PASSWORD --value $DEFAULT_APP_PASSWORD
|
||||
az keyvault secret set --vault-name ${keyVaultName} --name DEFAULT-APP-ADMIN-USERNAME --value $DEFAULT_APP_ADMIN_USERNAME
|
||||
az keyvault secret set --vault-name ${keyVaultName} --name DEFAULT-APP-ADMIN-PASSWORD --value $DEFAULT_APP_ADMIN_PASSWORD
|
||||
az keyvault secret set --vault-name ${keyVaultName} --name ENABLED-MODULES --value $ENABLED_MODULES
|
||||
'''
|
||||
|
||||
|
@ -1074,6 +1080,22 @@ resource webServerSecurityGroup 'Microsoft.Network/networkSecurityGroups@2023-11
|
|||
direction: 'Inbound'
|
||||
}
|
||||
}
|
||||
{
|
||||
name: 'WebRTC_traffic_TCP'
|
||||
properties: {
|
||||
protocol: 'Tcp'
|
||||
sourceAddressPrefix: '*'
|
||||
sourcePortRange: '*'
|
||||
destinationAddressPrefix: '*'
|
||||
destinationPortRanges: [
|
||||
'50000'
|
||||
'60000'
|
||||
]
|
||||
access: 'Allow'
|
||||
priority: 190
|
||||
direction: 'Inbound'
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
File diff suppressed because one or more lines are too long
|
@ -3,9 +3,9 @@
|
|||
set -eu
|
||||
export DOCKER_VERSION="${DOCKER_VERSION:-28.1.1}"
|
||||
export DOCKER_COMPOSE_VERSION="${DOCKER_COMPOSE_VERSION:-v2.35.1}"
|
||||
export OPENVIDU_VERSION="${OPENVIDU_VERSION:-main}"
|
||||
export OPENVIDU_VERSION="${OPENVIDU_VERSION:-3.3.0}"
|
||||
export INSTALLER_IMAGE="${INSTALLER_IMAGE:-docker.io/openvidu/openvidu-installer:${OPENVIDU_VERSION}}"
|
||||
export MINIO_SERVER_IMAGE="${MINIO_SERVER_IMAGE:-docker.io/openvidu/minio:2025.5.24-debian-12-r1}"
|
||||
export MINIO_SERVER_IMAGE="${MINIO_SERVER_IMAGE:-docker.io/bitnami/minio:2025.5.24-debian-12-r1}"
|
||||
export MINIO_CLIENT_IMAGE="${MINIO_CLIENT_IMAGE:-docker.io/minio/mc:RELEASE.2025-05-21T01-59-54Z}"
|
||||
export MONGO_SERVER_IMAGE="${MONGO_SERVER_IMAGE:-docker.io/mongo:8.0.9}"
|
||||
export REDIS_SERVER_IMAGE="${REDIS_SERVER_IMAGE:-docker.io/redis:7.4.4-alpine}"
|
||||
|
@ -15,17 +15,17 @@ export CADDY_SERVER_PRO_IMAGE="${CADDY_SERVER_PRO_IMAGE:-docker.io/openvidu/open
|
|||
export OPENVIDU_OPERATOR_IMAGE="${OPENVIDU_OPERATOR_IMAGE:-docker.io/openvidu/openvidu-operator:${OPENVIDU_VERSION}}"
|
||||
export OPENVIDU_SERVER_PRO_IMAGE="${OPENVIDU_SERVER_PRO_IMAGE:-docker.io/openvidu/openvidu-server-pro:${OPENVIDU_VERSION}}"
|
||||
export OPENVIDU_SERVER_IMAGE="${OPENVIDU_SERVER_IMAGE:-docker.io/openvidu/openvidu-server:${OPENVIDU_VERSION}}"
|
||||
export OPENVIDU_MEET_SERVER_IMAGE="${OPENVIDU_MEET_SERVER_IMAGE:-docker.io/openvidu/openvidu-meet:${OPENVIDU_VERSION}}"
|
||||
export OPENVIDU_CALL_SERVER_IMAGE="${OPENVIDU_CALL_SERVER_IMAGE:-docker.io/openvidu/openvidu-call:${OPENVIDU_VERSION}}"
|
||||
export OPENVIDU_DASHBOARD_PRO_IMAGE="${OPENVIDU_DASHBOARD_PRO_IMAGE:-docker.io/openvidu/openvidu-pro-dashboard:${OPENVIDU_VERSION}}"
|
||||
export OPENVIDU_DASHBOARD_IMAGE="${OPENVIDU_DASHBOARD_IMAGE:-docker.io/openvidu/openvidu-dashboard:${OPENVIDU_VERSION}}"
|
||||
export OPENVIDU_V2COMPATIBILITY_IMAGE="${OPENVIDU_V2COMPATIBILITY_IMAGE:-docker.io/openvidu/openvidu-v2compatibility:${OPENVIDU_VERSION}}"
|
||||
export OPENVIDU_AGENT_SPEECH_PROCESSING_IMAGE="${OPENVIDU_AGENT_SPEECH_PROCESSING_IMAGE:-docker.io/openvidu/agent-speech-processing:${OPENVIDU_VERSION}}"
|
||||
export LIVEKIT_INGRESS_SERVER_IMAGE="${LIVEKIT_INGRESS_SERVER_IMAGE:-docker.io/openvidu/ingress:${OPENVIDU_VERSION}}"
|
||||
export LIVEKIT_EGRESS_SERVER_IMAGE="${LIVEKIT_EGRESS_SERVER_IMAGE:-docker.io/livekit/egress:v1.10.0}"
|
||||
export LIVEKIT_EGRESS_SERVER_IMAGE="${LIVEKIT_EGRESS_SERVER_IMAGE:-docker.io/livekit/egress:v1.9.1}"
|
||||
export PROMETHEUS_IMAGE="${PROMETHEUS_IMAGE:-docker.io/prom/prometheus:v3.4.0}"
|
||||
export PROMTAIL_IMAGE="${PROMTAIL_IMAGE:-docker.io/grafana/promtail:3.5.1}"
|
||||
export LOKI_IMAGE="${LOKI_IMAGE:-docker.io/grafana/loki:3.5.1}"
|
||||
export MIMIR_IMAGE="${MIMIR_IMAGE:-docker.io/openvidu/grafana-mimir:2.16.0}"
|
||||
export MIMIR_IMAGE="${MIMIR_IMAGE:-docker.io/bitnami/grafana-mimir:2.16.0}"
|
||||
export GRAFANA_IMAGE="${GRAFANA_IMAGE:-docker.io/grafana/grafana:11.6.2}"
|
||||
|
||||
# Function to compare two version strings
|
||||
|
@ -181,7 +181,7 @@ COMMON_DOCKER_OPTIONS="--network=host -v ${TMP_DIR}:/output \
|
|||
-e OPENVIDU_OPERATOR_IMAGE=$OPENVIDU_OPERATOR_IMAGE \
|
||||
-e OPENVIDU_SERVER_PRO_IMAGE=$OPENVIDU_SERVER_PRO_IMAGE \
|
||||
-e OPENVIDU_SERVER_IMAGE=$OPENVIDU_SERVER_IMAGE \
|
||||
-e OPENVIDU_MEET_SERVER_IMAGE=$OPENVIDU_MEET_SERVER_IMAGE \
|
||||
-e OPENVIDU_CALL_SERVER_IMAGE=$OPENVIDU_CALL_SERVER_IMAGE \
|
||||
-e OPENVIDU_DASHBOARD_PRO_IMAGE=$OPENVIDU_DASHBOARD_PRO_IMAGE \
|
||||
-e OPENVIDU_DASHBOARD_IMAGE=$OPENVIDU_DASHBOARD_IMAGE \
|
||||
-e OPENVIDU_V2COMPATIBILITY_IMAGE=$OPENVIDU_V2COMPATIBILITY_IMAGE \
|
||||
|
|
|
@ -473,9 +473,10 @@ Resources:
|
|||
"GRAFANA_ADMIN_PASSWORD": "none",
|
||||
"LIVEKIT_API_KEY": "none",
|
||||
"LIVEKIT_API_SECRET": "none",
|
||||
"MEET_ADMIN_USER": "none",
|
||||
"MEET_ADMIN_SECRET": "none",
|
||||
"MEET_API_KEY": "none",
|
||||
"DEFAULT_APP_USERNAME": "none",
|
||||
"DEFAULT_APP_PASSWORD": "none",
|
||||
"DEFAULT_APP_ADMIN_USERNAME": "none",
|
||||
"DEFAULT_APP_ADMIN_PASSWORD": "none",
|
||||
"OPENVIDU_VERSION": "none",
|
||||
"ENABLED_MODULES": "none"
|
||||
}
|
||||
|
@ -661,7 +662,7 @@ Resources:
|
|||
content: !Sub |
|
||||
#!/bin/bash
|
||||
set -e
|
||||
OPENVIDU_VERSION=main
|
||||
OPENVIDU_VERSION=3.3.0
|
||||
DOMAIN=
|
||||
YQ_VERSION=v4.44.5
|
||||
|
||||
|
@ -733,12 +734,13 @@ Resources:
|
|||
DASHBOARD_ADMIN_PASSWORD="$(/usr/local/bin/store_secret.sh generate DASHBOARD_ADMIN_PASSWORD)"
|
||||
GRAFANA_ADMIN_USERNAME="$(/usr/local/bin/store_secret.sh save GRAFANA_ADMIN_USERNAME "grafanaadmin")"
|
||||
GRAFANA_ADMIN_PASSWORD="$(/usr/local/bin/store_secret.sh generate GRAFANA_ADMIN_PASSWORD)"
|
||||
MEET_ADMIN_USER="$(/usr/local/bin/store_secret.sh save MEET_ADMIN_USER "meetadmin")"
|
||||
MEET_ADMIN_SECRET="$(/usr/local/bin/store_secret.sh generate MEET_ADMIN_SECRET)"
|
||||
MEET_API_KEY="$(/usr/local/bin/store_secret.sh generate MEET_API_KEY)"
|
||||
DEFAULT_APP_USERNAME="$(/usr/local/bin/store_secret.sh save DEFAULT_APP_USERNAME "calluser")"
|
||||
DEFAULT_APP_PASSWORD="$(/usr/local/bin/store_secret.sh generate DEFAULT_APP_PASSWORD)"
|
||||
DEFAULT_APP_ADMIN_USERNAME="$(/usr/local/bin/store_secret.sh save DEFAULT_APP_ADMIN_USERNAME "calladmin")"
|
||||
DEFAULT_APP_ADMIN_PASSWORD="$(/usr/local/bin/store_secret.sh generate DEFAULT_APP_ADMIN_PASSWORD)"
|
||||
LIVEKIT_API_KEY="$(/usr/local/bin/store_secret.sh generate LIVEKIT_API_KEY "API" 12)"
|
||||
LIVEKIT_API_SECRET="$(/usr/local/bin/store_secret.sh generate LIVEKIT_API_SECRET)"
|
||||
ENABLED_MODULES="$(/usr/local/bin/store_secret.sh save ENABLED_MODULES "observability,v2compatibility,openviduMeet")"
|
||||
ENABLED_MODULES="$(/usr/local/bin/store_secret.sh save ENABLED_MODULES "observability,v2compatibility,app")"
|
||||
ALL_SECRETS_GENERATED="$(/usr/local/bin/store_secret.sh save ALL_SECRETS_GENERATED "true")"
|
||||
|
||||
# Base command
|
||||
|
@ -766,9 +768,10 @@ Resources:
|
|||
"--dashboard-admin-password=$DASHBOARD_ADMIN_PASSWORD"
|
||||
"--grafana-admin-user=$GRAFANA_ADMIN_USERNAME"
|
||||
"--grafana-admin-password=$GRAFANA_ADMIN_PASSWORD"
|
||||
"--meet-admin-user=$MEET_ADMIN_USER"
|
||||
"--meet-admin-password=$MEET_ADMIN_SECRET"
|
||||
"--meet-api-key=$MEET_API_KEY"
|
||||
"--default-app-user=$DEFAULT_APP_USERNAME"
|
||||
"--default-app-password=$DEFAULT_APP_PASSWORD"
|
||||
"--default-app-admin-user=$DEFAULT_APP_ADMIN_USERNAME"
|
||||
"--default-app-admin-password=$DEFAULT_APP_ADMIN_PASSWORD"
|
||||
"--livekit-api-key=$LIVEKIT_API_KEY"
|
||||
"--livekit-api-secret=$LIVEKIT_API_SECRET"
|
||||
)
|
||||
|
@ -967,9 +970,10 @@ Resources:
|
|||
sed -i "s/GRAFANA_ADMIN_PASSWORD=.*/GRAFANA_ADMIN_PASSWORD=$(echo $SHARED_SECRET | jq -r .GRAFANA_ADMIN_PASSWORD)/" "${!CLUSTER_CONFIG_DIR}/openvidu.env"
|
||||
sed -i "s/LIVEKIT_API_KEY=.*/LIVEKIT_API_KEY=$(echo $SHARED_SECRET | jq -r .LIVEKIT_API_KEY)/" "${!CLUSTER_CONFIG_DIR}/openvidu.env"
|
||||
sed -i "s/LIVEKIT_API_SECRET=.*/LIVEKIT_API_SECRET=$(echo $SHARED_SECRET | jq -r .LIVEKIT_API_SECRET)/" "${!CLUSTER_CONFIG_DIR}/openvidu.env"
|
||||
sed -i "s/MEET_ADMIN_USER=.*/MEET_ADMIN_USER=$(echo $SHARED_SECRET | jq -r .MEET_ADMIN_USER)/" "${!CLUSTER_CONFIG_DIR}/master_node/meet.env"
|
||||
sed -i "s/MEET_ADMIN_SECRET=.*/MEET_ADMIN_SECRET=$(echo $SHARED_SECRET | jq -r .MEET_ADMIN_SECRET)/" "${!CLUSTER_CONFIG_DIR}/master_node/meet.env"
|
||||
sed -i "s/MEET_API_KEY=.*/MEET_API_KEY=$(echo $SHARED_SECRET | jq -r .MEET_API_KEY)/" "${!CLUSTER_CONFIG_DIR}/master_node/meet.env"
|
||||
sed -i "s/CALL_USER=.*/CALL_USER=$(echo $SHARED_SECRET | jq -r .DEFAULT_APP_USERNAME)/" "${!CLUSTER_CONFIG_DIR}/master_node/app.env"
|
||||
sed -i "s/CALL_SECRET=.*/CALL_SECRET=$(echo $SHARED_SECRET | jq -r .DEFAULT_APP_PASSWORD)/" "${!CLUSTER_CONFIG_DIR}/master_node/app.env"
|
||||
sed -i "s/CALL_ADMIN_USER=.*/CALL_ADMIN_USER=$(echo $SHARED_SECRET | jq -r .DEFAULT_APP_ADMIN_USERNAME)/" "${!CLUSTER_CONFIG_DIR}/master_node/app.env"
|
||||
sed -i "s/CALL_ADMIN_SECRET=.*/CALL_ADMIN_SECRET=$(echo $SHARED_SECRET | jq -r .DEFAULT_APP_ADMIN_PASSWORD)/" "${!CLUSTER_CONFIG_DIR}/master_node/app.env"
|
||||
sed -i "s/ENABLED_MODULES=.*/ENABLED_MODULES=$(echo $SHARED_SECRET | jq -r .ENABLED_MODULES)/" "${!CLUSTER_CONFIG_DIR}/openvidu.env"
|
||||
|
||||
# Update URLs in secret
|
||||
|
@ -1025,9 +1029,10 @@ Resources:
|
|||
SHARED_SECRET="$(echo "$SHARED_SECRET" | jq '. + {"GRAFANA_ADMIN_PASSWORD": "'"$(/usr/local/bin/get_value_from_config.sh GRAFANA_ADMIN_PASSWORD "${!CLUSTER_CONFIG_DIR}/openvidu.env")"'"}')"
|
||||
SHARED_SECRET="$(echo "$SHARED_SECRET" | jq '. + {"LIVEKIT_API_KEY": "'"$(/usr/local/bin/get_value_from_config.sh LIVEKIT_API_KEY "${!CLUSTER_CONFIG_DIR}/openvidu.env")"'"}')"
|
||||
SHARED_SECRET="$(echo "$SHARED_SECRET" | jq '. + {"LIVEKIT_API_SECRET": "'"$(/usr/local/bin/get_value_from_config.sh LIVEKIT_API_SECRET "${!CLUSTER_CONFIG_DIR}/openvidu.env")"'"}')"
|
||||
SHARED_SECRET="$(echo "$SHARED_SECRET" | jq '. + {"MEET_ADMIN_USER": "'"$(/usr/local/bin/get_value_from_config.sh MEET_ADMIN_USER "${!CLUSTER_CONFIG_DIR}/master_node/meet.env")"'"}')"
|
||||
SHARED_SECRET="$(echo "$SHARED_SECRET" | jq '. + {"MEET_ADMIN_SECRET": "'"$(/usr/local/bin/get_value_from_config.sh MEET_ADMIN_SECRET "${!CLUSTER_CONFIG_DIR}/master_node/meet.env")"'"}')"
|
||||
SHARED_SECRET="$(echo "$SHARED_SECRET" | jq '. + {"MEET_API_KEY": "'"$(/usr/local/bin/get_value_from_config.sh MEET_API_KEY "${!CLUSTER_CONFIG_DIR}/master_node/meet.env")"'"}')"
|
||||
SHARED_SECRET="$(echo "$SHARED_SECRET" | jq '. + {"DEFAULT_APP_USERNAME": "'"$(/usr/local/bin/get_value_from_config.sh CALL_USER "${!CLUSTER_CONFIG_DIR}/master_node/app.env")"'"}')"
|
||||
SHARED_SECRET="$(echo "$SHARED_SECRET" | jq '. + {"DEFAULT_APP_PASSWORD": "'"$(/usr/local/bin/get_value_from_config.sh CALL_SECRET "${!CLUSTER_CONFIG_DIR}/master_node/app.env")"'"}')"
|
||||
SHARED_SECRET="$(echo "$SHARED_SECRET" | jq '. + {"DEFAULT_APP_ADMIN_USERNAME": "'"$(/usr/local/bin/get_value_from_config.sh CALL_ADMIN_USER "${!CLUSTER_CONFIG_DIR}/master_node/app.env")"'"}')"
|
||||
SHARED_SECRET="$(echo "$SHARED_SECRET" | jq '. + {"DEFAULT_APP_ADMIN_PASSWORD": "'"$(/usr/local/bin/get_value_from_config.sh CALL_ADMIN_SECRET "${!CLUSTER_CONFIG_DIR}/master_node/app.env")"'"}')"
|
||||
SHARED_SECRET="$(echo "$SHARED_SECRET" | jq '. + {"ENABLED_MODULES": "'"$(/usr/local/bin/get_value_from_config.sh ENABLED_MODULES "${!CLUSTER_CONFIG_DIR}/openvidu.env")"'"}')"
|
||||
|
||||
# Update shared secret
|
||||
|
@ -1691,7 +1696,7 @@ Resources:
|
|||
ToPort: 4443
|
||||
SourceSecurityGroupId: !GetAtt OpenViduMediaNodeSG.GroupId
|
||||
|
||||
OpenViduMediaNodeToMasterMeetWebhookIngress:
|
||||
OpenViduMediaNodeToMasterDefaultAppWebhookIngress:
|
||||
Type: AWS::EC2::SecurityGroupIngress
|
||||
Properties:
|
||||
GroupId: !GetAtt OpenViduMasterNodeSG.GroupId
|
||||
|
@ -1723,14 +1728,6 @@ Resources:
|
|||
FromPort: 443
|
||||
ToPort: 443
|
||||
CidrIpv6: ::/0
|
||||
- IpProtocol: tcp
|
||||
FromPort: 7881
|
||||
ToPort: 7881
|
||||
CidrIp: 0.0.0.0/0
|
||||
- IpProtocol: tcp
|
||||
FromPort: 7881
|
||||
ToPort: 7881
|
||||
CidrIpv6: ::/0
|
||||
- IpProtocol: udp
|
||||
FromPort: 7885
|
||||
ToPort: 7885
|
||||
|
@ -1747,14 +1744,6 @@ Resources:
|
|||
FromPort: 50000
|
||||
ToPort: 60000
|
||||
CidrIpv6: ::/0
|
||||
- IpProtocol: tcp
|
||||
FromPort: 50000
|
||||
ToPort: 60000
|
||||
CidrIp: 0.0.0.0/0
|
||||
- IpProtocol: tcp
|
||||
FromPort: 50000
|
||||
ToPort: 60000
|
||||
CidrIpv6: ::/0
|
||||
|
||||
OpenViduMasterNodeToMediaNodeRTMPIngress:
|
||||
Type: AWS::EC2::SecurityGroupIngress
|
||||
|
|
|
@ -427,7 +427,7 @@ var stringInterpolationParamsMaster = {
|
|||
|
||||
var installScriptTemplateMaster = '''
|
||||
#!/bin/bash -x
|
||||
OPENVIDU_VERSION=main
|
||||
OPENVIDU_VERSION=3.3.0
|
||||
DOMAIN=
|
||||
|
||||
# Assume azure cli is installed
|
||||
|
@ -490,13 +490,14 @@ DASHBOARD_ADMIN_USERNAME="$(/usr/local/bin/store_secret.sh save DASHBOARD-ADMIN-
|
|||
DASHBOARD_ADMIN_PASSWORD="$(/usr/local/bin/store_secret.sh generate DASHBOARD-ADMIN-PASSWORD)"
|
||||
GRAFANA_ADMIN_USERNAME="$(/usr/local/bin/store_secret.sh save GRAFANA-ADMIN-USERNAME "grafanaadmin")"
|
||||
GRAFANA_ADMIN_PASSWORD="$(/usr/local/bin/store_secret.sh generate GRAFANA-ADMIN-PASSWORD)"
|
||||
MEET_ADMIN_USER="$(/usr/local/bin/store_secret.sh save MEET-ADMIN-USER "meetadmin")"
|
||||
MEET_ADMIN_SECRET="$(/usr/local/bin/store_secret.sh generate MEET-ADMIN-SECRET)"
|
||||
MEET_API_KEY="$(/usr/local/bin/store_secret.sh generate MEET-API-KEY)"
|
||||
DEFAULT_APP_USERNAME="$(/usr/local/bin/store_secret.sh save DEFAULT-APP-USERNAME "calluser")"
|
||||
DEFAULT_APP_PASSWORD="$(/usr/local/bin/store_secret.sh generate DEFAULT-APP-PASSWORD)"
|
||||
DEFAULT_APP_ADMIN_USERNAME="$(/usr/local/bin/store_secret.sh save DEFAULT-APP-ADMIN-USERNAME "calladmin")"
|
||||
DEFAULT_APP_ADMIN_PASSWORD="$(/usr/local/bin/store_secret.sh generate DEFAULT-APP-ADMIN-PASSWORD)"
|
||||
LIVEKIT_API_KEY="$(/usr/local/bin/store_secret.sh generate LIVEKIT-API-KEY "API" 12)"
|
||||
LIVEKIT_API_SECRET="$(/usr/local/bin/store_secret.sh generate LIVEKIT-API-SECRET)"
|
||||
OPENVIDU_VERSION="$(/usr/local/bin/store_secret.sh save OPENVIDU-VERSION "${OPENVIDU_VERSION}")"
|
||||
ENABLED_MODULES="$(/usr/local/bin/store_secret.sh save ENABLED-MODULES "observability,openviduMeet,v2compatibility")"
|
||||
ENABLED_MODULES="$(/usr/local/bin/store_secret.sh save ENABLED-MODULES "observability,app,v2compatibility")"
|
||||
ALL_SECRETS_GENERATED="$(/usr/local/bin/store_secret.sh save ALL-SECRETS-GENERATED "true")"
|
||||
|
||||
# Base command
|
||||
|
@ -524,9 +525,10 @@ COMMON_ARGS=(
|
|||
"--dashboard-admin-password=$DASHBOARD_ADMIN_PASSWORD"
|
||||
"--grafana-admin-user=$GRAFANA_ADMIN_USERNAME"
|
||||
"--grafana-admin-password=$GRAFANA_ADMIN_PASSWORD"
|
||||
"--meet-admin-user=$MEET_ADMIN_USER"
|
||||
"--meet-admin-password=$MEET_ADMIN_SECRET"
|
||||
"--meet-api-key=$MEET_API_KEY"
|
||||
"--default-app-user=$DEFAULT_APP_USERNAME"
|
||||
"--default-app-password=$DEFAULT_APP_PASSWORD"
|
||||
"--default-app-admin-user=$DEFAULT_APP_ADMIN_USERNAME"
|
||||
"--default-app-admin-password=$DEFAULT_APP_ADMIN_PASSWORD"
|
||||
"--livekit-api-key=$LIVEKIT_API_KEY"
|
||||
"--livekit-api-secret=$LIVEKIT_API_SECRET"
|
||||
)
|
||||
|
@ -679,9 +681,10 @@ export GRAFANA_ADMIN_USERNAME=$(az keyvault secret show --vault-name ${keyVaultN
|
|||
export GRAFANA_ADMIN_PASSWORD=$(az keyvault secret show --vault-name ${keyVaultName} --name GRAFANA-ADMIN-PASSWORD --query value -o tsv)
|
||||
export LIVEKIT_API_KEY=$(az keyvault secret show --vault-name ${keyVaultName} --name LIVEKIT-API-KEY --query value -o tsv)
|
||||
export LIVEKIT_API_SECRET=$(az keyvault secret show --vault-name ${keyVaultName} --name LIVEKIT-API-SECRET --query value -o tsv)
|
||||
export MEET_ADMIN_USER=$(az keyvault secret show --vault-name ${keyVaultName} --name MEET-ADMIN-USER --query value -o tsv)
|
||||
export MEET_ADMIN_SECRET=$(az keyvault secret show --vault-name ${keyVaultName} --name MEET-ADMIN-SECRET --query value -o tsv)
|
||||
export MEET_API_KEY=$(az keyvault secret show --vault-name ${keyVaultName} --name MEET-API-KEY --query value -o tsv)
|
||||
export DEFAULT_APP_USERNAME=$(az keyvault secret show --vault-name ${keyVaultName} --name DEFAULT-APP-USERNAME --query value -o tsv)
|
||||
export DEFAULT_APP_PASSWORD=$(az keyvault secret show --vault-name ${keyVaultName} --name DEFAULT-APP-PASSWORD --query value -o tsv)
|
||||
export DEFAULT_APP_ADMIN_USERNAME=$(az keyvault secret show --vault-name ${keyVaultName} --name DEFAULT-APP-ADMIN-USERNAME --query value -o tsv)
|
||||
export DEFAULT_APP_ADMIN_PASSWORD=$(az keyvault secret show --vault-name ${keyVaultName} --name DEFAULT-APP-ADMIN-PASSWORD --query value -o tsv)
|
||||
export ENABLED_MODULES=$(az keyvault secret show --vault-name ${keyVaultName} --name ENABLED-MODULES --query value -o tsv)
|
||||
|
||||
# Replace rest of the values
|
||||
|
@ -699,9 +702,10 @@ sed -i "s/GRAFANA_ADMIN_USERNAME=.*/GRAFANA_ADMIN_USERNAME=$GRAFANA_ADMIN_USERNA
|
|||
sed -i "s/GRAFANA_ADMIN_PASSWORD=.*/GRAFANA_ADMIN_PASSWORD=$GRAFANA_ADMIN_PASSWORD/" "${CLUSTER_CONFIG_DIR}/openvidu.env"
|
||||
sed -i "s/LIVEKIT_API_KEY=.*/LIVEKIT_API_KEY=$LIVEKIT_API_KEY/" "${CLUSTER_CONFIG_DIR}/openvidu.env"
|
||||
sed -i "s/LIVEKIT_API_SECRET=.*/LIVEKIT_API_SECRET=$LIVEKIT_API_SECRET/" "${CLUSTER_CONFIG_DIR}/openvidu.env"
|
||||
sed -i "s/MEET_ADMIN_USER=.*/MEET_ADMIN_USER=$MEET_ADMIN_USER/" "${CLUSTER_CONFIG_DIR}/master_node/meet.env"
|
||||
sed -i "s/MEET_ADMIN_SECRET=.*/MEET_ADMIN_SECRET=$MEET_ADMIN_SECRET/" "${CLUSTER_CONFIG_DIR}/master_node/meet.env"
|
||||
sed -i "s/MEET_API_KEY=.*/MEET_API_KEY=$MEET_API_KEY/" "${CLUSTER_CONFIG_DIR}/master_node/meet.env"
|
||||
sed -i "s/CALL_USER=.*/CALL_USER=$DEFAULT_APP_USERNAME/" "${CLUSTER_CONFIG_DIR}/master_node/app.env"
|
||||
sed -i "s/CALL_SECRET=.*/CALL_SECRET=$DEFAULT_APP_PASSWORD/" "${CLUSTER_CONFIG_DIR}/master_node/app.env"
|
||||
sed -i "s/CALL_ADMIN_USER=.*/CALL_ADMIN_USER=$DEFAULT_APP_ADMIN_USERNAME/" "${CLUSTER_CONFIG_DIR}/master_node/app.env"
|
||||
sed -i "s/CALL_ADMIN_SECRET=.*/CALL_ADMIN_SECRET=$DEFAULT_APP_ADMIN_PASSWORD/" "${CLUSTER_CONFIG_DIR}/master_node/app.env"
|
||||
sed -i "s/ENABLED_MODULES=.*/ENABLED_MODULES=$ENABLED_MODULES/" "${CLUSTER_CONFIG_DIR}/openvidu.env"
|
||||
|
||||
# Update URLs in secret
|
||||
|
@ -749,9 +753,10 @@ GRAFANA_ADMIN_USERNAME="$(/usr/local/bin/get_value_from_config.sh GRAFANA_ADMIN_
|
|||
GRAFANA_ADMIN_PASSWORD="$(/usr/local/bin/get_value_from_config.sh GRAFANA_ADMIN_PASSWORD "${CLUSTER_CONFIG_DIR}/openvidu.env")"
|
||||
LIVEKIT_API_KEY="$(/usr/local/bin/get_value_from_config.sh LIVEKIT_API_KEY "${CLUSTER_CONFIG_DIR}/openvidu.env")"
|
||||
LIVEKIT_API_SECRET="$(/usr/local/bin/get_value_from_config.sh LIVEKIT_API_SECRET "${CLUSTER_CONFIG_DIR}/openvidu.env")"
|
||||
MEET_ADMIN_USER="$(/usr/local/bin/get_value_from_config.sh MEET_ADMIN_USER "${CLUSTER_CONFIG_DIR}/master_node/meet.env")"
|
||||
MEET_ADMIN_SECRET="$(/usr/local/bin/get_value_from_config.sh MEET_ADMIN_SECRET "${CLUSTER_CONFIG_DIR}/master_node/meet.env")"
|
||||
MEET_API_KEY="$(/usr/local/bin/get_value_from_config.sh MEET_API_KEY "${CLUSTER_CONFIG_DIR}/master_node/meet.env")"
|
||||
DEFAULT_APP_USERNAME="$(/usr/local/bin/get_value_from_config.sh CALL_USER "${CLUSTER_CONFIG_DIR}/master_node/app.env")"
|
||||
DEFAULT_APP_PASSWORD="$(/usr/local/bin/get_value_from_config.sh CALL_SECRET "${CLUSTER_CONFIG_DIR}/master_node/app.env")"
|
||||
DEFAULT_APP_ADMIN_USERNAME="$(/usr/local/bin/get_value_from_config.sh CALL_ADMIN_USER "${CLUSTER_CONFIG_DIR}/master_node/app.env")"
|
||||
DEFAULT_APP_ADMIN_PASSWORD="$(/usr/local/bin/get_value_from_config.sh CALL_ADMIN_SECRET "${CLUSTER_CONFIG_DIR}/master_node/app.env")"
|
||||
ENABLED_MODULES="$(/usr/local/bin/get_value_from_config.sh ENABLED_MODULES "${CLUSTER_CONFIG_DIR}/openvidu.env")"
|
||||
|
||||
# Update shared secret
|
||||
|
@ -771,9 +776,10 @@ az keyvault secret set --vault-name ${keyVaultName} --name GRAFANA-ADMIN-USERNAM
|
|||
az keyvault secret set --vault-name ${keyVaultName} --name GRAFANA-ADMIN-PASSWORD --value $GRAFANA_ADMIN_PASSWORD
|
||||
az keyvault secret set --vault-name ${keyVaultName} --name LIVEKIT-API-KEY --value $LIVEKIT_API_KEY
|
||||
az keyvault secret set --vault-name ${keyVaultName} --name LIVEKIT-API-SECRET --value $LIVEKIT_API_SECRET
|
||||
az keyvault secret set --vault-name ${keyVaultName} --name MEET-ADMIN-USER --value $MEET_ADMIN_USER
|
||||
az keyvault secret set --vault-name ${keyVaultName} --name MEET-ADMIN-SECRET --value $MEET_ADMIN_SECRET
|
||||
az keyvault secret set --vault-name ${keyVaultName} --name MEET-API-KEY --value $MEET_API_KEY
|
||||
az keyvault secret set --vault-name ${keyVaultName} --name DEFAULT-APP-USERNAME --value $DEFAULT_APP_USERNAME
|
||||
az keyvault secret set --vault-name ${keyVaultName} --name DEFAULT-APP-PASSWORD --value $DEFAULT_APP_PASSWORD
|
||||
az keyvault secret set --vault-name ${keyVaultName} --name DEFAULT-APP-ADMIN-USERNAME --value $DEFAULT_APP_ADMIN_USERNAME
|
||||
az keyvault secret set --vault-name ${keyVaultName} --name DEFAULT-APP-ADMIN-PASSWORD --value $DEFAULT_APP_ADMIN_PASSWORD
|
||||
az keyvault secret set --vault-name ${keyVaultName} --name ENABLED-MODULES --value $ENABLED_MODULES
|
||||
'''
|
||||
|
||||
|
@ -1857,9 +1863,9 @@ resource mediaToMasterV2CompatibilityWebhookIngress 'Microsoft.Network/networkSe
|
|||
}
|
||||
}
|
||||
|
||||
resource mediaToMasterMeetWebhookIngress 'Microsoft.Network/networkSecurityGroups/securityRules@2023-11-01' = {
|
||||
resource mediaToMasterDefaultAppWebhookIngress 'Microsoft.Network/networkSecurityGroups/securityRules@2023-11-01' = {
|
||||
parent: openviduMasterNodeNSG
|
||||
name: 'mediaNode_to_masterNode_MEET_WEBHOOK_INGRESS'
|
||||
name: 'mediaNode_to_masterNode_DEFAULTAPP_WEBHOOK_INGRESS'
|
||||
properties: {
|
||||
protocol: 'Tcp'
|
||||
sourceApplicationSecurityGroups: [
|
||||
|
@ -1953,22 +1959,6 @@ resource openviduMediaNodeNSG 'Microsoft.Network/networkSecurityGroups@2023-11-0
|
|||
direction: 'Inbound'
|
||||
}
|
||||
}
|
||||
{
|
||||
name: 'WebRTC_traffic_TCP'
|
||||
properties: {
|
||||
protocol: 'Tcp'
|
||||
sourceAddressPrefix: '*'
|
||||
sourcePortRange: '*'
|
||||
destinationAddressPrefix: '*'
|
||||
destinationPortRanges: [
|
||||
'50000'
|
||||
'60000'
|
||||
]
|
||||
access: 'Allow'
|
||||
priority: 150
|
||||
direction: 'Inbound'
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
@ -1996,7 +1986,7 @@ resource masterToMediaRtmpIngress 'Microsoft.Network/networkSecurityGroups/secur
|
|||
]
|
||||
destinationPortRange: '1935'
|
||||
access: 'Allow'
|
||||
priority: 160
|
||||
priority: 150
|
||||
direction: 'Inbound'
|
||||
}
|
||||
}
|
||||
|
@ -2019,7 +2009,7 @@ resource masterToMediaTurnTlsIngress 'Microsoft.Network/networkSecurityGroups/se
|
|||
]
|
||||
destinationPortRange: '5349'
|
||||
access: 'Allow'
|
||||
priority: 170
|
||||
priority: 160
|
||||
direction: 'Inbound'
|
||||
}
|
||||
}
|
||||
|
@ -2042,7 +2032,7 @@ resource masterToMediaServerIngress 'Microsoft.Network/networkSecurityGroups/sec
|
|||
]
|
||||
destinationPortRange: '7880'
|
||||
access: 'Allow'
|
||||
priority: 180
|
||||
priority: 170
|
||||
direction: 'Inbound'
|
||||
}
|
||||
}
|
||||
|
@ -2065,7 +2055,7 @@ resource masterToMediaHttpWhipIngress 'Microsoft.Network/networkSecurityGroups/s
|
|||
]
|
||||
destinationPortRange: '8080'
|
||||
access: 'Allow'
|
||||
priority: 190
|
||||
priority: 180
|
||||
direction: 'Inbound'
|
||||
}
|
||||
}
|
||||
|
|
File diff suppressed because one or more lines are too long
|
@ -3,9 +3,9 @@
|
|||
set -eu
|
||||
export DOCKER_VERSION="${DOCKER_VERSION:-28.1.1}"
|
||||
export DOCKER_COMPOSE_VERSION="${DOCKER_COMPOSE_VERSION:-v2.35.1}"
|
||||
export OPENVIDU_VERSION="${OPENVIDU_VERSION:-main}"
|
||||
export OPENVIDU_VERSION="${OPENVIDU_VERSION:-3.3.0}"
|
||||
export INSTALLER_IMAGE="${INSTALLER_IMAGE:-docker.io/openvidu/openvidu-installer:${OPENVIDU_VERSION}}"
|
||||
export MINIO_SERVER_IMAGE="${MINIO_SERVER_IMAGE:-docker.io/openvidu/minio:2025.5.24-debian-12-r1}"
|
||||
export MINIO_SERVER_IMAGE="${MINIO_SERVER_IMAGE:-docker.io/bitnami/minio:2025.5.24-debian-12-r1}"
|
||||
export MINIO_CLIENT_IMAGE="${MINIO_CLIENT_IMAGE:-docker.io/minio/mc:RELEASE.2025-05-21T01-59-54Z}"
|
||||
export MONGO_SERVER_IMAGE="${MONGO_SERVER_IMAGE:-docker.io/mongo:8.0.9}"
|
||||
export REDIS_SERVER_IMAGE="${REDIS_SERVER_IMAGE:-docker.io/redis:7.4.4-alpine}"
|
||||
|
@ -15,17 +15,17 @@ export CADDY_SERVER_PRO_IMAGE="${CADDY_SERVER_PRO_IMAGE:-docker.io/openvidu/open
|
|||
export OPENVIDU_OPERATOR_IMAGE="${OPENVIDU_OPERATOR_IMAGE:-docker.io/openvidu/openvidu-operator:${OPENVIDU_VERSION}}"
|
||||
export OPENVIDU_SERVER_PRO_IMAGE="${OPENVIDU_SERVER_PRO_IMAGE:-docker.io/openvidu/openvidu-server-pro:${OPENVIDU_VERSION}}"
|
||||
export OPENVIDU_SERVER_IMAGE="${OPENVIDU_SERVER_IMAGE:-docker.io/openvidu/openvidu-server:${OPENVIDU_VERSION}}"
|
||||
export OPENVIDU_MEET_SERVER_IMAGE="${OPENVIDU_MEET_SERVER_IMAGE:-docker.io/openvidu/openvidu-meet:${OPENVIDU_VERSION}}"
|
||||
export OPENVIDU_CALL_SERVER_IMAGE="${OPENVIDU_CALL_SERVER_IMAGE:-docker.io/openvidu/openvidu-call:${OPENVIDU_VERSION}}"
|
||||
export OPENVIDU_DASHBOARD_PRO_IMAGE="${OPENVIDU_DASHBOARD_PRO_IMAGE:-docker.io/openvidu/openvidu-pro-dashboard:${OPENVIDU_VERSION}}"
|
||||
export OPENVIDU_DASHBOARD_IMAGE="${OPENVIDU_DASHBOARD_IMAGE:-docker.io/openvidu/openvidu-dashboard:${OPENVIDU_VERSION}}"
|
||||
export OPENVIDU_V2COMPATIBILITY_IMAGE="${OPENVIDU_V2COMPATIBILITY_IMAGE:-docker.io/openvidu/openvidu-v2compatibility:${OPENVIDU_VERSION}}"
|
||||
export OPENVIDU_AGENT_SPEECH_PROCESSING_IMAGE="${OPENVIDU_AGENT_SPEECH_PROCESSING_IMAGE:-docker.io/openvidu/agent-speech-processing:${OPENVIDU_VERSION}}"
|
||||
export LIVEKIT_INGRESS_SERVER_IMAGE="${LIVEKIT_INGRESS_SERVER_IMAGE:-docker.io/openvidu/ingress:${OPENVIDU_VERSION}}"
|
||||
export LIVEKIT_EGRESS_SERVER_IMAGE="${LIVEKIT_EGRESS_SERVER_IMAGE:-docker.io/livekit/egress:v1.10.0}"
|
||||
export LIVEKIT_EGRESS_SERVER_IMAGE="${LIVEKIT_EGRESS_SERVER_IMAGE:-docker.io/livekit/egress:v1.9.1}"
|
||||
export PROMETHEUS_IMAGE="${PROMETHEUS_IMAGE:-docker.io/prom/prometheus:v3.4.0}"
|
||||
export PROMTAIL_IMAGE="${PROMTAIL_IMAGE:-docker.io/grafana/promtail:3.5.1}"
|
||||
export LOKI_IMAGE="${LOKI_IMAGE:-docker.io/grafana/loki:3.5.1}"
|
||||
export MIMIR_IMAGE="${MIMIR_IMAGE:-docker.io/openvidu/grafana-mimir:2.16.0}"
|
||||
export MIMIR_IMAGE="${MIMIR_IMAGE:-docker.io/bitnami/grafana-mimir:2.16.0}"
|
||||
export GRAFANA_IMAGE="${GRAFANA_IMAGE:-docker.io/grafana/grafana:11.6.2}"
|
||||
|
||||
# Function to compare two version strings
|
||||
|
@ -181,7 +181,7 @@ COMMON_DOCKER_OPTIONS="--network=host -v ${TMP_DIR}:/output \
|
|||
-e OPENVIDU_OPERATOR_IMAGE=$OPENVIDU_OPERATOR_IMAGE \
|
||||
-e OPENVIDU_SERVER_PRO_IMAGE=$OPENVIDU_SERVER_PRO_IMAGE \
|
||||
-e OPENVIDU_SERVER_IMAGE=$OPENVIDU_SERVER_IMAGE \
|
||||
-e OPENVIDU_MEET_SERVER_IMAGE=$OPENVIDU_MEET_SERVER_IMAGE \
|
||||
-e OPENVIDU_CALL_SERVER_IMAGE=$OPENVIDU_CALL_SERVER_IMAGE \
|
||||
-e OPENVIDU_DASHBOARD_PRO_IMAGE=$OPENVIDU_DASHBOARD_PRO_IMAGE \
|
||||
-e OPENVIDU_DASHBOARD_IMAGE=$OPENVIDU_DASHBOARD_IMAGE \
|
||||
-e OPENVIDU_V2COMPATIBILITY_IMAGE=$OPENVIDU_V2COMPATIBILITY_IMAGE \
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue