Compare commits

..

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

117 changed files with 4945 additions and 7388 deletions

View File

@ -63,7 +63,7 @@ jobs:
# - name: Run Browserless Chrome # - name: Run Browserless Chrome
# run: docker run -d -p 3000:3000 --network host browserless/chrome:1.57-chrome-stable # run: docker run -d -p 3000:3000 --network host browserless/chrome:1.57-chrome-stable
- name: Run Chrome - name: Run Chrome
run: docker run --network=host -d -p 4444:4444 selenium/standalone-chrome:138.0 run: docker run --network=host -d -p 4444:4444 selenium/standalone-chrome:127.0
- name: Run openvidu-local-deployment - name: Run openvidu-local-deployment
uses: OpenVidu/actions/start-openvidu-local-deployment@main uses: OpenVidu/actions/start-openvidu-local-deployment@main
- name: Start OpenVidu Call backend - name: Start OpenVidu Call backend
@ -100,7 +100,7 @@ jobs:
# - name: Run Browserless Chrome # - name: Run Browserless Chrome
# run: docker run -d -p 3000:3000 --network host browserless/chrome:1.57-chrome-stable # run: docker run -d -p 3000:3000 --network host browserless/chrome:1.57-chrome-stable
- name: Run Chrome - name: Run Chrome
run: docker run --network=host -d -p 4444:4444 selenium/standalone-chrome:138.0 run: docker run --network=host -d -p 4444:4444 selenium/standalone-chrome:127.0
- name: Run openvidu-local-deployment - name: Run openvidu-local-deployment
uses: OpenVidu/actions/start-openvidu-local-deployment@main uses: OpenVidu/actions/start-openvidu-local-deployment@main
- name: Start OpenVidu Call backend - name: Start OpenVidu Call backend
@ -133,7 +133,7 @@ jobs:
# - name: Run Browserless Chrome # - name: Run Browserless Chrome
# run: docker run -d -p 3000:3000 --network host browserless/chrome:1.57-chrome-stable # run: docker run -d -p 3000:3000 --network host browserless/chrome:1.57-chrome-stable
- name: Run Chrome - name: Run Chrome
run: docker run --network=host -d -p 4444:4444 selenium/standalone-chrome:138.0 run: docker run --network=host -d -p 4444:4444 selenium/standalone-chrome:127.0
- name: Run openvidu-local-deployment - name: Run openvidu-local-deployment
uses: OpenVidu/actions/start-openvidu-local-deployment@main uses: OpenVidu/actions/start-openvidu-local-deployment@main
- name: Start OpenVidu Call backend - name: Start OpenVidu Call backend
@ -166,7 +166,7 @@ jobs:
# - name: Run Browserless Chrome # - name: Run Browserless Chrome
# run: docker run -d -p 3000:3000 --network host browserless/chrome:1.57-chrome-stable # run: docker run -d -p 3000:3000 --network host browserless/chrome:1.57-chrome-stable
- name: Run Chrome - name: Run Chrome
run: docker run --network=host -d -p 4444:4444 selenium/standalone-chrome:138.0 run: docker run --network=host -d -p 4444:4444 selenium/standalone-chrome:127.0
- name: Run openvidu-local-deployment - name: Run openvidu-local-deployment
uses: OpenVidu/actions/start-openvidu-local-deployment@main uses: OpenVidu/actions/start-openvidu-local-deployment@main
- name: Start OpenVidu Call backend - name: Start OpenVidu Call backend
@ -180,38 +180,6 @@ jobs:
- name: Cleanup - name: Cleanup
if: always() if: always()
uses: OpenVidu/actions/cleanup@main 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: e2e_chat:
needs: test_setup needs: test_setup
@ -231,7 +199,7 @@ jobs:
# - name: Run Browserless Chrome # - name: Run Browserless Chrome
# run: docker run -d -p 3000:3000 --network host browserless/chrome:1.57-chrome-stable # run: docker run -d -p 3000:3000 --network host browserless/chrome:1.57-chrome-stable
- name: Run Chrome - name: Run Chrome
run: docker run --network=host -d -p 4444:4444 selenium/standalone-chrome:138.0 run: docker run --network=host -d -p 4444:4444 selenium/standalone-chrome:127.0
- name: Run openvidu-local-deployment - name: Run openvidu-local-deployment
uses: OpenVidu/actions/start-openvidu-local-deployment@main uses: OpenVidu/actions/start-openvidu-local-deployment@main
- name: Start OpenVidu Call backend - name: Start OpenVidu Call backend
@ -264,7 +232,7 @@ jobs:
# - name: Run Browserless Chrome # - name: Run Browserless Chrome
# run: docker run -d -p 3000:3000 --network host browserless/chrome:1.57-chrome-stable # run: docker run -d -p 3000:3000 --network host browserless/chrome:1.57-chrome-stable
- name: Run Chrome - name: Run Chrome
run: docker run --network=host -d -p 4444:4444 selenium/standalone-chrome:138.0 run: docker run --network=host -d -p 4444:4444 selenium/standalone-chrome:127.0
- name: Run openvidu-local-deployment - name: Run openvidu-local-deployment
uses: OpenVidu/actions/start-openvidu-local-deployment@main uses: OpenVidu/actions/start-openvidu-local-deployment@main
- name: Start OpenVidu Call backend - name: Start OpenVidu Call backend
@ -297,7 +265,7 @@ jobs:
# - name: Run Browserless Chrome # - name: Run Browserless Chrome
# run: docker run -d -p 3000:3000 --network host browserless/chrome:1.57-chrome-stable # run: docker run -d -p 3000:3000 --network host browserless/chrome:1.57-chrome-stable
- name: Run Chrome - name: Run Chrome
run: docker run --network=host -d -p 4444:4444 selenium/standalone-chrome:138.0 run: docker run --network=host -d -p 4444:4444 selenium/standalone-chrome:127.0
- name: Run openvidu-local-deployment - name: Run openvidu-local-deployment
uses: OpenVidu/actions/start-openvidu-local-deployment@main uses: OpenVidu/actions/start-openvidu-local-deployment@main
- name: Start OpenVidu Call backend - name: Start OpenVidu Call backend
@ -330,7 +298,7 @@ jobs:
# - name: Run Browserless Chrome # - name: Run Browserless Chrome
# run: docker run -d -p 3000:3000 --network host browserless/chrome:1.57-chrome-stable # run: docker run -d -p 3000:3000 --network host browserless/chrome:1.57-chrome-stable
- name: Run Chrome - name: Run Chrome
run: docker run --network=host -d -p 4444:4444 selenium/standalone-chrome:138.0 run: docker run --network=host -d -p 4444:4444 selenium/standalone-chrome:127.0
- name: Run openvidu-local-deployment - name: Run openvidu-local-deployment
uses: OpenVidu/actions/start-openvidu-local-deployment@main uses: OpenVidu/actions/start-openvidu-local-deployment@main
- name: Start OpenVidu Call backend - name: Start OpenVidu Call backend
@ -363,7 +331,7 @@ jobs:
# - name: Run Browserless Chrome # - name: Run Browserless Chrome
# run: docker run -d -p 3000:3000 --network host browserless/chrome:1.57-chrome-stable # run: docker run -d -p 3000:3000 --network host browserless/chrome:1.57-chrome-stable
- name: Run Chrome - name: Run Chrome
run: docker run --network=host -d -p 4444:4444 selenium/standalone-chrome:138.0 run: docker run --network=host -d -p 4444:4444 selenium/standalone-chrome:127.0
- name: Run openvidu-local-deployment - name: Run openvidu-local-deployment
uses: OpenVidu/actions/start-openvidu-local-deployment@main uses: OpenVidu/actions/start-openvidu-local-deployment@main
- name: Start OpenVidu Call backend - name: Start OpenVidu Call backend
@ -396,7 +364,7 @@ jobs:
# - name: Run Browserless Chrome # - name: Run Browserless Chrome
# run: docker run -d -p 3000:3000 --network host browserless/chrome:1.57-chrome-stable # run: docker run -d -p 3000:3000 --network host browserless/chrome:1.57-chrome-stable
- name: Run Chrome - name: Run Chrome
run: docker run --network=host -d -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 - name: Run openvidu-local-deployment
uses: OpenVidu/actions/start-openvidu-local-deployment@main uses: OpenVidu/actions/start-openvidu-local-deployment@main
- name: Start OpenVidu Call backend - name: Start OpenVidu Call backend
@ -429,7 +397,7 @@ jobs:
# - name: Run Browserless Chrome # - name: Run Browserless Chrome
# run: docker run -d -p 3000:3000 --network host browserless/chrome:1.57-chrome-stable # run: docker run -d -p 3000:3000 --network host browserless/chrome:1.57-chrome-stable
- name: Run Chrome - name: Run Chrome
run: docker run --network=host -d -p 4444:4444 selenium/standalone-chrome:138.0 run: docker run --network=host -d -p 4444:4444 selenium/standalone-chrome:127.0
- name: Run openvidu-local-deployment - name: Run openvidu-local-deployment
uses: OpenVidu/actions/start-openvidu-local-deployment@main uses: OpenVidu/actions/start-openvidu-local-deployment@main
- name: Start OpenVidu Call backend - name: Start OpenVidu Call backend

View File

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

View File

@ -679,13 +679,12 @@ describe('Stream UI controls and interaction features', () => {
await browser.sleep(1000); await browser.sleep(1000);
const tabs = await browser.getAllWindowHandles(); const tabs = await browser.getAllWindowHandles();
await browser.switchTo().window(tabs[1]);
await utils.clickOn('#mic-btn');
await browser.switchTo().window(tabs[0]); await browser.switchTo().window(tabs[0]);
await utils.waitForElement('.OV_stream.remote.speaking'); await utils.waitForElement('.OV_stream.remote.speaking');
expect(await utils.getNumberOfElements('.OV_stream.remote.speaking')).toEqual(1); expect(await utils.getNumberOfElements('.OV_stream.remote.speaking')).toEqual(1);
// Check only one element is marked as speaker due to the local participant is muted
await utils.waitForElement('.OV_stream.speaking');
expect(await utils.getNumberOfElements('.OV_stream.speaking')).toEqual(1); expect(await utils.getNumberOfElements('.OV_stream.speaking')).toEqual(1);
}); });
}); });

View File

@ -195,7 +195,5 @@ export class OpenViduComponentsPO {
await this.clickOn('#toolbar-settings-btn'); await this.clickOn('#toolbar-settings-btn');
break; break;
} }
await this.browser.sleep(500);
} }
} }

View File

@ -1,12 +1,12 @@
{ {
"name": "openvidu-components-testapp", "name": "openvidu-components-testapp",
"version": "3.3.0", "version": "3.2.0",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "openvidu-components-testapp", "name": "openvidu-components-testapp",
"version": "3.3.0", "version": "3.2.0",
"dependencies": { "dependencies": {
"@angular/animations": "19.2.8", "@angular/animations": "19.2.8",
"@angular/cdk": "19.2.11", "@angular/cdk": "19.2.11",
@ -35,7 +35,7 @@
"@types/node": "20.12.14", "@types/node": "20.12.14",
"@types/selenium-webdriver": "4.1.16", "@types/selenium-webdriver": "4.1.16",
"@types/ws": "^8.5.12", "@types/ws": "^8.5.12",
"chromedriver": "138.0.0", "chromedriver": "136.0.2",
"concat": "^1.0.3", "concat": "^1.0.3",
"cross-env": "^7.0.3", "cross-env": "^7.0.3",
"eslint-config-prettier": "^9.1.0", "eslint-config-prettier": "^9.1.0",
@ -8126,9 +8126,9 @@
} }
}, },
"node_modules/chromedriver": { "node_modules/chromedriver": {
"version": "138.0.0", "version": "136.0.2",
"resolved": "https://registry.npmjs.org/chromedriver/-/chromedriver-138.0.0.tgz", "resolved": "https://registry.npmjs.org/chromedriver/-/chromedriver-136.0.2.tgz",
"integrity": "sha512-bJ/DNm5Y0TbqM71ARaAohTWVwcQ2SsWciYC5Q9Ul7DC/oTxm6B1vI2h6WscFCOOi49ul4tXZVjA/LOruljjmjA==", "integrity": "sha512-yJ52GN01edLYWYK/OspYBv3plzF08Ucdq4ukYigJGOX8dWr/tP5PXSZPWFPVarmbmcO57pNLP9Im8hsYljMEjw==",
"dev": true, "dev": true,
"hasInstallScript": true, "hasInstallScript": true,
"license": "Apache-2.0", "license": "Apache-2.0",
@ -8145,7 +8145,7 @@
"chromedriver": "bin/chromedriver" "chromedriver": "bin/chromedriver"
}, },
"engines": { "engines": {
"node": ">=20" "node": ">=18"
} }
}, },
"node_modules/cli-cursor": { "node_modules/cli-cursor": {

View File

@ -27,7 +27,7 @@
"@types/node": "20.12.14", "@types/node": "20.12.14",
"@types/selenium-webdriver": "4.1.16", "@types/selenium-webdriver": "4.1.16",
"@types/ws": "^8.5.12", "@types/ws": "^8.5.12",
"chromedriver": "138.0.0", "chromedriver": "136.0.2",
"concat": "^1.0.3", "concat": "^1.0.3",
"cross-env": "^7.0.3", "cross-env": "^7.0.3",
"eslint-config-prettier": "^9.1.0", "eslint-config-prettier": "^9.1.0",
@ -89,7 +89,6 @@
"e2e:nested-structural-directives": "tsc --project ./e2e && npx jasmine --fail-fast ./e2e/dist/nested-components/structural-directives.test.js", "e2e:nested-structural-directives": "tsc --project ./e2e && npx jasmine --fail-fast ./e2e/dist/nested-components/structural-directives.test.js",
"e2e:nested-attribute-directives": "tsc --project ./e2e && npx jasmine --fail-fast ./e2e/dist/nested-components/attribute-directives.test.js", "e2e: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-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-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-events": "tsc --project ./e2e && npx jasmine ./e2e/dist/events.test.js",
"e2e:lib-media-devices": "tsc --project ./e2e && npx jasmine --fail-fast ./e2e/dist/media-devices.test.js", "e2e:lib-media-devices": "tsc --project ./e2e && npx jasmine --fail-fast ./e2e/dist/media-devices.test.js",
@ -100,5 +99,5 @@
"simulate:multiparty": "livekit-cli load-test --url ws://localhost:7880 --api-key devkey --api-secret secret --room daily-call --publishers 8 --audio-publishers 8 --identity-prefix Participant --identity publisher", "simulate:multiparty": "livekit-cli load-test --url ws://localhost:7880 --api-key devkey --api-secret secret --room daily-call --publishers 8 --audio-publishers 8 --identity-prefix Participant --identity publisher",
"husky": "cd .. && husky install" "husky": "cd .. && husky install"
}, },
"version": "3.3.0" "version": "3.2.0"
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -22,7 +22,7 @@
[value]="settingsOptions.GENERAL" [value]="settingsOptions.GENERAL"
> >
<mat-icon matListItemIcon>manage_accounts</mat-icon> <mat-icon matListItemIcon>manage_accounts</mat-icon>
<div *ngIf="!isMobile">{{ 'PANEL.SETTINGS.GENERAL' | translate }}</div> <div mat-line *ngIf="!isMobile">{{ 'PANEL.SETTINGS.GENERAL' | translate }}</div>
</mat-list-option> </mat-list-option>
<mat-list-option <mat-list-option
*ngIf="showCameraButton" *ngIf="showCameraButton"
@ -32,7 +32,7 @@
[value]="settingsOptions.VIDEO" [value]="settingsOptions.VIDEO"
> >
<mat-icon matListItemIcon>videocam</mat-icon> <mat-icon matListItemIcon>videocam</mat-icon>
<div *ngIf="!isMobile">{{ 'PANEL.SETTINGS.VIDEO' | translate }}</div> <div mat-line *ngIf="!isMobile">{{ 'PANEL.SETTINGS.VIDEO' | translate }}</div>
</mat-list-option> </mat-list-option>
<mat-list-option <mat-list-option
*ngIf="showMicrophoneButton" *ngIf="showMicrophoneButton"
@ -42,7 +42,7 @@
[value]="settingsOptions.AUDIO" [value]="settingsOptions.AUDIO"
> >
<mat-icon matListItemIcon>mic</mat-icon> <mat-icon matListItemIcon>mic</mat-icon>
<div *ngIf="!isMobile">{{ 'PANEL.SETTINGS.AUDIO' | translate }}</div> <div mat-line *ngIf="!isMobile">{{ 'PANEL.SETTINGS.AUDIO' | translate }}</div>
</mat-list-option> </mat-list-option>
<!-- <mat-list-option <!-- <mat-list-option
*ngIf="showCaptions" *ngIf="showCaptions"

View File

@ -1,5 +1,5 @@
import { Component, EventEmitter, OnInit, Output } from '@angular/core'; import { Component, EventEmitter, OnInit, Output } from '@angular/core';
import { Subject, takeUntil } from 'rxjs'; import { Subscription } from 'rxjs';
import { PanelStatusInfo, PanelSettingsOptions, PanelType } from '../../../models/panel.model'; import { PanelStatusInfo, PanelSettingsOptions, PanelType } from '../../../models/panel.model';
import { OpenViduComponentsConfigService } from '../../../services/config/directive-config.service'; import { OpenViduComponentsConfigService } from '../../../services/config/directive-config.service';
import { PanelService } from '../../../services/panel/panel.service'; import { PanelService } from '../../../services/panel/panel.service';
@ -27,8 +27,11 @@ export class SettingsPanelComponent implements OnInit {
showCameraButton: boolean = true; showCameraButton: boolean = true;
showMicrophoneButton: boolean = true; showMicrophoneButton: boolean = true;
showCaptions: boolean = true; showCaptions: boolean = true;
panelSubscription: Subscription;
isMobile: boolean = false; isMobile: boolean = false;
private destroy$ = new Subject<void>(); private cameraButtonSub: Subscription;
private microphoneButtonSub: Subscription;
private captionsSubs: Subscription;
constructor( constructor(
private panelService: PanelService, private panelService: PanelService,
private platformService: PlatformService, private platformService: PlatformService,
@ -41,8 +44,10 @@ export class SettingsPanelComponent implements OnInit {
} }
ngOnDestroy() { ngOnDestroy() {
this.destroy$.next(); if (this.panelSubscription) this.panelSubscription.unsubscribe();
this.destroy$.complete(); if (this.cameraButtonSub) this.cameraButtonSub.unsubscribe();
if (this.microphoneButtonSub) this.microphoneButtonSub.unsubscribe();
if (this.captionsSubs) this.captionsSubs.unsubscribe();
} }
close() { close() {
@ -53,13 +58,13 @@ export class SettingsPanelComponent implements OnInit {
} }
private subscribeToDirectives() { private subscribeToDirectives() {
this.libService.cameraButton$.pipe(takeUntil(this.destroy$)).subscribe((value: boolean) => (this.showCameraButton = value)); this.cameraButtonSub = this.libService.cameraButton$.subscribe((value: boolean) => (this.showCameraButton = value));
this.libService.microphoneButton$.pipe(takeUntil(this.destroy$)).subscribe((value: boolean) => (this.showMicrophoneButton = value)); this.microphoneButtonSub = this.libService.microphoneButton$.subscribe((value: boolean) => (this.showMicrophoneButton = value));
this.libService.captionsButton$.pipe(takeUntil(this.destroy$)).subscribe((value: boolean) => (this.showCaptions = value)); this.captionsSubs = this.libService.captionsButton$.subscribe((value: boolean) => (this.showCaptions = value));
} }
private subscribeToPanelToggling() { private subscribeToPanelToggling() {
this.panelService.panelStatusObs.pipe(takeUntil(this.destroy$)).subscribe((ev: PanelStatusInfo) => { this.panelSubscription = this.panelService.panelStatusObs.subscribe((ev: PanelStatusInfo) => {
if (ev.panelType === PanelType.SETTINGS && !!ev.subOptionType) { if (ev.panelType === PanelType.SETTINGS && !!ev.subOptionType) {
this.selectedOption = ev.subOptionType as PanelSettingsOptions; this.selectedOption = ev.subOptionType as PanelSettingsOptions;
} }

View File

@ -54,7 +54,7 @@
</div> </div>
<div class="join-btn-container"> <div class="join-btn-container">
<button mat-flat-button (click)="join()" id="join-button"> <button mat-flat-button (click)="joinSession()" id="join-button">
{{ 'PREJOIN.JOIN' | translate }} {{ 'PREJOIN.JOIN' | translate }}
</button> </button>
</div> </div>

View File

@ -9,7 +9,7 @@ import {
OnInit, OnInit,
Output Output
} from '@angular/core'; } from '@angular/core';
import { filter, Subject, takeUntil, tap } from 'rxjs'; import { Subscription } from 'rxjs';
import { ILogger } from '../../models/logger.model'; import { ILogger } from '../../models/logger.model';
import { CdkOverlayService } from '../../services/cdk-overlay/cdk-overlay.service'; import { CdkOverlayService } from '../../services/cdk-overlay/cdk-overlay.service';
import { OpenViduComponentsConfigService } from '../../services/config/directive-config.service'; import { OpenViduComponentsConfigService } from '../../services/config/directive-config.service';
@ -19,6 +19,7 @@ import { TranslateService } from '../../services/translate/translate.service';
import { LocalTrack } from 'livekit-client'; import { LocalTrack } from 'livekit-client';
import { CustomDevice } from '../../models/device.model'; import { CustomDevice } from '../../models/device.model';
import { LangOption } from '../../models/lang.model'; import { LangOption } from '../../models/lang.model';
import { StorageService } from '../../services/storage/storage.service';
/** /**
* @internal * @internal
@ -60,7 +61,11 @@ export class PreJoinComponent implements OnInit, OnDestroy {
audioTrack: LocalTrack | undefined; audioTrack: LocalTrack | undefined;
private tracks: LocalTrack[]; private tracks: LocalTrack[];
private log: ILogger; 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; private shouldRemoveTracksWhenComponentIsDestroyed: boolean = true;
@HostListener('window:resize') @HostListener('window:resize')
@ -73,6 +78,7 @@ export class PreJoinComponent implements OnInit, OnDestroy {
private libService: OpenViduComponentsConfigService, private libService: OpenViduComponentsConfigService,
private cdkSrv: CdkOverlayService, private cdkSrv: CdkOverlayService,
private openviduService: OpenViduService, private openviduService: OpenViduService,
private storageService: StorageService,
private translateService: TranslateService, private translateService: TranslateService,
private changeDetector: ChangeDetectorRef private changeDetector: ChangeDetectorRef
) { ) {
@ -93,12 +99,15 @@ export class PreJoinComponent implements OnInit, OnDestroy {
// } // }
async ngOnDestroy() { async ngOnDestroy() {
this.destroy$.next();
this.destroy$.complete();
this.cdkSrv.setSelector('body'); 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) { if (this.shouldRemoveTracksWhenComponentIsDestroyed) {
this.tracks?.forEach((track) => { this.tracks.forEach((track) => {
track.stop(); track.stop();
}); });
} }
@ -121,7 +130,7 @@ export class PreJoinComponent implements OnInit, OnDestroy {
this.cdkSrv.setSelector('#prejoin-container'); this.cdkSrv.setSelector('#prejoin-container');
} }
join() { joinSession() {
if (this.showParticipantName && !this.participantName) { if (this.showParticipantName && !this.participantName) {
this._error = this.translateService.translate('PREJOIN.NICKNAME_REQUIRED'); this._error = this.translateService.translate('PREJOIN.NICKNAME_REQUIRED');
return; return;
@ -131,22 +140,9 @@ export class PreJoinComponent implements OnInit, OnDestroy {
this.shouldRemoveTracksWhenComponentIsDestroyed = false; this.shouldRemoveTracksWhenComponentIsDestroyed = false;
// Assign participant name to the observable if it is defined // Assign participant name to the observable if it is defined
if (this.participantName) { if(this.participantName) this.libService.setParticipantName(this.participantName);
this.libService.updateGeneralConfig({ participantName: this.participantName });
// Wait for the next tick to ensure the participant name propagates this.onReadyToJoin.emit();
// through the observable before emitting onReadyToJoin
this.libService.participantName$
.pipe(
takeUntil(this.destroy$),
filter((name) => name === this.participantName),
tap(() => this.onReadyToJoin.emit())
)
.subscribe();
} else {
// No participant name to set, emit immediately
this.onReadyToJoin.emit();
}
} }
onParticipantNameChanged(name: string) { onParticipantNameChanged(name: string) {
@ -154,38 +150,33 @@ export class PreJoinComponent implements OnInit, OnDestroy {
} }
onEnterPressed() { onEnterPressed() {
this.join(); this.joinSession();
} }
private subscribeToPrejoinDirectives() { private subscribeToPrejoinDirectives() {
this.libService.minimal$.pipe(takeUntil(this.destroy$)).subscribe((value: boolean) => { this.minimalSub = this.libService.minimal$.subscribe((value: boolean) => {
this.isMinimal = value; this.isMinimal = value;
this.changeDetector.markForCheck(); this.changeDetector.markForCheck();
}); });
this.cameraButtonSub = this.libService.cameraButton$.subscribe((value: boolean) => {
this.libService.cameraButton$.pipe(takeUntil(this.destroy$)).subscribe((value: boolean) => {
this.showCameraButton = value; this.showCameraButton = value;
this.changeDetector.markForCheck(); this.changeDetector.markForCheck();
}); });
this.microphoneButtonSub = this.libService.microphoneButton$.subscribe((value: boolean) => {
this.libService.microphoneButton$.pipe(takeUntil(this.destroy$)).subscribe((value: boolean) => {
this.showMicrophoneButton = value; this.showMicrophoneButton = value;
this.changeDetector.markForCheck(); this.changeDetector.markForCheck();
}); });
this.displayLogoSub = this.libService.displayLogo$.subscribe((value: boolean) => {
this.libService.displayLogo$.pipe(takeUntil(this.destroy$)).subscribe((value: boolean) => {
this.showLogo = value; this.showLogo = value;
this.changeDetector.markForCheck(); this.changeDetector.markForCheck();
}); });
this.libService.participantName$.subscribe((value: string) => {
this.libService.participantName$.pipe(takeUntil(this.destroy$)).subscribe((value: string) => {
if (value) { if (value) {
this.participantName = value; this.participantName = value;
this.changeDetector.markForCheck(); this.changeDetector.markForCheck();
} }
}); });
this.displayParticipantNameSub = this.libService.prejoinDisplayParticipantName$.subscribe((value: boolean) => {
this.libService.prejoinDisplayParticipantName$.pipe(takeUntil(this.destroy$)).subscribe((value: boolean) => {
this.showParticipantName = value; this.showParticipantName = value;
this.changeDetector.markForCheck(); this.changeDetector.markForCheck();
}); });

View File

@ -16,7 +16,7 @@ import {
import { ILogger } from '../../models/logger.model'; import { ILogger } from '../../models/logger.model';
import { animate, style, transition, trigger } from '@angular/animations'; import { animate, style, transition, trigger } from '@angular/animations';
import { MatDrawerContainer, MatSidenav } from '@angular/material/sidenav'; import { MatDrawerContainer, MatSidenav } from '@angular/material/sidenav';
import { skip, Subject, takeUntil } from 'rxjs'; import { skip, Subscription } from 'rxjs';
import { SidenavMode } from '../../models/layout.model'; import { SidenavMode } from '../../models/layout.model';
import { PanelStatusInfo, PanelType } from '../../models/panel.model'; import { PanelStatusInfo, PanelType } from '../../models/panel.model';
import { DataTopic } from '../../models/data-topic.model'; import { DataTopic } from '../../models/data-topic.model';
@ -48,7 +48,6 @@ import {
} from 'livekit-client'; } from 'livekit-client';
import { ParticipantLeftEvent, ParticipantLeftReason, ParticipantModel } from '../../models/participant.model'; import { ParticipantLeftEvent, ParticipantLeftReason, ParticipantModel } from '../../models/participant.model';
import { RecordingStatus } from '../../models/recording.model'; import { RecordingStatus } from '../../models/recording.model';
import { TemplateManagerService, SessionTemplateConfiguration } from '../../services/template/template-manager.service';
/** /**
* @internal * @internal
@ -83,7 +82,7 @@ export class SessionComponent implements OnInit, OnDestroy {
/** /**
* Provides event notifications that fire when participant is disconnected from Room. * 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>(); @Output() onRoomDisconnected: EventEmitter<void> = new EventEmitter<void>();
@ -104,16 +103,12 @@ export class SessionComponent implements OnInit, OnDestroy {
drawer: MatDrawerContainer; drawer: MatDrawerContainer;
loading: boolean = true; loading: boolean = true;
/**
* @internal
* Template configuration managed by the service
*/
templateConfig: SessionTemplateConfiguration = {};
private shouldDisconnectRoomWhenComponentIsDestroyed: boolean = true; private shouldDisconnectRoomWhenComponentIsDestroyed: boolean = true;
private readonly SIDENAV_WIDTH_LIMIT_MODE = 790; private readonly SIDENAV_WIDTH_LIMIT_MODE = 790;
private destroy$ = new Subject<void>(); private menuSubscription: Subscription;
private layoutWidthSubscription: Subscription;
private updateLayoutInterval: NodeJS.Timeout; private updateLayoutInterval: NodeJS.Timeout;
private captionLanguageSubscription: Subscription;
private log: ILogger; private log: ILogger;
constructor( constructor(
@ -130,11 +125,9 @@ export class SessionComponent implements OnInit, OnDestroy {
private translateService: TranslateService, private translateService: TranslateService,
// private captionService: CaptionService, // private captionService: CaptionService,
private backgroundService: VirtualBackgroundService, private backgroundService: VirtualBackgroundService,
private cd: ChangeDetectorRef, private cd: ChangeDetectorRef
private templateManagerService: TemplateManagerService
) { ) {
this.log = this.loggerSrv.get('SessionComponent'); this.log = this.loggerSrv.get('SessionComponent');
this.setupTemplates();
} }
@HostListener('window:beforeunload') @HostListener('window:beforeunload')
@ -188,39 +181,15 @@ export class SessionComponent implements OnInit, OnDestroy {
set layoutContainer(container: ElementRef) { set layoutContainer(container: ElementRef) {
setTimeout(async () => { setTimeout(async () => {
if (container) { if (container) {
if (this.libService.showBackgroundEffectsButton()) { // Apply background from storage when layout container is in DOM
// Apply background from storage when layout container is in DOM only when background effects button is enabled await this.backgroundService.applyBackgroundFromStorage();
await this.backgroundService.applyBackgroundFromStorage();
}
} }
}, 0); }, 0);
} }
async ngOnInit() { async ngOnInit() {
this.shouldDisconnectRoomWhenComponentIsDestroyed = true; this.shouldDisconnectRoomWhenComponentIsDestroyed = true;
this.room = this.openviduService.getRoom();
// Check if room is available before proceeding
if (!this.openviduService.isRoomInitialized()) {
this.log.e('Room is not initialized when SessionComponent starts. This indicates a timing issue.');
this.actionService.openDialog(
this.translateService.translate('ERRORS.SESSION'),
'Room is not ready. Please ensure the token is properly configured.'
);
return;
}
// Get room instance
try {
this.room = this.openviduService.getRoom();
this.log.d('Room successfully obtained for SessionComponent');
} catch (error) {
this.log.e('Unexpected error getting room:', error);
this.actionService.openDialog(
this.translateService.translate('ERRORS.SESSION'),
'Failed to get room instance: ' + (error?.message || error)
);
return;
}
// this.subscribeToCaptionLanguage(); // this.subscribeToCaptionLanguage();
this.subcribeToActiveSpeakersChanged(); this.subcribeToActiveSpeakersChanged();
@ -233,15 +202,14 @@ export class SessionComponent implements OnInit, OnDestroy {
// this.subscribeToParticipantNameChanged(); // this.subscribeToParticipantNameChanged();
this.subscribeToDataMessage(); this.subscribeToDataMessage();
this.subscribeToReconnection(); this.subscribeToReconnection();
this.subscribeToVirtualBackground();
// if (this.libService.isRecordingEnabled()) { if (this.libService.isRecordingEnabled()) {
// this.subscribeToRecordingEvents(); // this.subscribeToRecordingEvents();
// } }
// if (this.libService.isBroadcastingEnabled()) { if (this.libService.isBroadcastingEnabled()) {
// this.subscribeToBroadcastingEvents(); // this.subscribeToBroadcastingEvents();
// } }
try { try {
await this.participantService.connect(); await this.participantService.connect();
// Send room created after participant connect for avoiding to send incomplete room payload // Send room created after participant connect for avoiding to send incomplete room payload
@ -260,18 +228,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() { async ngOnDestroy() {
if (this.shouldDisconnectRoomWhenComponentIsDestroyed) { if (this.shouldDisconnectRoomWhenComponentIsDestroyed) {
await this.disconnectRoom(ParticipantLeftReason.LEAVE); await this.disconnectRoom(ParticipantLeftReason.LEAVE);
@ -279,8 +235,8 @@ export class SessionComponent implements OnInit, OnDestroy {
if (this.room) this.room.removeAllListeners(); if (this.room) this.room.removeAllListeners();
this.participantService.clear(); this.participantService.clear();
// this.room = undefined; // this.room = undefined;
this.destroy$.next(); if (this.menuSubscription) this.menuSubscription.unsubscribe();
this.destroy$.complete(); if (this.layoutWidthSubscription) this.layoutWidthSubscription.unsubscribe();
// if (this.captionLanguageSubscription) this.captionLanguageSubscription.unsubscribe(); // if (this.captionLanguageSubscription) this.captionLanguageSubscription.unsubscribe();
} }
@ -290,8 +246,7 @@ export class SessionComponent implements OnInit, OnDestroy {
await this.openviduService.disconnectRoom(() => { await this.openviduService.disconnectRoom(() => {
this.onParticipantLeft.emit({ this.onParticipantLeft.emit({
roomName: this.openviduService.getRoomName(), roomName: this.openviduService.getRoomName(),
participantName: this.participantService.getLocalParticipant()?.name || '', participantName: this.participantService.getLocalParticipant()?.identity || '',
identity: this.participantService.getLocalParticipant()?.identity || '',
reason reason
}); });
}, false); }, false);
@ -311,7 +266,7 @@ export class SessionComponent implements OnInit, OnDestroy {
this.startUpdateLayoutInterval(); this.startUpdateLayoutInterval();
}); });
this.panelService.panelStatusObs.pipe(skip(1), takeUntil(this.destroy$)).subscribe((ev: PanelStatusInfo) => { this.menuSubscription = this.panelService.panelStatusObs.pipe(skip(1)).subscribe((ev: PanelStatusInfo) => {
if (this.sideMenu) { if (this.sideMenu) {
this.settingsPanelOpened = ev.isOpened && ev.panelType === PanelType.SETTINGS; this.settingsPanelOpened = ev.isOpened && ev.panelType === PanelType.SETTINGS;
@ -330,7 +285,7 @@ export class SessionComponent implements OnInit, OnDestroy {
} }
private subscribeToLayoutWidth() { private subscribeToLayoutWidth() {
this.layoutService.layoutWidthObs.pipe(takeUntil(this.destroy$)).subscribe((width) => { this.layoutWidthSubscription = this.layoutService.layoutWidthObs.subscribe((width) => {
this.sidenavMode = width <= this.SIDENAV_WIDTH_LIMIT_MODE ? SidenavMode.OVER : SidenavMode.SIDE; this.sidenavMode = width <= this.SIDENAV_WIDTH_LIMIT_MODE ? SidenavMode.OVER : SidenavMode.SIDE;
}); });
} }
@ -448,7 +403,7 @@ export class SessionComponent implements OnInit, OnDestroy {
this.log.d(`Data event received: ${topic}`); this.log.d(`Data event received: ${topic}`);
switch (topic) { switch (topic) {
case DataTopic.CHAT: case DataTopic.CHAT:
const participantName = participant?.name || 'Unknown'; const participantName = participant?.identity || 'Unknown';
this.chatService.addRemoteMessage(event.message, participantName); this.chatService.addRemoteMessage(event.message, participantName);
break; break;
case DataTopic.RECORDING_STARTING: case DataTopic.RECORDING_STARTING:
@ -500,9 +455,7 @@ export class SessionComponent implements OnInit, OnDestroy {
case DataTopic.ROOM_STATUS: case DataTopic.ROOM_STATUS:
const { recordingList, isRecordingStarted, isBroadcastingStarted, broadcastingId } = event as RoomStatusData; const { recordingList, isRecordingStarted, isBroadcastingStarted, broadcastingId } = event as RoomStatusData;
if (this.libService.showRecordingActivityRecordingsList()) { this.recordingService.setRecordingList(recordingList);
this.recordingService.setRecordingList(recordingList);
}
if (isRecordingStarted) { if (isRecordingStarted) {
const recordingActive = recordingList.find((recording) => recording.status === RecordingStatus.STARTED); const recordingActive = recordingList.find((recording) => recording.status === RecordingStatus.STARTED);
this.recordingService.setRecordingStarted(recordingActive); this.recordingService.setRecordingStarted(recordingActive);
@ -535,11 +488,9 @@ export class SessionComponent implements OnInit, OnDestroy {
this.room.on(RoomEvent.Disconnected, async (reason: DisconnectReason | undefined) => { this.room.on(RoomEvent.Disconnected, async (reason: DisconnectReason | undefined) => {
this.shouldDisconnectRoomWhenComponentIsDestroyed = false; this.shouldDisconnectRoomWhenComponentIsDestroyed = false;
this.actionService.closeConnectionDialog();
const participantLeftEvent: ParticipantLeftEvent = { const participantLeftEvent: ParticipantLeftEvent = {
roomName: this.openviduService.getRoomName(), roomName: this.openviduService.getRoomName(),
participantName: this.participantService.getLocalParticipant()?.name || '', participantName: this.participantService.getLocalParticipant()?.identity || '',
identity: this.participantService.getLocalParticipant()?.identity || '',
reason: ParticipantLeftReason.NETWORK_DISCONNECT reason: ParticipantLeftReason.NETWORK_DISCONNECT
}; };
const messageErrorKey = 'ERRORS.DISCONNECT'; const messageErrorKey = 'ERRORS.DISCONNECT';
@ -579,7 +530,7 @@ export class SessionComponent implements OnInit, OnDestroy {
this.log.d('Participant disconnected', participantLeftEvent); this.log.d('Participant disconnected', participantLeftEvent);
this.onParticipantLeft.emit(participantLeftEvent); this.onParticipantLeft.emit(participantLeftEvent);
this.onRoomDisconnected.emit(); this.onRoomDisconnected.emit();
if (this.libService.getShowDisconnectionDialog() && descriptionErrorKey) { if (descriptionErrorKey) {
this.actionService.openDialog( this.actionService.openDialog(
this.translateService.translate(messageErrorKey), this.translateService.translate(messageErrorKey),
this.translateService.translate(descriptionErrorKey) this.translateService.translate(descriptionErrorKey)
@ -588,17 +539,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() { private startUpdateLayoutInterval() {
this.updateLayoutInterval = setInterval(() => { this.updateLayoutInterval = setInterval(() => {
this.layoutService.update(); this.layoutService.update();

View File

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

View File

@ -1,6 +1,6 @@
import { Component, ElementRef, Input, OnDestroy, OnInit, ViewChild } from '@angular/core'; import { Component, ElementRef, Input, OnDestroy, OnInit, ViewChild } from '@angular/core';
import { MatMenuPanel, MatMenuTrigger } from '@angular/material/menu'; import { MatMenuPanel, MatMenuTrigger } from '@angular/material/menu';
import { Subject, takeUntil } from 'rxjs'; import { Subscription } from 'rxjs';
import { CdkOverlayService } from '../../services/cdk-overlay/cdk-overlay.service'; import { CdkOverlayService } from '../../services/cdk-overlay/cdk-overlay.service';
import { OpenViduComponentsConfigService } from '../../services/config/directive-config.service'; import { OpenViduComponentsConfigService } from '../../services/config/directive-config.service';
import { LayoutService } from '../../services/layout/layout.service'; import { LayoutService } from '../../services/layout/layout.service';
@ -92,7 +92,10 @@ export class StreamComponent implements OnInit, OnDestroy {
} }
private _streamContainer: ElementRef; private _streamContainer: ElementRef;
private destroy$ = new Subject<void>(); private minimalSub: Subscription;
private displayParticipantNameSub: Subscription;
private displayAudioDetectionSub: Subscription;
private videoControlsSub: Subscription;
private readonly HOVER_TIMEOUT = 3000; private readonly HOVER_TIMEOUT = 3000;
/** /**
@ -110,9 +113,11 @@ export class StreamComponent implements OnInit, OnDestroy {
} }
ngOnDestroy() { ngOnDestroy() {
this.destroy$.next();
this.destroy$.complete();
this.cdkSrv.setSelector('body'); this.cdkSrv.setSelector('body');
if (this.videoControlsSub) this.videoControlsSub.unsubscribe();
if (this.displayAudioDetectionSub) this.displayAudioDetectionSub.unsubscribe();
if (this.displayParticipantNameSub) this.displayParticipantNameSub.unsubscribe();
if (this.minimalSub) this.minimalSub.unsubscribe();
} }
/** /**
@ -178,31 +183,20 @@ export class StreamComponent implements OnInit, OnDestroy {
} }
private subscribeToStreamDirectives() { private subscribeToStreamDirectives() {
this.libService.minimal$ this.minimalSub = this.libService.minimal$.subscribe((value: boolean) => {
.pipe(takeUntil(this.destroy$)) this.isMinimal = value;
.subscribe((value: boolean) => { });
this.isMinimal = value; this.displayParticipantNameSub = this.libService.displayParticipantName$.subscribe((value: boolean) => {
}); this.showParticipantName = value;
// this.cd.markForCheck();
this.libService.displayParticipantName$ });
.pipe(takeUntil(this.destroy$)) this.displayAudioDetectionSub = this.libService.displayAudioDetection$.subscribe((value: boolean) => {
.subscribe((value: boolean) => { this.showAudioDetection = value;
this.showParticipantName = value; // this.cd.markForCheck();
// this.cd.markForCheck(); });
}); this.videoControlsSub = this.libService.streamVideoControls$.subscribe((value: boolean) => {
this.showVideoControls = value;
this.libService.displayAudioDetection$ // this.cd.markForCheck();
.pipe(takeUntil(this.destroy$)) });
.subscribe((value: boolean) => {
this.showAudioDetection = value;
// this.cd.markForCheck();
});
this.libService.streamVideoControls$
.pipe(takeUntil(this.destroy$))
.subscribe((value: boolean) => {
this.showVideoControls = value;
// this.cd.markForCheck();
});
} }
} }

View File

@ -6,26 +6,20 @@
id="session-info-container" id="session-info-container"
[class.collapsed]="recordingStatus === _recordingStatus.STARTED || broadcastingStatus === _broadcastingStatus.STARTED" [class.collapsed]="recordingStatus === _recordingStatus.STARTED || broadcastingStatus === _broadcastingStatus.STARTED"
> >
<span id="session-name" *ngIf="!isMinimal && showRoomName">{{ roomName }}</span> <span id="session-name" *ngIf="!isMinimal && room && room.name && showSessionName">{{ room.name }}</span>
<div <div
id="activities-tag" id="activities-tag"
*ngIf="recordingStatus === _recordingStatus.STARTED || broadcastingStatus === _broadcastingStatus.STARTED" *ngIf="recordingStatus === _recordingStatus.STARTED || broadcastingStatus === _broadcastingStatus.STARTED"
> >
@if (recordingStatus === _recordingStatus.STARTED) { <div *ngIf="recordingStatus === _recordingStatus.STARTED" id="recording-tag" class="recording-tag">
<div id="recording-tag" class="recording-tag" (click)="openRecordingActivityPanel()"> <mat-icon class="blink">radio_button_checked</mat-icon>
<mat-icon class="blink">radio_button_checked</mat-icon> <span class="blink">REC</span>
<span class="blink">REC</span> <span *ngIf="recordingTime"> | {{ recordingTime | date: 'H:mm:ss' }}</span>
<span *ngIf="recordingTime"> | {{ recordingTime | date: 'H:mm:ss' }}</span> </div>
</div> <div *ngIf="broadcastingStatus === _broadcastingStatus.STARTED" id="broadcasting-tag" class="broadcasting-tag">
} <mat-icon class="blink">sensors</mat-icon>
<span class="blink">LIVE</span>
@if (broadcastingStatus === _broadcastingStatus.STARTED) { </div>
<!-- Broadcasting tag -->
<div id="broadcasting-tag" class="broadcasting-tag">
<mat-icon class="blink">sensors</mat-icon>
<span class="blink">LIVE</span>
</div>
}
</div> </div>
</div> </div>
</div> </div>
@ -126,36 +120,18 @@
*ngIf="!isMinimal && showRecordingButton" *ngIf="!isMinimal && showRecordingButton"
mat-menu-item mat-menu-item
id="recording-btn" id="recording-btn"
[disabled]=" [disabled]="recordingStatus === _recordingStatus.STARTING || recordingStatus === _recordingStatus.STOPPING"
recordingStatus === _recordingStatus.STARTING ||
recordingStatus === _recordingStatus.STOPPING ||
!hasRoomTracksPublished
"
[matTooltip]="!hasRoomTracksPublished ? ('TOOLBAR.NO_TRACKS_PUBLISHED' | translate) : ''"
(click)="toggleRecording()" (click)="toggleRecording()"
> >
<mat-icon color="warn">radio_button_checked</mat-icon> <mat-icon color="warn">radio_button_checked</mat-icon>
@if ( <span *ngIf="recordingStatus === _recordingStatus.STOPPED || recordingStatus === _recordingStatus.STOPPING">
recordingStatus === _recordingStatus.STOPPED || {{ 'TOOLBAR.START_RECORDING' | translate }}
recordingStatus === _recordingStatus.STOPPING || </span>
recordingStatus === _recordingStatus.FAILED <span *ngIf="recordingStatus === _recordingStatus.STARTED || recordingStatus === _recordingStatus.STARTING">
) { {{ 'TOOLBAR.STOP_RECORDING' | translate }}
<span class="blink"> </span>
{{ 'TOOLBAR.START_RECORDING' | translate }}
</span>
} @else if (recordingStatus === _recordingStatus.STARTED || recordingStatus === _recordingStatus.STARTING) {
<span>{{ 'TOOLBAR.STOP_RECORDING' | translate }}</span>
}
</button> </button>
<!-- View recordings button -->
@if (!isMinimal && showViewRecordingsButton) {
<button mat-menu-item id="view-recordings-btn" (click)="onViewRecordingsClicked.emit()">
<mat-icon>video_library</mat-icon>
<span>{{ 'TOOLBAR.VIEW_RECORDINGS' | translate }}</span>
</button>
}
<!-- Broadcasting button --> <!-- Broadcasting button -->
<button <button
*ngIf="!isMinimal && showBroadcastingButton" *ngIf="!isMinimal && showBroadcastingButton"

View File

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

View File

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

View File

@ -12,7 +12,7 @@ import {
TemplateRef, TemplateRef,
ViewChild ViewChild
} from '@angular/core'; } from '@angular/core';
import { fromEvent, skip, Subject, takeUntil } from 'rxjs'; import { fromEvent, skip, Subscription } from 'rxjs';
import { ChatService } from '../../services/chat/chat.service'; import { ChatService } from '../../services/chat/chat.service';
import { DocumentService } from '../../services/document/document.service'; import { DocumentService } from '../../services/document/document.service';
import { PanelService } from '../../services/panel/panel.service'; import { PanelService } from '../../services/panel/panel.service';
@ -44,7 +44,6 @@ import { ParticipantService } from '../../services/participant/participant.servi
import { PlatformService } from '../../services/platform/platform.service'; import { PlatformService } from '../../services/platform/platform.service';
import { RecordingService } from '../../services/recording/recording.service'; import { RecordingService } from '../../services/recording/recording.service';
import { StorageService } from '../../services/storage/storage.service'; import { StorageService } from '../../services/storage/storage.service';
import { TemplateManagerService, ToolbarTemplateConfiguration } from '../../services/template/template-manager.service';
import { TranslateService } from '../../services/translate/translate.service'; import { TranslateService } from '../../services/translate/translate.service';
import { CdkOverlayService } from '../../services/cdk-overlay/cdk-overlay.service'; import { CdkOverlayService } from '../../services/cdk-overlay/cdk-overlay.service';
import { ParticipantLeftEvent, ParticipantLeftReason, ParticipantModel } from '../../models/participant.model'; import { ParticipantLeftEvent, ParticipantLeftReason, ParticipantModel } from '../../models/participant.model';
@ -78,9 +77,10 @@ export class ToolbarComponent implements OnInit, OnDestroy, AfterViewInit {
*/ */
@ContentChild(ToolbarAdditionalButtonsDirective) @ContentChild(ToolbarAdditionalButtonsDirective)
set externalAdditionalButtons(externalAdditionalButtons: ToolbarAdditionalButtonsDirective) { set externalAdditionalButtons(externalAdditionalButtons: ToolbarAdditionalButtonsDirective) {
this._externalAdditionalButtons = externalAdditionalButtons; // This directive will has value only when ADDITIONAL BUTTONS component (tagged with '*ovToolbarAdditionalButtons' directive)
// is inside of the TOOLBAR component tagged with '*ovToolbar' directive
if (externalAdditionalButtons) { if (externalAdditionalButtons) {
this.updateTemplatesAndMarkForCheck(); this.toolbarAdditionalButtonsTemplate = externalAdditionalButtons.template;
} }
} }
@ -89,15 +89,16 @@ export class ToolbarComponent implements OnInit, OnDestroy, AfterViewInit {
*/ */
@ContentChild(ToolbarAdditionalPanelButtonsDirective) @ContentChild(ToolbarAdditionalPanelButtonsDirective)
set externalAdditionalPanelButtons(externalAdditionalPanelButtons: ToolbarAdditionalPanelButtonsDirective) { set externalAdditionalPanelButtons(externalAdditionalPanelButtons: ToolbarAdditionalPanelButtonsDirective) {
this._externalAdditionalPanelButtons = externalAdditionalPanelButtons; // This directive will has value only when ADDITIONAL PANEL BUTTONS component tagged with '*ovToolbarAdditionalPanelButtons' directive
// is inside of the TOOLBAR component tagged with '*ovToolbar' directive
if (externalAdditionalPanelButtons) { if (externalAdditionalPanelButtons) {
this.updateTemplatesAndMarkForCheck(); this.toolbarAdditionalPanelButtonsTemplate = externalAdditionalPanelButtons.template;
} }
} }
/** /**
* This event is emitted when the room has been disconnected. * This event is emitted when the room has been disconnected.
* @deprecated Use {@link ToolbarComponent.onParticipantLeft} instead. * @deprecated Use {@link onParticipantLeft} instead.
*/ */
@Output() onRoomDisconnected: EventEmitter<void> = new EventEmitter<void>(); @Output() onRoomDisconnected: EventEmitter<void> = new EventEmitter<void>();
@ -144,12 +145,6 @@ export class ToolbarComponent implements OnInit, OnDestroy, AfterViewInit {
@Output() onBroadcastingStopRequested: EventEmitter<BroadcastingStopRequestedEvent> = @Output() onBroadcastingStopRequested: EventEmitter<BroadcastingStopRequestedEvent> =
new EventEmitter<BroadcastingStopRequestedEvent>(); new EventEmitter<BroadcastingStopRequestedEvent>();
/**
* @internal
* This event is fired when the user clicks on the view recordings button.
*/
@Output() onViewRecordingsClicked: EventEmitter<void> = new EventEmitter<void>();
/** /**
* @ignore * @ignore
*/ */
@ -245,11 +240,6 @@ export class ToolbarComponent implements OnInit, OnDestroy, AfterViewInit {
*/ */
showRecordingButton: boolean = true; showRecordingButton: boolean = true;
/**
* @ignore
*/
showViewRecordingsButton: boolean = false;
/** /**
* @ignore * @ignore
*/ */
@ -290,12 +280,7 @@ export class ToolbarComponent implements OnInit, OnDestroy, AfterViewInit {
/** /**
* @ignore * @ignore
*/ */
showRoomName: boolean = true; showSessionName: boolean = true;
/**
* @ignore
*/
roomName: string = '';
/** /**
* @ignore * @ignore
@ -327,11 +312,6 @@ export class ToolbarComponent implements OnInit, OnDestroy, AfterViewInit {
*/ */
recordingStatus: RecordingStatus = RecordingStatus.STOPPED; recordingStatus: RecordingStatus = RecordingStatus.STOPPED;
/**
* @ignore
*/
isRecordingReadOnlyMode: boolean = false;
/** /**
* @ignore * @ignore
*/ */
@ -359,18 +339,31 @@ export class ToolbarComponent implements OnInit, OnDestroy, AfterViewInit {
*/ */
recordingTime: Date; recordingTime: Date;
/**
* @internal
* Template configuration managed by the service
*/
templateConfig: ToolbarTemplateConfiguration = {};
// Store directive references for template setup
private _externalAdditionalButtons?: ToolbarAdditionalButtonsDirective;
private _externalAdditionalPanelButtons?: ToolbarAdditionalPanelButtonsDirective;
private log: ILogger; private log: ILogger;
private destroy$ = new Subject<void>(); private minimalSub: Subscription;
private panelTogglingSubscription: Subscription;
private chatMessagesSubscription: Subscription;
private localParticipantSubscription: Subscription;
private 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; private currentWindowHeight = window.innerHeight;
/** /**
@ -393,8 +386,7 @@ export class ToolbarComponent implements OnInit, OnDestroy, AfterViewInit {
private broadcastingService: BroadcastingService, private broadcastingService: BroadcastingService,
private translateService: TranslateService, private translateService: TranslateService,
private storageSrv: StorageService, private storageSrv: StorageService,
private cdkOverlayService: CdkOverlayService, private cdkOverlayService: CdkOverlayService
private templateManagerService: TemplateManagerService
) { ) {
this.log = this.loggerSrv.get('ToolbarComponent'); this.log = this.loggerSrv.get('ToolbarComponent');
} }
@ -424,12 +416,10 @@ export class ToolbarComponent implements OnInit, OnDestroy, AfterViewInit {
async ngOnInit() { async ngOnInit() {
this.room = this.openviduService.getRoom(); this.room = this.openviduService.getRoom();
this.evalAndSetRoomName(this.libService.getRoomName());
this.hasVideoDevices = this.oVDevicesService.hasVideoDeviceAvailable(); this.hasVideoDevices = this.oVDevicesService.hasVideoDeviceAvailable();
this.hasAudioDevices = this.oVDevicesService.hasAudioDeviceAvailable(); this.hasAudioDevices = this.oVDevicesService.hasAudioDeviceAvailable();
this.setupTemplates();
this.subscribeToToolbarDirectives(); this.subscribeToToolbarDirectives();
this.subscribeToUserMediaProperties(); this.subscribeToUserMediaProperties();
@ -447,55 +437,34 @@ export class ToolbarComponent implements OnInit, OnDestroy, AfterViewInit {
ngOnDestroy(): void { ngOnDestroy(): void {
this.panelService.clear(); this.panelService.clear();
this.destroy$.next(); if (this.panelTogglingSubscription) this.panelTogglingSubscription.unsubscribe();
this.destroy$.complete(); if (this.chatMessagesSubscription) this.chatMessagesSubscription.unsubscribe();
if (this.localParticipantSubscription) this.localParticipantSubscription.unsubscribe();
if (this.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.isFullscreenActive = false;
this.cdkOverlayService.setSelector('body'); this.cdkOverlayService.setSelector('body');
} }
/**
* @internal
* Sets up all templates using the template manager service
*/
private setupTemplates(): void {
this.templateConfig = this.templateManagerService.setupToolbarTemplates(
this._externalAdditionalButtons,
this._externalAdditionalPanelButtons
);
// Apply templates to component properties for backward compatibility
this.applyTemplateConfiguration();
}
/**
* @internal
* Applies the template configuration to component properties
*/
private applyTemplateConfiguration(): void {
if (this.templateConfig.toolbarAdditionalButtonsTemplate) {
this.toolbarAdditionalButtonsTemplate = this.templateConfig.toolbarAdditionalButtonsTemplate;
}
if (this.templateConfig.toolbarAdditionalPanelButtonsTemplate) {
this.toolbarAdditionalPanelButtonsTemplate = this.templateConfig.toolbarAdditionalPanelButtonsTemplate;
}
}
/**
* @internal
* Updates templates and triggers change detection
*/
private updateTemplatesAndMarkForCheck(): void {
this.setupTemplates();
this.cd.markForCheck();
}
/**
* @internal
*/
get hasRoomTracksPublished(): boolean {
return this.openviduService.hasRoomTracksPublished();
}
/** /**
* @ignore * @ignore
*/ */
@ -558,8 +527,7 @@ export class ToolbarComponent implements OnInit, OnDestroy, AfterViewInit {
await this.openviduService.disconnectRoom(() => { await this.openviduService.disconnectRoom(() => {
this.onParticipantLeft.emit({ this.onParticipantLeft.emit({
roomName: this.openviduService.getRoomName(), roomName: this.openviduService.getRoomName(),
participantName: this.participantService.getLocalParticipant()?.name || '', participantName: this.participantService.getLocalParticipant()?.identity || '',
identity: this.participantService.getLocalParticipant()?.identity || '',
reason: ParticipantLeftReason.LEAVE reason: ParticipantLeftReason.LEAVE
}); });
this.onRoomDisconnected.emit(); 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 * @ignore
*/ */
toggleRecording() { toggleRecording() {
if (this.recordingStatus === RecordingStatus.FAILED) {
this.openRecordingActivityPanel();
return;
}
const payload: RecordingStartRequestedEvent = { const payload: RecordingStartRequestedEvent = {
roomName: this.openviduService.getRoomName() roomName: this.openviduService.getRoomName()
}; };
@ -606,7 +551,9 @@ export class ToolbarComponent implements OnInit, OnDestroy, AfterViewInit {
this.onRecordingStopRequested.emit(payload); this.onRecordingStopRequested.emit(payload);
} else if (this.recordingStatus === RecordingStatus.STOPPED) { } else if (this.recordingStatus === RecordingStatus.STOPPED) {
this.onRecordingStartRequested.emit(payload); this.onRecordingStartRequested.emit(payload);
this.openRecordingActivityPanel(); if (this.showActivitiesPanelButton && !this.isActivitiesOpened) {
this.toggleActivitiesPanel('recording');
}
} }
} }
@ -623,7 +570,9 @@ export class ToolbarComponent implements OnInit, OnDestroy, AfterViewInit {
this.onBroadcastingStopRequested.emit(payload); this.onBroadcastingStopRequested.emit(payload);
this.broadcastingService.setBroadcastingStopped(); this.broadcastingService.setBroadcastingStopped();
} else if (this.broadcastingStatus === BroadcastingStatus.STOPPED) { } else if (this.broadcastingStatus === BroadcastingStatus.STOPPED) {
this.openBroadcastingActivityPanel(); if (this.showActivitiesPanelButton && !this.isActivitiesOpened) {
this.toggleActivitiesPanel('broadcasting');
}
} }
} }
@ -676,11 +625,7 @@ export class ToolbarComponent implements OnInit, OnDestroy, AfterViewInit {
this.documentService.toggleFullscreen('session-container'); this.documentService.toggleFullscreen('session-container');
} }
/** private toggleActivitiesPanel(expandPanel: string) {
* @internal
* @param expandPanel
*/
toggleActivitiesPanel(expandPanel: string) {
this.panelService.togglePanel(PanelType.ACTIVITIES, expandPanel); this.panelService.togglePanel(PanelType.ACTIVITIES, expandPanel);
} }
@ -695,23 +640,21 @@ export class ToolbarComponent implements OnInit, OnDestroy, AfterViewInit {
} }
private subscribeToFullscreenChanged() { private subscribeToFullscreenChanged() {
fromEvent(document, 'fullscreenchange') this.fullscreenChangeSubscription = fromEvent(document, 'fullscreenchange').subscribe(() => {
.pipe(takeUntil(this.destroy$)) const isFullscreen = Boolean(document.fullscreenElement);
.subscribe(() => { if (isFullscreen) {
const isFullscreen = Boolean(document.fullscreenElement); this.cdkOverlayService.setSelector('#session-container');
if (isFullscreen) { } else {
this.cdkOverlayService.setSelector('#session-container'); this.cdkOverlayService.setSelector('body');
} else { }
this.cdkOverlayService.setSelector('body'); this.isFullscreenActive = isFullscreen;
} this.onFullscreenEnabledChanged.emit(this.isFullscreenActive);
this.isFullscreenActive = isFullscreen; this.cd.detectChanges();
this.onFullscreenEnabledChanged.emit(this.isFullscreenActive); });
this.cd.detectChanges();
});
} }
private subscribeToMenuToggling() { private subscribeToMenuToggling() {
this.panelService.panelStatusObs.pipe(takeUntil(this.destroy$)).subscribe((ev: PanelStatusInfo) => { this.panelTogglingSubscription = this.panelService.panelStatusObs.subscribe((ev: PanelStatusInfo) => {
this.isChatOpened = ev.isOpened && ev.panelType === PanelType.CHAT; this.isChatOpened = ev.isOpened && ev.panelType === PanelType.CHAT;
this.isParticipantsOpened = ev.isOpened && ev.panelType === PanelType.PARTICIPANTS; this.isParticipantsOpened = ev.isOpened && ev.panelType === PanelType.PARTICIPANTS;
this.isActivitiesOpened = ev.isOpened && ev.panelType === PanelType.ACTIVITIES; this.isActivitiesOpened = ev.isOpened && ev.panelType === PanelType.ACTIVITIES;
@ -723,7 +666,7 @@ export class ToolbarComponent implements OnInit, OnDestroy, AfterViewInit {
} }
private subscribeToChatMessages() { private subscribeToChatMessages() {
this.chatService.messagesObs.pipe(skip(1), takeUntil(this.destroy$)).subscribe((messages) => { this.chatMessagesSubscription = this.chatService.messagesObs.pipe(skip(1)).subscribe((messages) => {
if (!this.panelService.isChatPanelOpened()) { if (!this.panelService.isChatPanelOpened()) {
this.unreadMessages++; this.unreadMessages++;
} }
@ -732,7 +675,7 @@ export class ToolbarComponent implements OnInit, OnDestroy, AfterViewInit {
}); });
} }
private subscribeToUserMediaProperties() { private subscribeToUserMediaProperties() {
this.participantService.localParticipant$.pipe(takeUntil(this.destroy$)).subscribe((p: ParticipantModel | undefined) => { this.localParticipantSubscription = this.participantService.localParticipant$.subscribe((p: ParticipantModel | undefined) => {
if (p) { if (p) {
if (this.isCameraEnabled !== p.isCameraEnabled) { if (this.isCameraEnabled !== p.isCameraEnabled) {
this.onVideoEnabledChanged.emit(p.isCameraEnabled); this.onVideoEnabledChanged.emit(p.isCameraEnabled);
@ -756,13 +699,8 @@ export class ToolbarComponent implements OnInit, OnDestroy, AfterViewInit {
} }
private subscribeToRecordingStatus() { private subscribeToRecordingStatus() {
this.libService.recordingActivityReadOnly$.pipe(takeUntil(this.destroy$)).subscribe((readOnly: boolean) => { this.recordingSubscription = this.recordingService.recordingStatusObs.subscribe((event: RecordingStatusInfo) => {
this.isRecordingReadOnlyMode = readOnly; const { status, recordingElapsedTime } = event;
this.cd.markForCheck();
});
this.recordingService.recordingStatusObs.pipe(takeUntil(this.destroy$)).subscribe((event: RecordingStatusInfo) => {
const { status, startedAt } = event;
this.recordingStatus = status; this.recordingStatus = status;
if (status === RecordingStatus.STARTED) { if (status === RecordingStatus.STARTED) {
this.startedRecording = event.recordingList.find((rec) => rec.status === RecordingStatus.STARTED); this.startedRecording = event.recordingList.find((rec) => rec.status === RecordingStatus.STARTED);
@ -770,15 +708,15 @@ export class ToolbarComponent implements OnInit, OnDestroy, AfterViewInit {
this.startedRecording = undefined; this.startedRecording = undefined;
} }
if (startedAt) { if (recordingElapsedTime) {
this.recordingTime = startedAt; this.recordingTime = recordingElapsedTime;
} }
this.cd.markForCheck(); this.cd.markForCheck();
}); });
} }
private subscribeToBroadcastingStatus() { private subscribeToBroadcastingStatus() {
this.broadcastingService.broadcastingStatusObs.pipe(takeUntil(this.destroy$)).subscribe((ev: BroadcastingStatusInfo) => { this.broadcastingSubscription = this.broadcastingService.broadcastingStatusObs.subscribe((ev: BroadcastingStatusInfo) => {
if (!!ev) { if (!!ev) {
this.broadcastingStatus = ev.status; this.broadcastingStatus = ev.status;
this.broadcastingId = ev.broadcastingId; this.broadcastingId = ev.broadcastingId;
@ -788,97 +726,86 @@ export class ToolbarComponent implements OnInit, OnDestroy, AfterViewInit {
} }
private subscribeToToolbarDirectives() { private subscribeToToolbarDirectives() {
this.libService.minimal$.pipe(takeUntil(this.destroy$)).subscribe((value: boolean) => { this.minimalSub = this.libService.minimal$.subscribe((value: boolean) => {
this.isMinimal = value; this.isMinimal = value;
this.cd.markForCheck(); this.cd.markForCheck();
}); });
this.libService.brandingLogo$.pipe(takeUntil(this.destroy$)).subscribe((value: string) => { this.brandingLogoSub = this.libService.brandingLogo$.subscribe((value: string) => {
this.brandingLogo = value; this.brandingLogo = value;
this.cd.markForCheck(); this.cd.markForCheck();
}); });
this.libService.toolbarViewRecordingsButton$.pipe(takeUntil(this.destroy$)).subscribe((value: boolean) => { this.cameraButtonSub = this.libService.cameraButton$.subscribe((value: boolean) => {
this.showViewRecordingsButton = value;
this.checkDisplayMoreOptions();
this.cd.markForCheck();
});
this.libService.cameraButton$.pipe(takeUntil(this.destroy$)).subscribe((value: boolean) => {
this.showCameraButton = value; this.showCameraButton = value;
this.cd.markForCheck(); 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.showMicrophoneButton = value;
this.cd.markForCheck(); 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.showScreenshareButton = value && !this.platformService.isMobile();
this.cd.markForCheck(); this.cd.markForCheck();
}); });
this.libService.fullscreenButton$.pipe(takeUntil(this.destroy$)).subscribe((value: boolean) => { this.fullscreenButtonSub = this.libService.fullscreenButton$.subscribe((value: boolean) => {
this.showFullscreenButton = value; this.showFullscreenButton = value;
this.checkDisplayMoreOptions(); this.checkDisplayMoreOptions();
this.cd.markForCheck(); this.cd.markForCheck();
}); });
this.libService.leaveButton$.pipe(takeUntil(this.destroy$)).subscribe((value: boolean) => { this.leaveButtonSub = this.libService.leaveButton$.subscribe((value: boolean) => {
this.showLeaveButton = value; this.showLeaveButton = value;
this.cd.markForCheck(); this.cd.markForCheck();
}); });
this.libService.recordingButton$.pipe(takeUntil(this.destroy$)).subscribe((value: boolean) => { this.recordingButtonSub = this.libService.recordingButton$.subscribe((value: boolean) => {
this.showRecordingButton = value; this.showRecordingButton = value;
this.checkDisplayMoreOptions(); this.checkDisplayMoreOptions();
this.cd.markForCheck(); this.cd.markForCheck();
}); });
this.libService.broadcastingButton$.pipe(takeUntil(this.destroy$)).subscribe((value: boolean) => { this.broadcastingButtonSub = this.libService.broadcastingButton$.subscribe((value: boolean) => {
this.showBroadcastingButton = value; this.showBroadcastingButton = value;
this.checkDisplayMoreOptions(); this.checkDisplayMoreOptions();
this.cd.markForCheck(); this.cd.markForCheck();
}); });
this.libService.toolbarSettingsButton$.pipe(takeUntil(this.destroy$)).subscribe((value: boolean) => { this.settingsButtonSub = this.libService.toolbarSettingsButton$.subscribe((value: boolean) => {
this.showSettingsButton = value; this.showSettingsButton = value;
this.checkDisplayMoreOptions(); this.checkDisplayMoreOptions();
this.cd.markForCheck(); this.cd.markForCheck();
}); });
this.libService.chatPanelButton$.pipe(takeUntil(this.destroy$)).subscribe((value: boolean) => { this.chatPanelButtonSub = this.libService.chatPanelButton$.subscribe((value: boolean) => {
this.showChatPanelButton = value; this.showChatPanelButton = value;
this.cd.markForCheck(); this.cd.markForCheck();
}); });
this.libService.participantsPanelButton$.pipe(takeUntil(this.destroy$)).subscribe((value: boolean) => { this.participantsPanelButtonSub = this.libService.participantsPanelButton$.subscribe((value: boolean) => {
this.showParticipantsPanelButton = value; this.showParticipantsPanelButton = value;
this.cd.markForCheck(); this.cd.markForCheck();
}); });
this.libService.activitiesPanelButton$.pipe(takeUntil(this.destroy$)).subscribe((value: boolean) => { this.activitiesPanelButtonSub = this.libService.activitiesPanelButton$.subscribe((value: boolean) => {
this.showActivitiesPanelButton = value; this.showActivitiesPanelButton = value;
this.cd.markForCheck(); this.cd.markForCheck();
}); });
this.libService.backgroundEffectsButton$.pipe(takeUntil(this.destroy$)).subscribe((value: boolean) => { this.backgroundEffectsButtonSub = this.libService.backgroundEffectsButton$.subscribe((value: boolean) => {
this.showBackgroundEffectsButton = value; this.showBackgroundEffectsButton = value;
this.checkDisplayMoreOptions(); this.checkDisplayMoreOptions();
this.cd.markForCheck(); this.cd.markForCheck();
}); });
this.libService.displayLogo$.pipe(takeUntil(this.destroy$)).subscribe((value: boolean) => { this.displayLogoSub = this.libService.displayLogo$.subscribe((value: boolean) => {
this.showLogo = value; this.showLogo = value;
this.cd.markForCheck(); this.cd.markForCheck();
}); });
this.libService.displayRoomName$.pipe(takeUntil(this.destroy$)).subscribe((value: boolean) => { this.displayRoomNameSub = this.libService.displayRoomName$.subscribe((value: boolean) => {
this.showRoomName = value; this.showSessionName = value;
this.cd.markForCheck();
});
this.captionsSubs = this.libService.captionsButton$.subscribe((value: boolean) => {
this.showCaptionsButton = value;
this.cd.markForCheck(); this.cd.markForCheck();
}); });
this.libService.roomName$.pipe(takeUntil(this.destroy$)).subscribe((value: string) => { this.additionalButtonsPositionSub = this.libService.toolbarAdditionalButtonsPosition$.subscribe(
this.evalAndSetRoomName(value); (value: ToolbarAdditionalButtonsPosition) => {
this.cd.markForCheck();
});
// this.libService.captionsButton$.pipe(takeUntil(this.destroy$)).subscribe((value: boolean) => {
// this.showCaptionsButton = value;
// this.cd.markForCheck();
// });
this.libService.toolbarAdditionalButtonsPosition$
.pipe(takeUntil(this.destroy$))
.subscribe((value: ToolbarAdditionalButtonsPosition) => {
// Using Promise.resolve() to defer change detection until the next microtask. // Using Promise.resolve() to defer change detection until the next microtask.
// This ensures that Angular's change detection has the latest value before updating the view. // This ensures that Angular's change detection has the latest value before updating the view.
// Without this, Angular's OnPush strategy might not immediately reflect the change, // Without this, Angular's OnPush strategy might not immediately reflect the change,
@ -888,11 +815,12 @@ export class ToolbarComponent implements OnInit, OnDestroy, AfterViewInit {
this.additionalButtonsPosition = value; this.additionalButtonsPosition = value;
this.cd.markForCheck(); this.cd.markForCheck();
}); });
}); }
);
} }
private subscribeToCaptionsToggling() { private subscribeToCaptionsToggling() {
this.layoutService.captionsTogglingObs.pipe(takeUntil(this.destroy$)).subscribe((value: boolean) => { this.captionsSubs = this.layoutService.captionsTogglingObs.subscribe((value: boolean) => {
this.captionsEnabled = value; this.captionsEnabled = value;
this.cd.markForCheck(); this.cd.markForCheck();
}); });
@ -906,14 +834,4 @@ export class ToolbarComponent implements OnInit, OnDestroy, AfterViewInit {
this.showBroadcastingButton || this.showBroadcastingButton ||
this.showSettingsButton; this.showSettingsButton;
} }
private evalAndSetRoomName(value: string) {
if (!!value) {
this.roomName = value;
} else if (!!this.room && this.room.name) {
this.roomName = this.room.name;
} else {
this.roomName = '';
}
}
} }

View File

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

View File

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

View File

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

View File

@ -49,7 +49,9 @@ export class ActivitiesPanelRecordingActivityDirective implements AfterViewInit,
} }
update(value: boolean) { 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) { update(value: boolean) {
this.libService.setBroadcastingActivity(value); if (this.libService.showBroadcastingActivity() !== value) {
this.libService.setBroadcastingActivity(value);
}
} }
} }

View File

@ -16,17 +16,15 @@ import { OpenViduComponentsConfigService } from '../../services/config/directive
standalone: false standalone: false
}) })
export class AdminDashboardRecordingsListDirective implements AfterViewInit, OnDestroy { export class AdminDashboardRecordingsListDirective implements AfterViewInit, OnDestroy {
@Input() set recordingsList(value: RecordingInfo[]) { @Input() set recordingsList(value: RecordingInfo[]) {
this.recordingsValue = value; this.recordingsValue = value;
this.update(this.recordingsValue); this.update(this.recordingsValue);
} }
recordingsValue: RecordingInfo[] = []; recordingsValue: RecordingInfo [] = [];
constructor( constructor(public elementRef: ElementRef, private libService: OpenViduComponentsConfigService) {}
public elementRef: ElementRef,
private libService: OpenViduComponentsConfigService
) {}
ngAfterViewInit() { ngAfterViewInit() {
this.update(this.recordingsValue); this.update(this.recordingsValue);
@ -40,7 +38,9 @@ export class AdminDashboardRecordingsListDirective implements AfterViewInit, OnD
} }
update(value: RecordingInfo[]) { update(value: RecordingInfo[]) {
this.libService.updateAdminConfig({ recordingsList: value }); if (this.libService.getAdminRecordingsList() !== value) {
this.libService.setAdminRecordingsList(value);
}
} }
} }
@ -58,17 +58,15 @@ export class AdminDashboardRecordingsListDirective implements AfterViewInit, OnD
standalone: false standalone: false
}) })
export class AdminDashboardTitleDirective implements AfterViewInit, OnDestroy { export class AdminDashboardTitleDirective implements AfterViewInit, OnDestroy {
@Input() set navbarTitle(value: string) {
@Input() set navbarTitle(value: any) {
this.navbarTitleValue = value; this.navbarTitleValue = value;
this.update(this.navbarTitleValue); this.update(this.navbarTitleValue);
} }
navbarTitleValue: string = 'OpenVidu Dashboard'; navbarTitleValue: any = null;
constructor( constructor(public elementRef: ElementRef, private libService: OpenViduComponentsConfigService) {}
public elementRef: ElementRef,
private libService: OpenViduComponentsConfigService
) {}
ngAfterViewInit() { ngAfterViewInit() {
this.update(this.navbarTitleValue); this.update(this.navbarTitleValue);
@ -77,15 +75,18 @@ export class AdminDashboardTitleDirective implements AfterViewInit, OnDestroy {
this.clear(); this.clear();
} }
clear() { clear() {
this.navbarTitleValue = 'OpenVidu Dashboard'; this.navbarTitleValue = null;
this.update(null); this.update(null);
} }
update(value: any) { 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}. * 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 standalone: false
}) })
export class AdminLoginTitleDirective implements AfterViewInit, OnDestroy { export class AdminLoginTitleDirective implements AfterViewInit, OnDestroy {
@Input() set navbarTitle(value: any) { @Input() set navbarTitle(value: any) {
this.navbarTitleValue = value; this.navbarTitleValue = value;
this.update(this.navbarTitleValue); this.update(this.navbarTitleValue);
@ -107,10 +109,7 @@ export class AdminLoginTitleDirective implements AfterViewInit, OnDestroy {
navbarTitleValue: any = null; navbarTitleValue: any = null;
constructor( constructor(public elementRef: ElementRef, private libService: OpenViduComponentsConfigService) {}
public elementRef: ElementRef,
private libService: OpenViduComponentsConfigService
) {}
ngAfterViewInit() { ngAfterViewInit() {
this.update(this.navbarTitleValue); this.update(this.navbarTitleValue);
@ -124,10 +123,14 @@ export class AdminLoginTitleDirective implements AfterViewInit, OnDestroy {
} }
update(value: any) { 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}. * 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> * <ov-admin-login [error]="error"></ov-admin-login>
* *
*/ */
@Directive({ @Directive({
selector: 'ov-admin-login[error]', selector: 'ov-admin-login[error]',
standalone: false standalone: false
}) })
export class AdminLoginErrorDirective implements AfterViewInit, OnDestroy { export class AdminLoginErrorDirective implements AfterViewInit, OnDestroy {
@Input() set error(value: any) { @Input() set error(value: any) {
this.errorValue = value; this.errorValue = value;
this.update(this.errorValue); this.update(this.errorValue);
@ -149,10 +153,7 @@ export class AdminLoginErrorDirective implements AfterViewInit, OnDestroy {
errorValue: any = null; errorValue: any = null;
constructor( constructor(public elementRef: ElementRef, private libService: OpenViduComponentsConfigService) {}
public elementRef: ElementRef,
private libService: OpenViduComponentsConfigService
) {}
ngAfterViewInit() { ngAfterViewInit() {
this.update(this.errorValue); this.update(this.errorValue);
@ -166,6 +167,9 @@ export class AdminLoginErrorDirective implements AfterViewInit, OnDestroy {
} }
update(value: any) { update(value: any) {
this.libService.updateAdminConfig({ loginError: value }); if (this.libService.getAdminLoginError() !== value) {
this.libService.setAdminLoginError(value);
}
} }
} }

View File

@ -1,23 +1,16 @@
import { NgModule } from '@angular/core'; import { NgModule } from '@angular/core';
import { ActivitiesPanelBroadcastingActivityDirective, ActivitiesPanelRecordingActivityDirective } from './activities-panel.directive'; import { ActivitiesPanelBroadcastingActivityDirective, ActivitiesPanelRecordingActivityDirective } from './activities-panel.directive';
import { import {
AdminDashboardRecordingsListDirective,
AdminDashboardTitleDirective,
AdminLoginErrorDirective, AdminLoginErrorDirective,
AdminLoginTitleDirective AdminDashboardRecordingsListDirective,
AdminLoginTitleDirective,
AdminDashboardTitleDirective
} from './admin.directive'; } from './admin.directive';
import { import {
FallbackLogoDirective,
LayoutRemoteParticipantsDirective, LayoutRemoteParticipantsDirective,
PrejoinDisplayParticipantName, FallbackLogoDirective,
ToolbarBrandingLogoDirective, ToolbarBrandingLogoDirective,
ToolbarViewRecordingsButtonDirective, PrejoinDisplayParticipantName
RecordingActivityReadOnlyDirective,
RecordingActivityShowControlsDirective,
StartStopRecordingButtonsDirective,
RecordingActivityViewRecordingsButtonDirective,
RecordingActivityShowRecordingsListDirective,
ToolbarRoomNameDirective
} from './internals.directive'; } from './internals.directive';
import { ParticipantPanelItemMuteButtonDirective } from './participant-panel-item.directive'; import { ParticipantPanelItemMuteButtonDirective } from './participant-panel-item.directive';
import { import {
@ -27,21 +20,21 @@ import {
} from './stream.directive'; } from './stream.directive';
import { import {
ToolbarActivitiesPanelButtonDirective, ToolbarActivitiesPanelButtonDirective,
ToolbarAdditionalButtonsPossitionDirective,
ToolbarBackgroundEffectsButtonDirective, ToolbarBackgroundEffectsButtonDirective,
ToolbarBroadcastingButtonDirective, ToolbarBroadcastingButtonDirective,
ToolbarCameraButtonDirective,
// ToolbarCaptionsButtonDirective, // ToolbarCaptionsButtonDirective,
ToolbarChatPanelButtonDirective, ToolbarChatPanelButtonDirective,
ToolbarDisplayLogoDirective, ToolbarDisplayLogoDirective,
ToolbarDisplayRoomNameDirective, ToolbarDisplayRoomNameDirective,
ToolbarFullscreenButtonDirective, ToolbarFullscreenButtonDirective,
ToolbarLeaveButtonDirective, ToolbarLeaveButtonDirective,
ToolbarMicrophoneButtonDirective,
ToolbarParticipantsPanelButtonDirective, ToolbarParticipantsPanelButtonDirective,
ToolbarRecordingButtonDirective, ToolbarRecordingButtonDirective,
ToolbarScreenshareButtonDirective, ToolbarScreenshareButtonDirective,
ToolbarSettingsButtonDirective ToolbarSettingsButtonDirective,
ToolbarAdditionalButtonsPossitionDirective,
ToolbarCameraButtonDirective,
ToolbarMicrophoneButtonDirective
} from './toolbar.directive'; } from './toolbar.directive';
import { import {
AudioEnabledDirective, AudioEnabledDirective,
@ -54,7 +47,6 @@ import {
ParticipantNameDirective, ParticipantNameDirective,
PrejoinDirective, PrejoinDirective,
RecordingStreamBaseUrlDirective, RecordingStreamBaseUrlDirective,
ShowDisconnectionDialogDirective,
TokenDirective, TokenDirective,
TokenErrorDirective, TokenErrorDirective,
VideoEnabledDirective VideoEnabledDirective
@ -72,10 +64,7 @@ const directives = [
PrejoinDirective, PrejoinDirective,
PrejoinDisplayParticipantName, PrejoinDisplayParticipantName,
VideoEnabledDirective, VideoEnabledDirective,
RecordingActivityReadOnlyDirective,
RecordingActivityShowControlsDirective,
AudioEnabledDirective, AudioEnabledDirective,
ShowDisconnectionDialogDirective,
RecordingStreamBaseUrlDirective, RecordingStreamBaseUrlDirective,
ToolbarCameraButtonDirective, ToolbarCameraButtonDirective,
ToolbarMicrophoneButtonDirective, ToolbarMicrophoneButtonDirective,
@ -93,7 +82,6 @@ const directives = [
ToolbarDisplayLogoDirective, ToolbarDisplayLogoDirective,
ToolbarSettingsButtonDirective, ToolbarSettingsButtonDirective,
ToolbarAdditionalButtonsPossitionDirective, ToolbarAdditionalButtonsPossitionDirective,
ToolbarViewRecordingsButtonDirective,
StreamDisplayParticipantNameDirective, StreamDisplayParticipantNameDirective,
StreamDisplayAudioDetectionDirective, StreamDisplayAudioDetectionDirective,
StreamVideoControlsDirective, StreamVideoControlsDirective,
@ -107,11 +95,7 @@ const directives = [
AdminLoginTitleDirective, AdminLoginTitleDirective,
AdminLoginErrorDirective, AdminLoginErrorDirective,
AdminDashboardTitleDirective, AdminDashboardTitleDirective,
LayoutRemoteParticipantsDirective, LayoutRemoteParticipantsDirective
StartStopRecordingButtonsDirective,
RecordingActivityViewRecordingsButtonDirective,
RecordingActivityShowRecordingsListDirective,
ToolbarRoomNameDirective
]; ];
@NgModule({ @NgModule({

View File

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

View File

@ -32,10 +32,7 @@ export class ParticipantPanelItemMuteButtonDirective implements AfterViewInit, O
muteValue: boolean = true; muteValue: boolean = true;
constructor( constructor(public elementRef: ElementRef, private libService: OpenViduComponentsConfigService) {}
public elementRef: ElementRef,
private libService: OpenViduComponentsConfigService
) {}
ngAfterViewInit() { ngAfterViewInit() {
this.update(this.muteValue); this.update(this.muteValue);
@ -49,6 +46,8 @@ export class ParticipantPanelItemMuteButtonDirective implements AfterViewInit, O
} }
update(value: boolean) { update(value: boolean) {
this.libService.updateStreamConfig({ participantItemMuteButton: value }); if (this.libService.showParticipantItemMuteButton() !== value) {
this.libService.setParticipantItemMuteButton(value);
}
} }
} }

View File

@ -46,7 +46,9 @@ export class StreamDisplayParticipantNameDirective implements AfterViewInit, OnD
} }
update(value: boolean) { update(value: boolean) {
this.libService.updateStreamConfig({ displayParticipantName: value }); if (this.libService.isParticipantNameDisplayed() !== value) {
this.libService.setDisplayParticipantName(value);
}
} }
clear() { clear() {
@ -98,7 +100,9 @@ export class StreamDisplayAudioDetectionDirective implements AfterViewInit, OnDe
} }
update(value: boolean) { update(value: boolean) {
this.libService.updateStreamConfig({ displayAudioDetection: value }); if (this.libService.isAudioDetectionDisplayed() !== value) {
this.libService.setDisplayAudioDetection(value);
}
} }
clear() { clear() {
this.update(true); this.update(true);
@ -150,7 +154,9 @@ export class StreamVideoControlsDirective implements AfterViewInit, OnDestroy {
} }
update(value: boolean) { update(value: boolean) {
this.libService.updateStreamConfig({ videoControls: value }); if (this.libService.showStreamVideoControls() !== value) {
this.libService.setStreamVideoControls(value);
}
} }
clear() { clear() {

View File

@ -62,7 +62,9 @@ export class ToolbarCameraButtonDirective implements AfterViewInit, OnDestroy {
} }
private update(value: boolean) { 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) { 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) { 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) { private update(value: boolean) {
this.libService.updateToolbarConfig({ recording: value }); if (this.libService.showRecordingButton() !== value) {
this.libService.setRecordingButton(value);
}
} }
} }
@ -313,7 +321,9 @@ export class ToolbarBroadcastingButtonDirective implements AfterViewInit, OnDest
} }
private update(value: boolean) { private update(value: boolean) {
this.libService.setBroadcastingButton(value); if (this.libService.showBroadcastingButton() !== value) {
this.libService.setBroadcastingButton(value);
}
} }
} }
@ -374,7 +384,9 @@ export class ToolbarFullscreenButtonDirective implements AfterViewInit, OnDestro
} }
private update(value: boolean) { 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) { 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) { 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) { 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) { 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) { 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) { 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) { 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) { 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) { private update(value: ToolbarAdditionalButtonsPosition) {
this.libService.updateToolbarConfig({ additionalButtonsPosition: value }); if (this.libService.getToolbarAdditionalButtonsPosition() !== value) {
this.libService.setToolbarAdditionalButtonsPosition(value);
}
} }
} }

View File

@ -55,7 +55,7 @@ export class LivekitUrlDirective implements OnDestroy {
* @ignore * @ignore
*/ */
update(value: string) { update(value: string) {
this.libService.updateGeneralConfig({ livekitUrl: value }); this.libService.setLivekitUrl(value);
} }
} }
@ -108,7 +108,7 @@ export class TokenDirective implements OnDestroy {
* @ignore * @ignore
*/ */
update(value: string) { update(value: string) {
this.libService.updateGeneralConfig({ token: value }); this.libService.setToken(value);
} }
} }
@ -160,7 +160,7 @@ export class TokenErrorDirective implements OnDestroy {
* @ignore * @ignore
*/ */
update(value: any) { update(value: any) {
this.libService.updateGeneralConfig({ tokenError: value }); this.libService.setTokenError(value);
} }
} }
@ -212,7 +212,9 @@ export class MinimalDirective implements OnDestroy {
* @ignore * @ignore
*/ */
update(value: boolean) { update(value: boolean) {
this.libService.updateGeneralConfig({ minimal: value }); if (this.libService.isMinimal() !== value) {
this.libService.setMinimal(value);
}
} }
} }
@ -223,7 +225,7 @@ export class MinimalDirective implements OnDestroy {
* *
* **Default:** English `en` * **Default:** English `en`
* *
* **Available Langs:** * **Available:**
* *
* * English: `en` * * English: `en`
* * Spanish: `es` * * Spanish: `es`
@ -536,7 +538,7 @@ export class ParticipantNameDirective implements AfterViewInit, OnDestroy {
* @ignore * @ignore
*/ */
update(value: string) { 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 * @ignore
*/ */
update(value: boolean) { 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 // Ensure libService state is consistent with the final enabled state
if (this.libService.isVideoEnabled() !== finalEnabledState) { 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); this.storageService.setMicrophoneEnabled(finalEnabledState);
if (this.libService.isAudioEnabled() !== enabled) { if (this.libService.isAudioEnabled() !== enabled) {
this.libService.updateStreamConfig({ audioEnabled: enabled }); this.libService.setAudioEnabled(enabled);
}
}
}
/**
* The **showDisconnectionDialog** directive allows to show/hide the disconnection dialog when the local participant is disconnected from the room.
*
* It is only available for {@link VideoconferenceComponent}.
*
* Default: `true`
*
* @example
* <ov-videoconference [showDisconnectionDialog]="false"></ov-videoconference>
*/
@Directive({
selector: 'ov-videoconference[showDisconnectionDialog]',
standalone: false
})
export class ShowDisconnectionDialogDirective implements OnDestroy {
/**
* @ignore
*/
@Input() set showDisconnectionDialog(value: boolean) {
this.update(value);
}
/**
* @ignore
*/
constructor(
public elementRef: ElementRef,
private libService: OpenViduComponentsConfigService
) {}
/**
* @ignore
*/
ngOnDestroy(): void {
this.clear();
}
/**
* @ignore
*/
clear() {
this.update(true);
}
/**
* @ignore
*/
update(value: boolean) {
if (this.libService.getShowDisconnectionDialog() !== value) {
this.libService.updateGeneralConfig({ showDisconnectionDialog: value });
} }
} }
} }
@ -853,6 +803,6 @@ export class RecordingStreamBaseUrlDirective implements AfterViewInit, OnDestroy
* @ignore * @ignore
*/ */
update(value: string) { update(value: string) {
if (value) this.libService.updateGeneralConfig({ recordingStreamBaseUrl: value }); if (value) this.libService.setRecordingStreamBaseUrl(value);
} }
} }

View File

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

View File

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

View File

@ -1814,4 +1814,3 @@ export class StreamDirective {
public container: ViewContainerRef public container: ViewContainerRef
) {} ) {}
} }

View File

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

View File

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

View File

@ -30,8 +30,8 @@
"AUDIO_DEVICE": "Audio device", "AUDIO_DEVICE": "Audio device",
"NO_VIDEO_DEVICE": "Video device not found", "NO_VIDEO_DEVICE": "Video device not found",
"NO_AUDIO_DEVICE": "Audio device not found", "NO_AUDIO_DEVICE": "Audio device not found",
"JOIN": "Join room", "JOIN": "Join session",
"PREPARING": "Preparing room..." "PREPARING": "Preparing session..."
}, },
"ROOM": { "ROOM": {
"JOINING": "Joining room..." "JOINING": "Joining room..."
@ -52,12 +52,10 @@
"START_RECORDING": "Start recording", "START_RECORDING": "Start recording",
"STOP_RECORDING": "Stop recording", "STOP_RECORDING": "Stop recording",
"SETTINGS": "Settings", "SETTINGS": "Settings",
"LEAVE": "Leave the room", "LEAVE": "Leave the session",
"PARTICIPANTS": "Participants", "PARTICIPANTS": "Participants",
"CHAT": "Chat", "CHAT": "Chat",
"ACTIVITIES": "Activities", "ACTIVITIES": "Activities"
"NO_TRACKS_PUBLISHED": "Share audio or video to start recording.",
"VIEW_RECORDINGS": "View recordings"
}, },
"STREAM": { "STREAM": {
"SETTINGS": "Settings", "SETTINGS": "Settings",
@ -79,7 +77,7 @@
"CHAT": { "CHAT": {
"TITLE": "Chat", "TITLE": "Chat",
"YOU": "You", "YOU": "You",
"SUBTITLE": "Messages will be removed at the end of the room", "SUBTITLE": "Messages will be removed at the end of the session",
"PLACEHOLDER": "Send a message...", "PLACEHOLDER": "Send a message...",
"SEND": "Send", "SEND": "Send",
"MESSAGE_SENT_NOTIFICATION": "message sent", "MESSAGE_SENT_NOTIFICATION": "message sent",
@ -91,9 +89,7 @@
"MICROPHONE": "MICROPHONE", "MICROPHONE": "MICROPHONE",
"SCREEN": "SCREEN", "SCREEN": "SCREEN",
"NO_STREAMS": "NONE", "NO_STREAMS": "NONE",
"YOU": "You", "YOU": "You"
"MUTE": "Mute",
"UNMUTE": "Unmute"
}, },
"SETTINGS": { "SETTINGS": {
"TITLE": "Settings", "TITLE": "Settings",
@ -104,7 +100,7 @@
"CAPTIONS": "Captions", "CAPTIONS": "Captions",
"DISABLED_AUDIO": "Audio disabled", "DISABLED_AUDIO": "Audio disabled",
"DISABLED_VIDEO": "Video disabled", "DISABLED_VIDEO": "Video disabled",
"CAPTIONS_LANG_TEXT": "Select the language that the participants of the room will use. The captions will appear in that language." "CAPTIONS_LANG_TEXT": "Select the language that the participants of the session will use. The captions will appear in that language."
}, },
"BACKGROUND": { "BACKGROUND": {
"TITLE": "Background effects", "TITLE": "Background effects",
@ -118,13 +114,6 @@
"SUBTITLE": "Record your meeting for posterity", "SUBTITLE": "Record your meeting for posterity",
"CONTENT_TITLE": "Record your video call", "CONTENT_TITLE": "Record your video call",
"CONTENT_SUBTITLE": "When recording has finished you will be able to download it with ease", "CONTENT_SUBTITLE": "When recording has finished you will be able to download it with ease",
"VIEW_ONLY_TITLE": "Available recordings",
"VIEW_ONLY_SUBTITLE": "View and access room recordings",
"VIEW_ONLY_CONTENT_TITLE": "Video call recordings",
"VIEW_ONLY_CONTENT_SUBTITLE": "Here you can access all available recordings",
"VIEW": "View",
"WATCH": "Watch",
"ACCESS": "Access",
"STARTING": "Starting recording", "STARTING": "Starting recording",
"STOPPING": "Stopping recording", "STOPPING": "Stopping recording",
"IN_PROGRESS": "Recording in progress ...", "IN_PROGRESS": "Recording in progress ...",
@ -135,11 +124,7 @@
"DELETE_QUESTION": "Are you sure you want to delete the recording?", "DELETE_QUESTION": "Are you sure you want to delete the recording?",
"DOWNLOAD": "Download", "DOWNLOAD": "Download",
"RECORDINGS": "RECORDINGS", "RECORDINGS": "RECORDINGS",
"NO_MODERATOR": "Only the MODERATOR can start the recording", "NO_MODERATOR": "Only the MODERATOR can start the recording"
"NO_TRACKS_PUBLISHED": "Share audio or video to start recording.",
"NO_RECORDINGS_AVAILABLE": "No recordings available at this time",
"BROWSE_RECORDINGS": "Browse saved recordings",
"ERROR_STARTING": "Error starting recording"
}, },
"STREAMING": { "STREAMING": {
"TITLE": "Streaming", "TITLE": "Streaming",

View File

@ -33,7 +33,7 @@
"AUDIO_DEVICE": "Dispositivo de audio", "AUDIO_DEVICE": "Dispositivo de audio",
"NO_VIDEO_DEVICE": "Dispositivo de vídeo no encontrado", "NO_VIDEO_DEVICE": "Dispositivo de vídeo no encontrado",
"NO_AUDIO_DEVICE": "Dispositivo de audio no encontrado", "NO_AUDIO_DEVICE": "Dispositivo de audio no encontrado",
"PREPARING": "Preparando la sala ...", "PREPARING": "Preparando la session ...",
"JOIN": "Unirme ahora" "JOIN": "Unirme ahora"
}, },
"TOOLBAR": { "TOOLBAR": {
@ -55,9 +55,7 @@
"LEAVE": "Salir de la sala", "LEAVE": "Salir de la sala",
"PARTICIPANTS": "Participantes", "PARTICIPANTS": "Participantes",
"CHAT": "Chat", "CHAT": "Chat",
"ACTIVITIES": "Actividades", "ACTIVITIES": "Actividades"
"NO_TRACKS_PUBLISHED": "Comparte audio o video para poder empezar a grabar.",
"VIEW_RECORDINGS": "Ver grabaciones"
}, },
"STREAM": { "STREAM": {
"SETTINGS": "Ajustes", "SETTINGS": "Ajustes",
@ -91,9 +89,7 @@
"MICROPHONE": "MICRÓFONO", "MICROPHONE": "MICRÓFONO",
"SCREEN": "PANTALLA", "SCREEN": "PANTALLA",
"NO_STREAMS": "NINGUNO", "NO_STREAMS": "NINGUNO",
"YOU": "Tú", "YOU": "Tú"
"MUTE": "Silenciar",
"UNMUTE": "Activar audio"
}, },
"SETTINGS": { "SETTINGS": {
"TITLE": "Configuración", "TITLE": "Configuración",
@ -118,10 +114,6 @@
"SUBTITLE": "Graba tus llamadas para la posteridad", "SUBTITLE": "Graba tus llamadas para la posteridad",
"CONTENT_TITLE": "Graba tu video conferencia", "CONTENT_TITLE": "Graba tu video conferencia",
"CONTENT_SUBTITLE": "Cuando la grabación haya finalizado, podrás descargarla con facilidad", "CONTENT_SUBTITLE": "Cuando la grabación haya finalizado, podrás descargarla con facilidad",
"VIEW_ONLY_SUBTITLE": "Visualiza y accede a las grabaciones de la sala",
"VIEW_ONLY_CONTENT_TITLE": "Grabaciones de la video conferencia",
"VIEW_ONLY_CONTENT_SUBTITLE": "Aquí puedes acceder a todas las grabaciones disponibles",
"WATCH": "Visualizar",
"STARTING": "Iniciando grabación...", "STARTING": "Iniciando grabación...",
"STOPPING": "Parando grabación", "STOPPING": "Parando grabación",
"IN_PROGRESS": "Grabación en curso", "IN_PROGRESS": "Grabación en curso",
@ -132,10 +124,7 @@
"DELETE_QUESTION": "¿Estás seguro/a de que deseas borrar la grabación?", "DELETE_QUESTION": "¿Estás seguro/a de que deseas borrar la grabación?",
"DOWNLOAD": "Descargar", "DOWNLOAD": "Descargar",
"RECORDINGS": "GRABACIONES", "RECORDINGS": "GRABACIONES",
"NO_MODERATOR": "Sólo el MODERADOR puede iniciar la grabación", "NO_MODERATOR": "Sólo el MODERADOR puede iniciar la grabación"
"NO_TRACKS_PUBLISHED": "Comparte audio o video para poder empezar a grabar.",
"NO_RECORDINGS_AVAILABLE": "No hay grabaciones disponibles en este momento",
"ERROR_STARTING": "Error iniciando la grabación"
}, },
"STREAMING": { "STREAMING": {
"TITLE": "Streaming", "TITLE": "Streaming",

View File

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

View File

@ -33,8 +33,8 @@
"AUDIO_DEVICE": "ऑडियो डिवाइस", "AUDIO_DEVICE": "ऑडियो डिवाइस",
"NO_VIDEO_DEVICE": "वीडियो डिवाइस नहीं मिला", "NO_VIDEO_DEVICE": "वीडियो डिवाइस नहीं मिला",
"NO_AUDIO_DEVICE": "ऑडियो डिवाइस नहीं मिला", "NO_AUDIO_DEVICE": "ऑडियो डिवाइस नहीं मिला",
"JOIN": "कमरा में शामिल हों", "JOIN": "सत्र में शामिल हों",
"PREPARING": "कमरा तैयार कर रहा है ..." "PREPARING": "सत्र तैयार कर रहा है ..."
}, },
"TOOLBAR": { "TOOLBAR": {
"MUTE_AUDIO": "अपनी ऑडियो को मौन करें", "MUTE_AUDIO": "अपनी ऑडियो को मौन करें",
@ -52,12 +52,10 @@
"START_RECORDING": "रिकॉर्डिंग प्रारंभ करें", "START_RECORDING": "रिकॉर्डिंग प्रारंभ करें",
"STOP_RECORDING": "रिकॉर्डिंग रोकें", "STOP_RECORDING": "रिकॉर्डिंग रोकें",
"SETTINGS": "सेटिंग्स", "SETTINGS": "सेटिंग्स",
"LEAVE": "कमरा छोड़ें", "LEAVE": "सत्र छोड़ें",
"PARTICIPANTS": "सदस्य", "PARTICIPANTS": "सदस्य",
"CHAT": "बातचीत", "CHAT": "बातचीत",
"ACTIVITIES": "गतिविधियाँ", "ACTIVITIES": "गतिविधियाँ"
"NO_TRACKS_PUBLISHED": "रिकॉर्डिंग शुरू करने के लिए ऑडियो या वीडियो साझा करें।",
"VIEW_RECORDINGS": "रिकॉर्डिंग देखें"
}, },
"STREAM": { "STREAM": {
"SETTINGS": "सेटिंग्स", "SETTINGS": "सेटिंग्स",
@ -79,7 +77,7 @@
"CHAT": { "CHAT": {
"TITLE": "बातचीत", "TITLE": "बातचीत",
"YOU": "आप", "YOU": "आप",
"SUBTITLE": "कमरा समाप्त होने पर संदेश हटा दिए जाएंगे", "SUBTITLE": "सत्र समाप्त होने पर संदेश हटा दिए जाएंगे",
"PLACEHOLDER": "एक संदेश भेजें ...", "PLACEHOLDER": "एक संदेश भेजें ...",
"SEND": "भेजें", "SEND": "भेजें",
"MESSAGE_SENT_NOTIFICATION": "संदेश भेजा गया", "MESSAGE_SENT_NOTIFICATION": "संदेश भेजा गया",
@ -91,9 +89,7 @@
"MICROPHONE": "माइक्रोफ़ोन", "MICROPHONE": "माइक्रोफ़ोन",
"SCREEN": "स्क्रीन", "SCREEN": "स्क्रीन",
"NO_STREAMS": "कोई_स्ट्रीम_नहीं", "NO_STREAMS": "कोई_स्ट्रीम_नहीं",
"YOU": "आप", "YOU": "आप"
"MUTE": "मौन",
"UNMUTE": "अनमौन"
}, },
"SETTINGS": { "SETTINGS": {
"TITLE": "सेटिंग्स", "TITLE": "सेटिंग्स",
@ -104,7 +100,7 @@
"CAPTIONS": "उपशीर्षक", "CAPTIONS": "उपशीर्षक",
"DISABLED_AUDIO": "ऑडियो अक्षम", "DISABLED_AUDIO": "ऑडियो अक्षम",
"DISABLED_VIDEO": "वीडियो अक्षम", "DISABLED_VIDEO": "वीडियो अक्षम",
"CAPTIONS_LANG_TEXT": "उस भाषा का चयन करें जिसका उपयोग कमरा के प्रतिभागी करेंगे। उपशीर्षक उस भाषा में दिखाई देंगे।" "CAPTIONS_LANG_TEXT": "उस भाषा का चयन करें जिसका उपयोग सत्र के प्रतिभागी करेंगे। उपशीर्षक उस भाषा में दिखाई देंगे।"
}, },
"BACKGROUND": { "BACKGROUND": {
"TITLE": "पृष्ठभूमि प्रभाव", "TITLE": "पृष्ठभूमि प्रभाव",
@ -118,10 +114,6 @@
"SUBTITLE": "अपनी बैठक को भावी पीढ़ी के लिए रिकॉर्ड करें", "SUBTITLE": "अपनी बैठक को भावी पीढ़ी के लिए रिकॉर्ड करें",
"CONTENT_TITLE": "अपना वीडियो कॉल रिकॉर्ड करें", "CONTENT_TITLE": "अपना वीडियो कॉल रिकॉर्ड करें",
"CONTENT_SUBTITLE": "रिकॉर्डिंग समाप्त हो जाने पर आप इसे आसानी से डाउनलोड कर सकेंगे", "CONTENT_SUBTITLE": "रिकॉर्डिंग समाप्त हो जाने पर आप इसे आसानी से डाउनलोड कर सकेंगे",
"VIEW_ONLY_SUBTITLE": "कमरे की रिकॉर्डिंग देखें और एक्सेस करें",
"VIEW_ONLY_CONTENT_TITLE": "वीडियो कॉल रिकॉर्डिंग",
"VIEW_ONLY_CONTENT_SUBTITLE": "यहाँ आप सभी उपलब्ध रिकॉर्डिंग तक पहुँच सकते हैं",
"WATCH": "देखना",
"STARTING": "रिकॉर्डिंग शुरू कर रहा है", "STARTING": "रिकॉर्डिंग शुरू कर रहा है",
"STOPPING": "रिकॉर्डिंग बंद करना", "STOPPING": "रिकॉर्डिंग बंद करना",
"IN_PROGRESS": "रिकॉर्डिंग चल रही है", "IN_PROGRESS": "रिकॉर्डिंग चल रही है",
@ -132,10 +124,7 @@
"DELETE_QUESTION": "क्या आप वाकई रिकॉर्डिंग हटाना चाहते हैं", "DELETE_QUESTION": "क्या आप वाकई रिकॉर्डिंग हटाना चाहते हैं",
"DOWNLOAD": "डाउनलोड", "DOWNLOAD": "डाउनलोड",
"RECORDINGS": "रिकॉर्डिंग", "RECORDINGS": "रिकॉर्डिंग",
"NO_MODERATOR": "केवल मॉडरेटर ही रिकॉर्डिंग शुरू कर सकता है", "NO_MODERATOR": "केवल मॉडरेटर ही रिकॉर्डिंग शुरू कर सकता है"
"NO_TRACKS_PUBLISHED": "रिकॉर्डिंग शुरू करने के लिए ऑडियो या वीडियो साझा करें।",
"NO_RECORDINGS_AVAILABLE": "इस समय कोई रिकॉर्डिंग उपलब्ध नहीं है",
"ERROR_STARTING": "रिकॉर्डिंग शुरू करने में त्रुटि"
}, },
"STREAMING": { "STREAMING": {
"TITLE": "स्ट्रीमिंग", "TITLE": "स्ट्रीमिंग",
@ -150,9 +139,9 @@
} }
}, },
"ERRORS": { "ERRORS": {
"SESSION": "कमरा से जुड़ने में त्रुटि हुई", "SESSION": "सत्र से जुड़ने में त्रुटि हुई",
"CONNECTION": "कनेक्शन खो गया", "CONNECTION": "कनेक्शन खो गया",
"RECONNECT": "ओह! कमरा से फिर से कनेक्ट करने का प्रयास कर रहा है", "RECONNECT": "ओह! सत्र से फिर से कनेक्ट करने का प्रयास कर रहा है",
"DISCONNECT": "आपको डिस्कनेक्ट कर दिया गया है", "DISCONNECT": "आपको डिस्कनेक्ट कर दिया गया है",
"NETWORK_DISCONNECT": "नेटवर्क कनेक्टिविटी समस्या के कारण आपका कनेक्शन टूट गया", "NETWORK_DISCONNECT": "नेटवर्क कनेक्टिविटी समस्या के कारण आपका कनेक्शन टूट गया",
"SIGNAL_CLOSE": "सर्वर से कनेक्शन अप्रत्याशित रूप से बंद हो गया", "SIGNAL_CLOSE": "सर्वर से कनेक्शन अप्रत्याशित रूप से बंद हो गया",

View File

@ -33,8 +33,8 @@
"AUDIO_DEVICE": "Dispositivo audio", "AUDIO_DEVICE": "Dispositivo audio",
"NO_VIDEO_DEVICE": "Dispositivo video non trovato", "NO_VIDEO_DEVICE": "Dispositivo video non trovato",
"NO_AUDIO_DEVICE": "Dispositivo audio non trovato", "NO_AUDIO_DEVICE": "Dispositivo audio non trovato",
"JOIN": "Unisciti alla stanza", "JOIN": "Unisciti alla sessione",
"PREPARING": "Preparazione della stanza in corso..." "PREPARING": "Preparazione della sessione in corso..."
}, },
"TOOLBAR": { "TOOLBAR": {
"MUTE_AUDIO": "Disattiva l'audio", "MUTE_AUDIO": "Disattiva l'audio",
@ -52,12 +52,10 @@
"START_RECORDING": "Avvia registrazione", "START_RECORDING": "Avvia registrazione",
"STOP_RECORDING": "Interrompi registrazione", "STOP_RECORDING": "Interrompi registrazione",
"SETTINGS": "Impostazioni", "SETTINGS": "Impostazioni",
"LEAVE": "Abbandona la stanza", "LEAVE": "Abbandona la sessione",
"PARTICIPANTS": "Partecipanti", "PARTICIPANTS": "Partecipanti",
"CHAT": "Chat", "CHAT": "Chat",
"ACTIVITIES": "Attività", "ACTIVITIES": "Attività"
"NO_TRACKS_PUBLISHED": "Condividi audio o video per iniziare la registrazione.",
"VIEW_RECORDINGS": "Visualizza registrazioni"
}, },
"STREAM": { "STREAM": {
"SETTINGS": "Impostazioni", "SETTINGS": "Impostazioni",
@ -79,7 +77,7 @@
"CHAT": { "CHAT": {
"TITLE": "Chat", "TITLE": "Chat",
"YOU": "Tu", "YOU": "Tu",
"SUBTITLE": "I messaggi verranno rimossi alla fine della stanza", "SUBTITLE": "I messaggi verranno rimossi alla fine della sessione",
"PLACEHOLDER": "Invia un messaggio...", "PLACEHOLDER": "Invia un messaggio...",
"SEND": "Invia", "SEND": "Invia",
"MESSAGE_SENT_NOTIFICATION": "messaggio inviato", "MESSAGE_SENT_NOTIFICATION": "messaggio inviato",
@ -91,9 +89,7 @@
"MICROPHONE": "MICROFONO", "MICROPHONE": "MICROFONO",
"SCREEN": "SCREEN", "SCREEN": "SCREEN",
"NO_STREAMS": "NESSUNO", "NO_STREAMS": "NESSUNO",
"YOU": "Tu", "YOU": "Tu"
"MUTE": "Disattiva l'audio",
"UNMUTE": "Attiva l'audio"
}, },
"SETTINGS": { "SETTINGS": {
"TITLE": "Impostazioni", "TITLE": "Impostazioni",
@ -104,7 +100,7 @@
"CAPTIONS": "Sottotitoli", "CAPTIONS": "Sottotitoli",
"DISABLED_AUDIO": "Disattiva l'audio", "DISABLED_AUDIO": "Disattiva l'audio",
"DISABLED_VIDEO": "Disattiva il video", "DISABLED_VIDEO": "Disattiva il video",
"CAPTIONS_LANG_TEXT": "Seleziona la lingua che i partecipanti della stanza useranno. I sottotitoli appariranno in quella lingua." "CAPTIONS_LANG_TEXT": "Seleziona la lingua che i partecipanti della sessione useranno. I sottotitoli appariranno in quella lingua."
}, },
"BACKGROUND": { "BACKGROUND": {
"TITLE": "Effetti di sfondo", "TITLE": "Effetti di sfondo",
@ -118,10 +114,6 @@
"SUBTITLE": "Registra la tua riunione per i posteri", "SUBTITLE": "Registra la tua riunione per i posteri",
"CONTENT_TITLE": "Registra la tua videochiamata", "CONTENT_TITLE": "Registra la tua videochiamata",
"CONTENT_SUBTITLE": "Al termine della registrazione potrete scaricarla con facilità", "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", "STARTING": "Avvio della registrazione",
"STOPPING": "Interruzione della registrazione", "STOPPING": "Interruzione della registrazione",
"IN_PROGRESS": "Registrazione in corso", "IN_PROGRESS": "Registrazione in corso",
@ -132,10 +124,7 @@
"DELETE_QUESTION": "Sei sicuro di voler eliminare la registrazione?", "DELETE_QUESTION": "Sei sicuro di voler eliminare la registrazione?",
"DOWNLOAD": "Scarica", "DOWNLOAD": "Scarica",
"RECORDINGS": "REGISTRAZIONI", "RECORDINGS": "REGISTRAZIONI",
"NO_MODERATOR": "Solo il MODERATORE può avviare la registrazione", "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"
}, },
"STREAMING": { "STREAMING": {
"TITLE": "Streaming", "TITLE": "Streaming",
@ -150,9 +139,9 @@
} }
}, },
"ERRORS": { "ERRORS": {
"SESSION": "Si è verificato un errore di connessione alla stanza", "SESSION": "Si è verificato un errore di connessione alla sessione",
"CONNECTION": "Connessione persa", "CONNECTION": "Connessione persa",
"RECONNECT": "Oops! Si sta cercando di riconnettersi alla stanza...", "RECONNECT": "Oops! Si sta cercando di riconnettersi alla sessione...",
"DISCONNECT": "Sei stato disconnesso", "DISCONNECT": "Sei stato disconnesso",
"NETWORK_DISCONNECT": "Sei stato disconnesso a causa di un problema di connettività di rete", "NETWORK_DISCONNECT": "Sei stato disconnesso a causa di un problema di connettività di rete",
"SIGNAL_CLOSE": "La connessione al server è stata chiusa inaspettatamente", "SIGNAL_CLOSE": "La connessione al server è stata chiusa inaspettatamente",

View File

@ -33,8 +33,8 @@
"AUDIO_DEVICE": "オーディオデバイス", "AUDIO_DEVICE": "オーディオデバイス",
"NO_VIDEO_DEVICE": "ビデオデバイスが見つかりません", "NO_VIDEO_DEVICE": "ビデオデバイスが見つかりません",
"NO_AUDIO_DEVICE": "オーディオデバイスが見つかりません", "NO_AUDIO_DEVICE": "オーディオデバイスが見つかりません",
"JOIN": "ルームに参加する", "JOIN": "セッションに参加する",
"PREPARING": "ルームの準備中..." "PREPARING": "セッションの準備中..."
}, },
"TOOLBAR": { "TOOLBAR": {
"MUTE_AUDIO": "オーディオをミュートする", "MUTE_AUDIO": "オーディオをミュートする",
@ -52,12 +52,10 @@
"START_RECORDING": "録画開始", "START_RECORDING": "録画開始",
"STOP_RECORDING": "録画の停止", "STOP_RECORDING": "録画の停止",
"SETTINGS": "設定", "SETTINGS": "設定",
"LEAVE": "ルームを終了する", "LEAVE": "セッションを終了する",
"PARTICIPANTS": "参加者", "PARTICIPANTS": "参加者",
"CHAT": "チャット", "CHAT": "チャット",
"ACTIVITIES": "アクティビティ", "ACTIVITIES": "アクティビティ"
"NO_TRACKS_PUBLISHED": "録音を開始するには、音声または動画を共有してください。",
"VIEW_RECORDINGS": "録画を表示"
}, },
"STREAM": { "STREAM": {
"SETTINGS": "設定", "SETTINGS": "設定",
@ -79,7 +77,7 @@
"CHAT": { "CHAT": {
"TITLE": "チャット", "TITLE": "チャット",
"YOU": "あなた", "YOU": "あなた",
"SUBTITLE": "メッセージはルーム終了時に削除されます", "SUBTITLE": "メッセージはセッション終了時に削除されます",
"PLACEHOLDER": "メッセージを送信...", "PLACEHOLDER": "メッセージを送信...",
"SEND": "送信する", "SEND": "送信する",
"MESSAGE_SENT_NOTIFICATION": "メッセージを送信しました", "MESSAGE_SENT_NOTIFICATION": "メッセージを送信しました",
@ -91,9 +89,7 @@
"MICROPHONE": "マイクロフォン", "MICROPHONE": "マイクロフォン",
"SCREEN": "スクリーン", "SCREEN": "スクリーン",
"NO_STREAMS": "ストリームなし", "NO_STREAMS": "ストリームなし",
"YOU": "あなた", "YOU": "あなた"
"MUTE": "ミュート",
"UNMUTE": "ミュート解除"
}, },
"SETTINGS": { "SETTINGS": {
"TITLE": "設定", "TITLE": "設定",
@ -104,7 +100,7 @@
"CAPTIONS": "字幕", "CAPTIONS": "字幕",
"DISABLED_AUDIO": "オーディオを無効にする", "DISABLED_AUDIO": "オーディオを無効にする",
"DISABLED_VIDEO": "ビデオを無効にする", "DISABLED_VIDEO": "ビデオを無効にする",
"CAPTIONS_LANG_TEXT": "ルームの参加者が使用する言語を選択します。キャプションはその言語で表示されます。" "CAPTIONS_LANG_TEXT": "セッションの参加者が使用する言語を選択します。キャプションはその言語で表示されます。"
}, },
"BACKGROUND": { "BACKGROUND": {
"TITLE": "背景効果", "TITLE": "背景効果",
@ -118,10 +114,6 @@
"SUBTITLE": "会議を録画して保存する", "SUBTITLE": "会議を録画して保存する",
"CONTENT_TITLE": "ビデオ通話を録音する", "CONTENT_TITLE": "ビデオ通話を録音する",
"CONTENT_SUBTITLE": "録画が完了したら、簡単にダウンロードできます", "CONTENT_SUBTITLE": "録画が完了したら、簡単にダウンロードできます",
"VIEW_ONLY_SUBTITLE": "ルームの録画を表示してアクセスする",
"VIEW_ONLY_CONTENT_TITLE": "ビデオ通話の録画",
"VIEW_ONLY_CONTENT_SUBTITLE": "ここで利用可能なすべての録画にアクセスできます",
"WATCH": "視聴する",
"STARTING": "録画開始", "STARTING": "録画開始",
"STOPPING": "録音停止", "STOPPING": "録音停止",
"IN_PROGRESS": "録画中", "IN_PROGRESS": "録画中",
@ -132,10 +124,7 @@
"DELETE_QUESTION": "録画を削除してもよろしいですか", "DELETE_QUESTION": "録画を削除してもよろしいですか",
"DOWNLOAD": "保存", "DOWNLOAD": "保存",
"RECORDINGS": "録画", "RECORDINGS": "録画",
"NO_MODERATOR": "録画を開始できるのは、モデレーターのみです", "NO_MODERATOR": "録画を開始できるのは、モデレーターのみです"
"NO_TRACKS_PUBLISHED": "録音を開始するには、音声または動画を共有してください。",
"NO_RECORDINGS_AVAILABLE": "現在利用可能な録画はありません",
"ERROR_STARTING": "録画開始エラー"
}, },
"STREAMING": { "STREAMING": {
"TITLE": "ストリーミング", "TITLE": "ストリーミング",
@ -150,9 +139,9 @@
} }
}, },
"ERRORS": { "ERRORS": {
"SESSION": "ルームへの接続にエラーが発生しました", "SESSION": "セッションへの接続にエラーが発生しました",
"CONNECTION": "接続が失われました", "CONNECTION": "接続が失われました",
"RECONNECT": "ルームへの再接続を試みています", "RECONNECT": "セッションへの再接続を試みています",
"DISCONNECT": "接続が切断されました", "DISCONNECT": "接続が切断されました",
"NETWORK_DISCONNECT": "ネットワーク接続の問題により切断されました", "NETWORK_DISCONNECT": "ネットワーク接続の問題により切断されました",
"SIGNAL_CLOSE": "サーバーへの接続が予期せず切断されました", "SIGNAL_CLOSE": "サーバーへの接続が予期せず切断されました",

View File

@ -33,8 +33,8 @@
"AUDIO_DEVICE": "Audiospeler", "AUDIO_DEVICE": "Audiospeler",
"NO_VIDEO_DEVICE": "Videoapparaat niet gevonden", "NO_VIDEO_DEVICE": "Videoapparaat niet gevonden",
"NO_AUDIO_DEVICE": "Audioapparaat niet gevonden", "NO_AUDIO_DEVICE": "Audioapparaat niet gevonden",
"JOIN": "Deelnemen aan kamer", "JOIN": "Deelnemen aan sessie",
"PREPARING": "Kamer voorbereiden ..." "PREPARING": "Sessie voorbereiden ..."
}, },
"TOOLBAR": { "TOOLBAR": {
"MUTE_AUDIO": "Audio dempen", "MUTE_AUDIO": "Audio dempen",
@ -52,12 +52,10 @@
"START_RECORDING": "Start opname", "START_RECORDING": "Start opname",
"STOP_RECORDING": "Stop opname", "STOP_RECORDING": "Stop opname",
"SETTINGS": "Instellingen", "SETTINGS": "Instellingen",
"LEAVE": "Verlaat de kamer", "LEAVE": "Verlaat de sessie",
"PARTICIPANTS": "Deelnemers", "PARTICIPANTS": "Deelnemers",
"CHAT": "Chat", "CHAT": "Chat",
"ACTIVITIES": "Activiteiten", "ACTIVITIES": "Activiteiten"
"NO_TRACKS_PUBLISHED": "Deel audio of video om met opnemen te beginnen.",
"VIEW_RECORDINGS": "Opnames bekijken"
}, },
"STREAM": { "STREAM": {
"SETTINGS": "Instellingen", "SETTINGS": "Instellingen",
@ -79,7 +77,7 @@
"CHAT": { "CHAT": {
"TITLE": "Chat", "TITLE": "Chat",
"YOU": "Jij", "YOU": "Jij",
"SUBTITLE": "Berichten worden aan het einde van de kamer verwijderd", "SUBTITLE": "Berichten worden aan het einde van de sessie verwijderd",
"PLACEHOLDER": "Stuur een bericht ...", "PLACEHOLDER": "Stuur een bericht ...",
"SEND": "Versturen", "SEND": "Versturen",
"MESSAGE_SENT_NOTIFICATION": "bericht verzonden", "MESSAGE_SENT_NOTIFICATION": "bericht verzonden",
@ -91,9 +89,7 @@
"MICROPHONE": "MICROFOON", "MICROPHONE": "MICROFOON",
"SCREEN": "SCHERM", "SCREEN": "SCHERM",
"NO_STREAMS": "GEEN", "NO_STREAMS": "GEEN",
"YOU": "Jij", "YOU": "Jij"
"MUTE": "Dempen",
"UNMUTE": "Dempen opheffen"
}, },
"SETTINGS": { "SETTINGS": {
"TITLE": "Instellingen", "TITLE": "Instellingen",
@ -104,7 +100,7 @@
"CAPTIONS": "Ondertitels", "CAPTIONS": "Ondertitels",
"DISABLED_AUDIO": "Geen audio", "DISABLED_AUDIO": "Geen audio",
"DISABLED_VIDEO": "Geen video", "DISABLED_VIDEO": "Geen video",
"CAPTIONS_LANG_TEXT": "Selecteer de taal die de deelnemers van de kamer zullen gebruiken. De ondertiteling zal in die taal verschijnen." "CAPTIONS_LANG_TEXT": "Selecteer de taal die de deelnemers van de sessie zullen gebruiken. De ondertiteling zal in die taal verschijnen."
}, },
"BACKGROUND": { "BACKGROUND": {
"TITLE": "Achtergrondeffecten", "TITLE": "Achtergrondeffecten",
@ -118,10 +114,6 @@
"SUBTITLE": "Neem uw vergadering op voor het nageslacht", "SUBTITLE": "Neem uw vergadering op voor het nageslacht",
"CONTENT_TITLE": "Neem uw videogesprek op", "CONTENT_TITLE": "Neem uw videogesprek op",
"CONTENT_SUBTITLE": "Als de opname klaar is kunt u deze met gemak downloaden", "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", "STARTING": "Beginnen met opnemen",
"STOPPING": "Opname stoppen", "STOPPING": "Opname stoppen",
"IN_PROGRESS": "Opname in uitvoering", "IN_PROGRESS": "Opname in uitvoering",
@ -132,10 +124,7 @@
"DELETE_QUESTION": "Weet je zeker dat je de opname wilt verwijderen?", "DELETE_QUESTION": "Weet je zeker dat je de opname wilt verwijderen?",
"DOWNLOAD": "Downloaden", "DOWNLOAD": "Downloaden",
"RECORDINGS": "OPNAME", "RECORDINGS": "OPNAME",
"NO_MODERATOR": "Alleen de MOEDERATOR kan de opname starten", "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"
}, },
"STREAMING": { "STREAMING": {
"TITLE": "Streaming", "TITLE": "Streaming",
@ -150,9 +139,9 @@
} }
}, },
"ERRORS": { "ERRORS": {
"SESSION": "Er is een fout opgetreden bij het verbinden met de kamer", "SESSION": "Er is een fout opgetreden bij het verbinden met de sessie",
"CONNECTION": "Verbinding verloren", "CONNECTION": "Verbinding verloren",
"RECONNECT": "Proberen opnieuw verbinding te maken met de kamer...", "RECONNECT": "Proberen opnieuw verbinding te maken met de sessie...",
"DISCONNECT": "Je bent losgekoppeld", "DISCONNECT": "Je bent losgekoppeld",
"NETWORK_DISCONNECT": "Je bent losgekoppeld vanwege een netwerkprobleem", "NETWORK_DISCONNECT": "Je bent losgekoppeld vanwege een netwerkprobleem",
"SIGNAL_CLOSE": "De verbinding met de server werd onverwacht verbroken", "SIGNAL_CLOSE": "De verbinding met de server werd onverwacht verbroken",

View File

@ -33,8 +33,8 @@
"AUDIO_DEVICE": "Dispositivo de áudio", "AUDIO_DEVICE": "Dispositivo de áudio",
"NO_VIDEO_DEVICE": "Dispositivo de vídeo não encontrado", "NO_VIDEO_DEVICE": "Dispositivo de vídeo não encontrado",
"NO_AUDIO_DEVICE": "Dispositivo de áudio não encontrado", "NO_AUDIO_DEVICE": "Dispositivo de áudio não encontrado",
"JOIN": "Entrar na sala", "JOIN": "Entrar na sessão",
"PREPARING": "Preparando sala..." "PREPARING": "Preparando sessão..."
}, },
"TOOLBAR": { "TOOLBAR": {
"MUTE_AUDIO": "Mute seu áudio", "MUTE_AUDIO": "Mute seu áudio",
@ -52,12 +52,10 @@
"START_RECORDING": "Iniciar_gravação", "START_RECORDING": "Iniciar_gravação",
"STOP_RECORDING": "Parar de gravar", "STOP_RECORDING": "Parar de gravar",
"SETTINGS": "Configurações", "SETTINGS": "Configurações",
"LEAVE": "Sair da sala", "LEAVE": "Sair da sessão",
"PARTICIPANTS": "Participantes", "PARTICIPANTS": "Participantes",
"CHAT": "Chat", "CHAT": "Chat",
"ACTIVITIES": "Actividades", "ACTIVITIES": "Actividades"
"NO_TRACKS_PUBLISHED": "Compartilhe áudio ou vídeo para começar a gravar.",
"VIEW_RECORDINGS": "Ver gravações"
}, },
"STREAM": { "STREAM": {
"SETTINGS": "Configurações", "SETTINGS": "Configurações",
@ -79,7 +77,7 @@
"CHAT": { "CHAT": {
"TITLE": "Chat", "TITLE": "Chat",
"YOU": "Você", "YOU": "Você",
"SUBTITLE": "As mensagens serão removidas no final da sala", "SUBTITLE": "As mensagens serão removidas no final da sessão",
"PLACEHOLDER": "Enviar uma mensagem...", "PLACEHOLDER": "Enviar uma mensagem...",
"SEND": "Enviar", "SEND": "Enviar",
"MESSAGE_SENT_NOTIFICATION": "mensagem enviada", "MESSAGE_SENT_NOTIFICATION": "mensagem enviada",
@ -91,9 +89,7 @@
"MICROPHONE": "MICROFONE", "MICROPHONE": "MICROFONE",
"SCREEN": "TELA", "SCREEN": "TELA",
"NO_STREAMS": "NENHUM", "NO_STREAMS": "NENHUM",
"YOU": "Você (eu)", "YOU": "Você (eu)"
"MUTE": "Silenciar",
"UNMUTE": "Ativar som"
}, },
"SETTINGS": { "SETTINGS": {
"TITLE": "Configurações", "TITLE": "Configurações",
@ -104,7 +100,7 @@
"CAPTIONS": "Legendas", "CAPTIONS": "Legendas",
"DISABLED_AUDIO": "Áudio desativado", "DISABLED_AUDIO": "Áudio desativado",
"DISABLED_VIDEO": "Vídeo desativado", "DISABLED_VIDEO": "Vídeo desativado",
"CAPTIONS_LANG_TEXT": "Selecione o idioma que os participantes da sala utilizarão. Os legendas aparecerão nesse idioma." "CAPTIONS_LANG_TEXT": "Selecione o idioma que os participantes da sessão utilizarão. Os legendas aparecerão nesse idioma."
}, },
"BACKGROUND": { "BACKGROUND": {
"TITLE": "Efeitos de fundo", "TITLE": "Efeitos de fundo",
@ -118,10 +114,6 @@
"SUBTITLE": "Grave a sua reunião para a posteridade", "SUBTITLE": "Grave a sua reunião para a posteridade",
"CONTENT_TITLE": "Grave a sua videochamada", "CONTENT_TITLE": "Grave a sua videochamada",
"CONTENT_SUBTITLE": "Quando a gravação tiver terminado, poderá descarregá-la com facilidade", "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", "STARTING": "Começar a gravação",
"STOPPING": "Parando a gravação", "STOPPING": "Parando a gravação",
"IN_PROGRESS": "Gravação em andamento", "IN_PROGRESS": "Gravação em andamento",
@ -132,10 +124,7 @@
"DELETE_QUESTION": "Tem certeza de que deseja excluir a gravação?", "DELETE_QUESTION": "Tem certeza de que deseja excluir a gravação?",
"DOWNLOAD": "Download", "DOWNLOAD": "Download",
"RECORDINGS": "GRAVAÇÕES", "RECORDINGS": "GRAVAÇÕES",
"NO_MODERATOR": "Só o MODERADOR pode iniciar a gravação", "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"
}, },
"STREAMING": { "STREAMING": {
"TITLE": "Streaming", "TITLE": "Streaming",
@ -150,9 +139,9 @@
} }
}, },
"ERRORS": { "ERRORS": {
"SESSION": "Houve um erro de ligação à sala", "SESSION": "Houve um erro de ligação à sessão",
"CONNECTION": "Ligação perdida", "CONNECTION": "Ligação perdida",
"RECONNECT": "A tentar restabelecer a ligação à sala...", "RECONNECT": "A tentar restabelecer a ligação à sessão...",
"DISCONNECT": "Você foi desconectado", "DISCONNECT": "Você foi desconectado",
"NETWORK_DISCONNECT": "Você foi desconectado devido a um problema de conectividade de rede", "NETWORK_DISCONNECT": "Você foi desconectado devido a um problema de conectividade de rede",
"SIGNAL_CLOSE": "A conexão com o servidor foi encerrada inesperadamente", "SIGNAL_CLOSE": "A conexão com o servidor foi encerrada inesperadamente",

View File

@ -3,7 +3,6 @@
*/ */
export interface ILogger { export interface ILogger {
d(...args: any[]): void; d(...args: any[]): void;
v(...args: any[]): void;
w(...args: any[]): void; w(...args: any[]): void;
e(...args: any[]): void; e(...args: any[]): void;
} }

View File

@ -18,7 +18,6 @@ import {
export interface ParticipantLeftEvent { export interface ParticipantLeftEvent {
roomName: string; roomName: string;
participantName: string; participantName: string;
identity: string;
reason: ParticipantLeftReason; reason: ParticipantLeftReason;
} }

View File

@ -21,7 +21,7 @@ export enum RecordingOutputMode {
export interface RecordingStatusInfo { export interface RecordingStatusInfo {
status: RecordingStatus; status: RecordingStatus;
recordingList: RecordingInfo[]; recordingList: RecordingInfo[];
startedAt?: Date; recordingElapsedTime?: Date;
error?: string; error?: string;
} }

View File

@ -9,36 +9,7 @@ export enum StorageKeys {
CAMERA_ENABLED = 'cameraEnabled', CAMERA_ENABLED = 'cameraEnabled',
LANG = 'lang', LANG = 'lang',
CAPTION_LANG = 'captionLang', CAPTION_LANG = 'captionLang',
BACKGROUND = 'virtualBg', BACKGROUND = "virtualBg"
TAB_ID = 'tabId',
ACTIVE_TABS = 'activeTabs'
} }
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-'; export const STORAGE_PREFIX = 'ovComponents-';

View File

@ -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;
};
}

View File

@ -1,102 +1,9 @@
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { BehaviorSubject, Observable } from 'rxjs'; import { BehaviorSubject, Observable } from 'rxjs';
import { distinctUntilChanged, shareReplay, map } from 'rxjs/operators';
import { RecordingInfo } from '../../models/recording.model'; import { RecordingInfo } from '../../models/recording.model';
import { ToolbarAdditionalButtonsPosition } from '../../models/toolbar.model'; import { ToolbarAdditionalButtonsPosition } from '../../models/toolbar.model';
import { ParticipantModel } from '../../models/participant.model'; import { ParticipantModel } from '../../models/participant.model';
/**
* Configuration item for the service
*/
interface ConfigItem<T> {
subject: BehaviorSubject<T>;
observable$: Observable<T>;
}
/**
* Recording activity controls configuration
*/
interface RecordingControls {
play: boolean;
download: boolean;
delete: boolean;
externalView: boolean;
}
/**
* Toolbar configuration grouped by domain
*/
interface ToolbarConfig {
camera: boolean;
microphone: boolean;
screenshare: boolean;
fullscreen: boolean;
captions: boolean;
settings: boolean;
leave: boolean;
participantsPanel: boolean;
chatPanel: boolean;
activitiesPanel: boolean;
displayRoomName: boolean;
roomName: string;
displayLogo: boolean;
backgroundEffects: boolean;
recording: boolean;
viewRecordings: boolean;
broadcasting: boolean;
brandingLogo: string;
additionalButtonsPosition: ToolbarAdditionalButtonsPosition;
}
/**
* Stream/Video configuration
*/
interface StreamConfig {
videoEnabled: boolean;
audioEnabled: boolean;
displayParticipantName: boolean;
displayAudioDetection: boolean;
videoControls: boolean;
participantItemMuteButton: boolean;
}
/**
* Recording activity configuration
*/
interface RecordingActivityConfig {
enabled: boolean;
readOnly: boolean;
showControls: RecordingControls;
startStopButton: boolean;
viewRecordingsButton: boolean;
showRecordingsList: boolean;
}
/**
* Admin dashboard configuration
*/
interface AdminConfig {
recordingsList: RecordingInfo[];
loginError: any;
loginTitle: string;
dashboardTitle: string;
}
/**
* General application configuration
*/
interface GeneralConfig {
token: string;
livekitUrl: string;
tokenError: any;
minimal: boolean;
participantName: string;
prejoin: boolean;
prejoinDisplayParticipantName: boolean;
showDisconnectionDialog: boolean;
recordingStreamBaseUrl: string;
}
/** /**
* @internal * @internal
*/ */
@ -104,483 +11,450 @@ interface GeneralConfig {
providedIn: 'root' providedIn: 'root'
}) })
export class OpenViduComponentsConfigService { export class OpenViduComponentsConfigService {
/** private token = <BehaviorSubject<string>>new BehaviorSubject('');
* Helper method to create a configuration item with BehaviorSubject and Observable token$: Observable<string>;
*/
private createConfigItem<T>(initialValue: T): ConfigItem<T> {
const subject = new BehaviorSubject<T>(initialValue);
const observable$ = subject.asObservable().pipe(distinctUntilChanged(), shareReplay(1));
return { subject, observable$ };
}
/** private livekitUrl = <BehaviorSubject<string>>new BehaviorSubject('');
* Helper method for array configurations with optimized comparison livekitUrl$: Observable<string>;
*/
private createArrayConfigItem<T>(initialValue: T[]): ConfigItem<T[]> {
const subject = new BehaviorSubject<T[]>(initialValue);
const observable$ = subject.asObservable().pipe(
distinctUntilChanged((prev, curr) => {
if (prev.length !== curr.length) return false;
return prev.every((item, index) => this.deepEqual(item, curr[index]));
}),
shareReplay(1)
);
return { subject, observable$ };
}
/** private tokenError = <BehaviorSubject<any>>new BehaviorSubject(null);
* Helper method for RecordingControls with specific comparison tokenError$: Observable<any>;
*/ private minimal = <BehaviorSubject<boolean>>new BehaviorSubject(false);
private createRecordingControlsConfigItem(initialValue: RecordingControls): ConfigItem<RecordingControls> { minimal$: Observable<boolean>;
const subject = new BehaviorSubject<RecordingControls>(initialValue); private participantName = <BehaviorSubject<string>>new BehaviorSubject('');
const observable$ = subject.asObservable().pipe( participantName$: Observable<string>;
distinctUntilChanged( private prejoin = <BehaviorSubject<boolean>>new BehaviorSubject(true);
(prev, curr) => prejoin$: Observable<boolean>;
prev.play === curr.play && private prejoinDisplayParticipantName = <BehaviorSubject<boolean>>new BehaviorSubject(true);
prev.download === curr.download && prejoinDisplayParticipantName$: Observable<boolean>;
prev.delete === curr.delete &&
prev.externalView === curr.externalView
),
shareReplay(1)
);
return { subject, observable$ };
}
/** private videoEnabled = <BehaviorSubject<boolean>>new BehaviorSubject(true);
* Helper method for ToolbarConfig with specific comparison videoEnabled$: Observable<boolean>;
*/ private audioEnabled = <BehaviorSubject<boolean>>new BehaviorSubject(true);
private createToolbarConfigItem(initialValue: ToolbarConfig): ConfigItem<ToolbarConfig> { audioEnabled$: Observable<boolean>;
const subject = new BehaviorSubject<ToolbarConfig>(initialValue);
const observable$ = subject.asObservable().pipe(
distinctUntilChanged((prev, curr) => this.compareToolbarConfig(prev, curr)),
shareReplay(1)
);
return { subject, observable$ };
}
/** private recordingStreamBaseUrl = <BehaviorSubject<string>>new BehaviorSubject('call/api/recordings');
* Helper method for StreamConfig with specific comparison recordingStreamBaseUrl$: Observable<string>;
*/
private createStreamConfigItem(initialValue: StreamConfig): ConfigItem<StreamConfig> {
const subject = new BehaviorSubject<StreamConfig>(initialValue);
const observable$ = subject.asObservable().pipe(
distinctUntilChanged((prev, curr) => this.compareStreamConfig(prev, curr)),
shareReplay(1)
);
return { subject, observable$ };
}
/** //Toolbar settings
* Helper method for RecordingActivityConfig with specific comparison private cameraButton = <BehaviorSubject<boolean>>new BehaviorSubject(true);
*/ cameraButton$: Observable<boolean>;
private createRecordingActivityConfigItem(initialValue: RecordingActivityConfig): ConfigItem<RecordingActivityConfig> {
const subject = new BehaviorSubject<RecordingActivityConfig>(initialValue);
const observable$ = subject.asObservable().pipe(
distinctUntilChanged((prev, curr) => this.compareRecordingActivityConfig(prev, curr)),
shareReplay(1)
);
return { subject, observable$ };
}
/** private microphoneButton = <BehaviorSubject<boolean>>new BehaviorSubject(true);
* Helper method for AdminConfig with specific comparison microphoneButton$: Observable<boolean>;
*/
private createAdminConfigItem(initialValue: AdminConfig): ConfigItem<AdminConfig> {
const subject = new BehaviorSubject<AdminConfig>(initialValue);
const observable$ = subject.asObservable().pipe(
distinctUntilChanged((prev, curr) => this.compareAdminConfig(prev, curr)),
shareReplay(1)
);
return { subject, observable$ };
}
/** private screenshareButton = <BehaviorSubject<boolean>>new BehaviorSubject(true);
* Helper method for GeneralConfig with specific comparison screenshareButton$: Observable<boolean>;
*/
private createGeneralConfigItem(initialValue: GeneralConfig): ConfigItem<GeneralConfig> {
const subject = new BehaviorSubject<GeneralConfig>(initialValue);
const observable$ = subject.asObservable().pipe(
distinctUntilChanged((prev, curr) => this.compareGeneralConfig(prev, curr)),
shareReplay(1)
);
return { subject, observable$ };
}
/** private fullscreenButton = <BehaviorSubject<boolean>>new BehaviorSubject(true);
* Optimized deep equality check fullscreenButton$: Observable<boolean>;
*/
private deepEqual(a: any, b: any): boolean {
if (a === b) return true;
if (a == null || b == null) return a === b;
if (typeof a !== typeof b) return false;
if (typeof a !== 'object') return a === b;
const keysA = Object.keys(a); private captionsButton = <BehaviorSubject<boolean>>new BehaviorSubject(true);
const keysB = Object.keys(b); captionsButton$: Observable<boolean>;
if (keysA.length !== keysB.length) return false;
return keysA.every((key) => this.deepEqual(a[key], b[key])); private toolbarSettingsButton = <BehaviorSubject<boolean>>new BehaviorSubject(true);
} toolbarSettingsButton$: Observable<boolean>;
/** private leaveButton = <BehaviorSubject<boolean>>new BehaviorSubject(true);
* Compare ToolbarConfig efficiently leaveButton$: Observable<boolean>;
*/
private compareToolbarConfig(prev: ToolbarConfig, curr: ToolbarConfig): boolean {
return (
prev.camera === curr.camera &&
prev.microphone === curr.microphone &&
prev.screenshare === curr.screenshare &&
prev.fullscreen === curr.fullscreen &&
prev.captions === curr.captions &&
prev.settings === curr.settings &&
prev.leave === curr.leave &&
prev.participantsPanel === curr.participantsPanel &&
prev.chatPanel === curr.chatPanel &&
prev.activitiesPanel === curr.activitiesPanel &&
prev.displayRoomName === curr.displayRoomName &&
prev.roomName === curr.roomName &&
prev.displayLogo === curr.displayLogo &&
prev.backgroundEffects === curr.backgroundEffects &&
prev.recording === curr.recording &&
prev.viewRecordings === curr.viewRecordings &&
prev.broadcasting === curr.broadcasting &&
prev.brandingLogo === curr.brandingLogo &&
prev.additionalButtonsPosition === curr.additionalButtonsPosition
);
}
/** private participantsPanelButton = <BehaviorSubject<boolean>>new BehaviorSubject(true);
* Compare StreamConfig efficiently participantsPanelButton$: Observable<boolean>;
*/
private compareStreamConfig(prev: StreamConfig, curr: StreamConfig): boolean {
return (
prev.videoEnabled === curr.videoEnabled &&
prev.audioEnabled === curr.audioEnabled &&
prev.displayParticipantName === curr.displayParticipantName &&
prev.displayAudioDetection === curr.displayAudioDetection &&
prev.videoControls === curr.videoControls &&
prev.participantItemMuteButton === curr.participantItemMuteButton
);
}
/** private chatPanelButton = <BehaviorSubject<boolean>>new BehaviorSubject(true);
* Compare RecordingActivityConfig efficiently chatPanelButton$: Observable<boolean>;
*/
private compareRecordingActivityConfig(prev: RecordingActivityConfig, curr: RecordingActivityConfig): boolean {
return (
prev.enabled === curr.enabled &&
prev.readOnly === curr.readOnly &&
prev.startStopButton === curr.startStopButton &&
prev.viewRecordingsButton === curr.viewRecordingsButton &&
prev.showRecordingsList === curr.showRecordingsList &&
prev.showControls.play === curr.showControls.play &&
prev.showControls.download === curr.showControls.download &&
prev.showControls.delete === curr.showControls.delete &&
prev.showControls.externalView === curr.showControls.externalView
);
}
/** private activitiesPanelButton = <BehaviorSubject<boolean>>new BehaviorSubject(true);
* Compare AdminConfig efficiently activitiesPanelButton$: Observable<boolean>;
*/
private compareAdminConfig(prev: AdminConfig, curr: AdminConfig): boolean {
return (
prev.loginError === curr.loginError &&
prev.loginTitle === curr.loginTitle &&
prev.dashboardTitle === curr.dashboardTitle &&
prev.recordingsList.length === curr.recordingsList.length &&
prev.recordingsList.every((item, index) => this.deepEqual(item, curr.recordingsList[index]))
);
}
/** private displayRoomName = <BehaviorSubject<boolean>>new BehaviorSubject(true);
* Compare GeneralConfig efficiently displayRoomName$: Observable<boolean>;
*/
private compareGeneralConfig(prev: GeneralConfig, curr: GeneralConfig): boolean {
return (
prev.token === curr.token &&
prev.livekitUrl === curr.livekitUrl &&
prev.tokenError === curr.tokenError &&
prev.minimal === curr.minimal &&
prev.participantName === curr.participantName &&
prev.prejoin === curr.prejoin &&
prev.prejoinDisplayParticipantName === curr.prejoinDisplayParticipantName &&
prev.showDisconnectionDialog === curr.showDisconnectionDialog &&
prev.recordingStreamBaseUrl === curr.recordingStreamBaseUrl
);
}
// Grouped configuration items by domain private brandingLogo = <BehaviorSubject<string>>new BehaviorSubject('');
private generalConfig = this.createGeneralConfigItem({ brandingLogo$: Observable<string>;
token: '',
livekitUrl: '',
tokenError: null,
minimal: false,
participantName: '',
prejoin: true,
prejoinDisplayParticipantName: true,
showDisconnectionDialog: true,
recordingStreamBaseUrl: 'call/api/recordings'
});
private toolbarConfig = this.createToolbarConfigItem({ private displayLogo = <BehaviorSubject<boolean>>new BehaviorSubject(true);
camera: true, displayLogo$: Observable<boolean>;
microphone: true,
screenshare: true,
fullscreen: true,
captions: true,
settings: true,
leave: true,
participantsPanel: true,
chatPanel: true,
activitiesPanel: true,
displayRoomName: true,
roomName: '',
displayLogo: true,
backgroundEffects: true,
recording: true,
viewRecordings: false,
broadcasting: true,
brandingLogo: '',
additionalButtonsPosition: ToolbarAdditionalButtonsPosition.AFTER_MENU
});
private streamConfig = this.createStreamConfigItem({ private toolbarAdditionalButtonsPosition = <BehaviorSubject<ToolbarAdditionalButtonsPosition>>(
videoEnabled: true, new BehaviorSubject(ToolbarAdditionalButtonsPosition.AFTER_MENU)
audioEnabled: true,
displayParticipantName: true,
displayAudioDetection: true,
videoControls: true,
participantItemMuteButton: true
});
private recordingActivityConfig = this.createRecordingActivityConfigItem({
enabled: true,
readOnly: false,
showControls: {
play: true,
download: true,
delete: true,
externalView: false
},
startStopButton: true,
viewRecordingsButton: false,
showRecordingsList: true
});
private adminConfig = this.createAdminConfigItem({
recordingsList: [],
loginError: null,
loginTitle: '',
dashboardTitle: ''
});
// Individual configs that don't fit into groups
private broadcastingActivityConfig = this.createConfigItem(true);
private layoutRemoteParticipantsConfig = this.createConfigItem<ParticipantModel[] | undefined>(undefined);
// General observables
token$: Observable<string> = this.generalConfig.observable$.pipe(map((config) => config.token));
livekitUrl$: Observable<string> = this.generalConfig.observable$.pipe(map((config) => config.livekitUrl));
tokenError$: Observable<any> = this.generalConfig.observable$.pipe(map((config) => config.tokenError));
minimal$: Observable<boolean> = this.generalConfig.observable$.pipe(map((config) => config.minimal));
participantName$: Observable<string> = this.generalConfig.observable$.pipe(map((config) => config.participantName));
prejoin$: Observable<boolean> = this.generalConfig.observable$.pipe(map((config) => config.prejoin));
prejoinDisplayParticipantName$: Observable<boolean> = this.generalConfig.observable$.pipe(
map((config) => config.prejoinDisplayParticipantName)
); );
showDisconnectionDialog$: Observable<boolean> = this.generalConfig.observable$.pipe(map((config) => config.showDisconnectionDialog)); toolbarAdditionalButtonsPosition$: Observable<ToolbarAdditionalButtonsPosition>;
recordingStreamBaseUrl$: Observable<string> = this.generalConfig.observable$.pipe(map((config) => config.recordingStreamBaseUrl));
// Stream observables private displayParticipantName = <BehaviorSubject<boolean>>new BehaviorSubject(true);
videoEnabled$: Observable<boolean> = this.streamConfig.observable$.pipe(map((config) => config.videoEnabled)); displayParticipantName$: Observable<boolean>;
audioEnabled$: Observable<boolean> = this.streamConfig.observable$.pipe(map((config) => config.audioEnabled)); private displayAudioDetection = <BehaviorSubject<boolean>>new BehaviorSubject(true);
displayParticipantName$: Observable<boolean> = this.streamConfig.observable$.pipe(map((config) => config.displayParticipantName)); displayAudioDetection$: Observable<boolean>;
displayAudioDetection$: Observable<boolean> = this.streamConfig.observable$.pipe(map((config) => config.displayAudioDetection)); private streamVideoControls = <BehaviorSubject<boolean>>new BehaviorSubject(true);
streamVideoControls$: Observable<boolean> = this.streamConfig.observable$.pipe(map((config) => config.videoControls)); streamVideoControls$: Observable<boolean>;
participantItemMuteButton$: Observable<boolean> = this.streamConfig.observable$.pipe(map((config) => config.participantItemMuteButton)); private participantItemMuteButton = <BehaviorSubject<boolean>>new BehaviorSubject(true);
participantItemMuteButton$: Observable<boolean>;
private backgroundEffectsButton = <BehaviorSubject<boolean>>new BehaviorSubject(true);
backgroundEffectsButton$: Observable<boolean>;
private recordingButton = <BehaviorSubject<boolean>>new BehaviorSubject(true);
recordingButton$: Observable<boolean>;
private broadcastingButton = <BehaviorSubject<boolean>>new BehaviorSubject(true);
broadcastingButton$: Observable<boolean>;
private recordingActivity = <BehaviorSubject<boolean>>new BehaviorSubject(true);
recordingActivity$: Observable<boolean>;
private broadcastingActivity = <BehaviorSubject<boolean>>new BehaviorSubject(true);
broadcastingActivity$: Observable<boolean>;
// Toolbar observables // Admin
cameraButton$: Observable<boolean> = this.toolbarConfig.observable$.pipe(map((config) => config.camera)); private adminRecordingsList: BehaviorSubject<RecordingInfo[]> = new BehaviorSubject(<RecordingInfo[]>[]);
microphoneButton$: Observable<boolean> = this.toolbarConfig.observable$.pipe(map((config) => config.microphone)); adminRecordingsList$: Observable<RecordingInfo[]>;
screenshareButton$: Observable<boolean> = this.toolbarConfig.observable$.pipe(map((config) => config.screenshare)); private adminLoginError = <BehaviorSubject<any>>new BehaviorSubject(null);
fullscreenButton$: Observable<boolean> = this.toolbarConfig.observable$.pipe(map((config) => config.fullscreen)); private adminLoginTitle = <BehaviorSubject<string>>new BehaviorSubject('');
captionsButton$: Observable<boolean> = this.toolbarConfig.observable$.pipe(map((config) => config.captions)); private adminDashboardTitle = <BehaviorSubject<string>>new BehaviorSubject('');
toolbarSettingsButton$: Observable<boolean> = this.toolbarConfig.observable$.pipe(map((config) => config.settings)); adminLoginTitle$: Observable<string>;
leaveButton$: Observable<boolean> = this.toolbarConfig.observable$.pipe(map((config) => config.leave)); adminDashboardTitle$: Observable<string>;
participantsPanelButton$: Observable<boolean> = this.toolbarConfig.observable$.pipe(map((config) => config.participantsPanel)); adminLoginError$: Observable<any>;
chatPanelButton$: Observable<boolean> = this.toolbarConfig.observable$.pipe(map((config) => config.chatPanel));
activitiesPanelButton$: Observable<boolean> = this.toolbarConfig.observable$.pipe(map((config) => config.activitiesPanel));
displayRoomName$: Observable<boolean> = this.toolbarConfig.observable$.pipe(map((config) => config.displayRoomName));
roomName$: Observable<string> = this.toolbarConfig.observable$.pipe(map((config) => config.roomName));
brandingLogo$: Observable<string> = this.toolbarConfig.observable$.pipe(map((config) => config.brandingLogo));
displayLogo$: Observable<boolean> = this.toolbarConfig.observable$.pipe(map((config) => config.displayLogo));
toolbarAdditionalButtonsPosition$: Observable<ToolbarAdditionalButtonsPosition> = this.toolbarConfig.observable$.pipe(
map((config) => config.additionalButtonsPosition)
);
backgroundEffectsButton$: Observable<boolean> = this.toolbarConfig.observable$.pipe(map((config) => config.backgroundEffects));
recordingButton$: Observable<boolean> = this.toolbarConfig.observable$.pipe(map((config) => config.recording));
toolbarViewRecordingsButton$: Observable<boolean> = this.toolbarConfig.observable$.pipe(map((config) => config.viewRecordings));
broadcastingButton$: Observable<boolean> = this.toolbarConfig.observable$.pipe(map((config) => config.broadcasting));
// Recording activity observables // Internals
recordingActivity$: Observable<boolean> = this.recordingActivityConfig.observable$.pipe(map((config) => config.enabled)); private layoutRemoteParticipants: BehaviorSubject<ParticipantModel[] | undefined> = new BehaviorSubject(<any>undefined);
recordingActivityReadOnly$: Observable<boolean> = this.recordingActivityConfig.observable$.pipe(map((config) => config.readOnly)); layoutRemoteParticipants$: Observable<ParticipantModel[] | undefined>;
recordingActivityShowControls$: Observable<RecordingControls> = this.recordingActivityConfig.observable$.pipe(
map((config) => config.showControls)
);
recordingActivityStartStopRecordingButton$: Observable<boolean> = this.recordingActivityConfig.observable$.pipe(
map((config) => config.startStopButton)
);
recordingActivityViewRecordingsButton$: Observable<boolean> = this.recordingActivityConfig.observable$.pipe(
map((config) => config.viewRecordingsButton)
);
recordingActivityShowRecordingsList$: Observable<boolean> = this.recordingActivityConfig.observable$.pipe(
map((config) => config.showRecordingsList)
);
// Admin observables
adminRecordingsList$: Observable<RecordingInfo[]> = this.adminConfig.observable$.pipe(map((config) => config.recordingsList));
adminLoginError$: Observable<any> = this.adminConfig.observable$.pipe(map((config) => config.loginError));
adminLoginTitle$: Observable<string> = this.adminConfig.observable$.pipe(map((config) => config.loginTitle));
adminDashboardTitle$: Observable<string> = this.adminConfig.observable$.pipe(map((config) => config.dashboardTitle));
// Individual observables that don't fit into groups
broadcastingActivity$: Observable<boolean> = this.broadcastingActivityConfig.observable$;
layoutRemoteParticipants$: Observable<ParticipantModel[] | undefined> = this.layoutRemoteParticipantsConfig.observable$;
constructor() { constructor() {
// Constructor no longer needed - all observables are initialized directly this.token$ = this.token.asObservable();
this.livekitUrl$ = this.livekitUrl.asObservable();
this.tokenError$ = this.tokenError.asObservable();
this.minimal$ = this.minimal.asObservable();
this.participantName$ = this.participantName.asObservable();
this.prejoin$ = this.prejoin.asObservable();
this.prejoinDisplayParticipantName$ = this.prejoinDisplayParticipantName.asObservable();
this.videoEnabled$ = this.videoEnabled.asObservable();
this.audioEnabled$ = this.audioEnabled.asObservable();
this.recordingStreamBaseUrl$ = this.recordingStreamBaseUrl.asObservable();
//Toolbar observables
this.cameraButton$ = this.cameraButton.asObservable();
this.microphoneButton$ = this.microphoneButton.asObservable();
this.screenshareButton$ = this.screenshareButton.asObservable();
this.fullscreenButton$ = this.fullscreenButton.asObservable();
this.backgroundEffectsButton$ = this.backgroundEffectsButton.asObservable();
this.leaveButton$ = this.leaveButton.asObservable();
this.participantsPanelButton$ = this.participantsPanelButton.asObservable();
this.chatPanelButton$ = this.chatPanelButton.asObservable();
this.activitiesPanelButton$ = this.activitiesPanelButton.asObservable();
this.displayRoomName$ = this.displayRoomName.asObservable();
this.displayLogo$ = this.displayLogo.asObservable();
this.brandingLogo$ = this.brandingLogo.asObservable();
this.recordingButton$ = this.recordingButton.asObservable();
this.broadcastingButton$ = this.broadcastingButton.asObservable();
this.toolbarSettingsButton$ = this.toolbarSettingsButton.asObservable();
this.captionsButton$ = this.captionsButton.asObservable();
this.toolbarAdditionalButtonsPosition$ = this.toolbarAdditionalButtonsPosition.asObservable();
//Stream observables
this.displayParticipantName$ = this.displayParticipantName.asObservable();
this.displayAudioDetection$ = this.displayAudioDetection.asObservable();
this.streamVideoControls$ = this.streamVideoControls.asObservable();
// Participant item observables
this.participantItemMuteButton$ = this.participantItemMuteButton.asObservable();
// Recording activity observables
this.recordingActivity$ = this.recordingActivity.asObservable();
// Broadcasting activity
this.broadcastingActivity$ = this.broadcastingActivity.asObservable();
// Admin dashboard
this.adminRecordingsList$ = this.adminRecordingsList.asObservable();
this.adminLoginError$ = this.adminLoginError.asObservable();
this.adminLoginTitle$ = this.adminLoginTitle.asObservable();
this.adminDashboardTitle$ = this.adminDashboardTitle.asObservable();
// Internals
this.layoutRemoteParticipants$ = this.layoutRemoteParticipants.asObservable();
} }
// ============================================ setToken(token: string) {
// BATCH UPDATE METHODS this.token.next(token);
// ============================================
/**
* Update multiple general configuration properties at once
*/
updateGeneralConfig(partialConfig: Partial<GeneralConfig>): void {
const current = this.generalConfig.subject.getValue();
this.generalConfig.subject.next({ ...current, ...partialConfig });
} }
/** setLivekitUrl(livekitUrl: string) {
* Update multiple toolbar configuration properties at once this.livekitUrl.next(livekitUrl);
*/
updateToolbarConfig(partialConfig: Partial<ToolbarConfig>): void {
const current = this.toolbarConfig.subject.getValue();
this.toolbarConfig.subject.next({ ...current, ...partialConfig });
} }
/**
* Update multiple stream configuration properties at once
*/
updateStreamConfig(partialConfig: Partial<StreamConfig>): void {
const current = this.streamConfig.subject.getValue();
this.streamConfig.subject.next({ ...current, ...partialConfig });
}
/**
* Update multiple recording activity configuration properties at once
*/
updateRecordingActivityConfig(partialConfig: Partial<RecordingActivityConfig>): void {
const current = this.recordingActivityConfig.subject.getValue();
this.recordingActivityConfig.subject.next({ ...current, ...partialConfig });
}
/**
* Update multiple admin configuration properties at once
*/
updateAdminConfig(partialConfig: Partial<AdminConfig>): void {
const current = this.adminConfig.subject.getValue();
this.adminConfig.subject.next({ ...current, ...partialConfig });
}
/**
* Update recording controls specifically with batch support
*/
updateRecordingControls(partialControls: Partial<RecordingControls>): void {
const current = this.recordingActivityConfig.subject.getValue();
const updatedControls = { ...current.showControls, ...partialControls };
this.updateRecordingActivityConfig({ showControls: updatedControls });
}
// ============================================
// DIRECT ACCESS METHODS (for internal use)
// ============================================
/**
* @internal
* Get current participant name directly
*/
getCurrentParticipantName(): string {
return this.generalConfig.subject.getValue().participantName;
}
// ============================================
// INDIVIDUAL GETTER/SETTER METHODS
// ============================================
// General configuration methods
getLivekitUrl(): string { getLivekitUrl(): string {
return this.generalConfig.subject.getValue().livekitUrl; return this.livekitUrl.getValue();
} }
showPrejoin(): boolean { setTokenError(error: any) {
return this.generalConfig.subject.getValue().prejoin; this.tokenError.next(error);
} }
getShowDisconnectionDialog(): boolean { setMinimal(minimal: boolean) {
return this.generalConfig.subject.getValue().showDisconnectionDialog; this.minimal.next(minimal);
}
isMinimal(): boolean {
return this.minimal.getValue();
}
setParticipantName(participantName: string) {
this.participantName.next(participantName);
}
setPrejoin(prejoin: boolean) {
this.prejoin.next(prejoin);
}
setPrejoinDisplayParticipantName(prejoinDisplayParticipantName: boolean) {
this.prejoinDisplayParticipantName.next(prejoinDisplayParticipantName);
}
isPrejoin(): boolean {
return this.prejoin.getValue();
}
setVideoEnabled(videoEnabled: boolean) {
this.videoEnabled.next(videoEnabled);
}
isVideoEnabled(): boolean {
return this.videoEnabled.getValue();
}
setAudioEnabled(audioEnabled: boolean) {
this.audioEnabled.next(audioEnabled);
}
isAudioEnabled(): boolean {
return this.audioEnabled.getValue();
}
setRecordingStreamBaseUrl(recordingStreamBaseUrl: string) {
this.recordingStreamBaseUrl.next(recordingStreamBaseUrl);
} }
getRecordingStreamBaseUrl(): string { getRecordingStreamBaseUrl(): string {
let baseUrl = this.generalConfig.subject.getValue().recordingStreamBaseUrl; let baseUrl = this.recordingStreamBaseUrl.getValue();
// Add trailing slash if not present // Add trailing slash if not present
baseUrl += baseUrl.endsWith('/') ? '' : '/'; baseUrl += baseUrl.endsWith('/') ? '' : '/';
return baseUrl; return baseUrl;
} }
// Stream configuration methods //Toolbar settings
isVideoEnabled(): boolean { setCameraButton(cameraButton: boolean) {
return this.streamConfig.subject.getValue().videoEnabled; this.cameraButton.next(cameraButton);
} }
isAudioEnabled(): boolean { showCameraButton(): boolean {
return this.streamConfig.subject.getValue().audioEnabled; return this.cameraButton.getValue();
} }
// Toolbar configuration methods setMicrophoneButton(microphoneButton: boolean) {
this.microphoneButton.next(microphoneButton);
}
getRoomName(): string { showMicrophoneButton(): boolean {
return this.toolbarConfig.subject.getValue().roomName; return this.microphoneButton.getValue();
}
setScreenshareButton(screenshareButton: boolean) {
this.screenshareButton.next(screenshareButton);
}
showScreenshareButton(): boolean {
return this.screenshareButton.getValue();
}
setFullscreenButton(fullscreenButton: boolean) {
this.fullscreenButton.next(fullscreenButton);
}
showFullscreenButton(): boolean {
return this.fullscreenButton.getValue();
}
setCaptionsButton(captionsButton: boolean) {
this.captionsButton.next(captionsButton);
}
showCaptionsButton(): boolean {
return this.captionsButton.getValue();
}
setToolbarSettingsButton(toolbarSettingsButton: boolean) {
this.toolbarSettingsButton.next(toolbarSettingsButton);
}
showToolbarSettingsButton(): boolean {
return this.toolbarSettingsButton.getValue();
}
setLeaveButton(leaveButton: boolean) {
this.leaveButton.next(leaveButton);
}
showLeaveButton(): boolean {
return this.leaveButton.getValue();
}
setParticipantsPanelButton(participantsPanelButton: boolean) {
this.participantsPanelButton.next(participantsPanelButton);
}
showParticipantsPanelButton(): boolean {
return this.participantsPanelButton.getValue();
}
setChatPanelButton(chatPanelButton: boolean) {
this.chatPanelButton.next(chatPanelButton);
}
showChatPanelButton(): boolean {
return this.chatPanelButton.getValue();
}
setActivitiesPanelButton(activitiesPanelButton: boolean) {
this.activitiesPanelButton.next(activitiesPanelButton);
}
showActivitiesPanelButton(): boolean {
return this.activitiesPanelButton.getValue();
}
setDisplayRoomName(displayRoomName: boolean) {
this.displayRoomName.next(displayRoomName);
}
setBrandingLogo(brandingLogo: string) {
this.brandingLogo.next(brandingLogo);
}
showRoomName(): boolean {
return this.displayRoomName.getValue();
}
setDisplayLogo(displayLogo: boolean) {
this.displayLogo.next(displayLogo);
}
showLogo(): boolean {
return this.displayLogo.getValue();
}
getToolbarAdditionalButtonsPosition(): ToolbarAdditionalButtonsPosition {
return this.toolbarAdditionalButtonsPosition.getValue();
}
setToolbarAdditionalButtonsPosition(toolbarAdditionalButtonsPosition: ToolbarAdditionalButtonsPosition) {
this.toolbarAdditionalButtonsPosition.next(toolbarAdditionalButtonsPosition);
}
setRecordingButton(recordingButton: boolean) {
this.recordingButton.next(recordingButton);
}
showRecordingButton(): boolean {
return this.recordingButton.getValue();
} }
setBroadcastingButton(broadcastingButton: boolean) { setBroadcastingButton(broadcastingButton: boolean) {
this.updateToolbarConfig({ broadcasting: broadcastingButton }); this.broadcastingButton.next(broadcastingButton);
}
showBroadcastingButton(): boolean {
return this.broadcastingButton.getValue();
}
setRecordingActivity(recordingActivity: boolean) {
this.recordingActivity.next(recordingActivity);
}
showRecordingActivity(): boolean {
return this.recordingActivity.getValue();
}
setBroadcastingActivity(broadcastingActivity: boolean) {
this.broadcastingActivity.next(broadcastingActivity);
}
showBroadcastingActivity(): boolean {
return this.broadcastingActivity.getValue();
}
//Stream settings
setDisplayParticipantName(displayParticipantName: boolean) {
this.displayParticipantName.next(displayParticipantName);
}
isParticipantNameDisplayed(): boolean {
return this.displayParticipantName.getValue();
}
setDisplayAudioDetection(displayAudioDetection: boolean) {
this.displayAudioDetection.next(displayAudioDetection);
}
isAudioDetectionDisplayed(): boolean {
return this.displayAudioDetection.getValue();
}
setStreamVideoControls(streamVideoControls: boolean) {
this.streamVideoControls.next(streamVideoControls);
}
showStreamVideoControls(): boolean {
return this.streamVideoControls.getValue();
}
setParticipantItemMuteButton(participantItemMuteButton: boolean) {
this.participantItemMuteButton.next(participantItemMuteButton);
}
showParticipantItemMuteButton(): boolean {
return this.participantItemMuteButton.getValue();
}
setBackgroundEffectsButton(backgroundEffectsButton: boolean) {
this.backgroundEffectsButton.next(backgroundEffectsButton);
} }
showBackgroundEffectsButton(): boolean { showBackgroundEffectsButton(): boolean {
return this.toolbarConfig.subject.getValue().backgroundEffects; return this.backgroundEffectsButton.getValue();
} }
// Activity methods (these remain individual as they don't fit cleanly into toolbar config) // Admin dashboard
setBroadcastingActivity(broadcastingActivity: boolean) { setAdminRecordingsList(adminRecordingsList: RecordingInfo[]) {
this.broadcastingActivityConfig.subject.next(broadcastingActivity); this.adminRecordingsList.next(adminRecordingsList);
}
getAdminRecordingsList(): RecordingInfo[] {
return this.adminRecordingsList.getValue();
}
setAdminLoginError(adminLoginError: any) {
this.adminLoginError.next(adminLoginError);
}
getAdminLoginError(): any {
return this.adminLoginError.getValue();
}
getAdminLoginTitle(): string {
return this.adminLoginTitle.getValue();
}
setAdminLoginTitle(title: string) {
this.adminLoginTitle.next(title);
}
getAdminDashboardTitle(): string {
return this.adminDashboardTitle.getValue();
}
setAdminDashboardTitle(title: string) {
this.adminDashboardTitle.next(title);
}
isRecordingEnabled(): boolean {
return this.recordingButton.getValue() && this.recordingActivity.getValue();
}
isBroadcastingEnabled(): boolean {
return this.broadcastingButton.getValue() && this.broadcastingActivity.getValue();
} }
// Internals // Internals
setLayoutRemoteParticipants(participants: ParticipantModel[] | undefined) { setLayoutRemoteParticipants(participants: ParticipantModel[] | undefined) {
this.layoutRemoteParticipantsConfig.subject.next(participants); this.layoutRemoteParticipants.next(participants);
}
// Recording Activity Configuration methods
showRecordingActivityRecordingsList(): boolean {
return this.recordingActivityConfig.subject.getValue().showRecordingsList;
} }
} }

View File

@ -1,5 +1,6 @@
import { Injectable } from '@angular/core'; 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'; import { GlobalConfigService } from '../config/global-config.service';
/** /**
@ -9,87 +10,42 @@ import { GlobalConfigService } from '../config/global-config.service';
providedIn: 'root' providedIn: 'root'
}) })
export class LoggerService implements ILogService { export class LoggerService implements ILogService {
private log: Console; public log;
private LOG_FNS: Function[] = []; public LOG_FNS = [];
private MSG_PREFIXES: string[][] = [ public MSG_PREFIXES = [
['[', '] DEBUG: '], ['[', ']'],
['[', '] VERBOSE: '],
['[', '] WARN: '], ['[', '] WARN: '],
['[', '] ERROR: '] ['[', '] ERROR: ']
]; ];
private loggerCache: Map<string, ILogger> = new Map();
constructor(private globalService: GlobalConfigService) { constructor(private globalService: GlobalConfigService) {
this.initializeLogger();
} }
private initializeLogger(): void { private getLoggerFns(prefix: string) {
this.log = window.console; this.log = window.console;
this.LOG_FNS = [ this.LOG_FNS = [this.log.log, this.log.warn, this.log.error];
this.log.log.bind(this.log), const loggerFns = this.LOG_FNS.map((logTemplFn, i) => {
this.log.debug.bind(this.log), return logTemplFn.bind(this.log, this.MSG_PREFIXES[i][0] + prefix + this.MSG_PREFIXES[i][1]);
this.log.warn.bind(this.log), });
this.log.error.bind(this.log) return loggerFns;
];
} }
private createLoggerFunctions( public get(prefix: string) {
prefix: string
): [(...args: any[]) => void, (...args: any[]) => void, (...args: any[]) => void, (...args: any[]) => void] {
const prodMode = this.globalService.isProduction(); const prodMode = this.globalService.isProduction();
const loggerService = this;
return {
d: function(...args: any[]) {
if (!prodMode) {
loggerService.getLoggerFns(prefix)[0].apply(this.log, arguments);
}
},
w: function(...args: any[]) {
loggerService.getLoggerFns(prefix)[1].apply(this.log, arguments);
const debugFn = (...args: any[]): void => { },
if (!prodMode) { e: function(...args: any[]) {
// Only log debug messages in non-production mode loggerService.getLoggerFns(prefix)[2].apply(this.log, arguments);
this.LOG_FNS[0](this.MSG_PREFIXES[0][0] + prefix + this.MSG_PREFIXES[0][1], ...args);
} }
}; };
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();
} }
} }

View File

@ -64,12 +64,6 @@ export class OpenViduService {
* @internal * @internal
*/ */
initRoom(): void { 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 videoDeviceId = this.deviceService.getCameraSelected()?.device ?? undefined;
const audioDeviceId = this.deviceService.getMicrophoneSelected()?.device ?? undefined; const audioDeviceId = this.deviceService.getMicrophoneSelected()?.device ?? undefined;
const roomOptions: RoomOptions = { const roomOptions: RoomOptions = {
@ -94,7 +88,6 @@ export class OpenViduService {
disconnectOnPageLeave: true disconnectOnPageLeave: true
}; };
this.room = new Room(roomOptions); this.room = new Room(roomOptions);
this.log.d('Room initialized successfully');
} }
/** /**
@ -137,20 +130,12 @@ export class OpenViduService {
*/ */
getRoom(): Room { getRoom(): Room {
if (!this.room) { if (!this.room) {
this.log.e('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. Make sure token is set before accessing the room.'); throw new Error('Room is not initialized');
} }
return this.room; 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 * Returns the room name
*/ */
@ -166,14 +151,6 @@ export class OpenViduService {
return this.room?.state === ConnectionState.Connected; 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 * @internal
*/ */
@ -186,13 +163,6 @@ export class OpenViduService {
this.log.e('LiveKit URL is not defined. Please, check the livekitUrl parameter of the VideoConferenceComponent'); this.log.e('LiveKit URL is not defined. Please, check the livekitUrl parameter of the VideoConferenceComponent');
throw new Error('Livekit URL is not defined'); throw new Error('Livekit URL is not defined');
} }
// 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); // return this.room.prepareConnection(this.livekitUrl, this.livekitToken);
} }

View File

@ -20,7 +20,7 @@ export class RecordingService {
private recordingStatus = <BehaviorSubject<RecordingStatusInfo>>new BehaviorSubject({ private recordingStatus = <BehaviorSubject<RecordingStatusInfo>>new BehaviorSubject({
status: RecordingStatus.STOPPED, status: RecordingStatus.STOPPED,
recordingList: [] as RecordingInfo[], recordingList: [] as RecordingInfo[],
startedAt: new Date(0, 0, 0, 0, 0, 0) recordingElapsedTime: new Date(0, 0, 0, 0, 0, 0)
}); });
private log: ILogger; private log: ILogger;
@ -41,9 +41,13 @@ export class RecordingService {
* @internal * @internal
*/ */
setRecordingStarted(recordingInfo?: RecordingInfo, startTimestamp?: number) { setRecordingStarted(recordingInfo?: RecordingInfo, startTimestamp?: number) {
// Determine the actual start timestamp of the recording // Register the start timestamp of the recording
// Priority: startTimestamp parameter > recordingInfo.startedAt > current time // to calculate the elapsed time
this.recordingStartTimestamp = startTimestamp || recordingInfo?.startedAt || Date.now(); debugger;
this.recordingStartTimestamp = recordingInfo?.startedAt || Date.now();
// Initialize the recording elapsed time
this.startRecordingTimer();
const { recordingList } = this.recordingStatus.getValue(); const { recordingList } = this.recordingStatus.getValue();
let updatedRecordingList = [...recordingList]; let updatedRecordingList = [...recordingList];
@ -58,22 +62,17 @@ export class RecordingService {
updatedRecordingList = [recordingInfo, ...updatedRecordingList]; updatedRecordingList = [recordingInfo, ...updatedRecordingList];
} }
} }
// Calculate the elapsed time based on the actual start timestamp
const recordingElapsedTime = new Date(0, 0, 0, 0, 0, 0); const recordingElapsedTime = new Date(0, 0, 0, 0, 0, 0);
if (this.recordingStartTimestamp) { if (startTimestamp) {
const elapsedSeconds = Math.floor((Date.now() - this.recordingStartTimestamp) / 1000); const elapsedSeconds = Math.floor((Date.now() - startTimestamp) / 1000);
recordingElapsedTime.setSeconds(Math.max(0, elapsedSeconds)); // Ensure non-negative recordingElapsedTime.setSeconds(elapsedSeconds);
} }
this.updateStatus({ this.updateStatus({
status: RecordingStatus.STARTED, status: RecordingStatus.STARTED,
recordingList: updatedRecordingList, recordingList: updatedRecordingList,
startedAt: recordingElapsedTime recordingElapsedTime
}); });
// Start the timer after updating the initial state
this.startRecordingTimer();
} }
/** /**
@ -98,7 +97,7 @@ export class RecordingService {
this.updateStatus({ this.updateStatus({
status: RecordingStatus.STOPPED, status: RecordingStatus.STOPPED,
recordingList: updatedRecordingList, recordingList: updatedRecordingList,
startedAt: new Date(0, 0, 0, 0, 0, 0) recordingElapsedTime: new Date(0, 0, 0, 0, 0, 0)
}); });
this.recordingStartTimestamp = null; this.recordingStartTimestamp = null;
@ -109,11 +108,11 @@ export class RecordingService {
* The `started` stastus will be updated automatically when the recording is actually started. * The `started` stastus will be updated automatically when the recording is actually started.
*/ */
setRecordingStarting() { setRecordingStarting() {
const { recordingList, startedAt } = this.recordingStatus.getValue(); const { recordingList, recordingElapsedTime } = this.recordingStatus.getValue();
this.updateStatus({ this.updateStatus({
status: RecordingStatus.STARTING, status: RecordingStatus.STARTING,
recordingList, recordingList,
startedAt recordingElapsedTime
}); });
} }
@ -123,11 +122,11 @@ export class RecordingService {
*/ */
setRecordingFailed(error: string) { setRecordingFailed(error: string) {
this.stopRecordingTimer(); this.stopRecordingTimer();
const { startedAt, recordingList } = this.recordingStatus.getValue(); const { recordingElapsedTime, recordingList } = this.recordingStatus.getValue();
const statusInfo: RecordingStatusInfo = { const statusInfo: RecordingStatusInfo = {
status: RecordingStatus.FAILED, status: RecordingStatus.FAILED,
recordingList, recordingList,
startedAt, recordingElapsedTime,
error error
}; };
this.updateStatus(statusInfo); this.updateStatus(statusInfo);
@ -138,12 +137,12 @@ export class RecordingService {
* The `stopped` stastus will be updated automatically when the recording is actually stopped. * The `stopped` stastus will be updated automatically when the recording is actually stopped.
*/ */
setRecordingStopping() { setRecordingStopping() {
const { startedAt, recordingList } = this.recordingStatus.getValue(); const { recordingElapsedTime, recordingList } = this.recordingStatus.getValue();
this.updateStatus({ this.updateStatus({
status: RecordingStatus.STOPPING, status: RecordingStatus.STOPPING,
recordingList, recordingList,
startedAt recordingElapsedTime
}); });
} }
@ -201,14 +200,14 @@ export class RecordingService {
* @internal * @internal
*/ */
deleteRecording(recording: RecordingInfo) { 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); const updatedList = recordingList.filter((item) => item.id !== recording.id);
if (updatedList.length !== recordingList.length) { if (updatedList.length !== recordingList.length) {
this.updateStatus({ this.updateStatus({
status, status,
recordingList: updatedList, recordingList: updatedList,
startedAt recordingElapsedTime
}); });
return true; return true;
} }
@ -221,11 +220,11 @@ export class RecordingService {
* @internal * @internal
*/ */
setRecordingList(recordings: RecordingInfo[]) { setRecordingList(recordings: RecordingInfo[]) {
const { status, startedAt, error } = this.recordingStatus.getValue(); const { status, recordingElapsedTime, error } = this.recordingStatus.getValue();
this.updateStatus({ this.updateStatus({
status, status,
recordingList: recordings, recordingList: recordings,
startedAt, recordingElapsedTime,
error error
}); });
} }
@ -235,21 +234,19 @@ export class RecordingService {
* @param status {@link RecordingStatus} * @param status {@link RecordingStatus}
*/ */
private updateStatus(statusInfo: RecordingStatusInfo) { private updateStatus(statusInfo: RecordingStatusInfo) {
const { status, recordingList, error, startedAt } = statusInfo; const { status, recordingList, error, recordingElapsedTime } = statusInfo;
this.recordingStatus.next({ this.recordingStatus.next({
status, status,
recordingList, recordingList,
startedAt, recordingElapsedTime,
error error
}); });
} }
private startRecordingTimer() { private startRecordingTimer() {
// Don't override the timestamp if it's already set correctly
if (this.recordingStartTimestamp === null) { if (this.recordingStartTimestamp === null) {
this.recordingStartTimestamp = Date.now(); this.recordingStartTimestamp = Date.now();
} }
if (this.recordingTimeInterval) { if (this.recordingTimeInterval) {
clearInterval(this.recordingTimeInterval); clearInterval(this.recordingTimeInterval);
} }
@ -257,29 +254,29 @@ export class RecordingService {
this.recordingTimeInterval = setInterval(() => { this.recordingTimeInterval = setInterval(() => {
if (!this.recordingStartTimestamp) return; if (!this.recordingStartTimestamp) return;
// Calculate elapsed time based on the actual recording start timestamp let { recordingElapsedTime } = this.recordingStatus.getValue();
const elapsedSeconds = Math.floor((Date.now() - this.recordingStartTimestamp) / 1000); if (recordingElapsedTime) {
const startedAt = new Date(0, 0, 0, 0, 0, 0); // Calculamos con precisión el tiempo transcurrido
startedAt.setSeconds(Math.max(0, elapsedSeconds)); // Ensure non-negative const elapsedSeconds = Math.floor((Date.now() - this.recordingStartTimestamp) / 1000);
const updatedElapsedTime = new Date(0, 0, 0, 0, 0, 0);
updatedElapsedTime.setSeconds(elapsedSeconds);
const { recordingList, status } = this.recordingStatus.getValue(); const { recordingList, status } = this.recordingStatus.getValue();
this.updateStatus({ this.updateStatus({
status, status,
recordingList, recordingList,
startedAt recordingElapsedTime: updatedElapsedTime
}); });
}
}, 1000); }, 1000);
} }
private stopRecordingTimer() { private stopRecordingTimer() {
if (this.recordingTimeInterval) { clearInterval(this.recordingTimeInterval);
clearInterval(this.recordingTimeInterval);
}
const { recordingList, status, error } = this.recordingStatus.getValue(); const { recordingList, status, error } = this.recordingStatus.getValue();
const statusInfo: RecordingStatusInfo = { const statusInfo: RecordingStatusInfo = {
status, status,
recordingList, recordingList,
startedAt: new Date(0, 0, 0, 0, 0, 0), // Reset elapsed time when stopped
error error
}; };
this.updateStatus(statusInfo); this.updateStatus(statusInfo);

View File

@ -1,6 +1,6 @@
import { Injectable, OnDestroy } from '@angular/core'; import { Injectable } from '@angular/core';
import { ILogger } from '../../models/logger.model'; 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 { LoggerService } from '../logger/logger.service';
import { CustomDevice } from '../../models/device.model'; import { CustomDevice } from '../../models/device.model';
@ -10,125 +10,13 @@ import { CustomDevice } from '../../models/device.model';
@Injectable({ @Injectable({
providedIn: 'root' providedIn: 'root'
}) })
export class StorageService implements OnDestroy { export class StorageService {
public localStorage = window.localStorage; public storage = window.localStorage;
public sessionStorage = window.sessionStorage;
public log: ILogger; public log: ILogger;
protected PREFIX_KEY = STORAGE_PREFIX; protected PREFIX_KEY = STORAGE_PREFIX;
private tabId: string;
private readonly TAB_CLEANUP_INTERVAL = 30000; // 30 seconds
private cleanupInterval: any;
constructor(protected loggerSrv: LoggerService) { constructor(protected loggerSrv: LoggerService) {
this.log = this.loggerSrv.get('StorageService'); 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 { getParticipantName(): string | null {
@ -218,164 +106,24 @@ export class StorageService implements OnDestroy {
} }
protected set(key: string, item: any) { protected set(key: string, item: any) {
if (SESSION_KEYS.includes(key as StorageKeys)) { const value = JSON.stringify({ item: item });
this.setSessionValue(key, item); this.storage.setItem(this.PREFIX_KEY + key, value);
} else {
this.setLocalValue(key, item);
}
} }
protected get(key: string): any { protected get(key: string): any {
if (SESSION_KEYS.includes(key as StorageKeys)) { const str = this.storage.getItem(this.PREFIX_KEY + key);
return this.getSessionValue(key); if (!!str) {
} else { return JSON.parse(str).item;
return this.getLocalValue(key);
} }
return null;
} }
protected remove(key: string) { protected remove(key: string) {
if (SESSION_KEYS.includes(key as StorageKeys)) { this.storage.removeItem(this.PREFIX_KEY + key);
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);
} }
public clear() { public clear() {
this.log.d('Clearing localStorage and sessionStorage'); this.log.d('Clearing localStorage');
this.storage.clear();
// 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();
} }
} }

View File

@ -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
};
}
}

View File

@ -18,7 +18,6 @@ export * from './lib/components/toolbar/toolbar.component';
export * from './lib/components/videoconference/videoconference.component'; export * from './lib/components/videoconference/videoconference.component';
export * from './lib/config/openvidu-components-angular.config'; export * from './lib/config/openvidu-components-angular.config';
// Directives // Directives
export * from './lib/directives/template/internals.directive';
export * from './lib/directives/api/activities-panel.directive'; export * from './lib/directives/api/activities-panel.directive';
export * from './lib/directives/api/admin.directive'; export * from './lib/directives/api/admin.directive';
export * from './lib/directives/api/api.directive.module'; export * from './lib/directives/api/api.directive.module';

View File

@ -32,17 +32,6 @@
[activitiesPanelRecordingActivity]="activitiesPanelRecordingActivity" [activitiesPanelRecordingActivity]="activitiesPanelRecordingActivity"
[activitiesPanelBroadcastingActivity]="activitiesPanelBroadcastingActivity" [activitiesPanelBroadcastingActivity]="activitiesPanelBroadcastingActivity"
[toolbarSettingsButton]="toolbarSettingsButton" [toolbarSettingsButton]="toolbarSettingsButton"
[toolbarViewRecordingsButton]="toolbarViewRecordingsButton"
[recordingActivityShowControls]="{
play: false,
download: false,
delete: false,
externalView: true
}"
[recordingActivityReadOnly]="false"
[recordingActivityStartStopRecordingButton]="recordingActivityStartStopRecordingButton"
[recordingActivityViewRecordingsButton]="recordingActivityViewRecordingsButton"
[recordingActivityShowRecordingsList]="true"
(onTokenRequested)="onTokenRequested($event)" (onTokenRequested)="onTokenRequested($event)"
(onReadyToJoin)="onReadyToJoin()" (onReadyToJoin)="onReadyToJoin()"
(onRoomCreated)="onRoomCreated($event)" (onRoomCreated)="onRoomCreated($event)"
@ -65,7 +54,6 @@
(onBroadcastingStopRequested)="onBroadcastingStopRequested($event)" (onBroadcastingStopRequested)="onBroadcastingStopRequested($event)"
(onSettingsPanelStatusChanged)="onSettingsPanelStatusChanged($event)" (onSettingsPanelStatusChanged)="onSettingsPanelStatusChanged($event)"
(onActivitiesPanelStatusChanged)="onActivitiesPanelStatusChanged($event)" (onActivitiesPanelStatusChanged)="onActivitiesPanelStatusChanged($event)"
(onViewRecordingClicked)="onRoomDisconnected()"
> >
</ov-videoconference> </ov-videoconference>
} }

View File

@ -34,6 +34,7 @@ export class CallComponent implements OnInit {
{ name: 'custom', lang: 'cus' } { name: 'custom', lang: 'cus' }
]; ];
prejoin: boolean = true; prejoin: boolean = true;
prejoinDisplayParticipantName: boolean = true;
participantName: string = `Participant${Math.floor(Math.random() * 1000)}`; participantName: string = `Participant${Math.floor(Math.random() * 1000)}`;
videoEnabled: boolean = true; videoEnabled: boolean = true;
audioEnabled: boolean = true; audioEnabled: boolean = true;
@ -58,13 +59,6 @@ export class CallComponent implements OnInit {
activitiesPanelBroadcastingActivity: boolean = true; activitiesPanelBroadcastingActivity: boolean = true;
toolbarSettingsButton: boolean = true; toolbarSettingsButton: boolean = true;
fakeDevices: boolean = false; 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 redirectToHomeOnLeaves: boolean = true;
private staticVideos = [ private staticVideos = [
@ -110,6 +104,8 @@ export class CallComponent implements OnInit {
} catch {} } catch {}
} }
if (params['prejoin'] !== undefined) this.prejoin = params['prejoin'] === 'true'; 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['participantName']) this.participantName = params['participantName'];
if (params['videoEnabled'] !== undefined) this.videoEnabled = params['videoEnabled'] === 'true'; if (params['videoEnabled'] !== undefined) this.videoEnabled = params['videoEnabled'] === 'true';
if (params['audioEnabled'] !== undefined) this.audioEnabled = params['audioEnabled'] === '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'; 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) { if (params['redirectToHome'] === undefined) {
this.redirectToHomeOnLeaves = true; this.redirectToHomeOnLeaves = true;
} else { } else {
@ -211,9 +198,7 @@ export class CallComponent implements OnInit {
if (publication.videoTrack?.attachedElements) { if (publication.videoTrack?.attachedElements) {
this.replaceWithStaticVideos(publication.videoTrack?.attachedElements); this.replaceWithStaticVideos(publication.videoTrack?.attachedElements);
const firstVideo = this.staticVideos.shift(); const firstVideo = this.staticVideos.shift();
if (firstVideo) { this.staticVideos.push(firstVideo);
this.staticVideos.push(firstVideo);
}
} }
}, 2000); }, 2000);
} }

View File

@ -41,13 +41,6 @@ Parameters:
Description: "If certificate type is 'letsencrypt', this email will be used for Let's Encrypt notifications" Description: "If certificate type is 'letsencrypt', this email will be used for Let's Encrypt notifications"
Type: String Type: String
AdditionalInstallFlags:
Description: Additional optional flags to pass to the OpenVidu installer (comma-separated, e.g., "--flag1=value, --flag2").
Type: String
Default: ""
AllowedPattern: '^[A-Za-z0-9, =_.\-]*$' # Allows letters, numbers, comma, space, underscore, dot, equals, and hyphen
ConstraintDescription: Must be a comma-separated list of flags (for example, --flag=value, --bool-flag).
TurnDomainName: TurnDomainName:
Description: '(Optional) Domain name for the TURN server with TLS. Only needed if your users are behind restrictive firewalls' Description: '(Optional) Domain name for the TURN server with TLS. Only needed if your users are behind restrictive firewalls'
Type: String Type: String
@ -227,10 +220,6 @@ Metadata:
default: S3 bucket for application data and recordings default: S3 bucket for application data and recordings
Parameters: Parameters:
- S3AppDataBucketName - S3AppDataBucketName
- Label:
default: "(Optional) Additional Installer Flags"
Parameters:
- AdditionalInstallFlags
- Label: - Label:
default: (Optional) TURN server configuration with TLS default: (Optional) TURN server configuration with TLS
Parameters: Parameters:
@ -271,9 +260,10 @@ Resources:
"GRAFANA_ADMIN_PASSWORD": "none", "GRAFANA_ADMIN_PASSWORD": "none",
"LIVEKIT_API_KEY": "none", "LIVEKIT_API_KEY": "none",
"LIVEKIT_API_SECRET": "none", "LIVEKIT_API_SECRET": "none",
"MEET_ADMIN_USER": "none", "DEFAULT_APP_USERNAME": "none",
"MEET_ADMIN_SECRET": "none", "DEFAULT_APP_PASSWORD": "none",
"MEET_API_KEY": "none", "DEFAULT_APP_ADMIN_USERNAME": "none",
"DEFAULT_APP_ADMIN_PASSWORD": "none",
"ENABLED_MODULES": "none" "ENABLED_MODULES": "none"
} }
@ -366,7 +356,7 @@ Resources:
'/usr/local/bin/install.sh': '/usr/local/bin/install.sh':
content: !Sub | content: !Sub |
#!/bin/bash -x #!/bin/bash -x
OPENVIDU_VERSION=main OPENVIDU_VERSION=3.2.0
DOMAIN= DOMAIN=
YQ_VERSION=v4.44.5 YQ_VERSION=v4.44.5
@ -409,10 +399,11 @@ Resources:
DASHBOARD_ADMIN_PASSWORD="$(/usr/local/bin/store_secret.sh generate DASHBOARD_ADMIN_PASSWORD)" 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_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)" 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")" DEFAULT_APP_USERNAME="$(/usr/local/bin/store_secret.sh save DEFAULT_APP_USERNAME "calluser")"
MEET_ADMIN_SECRET="$(/usr/local/bin/store_secret.sh generate MEET_ADMIN_SECRET)" DEFAULT_APP_PASSWORD="$(/usr/local/bin/store_secret.sh generate DEFAULT_APP_PASSWORD)"
MEET_API_KEY="$(/usr/local/bin/store_secret.sh generate MEET_API_KEY)" DEFAULT_APP_ADMIN_USERNAME="$(/usr/local/bin/store_secret.sh save DEFAULT_APP_ADMIN_USERNAME "calladmin")"
ENABLED_MODULES="$(/usr/local/bin/store_secret.sh save ENABLED_MODULES "observability,openviduMeet")" 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_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)" LIVEKIT_API_SECRET="$(/usr/local/bin/store_secret.sh generate LIVEKIT_API_SECRET)"
@ -437,25 +428,14 @@ Resources:
"--dashboard-admin-password=$DASHBOARD_ADMIN_PASSWORD" "--dashboard-admin-password=$DASHBOARD_ADMIN_PASSWORD"
"--grafana-admin-user=$GRAFANA_ADMIN_USERNAME" "--grafana-admin-user=$GRAFANA_ADMIN_USERNAME"
"--grafana-admin-password=$GRAFANA_ADMIN_PASSWORD" "--grafana-admin-password=$GRAFANA_ADMIN_PASSWORD"
"--meet-admin-user=$MEET_ADMIN_USER" "--default-app-user=$DEFAULT_APP_USERNAME"
"--meet-admin-password=$MEET_ADMIN_SECRET" "--default-app-password=$DEFAULT_APP_PASSWORD"
"--meet-api-key=$MEET_API_KEY" "--default-app-admin-user=$DEFAULT_APP_ADMIN_USERNAME"
"--default-app-admin-password=$DEFAULT_APP_ADMIN_PASSWORD"
"--livekit-api-key=$LIVEKIT_API_KEY" "--livekit-api-key=$LIVEKIT_API_KEY"
"--livekit-api-secret=$LIVEKIT_API_SECRET" "--livekit-api-secret=$LIVEKIT_API_SECRET"
) )
# Include additional installer flags provided by the user
if [[ "${AdditionalInstallFlags}" != "" ]]; then
IFS=',' read -ra EXTRA_FLAGS <<< "${AdditionalInstallFlags}"
for extra_flag in "${!EXTRA_FLAGS[@]}"; do
# Trim whitespace around each flag
extra_flag="$(echo -e "${!extra_flag}" | sed -e 's/^[ \t]*//' -e 's/[ \t]*$//')"
if [[ "$extra_flag" != "" ]]; then
COMMON_ARGS+=("$extra_flag")
fi
done
fi
# Turn with TLS # Turn with TLS
if [[ "${TurnDomainName}" != '' ]]; then if [[ "${TurnDomainName}" != '' ]]; then
LIVEKIT_TURN_DOMAIN_NAME=$(/usr/local/bin/store_secret.sh save LIVEKIT_TURN_DOMAIN_NAME "${TurnDomainName}") LIVEKIT_TURN_DOMAIN_NAME=$(/usr/local/bin/store_secret.sh save LIVEKIT_TURN_DOMAIN_NAME "${TurnDomainName}")
@ -635,9 +615,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/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_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/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/CALL_USER=.*/CALL_USER=$(echo $SHARED_SECRET | jq -r .DEFAULT_APP_USERNAME)/" "${!CONFIG_DIR}/app.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/CALL_SECRET=.*/CALL_SECRET=$(echo $SHARED_SECRET | jq -r .DEFAULT_APP_PASSWORD)/" "${!CONFIG_DIR}/app.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_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" sed -i "s/ENABLED_MODULES=.*/ENABLED_MODULES=$(echo $SHARED_SECRET | jq -r .ENABLED_MODULES)/" "${!CONFIG_DIR}/openvidu.env"
# Update URLs in secret # Update URLs in secret
@ -688,9 +669,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 '. + {"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_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 '. + {"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 '. + {"DEFAULT_APP_USERNAME": "'"$(/usr/local/bin/get_value_from_config.sh CALL_USER "${!CONFIG_DIR}/app.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 '. + {"DEFAULT_APP_PASSWORD": "'"$(/usr/local/bin/get_value_from_config.sh CALL_SECRET "${!CONFIG_DIR}/app.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_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")"'"}')" 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 # Update shared secret
@ -931,14 +913,6 @@ Resources:
FromPort: 1935 FromPort: 1935
ToPort: 1935 ToPort: 1935
CidrIpv6: ::/0 CidrIpv6: ::/0
- IpProtocol: tcp
FromPort: 7881
ToPort: 7881
CidrIp: 0.0.0.0/0
- IpProtocol: tcp
FromPort: 7881
ToPort: 7881
CidrIpv6: ::/0
- IpProtocol: udp - IpProtocol: udp
FromPort: 7885 FromPort: 7885
ToPort: 7885 ToPort: 7885
@ -955,6 +929,14 @@ Resources:
FromPort: 50000 FromPort: 50000
ToPort: 60000 ToPort: 60000
CidrIpv6: ::/0 CidrIpv6: ::/0
- IpProtocol: tcp
FromPort: 50000
ToPort: 60000
CidrIp: 0.0.0.0/0
- IpProtocol: tcp
FromPort: 50000
ToPort: 60000
CidrIpv6: ::/0
Outputs: Outputs:
ServicesAndCredentials: ServicesAndCredentials:

View File

@ -159,8 +159,6 @@ param adminUsername string
@secure() @secure()
param adminSshKey object param adminSshKey object
param additionalInstallFlags string = ''
/*------------------------------------------- VARIABLES AND VALIDATIONS -------------------------------------------*/ /*------------------------------------------- VARIABLES AND VALIDATIONS -------------------------------------------*/
//Condition for ipValid if is filled //Condition for ipValid if is filled
@ -265,12 +263,11 @@ var stringInterpolationParams = {
turnOwnPublicCertificate: turnOwnPublicCertificate turnOwnPublicCertificate: turnOwnPublicCertificate
turnOwnPrivateCertificate: turnOwnPrivateCertificate turnOwnPrivateCertificate: turnOwnPrivateCertificate
keyVaultName: keyVaultName keyVaultName: keyVaultName
additionalInstallFlags: additionalInstallFlags
} }
var installScriptTemplate = ''' var installScriptTemplate = '''
#!/bin/bash -x #!/bin/bash -x
OPENVIDU_VERSION=main OPENVIDU_VERSION=3.2.0
DOMAIN= DOMAIN=
apt-get update && apt-get install -y \ apt-get update && apt-get install -y \
@ -300,12 +297,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)" 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_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)" 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")" DEFAULT_APP_USERNAME="$(/usr/local/bin/store_secret.sh save DEFAULT-APP-USERNAME "calluser")"
MEET_ADMIN_SECRET="$(/usr/local/bin/store_secret.sh generate MEET-ADMIN-SECRET)" DEFAULT_APP_PASSWORD="$(/usr/local/bin/store_secret.sh generate DEFAULT-APP-PASSWORD)"
MEET_API_KEY="$(/usr/local/bin/store_secret.sh generate MEET-API-KEY)" 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_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)" 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 # Base command
INSTALL_COMMAND="sh <(curl -fsSL http://get.openvidu.io/community/singlenode/$OPENVIDU_VERSION/install.sh)" INSTALL_COMMAND="sh <(curl -fsSL http://get.openvidu.io/community/singlenode/$OPENVIDU_VERSION/install.sh)"
@ -328,25 +326,14 @@ COMMON_ARGS=(
"--dashboard-admin-password=$DASHBOARD_ADMIN_PASSWORD" "--dashboard-admin-password=$DASHBOARD_ADMIN_PASSWORD"
"--grafana-admin-user=$GRAFANA_ADMIN_USERNAME" "--grafana-admin-user=$GRAFANA_ADMIN_USERNAME"
"--grafana-admin-password=$GRAFANA_ADMIN_PASSWORD" "--grafana-admin-password=$GRAFANA_ADMIN_PASSWORD"
"--meet-admin-user=$MEET_ADMIN_USER" "--default-app-user=$DEFAULT_APP_USERNAME"
"--meet-admin-password=$MEET_ADMIN_SECRET" "--default-app-password=$DEFAULT_APP_PASSWORD"
"--meet-api-key=$MEET_API_KEY" "--default-app-admin-user=$DEFAULT_APP_ADMIN_USERNAME"
"--default-app-admin-password=$DEFAULT_APP_ADMIN_PASSWORD"
"--livekit-api-key=$LIVEKIT_API_KEY" "--livekit-api-key=$LIVEKIT_API_KEY"
"--livekit-api-secret=$LIVEKIT_API_SECRET" "--livekit-api-secret=$LIVEKIT_API_SECRET"
) )
# Include additional installer flags provided by the user
if [[ "${additionalInstallFlags}" != "" ]]; then
IFS=',' read -ra EXTRA_FLAGS <<< "${additionalInstallFlags}"
for extra_flag in "${EXTRA_FLAGS[@]}"; do
# Trim whitespace around each flag
extra_flag="$(echo -e "${extra_flag}" | sed -e 's/^[ \t]*//' -e 's/[ \t]*$//')"
if [[ "$extra_flag" != "" ]]; then
COMMON_ARGS+=("$extra_flag")
fi
done
fi
# Turn with TLS # Turn with TLS
if [[ "${turnDomainName}" != '' ]]; then if [[ "${turnDomainName}" != '' ]]; then
LIVEKIT_TURN_DOMAIN_NAME=$(/usr/local/bin/store_secret.sh save LIVEKIT-TURN-DOMAIN-NAME "${turnDomainName}") LIVEKIT_TURN_DOMAIN_NAME=$(/usr/local/bin/store_secret.sh save LIVEKIT-TURN-DOMAIN-NAME "${turnDomainName}")
@ -475,9 +462,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 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_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 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 DEFAULT_APP_USERNAME=$(az keyvault secret show --vault-name ${keyVaultName} --name DEFAULT-APP-USERNAME --query value -o tsv)
export MEET_ADMIN_SECRET=$(az keyvault secret show --vault-name ${keyVaultName} --name MEET-ADMIN-SECRET --query value -o tsv) export DEFAULT_APP_PASSWORD=$(az keyvault secret show --vault-name ${keyVaultName} --name DEFAULT-APP-PASSWORD --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_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) export ENABLED_MODULES=$(az keyvault secret show --vault-name ${keyVaultName} --name ENABLED-MODULES --query value -o tsv)
@ -494,9 +482,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/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_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/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/CALL_USER=.*/CALL_USER=$DEFAULT_APP_USERNAME/" "${CONFIG_DIR}/app.env"
sed -i "s/MEET_ADMIN_SECRET=.*/MEET_ADMIN_SECRET=$MEET_ADMIN_SECRET/" "${CONFIG_DIR}/meet.env" sed -i "s/CALL_SECRET=.*/CALL_SECRET=$DEFAULT_APP_PASSWORD/" "${CONFIG_DIR}/app.env"
sed -i "s/MEET_API_KEY=.*/MEET_API_KEY=$MEET_API_KEY/" "${CONFIG_DIR}/meet.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" sed -i "s/ENABLED_MODULES=.*/ENABLED_MODULES=$ENABLED_MODULES/" "${CONFIG_DIR}/openvidu.env"
@ -543,9 +532,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")" 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_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")" 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")" DEFAULT_APP_USERNAME="$(/usr/local/bin/get_value_from_config.sh CALL_USER "${CONFIG_DIR}/app.env")"
MEET_ADMIN_SECRET="$(/usr/local/bin/get_value_from_config.sh MEET_ADMIN_SECRET "${CONFIG_DIR}/meet.env")" DEFAULT_APP_PASSWORD="$(/usr/local/bin/get_value_from_config.sh CALL_SECRET "${CONFIG_DIR}/app.env")"
MEET_API_KEY="$(/usr/local/bin/get_value_from_config.sh MEET_API_KEY "${CONFIG_DIR}/meet.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")" ENABLED_MODULES="$(/usr/local/bin/get_value_from_config.sh ENABLED_MODULES "${CONFIG_DIR}/openvidu.env")"
@ -564,9 +554,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 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-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 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 DEFAULT-APP-USERNAME --value $DEFAULT_APP_USERNAME
az keyvault secret set --vault-name ${keyVaultName} --name MEET-ADMIN-SECRET --value $MEET_ADMIN_SECRET az keyvault secret set --vault-name ${keyVaultName} --name DEFAULT-APP-PASSWORD --value $DEFAULT_APP_PASSWORD
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-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 az keyvault secret set --vault-name ${keyVaultName} --name ENABLED-MODULES --value $ENABLED_MODULES
''' '''
@ -1074,6 +1065,22 @@ resource webServerSecurityGroup 'Microsoft.Network/networkSecurityGroups@2023-11
direction: 'Inbound' 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

View File

@ -285,28 +285,6 @@
} }
] ]
}, },
{
"name": "FLAGS",
"label": "(Optional) Additional Install Flags",
"elements": [
{
"name": "additionalInstallFlags",
"type": "Microsoft.Common.TextBox",
"label": "Additional Install Flags",
"subLabel": "Additional optional flags to pass to the OpenVidu installer (comma-separated, e.g., \"--flag1=value, --flag2\")",
"defaultValue": "",
"toolTip": "",
"constraints": {
"required": false,
"regex": "^[A-Za-z0-9, =_.\\-]*$",
"validationMessage": "Must be a comma-separated list of flags (for example, --flag=value, --bool-flag)",
"validations": []
},
"infoMessages": [],
"visible": true
}
]
},
{ {
"name": "parameters TURN", "name": "parameters TURN",
"label": "(Optional) TURN server configuration with TLS", "label": "(Optional) TURN server configuration with TLS",
@ -392,8 +370,7 @@
"adminUsername": "[steps('parameters INSTANCE').adminUsername]", "adminUsername": "[steps('parameters INSTANCE').adminUsername]",
"adminSshKey": "[steps('parameters INSTANCE').adminSshKey]", "adminSshKey": "[steps('parameters INSTANCE').adminSshKey]",
"storageAccountName": "[steps('parameters STORAGE').storageAccountName]", "storageAccountName": "[steps('parameters STORAGE').storageAccountName]",
"containerName": "[steps('parameters STORAGE').containerName]", "containerName": "[steps('parameters STORAGE').containerName]"
"additionalInstallFlags": "[steps('FLAGS').additionalInstallFlags]"
} }
} }
} }

View File

@ -3,9 +3,9 @@
set -eu set -eu
export DOCKER_VERSION="${DOCKER_VERSION:-28.1.1}" export DOCKER_VERSION="${DOCKER_VERSION:-28.1.1}"
export DOCKER_COMPOSE_VERSION="${DOCKER_COMPOSE_VERSION:-v2.35.1}" export DOCKER_COMPOSE_VERSION="${DOCKER_COMPOSE_VERSION:-v2.35.1}"
export OPENVIDU_VERSION="${OPENVIDU_VERSION:-main}" export OPENVIDU_VERSION="${OPENVIDU_VERSION:-3.2.0}"
export INSTALLER_IMAGE="${INSTALLER_IMAGE:-docker.io/openvidu/openvidu-installer:${OPENVIDU_VERSION}}" 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 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 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}" export REDIS_SERVER_IMAGE="${REDIS_SERVER_IMAGE:-docker.io/redis:7.4.4-alpine}"
@ -15,17 +15,16 @@ 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_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_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_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_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_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_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_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.9.1}" 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 PROMETHEUS_IMAGE="${PROMETHEUS_IMAGE:-docker.io/prom/prometheus:v3.4.0}"
export PROMTAIL_IMAGE="${PROMTAIL_IMAGE:-docker.io/grafana/promtail:3.5.1}" 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 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}" export GRAFANA_IMAGE="${GRAFANA_IMAGE:-docker.io/grafana/grafana:11.6.2}"
# Function to compare two version strings # Function to compare two version strings
@ -181,11 +180,10 @@ COMMON_DOCKER_OPTIONS="--network=host -v ${TMP_DIR}:/output \
-e OPENVIDU_OPERATOR_IMAGE=$OPENVIDU_OPERATOR_IMAGE \ -e OPENVIDU_OPERATOR_IMAGE=$OPENVIDU_OPERATOR_IMAGE \
-e OPENVIDU_SERVER_PRO_IMAGE=$OPENVIDU_SERVER_PRO_IMAGE \ -e OPENVIDU_SERVER_PRO_IMAGE=$OPENVIDU_SERVER_PRO_IMAGE \
-e OPENVIDU_SERVER_IMAGE=$OPENVIDU_SERVER_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_PRO_IMAGE=$OPENVIDU_DASHBOARD_PRO_IMAGE \
-e OPENVIDU_DASHBOARD_IMAGE=$OPENVIDU_DASHBOARD_IMAGE \ -e OPENVIDU_DASHBOARD_IMAGE=$OPENVIDU_DASHBOARD_IMAGE \
-e OPENVIDU_V2COMPATIBILITY_IMAGE=$OPENVIDU_V2COMPATIBILITY_IMAGE \ -e OPENVIDU_V2COMPATIBILITY_IMAGE=$OPENVIDU_V2COMPATIBILITY_IMAGE \
-e OPENVIDU_AGENT_SPEECH_PROCESSING_IMAGE=$OPENVIDU_AGENT_SPEECH_PROCESSING_IMAGE \
-e LIVEKIT_INGRESS_SERVER_IMAGE=$LIVEKIT_INGRESS_SERVER_IMAGE \ -e LIVEKIT_INGRESS_SERVER_IMAGE=$LIVEKIT_INGRESS_SERVER_IMAGE \
-e LIVEKIT_EGRESS_SERVER_IMAGE=$LIVEKIT_EGRESS_SERVER_IMAGE \ -e LIVEKIT_EGRESS_SERVER_IMAGE=$LIVEKIT_EGRESS_SERVER_IMAGE \
-e PROMETHEUS_IMAGE=$PROMETHEUS_IMAGE \ -e PROMETHEUS_IMAGE=$PROMETHEUS_IMAGE \

View File

@ -41,13 +41,6 @@ Parameters:
Description: "If certificate type is 'letsencrypt', this email will be used for Let's Encrypt notifications" Description: "If certificate type is 'letsencrypt', this email will be used for Let's Encrypt notifications"
Type: String Type: String
AdditionalInstallFlags:
Description: Additional optional flags to pass to the OpenVidu installer (comma-separated, e.g., "--flag1=value, --flag2").
Type: String
Default: ""
AllowedPattern: '^[A-Za-z0-9, =_.\-]*$' # Allows letters, numbers, comma, space, underscore, dot, equals, and hyphen
ConstraintDescription: Must be a comma-separated list of flags (for example, --flag=value, --bool-flag).
TurnDomainName: TurnDomainName:
Description: '(Optional) Domain name for the TURN server with TLS. Only needed if your users are behind restrictive firewalls' Description: '(Optional) Domain name for the TURN server with TLS. Only needed if your users are behind restrictive firewalls'
Type: String Type: String
@ -424,10 +417,6 @@ Metadata:
- OpenViduVPC - OpenViduVPC
- OpenViduMasterNodeSubnet - OpenViduMasterNodeSubnet
- OpenViduMediaNodeSubnets - OpenViduMediaNodeSubnets
- Label:
default: "(Optional) Additional Installer Flags"
Parameters:
- AdditionalInstallFlags
- Label: - Label:
default: (Optional) TURN server configuration with TLS default: (Optional) TURN server configuration with TLS
Parameters: Parameters:
@ -473,9 +462,10 @@ Resources:
"GRAFANA_ADMIN_PASSWORD": "none", "GRAFANA_ADMIN_PASSWORD": "none",
"LIVEKIT_API_KEY": "none", "LIVEKIT_API_KEY": "none",
"LIVEKIT_API_SECRET": "none", "LIVEKIT_API_SECRET": "none",
"MEET_ADMIN_USER": "none", "DEFAULT_APP_USERNAME": "none",
"MEET_ADMIN_SECRET": "none", "DEFAULT_APP_PASSWORD": "none",
"MEET_API_KEY": "none", "DEFAULT_APP_ADMIN_USERNAME": "none",
"DEFAULT_APP_ADMIN_PASSWORD": "none",
"OPENVIDU_VERSION": "none", "OPENVIDU_VERSION": "none",
"ENABLED_MODULES": "none" "ENABLED_MODULES": "none"
} }
@ -661,7 +651,7 @@ Resources:
content: !Sub | content: !Sub |
#!/bin/bash #!/bin/bash
set -e set -e
OPENVIDU_VERSION=main OPENVIDU_VERSION=3.2.0
DOMAIN= DOMAIN=
YQ_VERSION=v4.44.5 YQ_VERSION=v4.44.5
@ -733,12 +723,13 @@ Resources:
DASHBOARD_ADMIN_PASSWORD="$(/usr/local/bin/store_secret.sh generate DASHBOARD_ADMIN_PASSWORD)" 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_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)" 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")" DEFAULT_APP_USERNAME="$(/usr/local/bin/store_secret.sh save DEFAULT_APP_USERNAME "calluser")"
MEET_ADMIN_SECRET="$(/usr/local/bin/store_secret.sh generate MEET_ADMIN_SECRET)" DEFAULT_APP_PASSWORD="$(/usr/local/bin/store_secret.sh generate DEFAULT_APP_PASSWORD)"
MEET_API_KEY="$(/usr/local/bin/store_secret.sh generate MEET_API_KEY)" 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_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)" 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")" ALL_SECRETS_GENERATED="$(/usr/local/bin/store_secret.sh save ALL_SECRETS_GENERATED "true")"
# Base command # Base command
@ -766,25 +757,14 @@ Resources:
"--dashboard-admin-password=$DASHBOARD_ADMIN_PASSWORD" "--dashboard-admin-password=$DASHBOARD_ADMIN_PASSWORD"
"--grafana-admin-user=$GRAFANA_ADMIN_USERNAME" "--grafana-admin-user=$GRAFANA_ADMIN_USERNAME"
"--grafana-admin-password=$GRAFANA_ADMIN_PASSWORD" "--grafana-admin-password=$GRAFANA_ADMIN_PASSWORD"
"--meet-admin-user=$MEET_ADMIN_USER" "--default-app-user=$DEFAULT_APP_USERNAME"
"--meet-admin-password=$MEET_ADMIN_SECRET" "--default-app-password=$DEFAULT_APP_PASSWORD"
"--meet-api-key=$MEET_API_KEY" "--default-app-admin-user=$DEFAULT_APP_ADMIN_USERNAME"
"--default-app-admin-password=$DEFAULT_APP_ADMIN_PASSWORD"
"--livekit-api-key=$LIVEKIT_API_KEY" "--livekit-api-key=$LIVEKIT_API_KEY"
"--livekit-api-secret=$LIVEKIT_API_SECRET" "--livekit-api-secret=$LIVEKIT_API_SECRET"
) )
# Include additional installer flags provided by the user
if [[ "${AdditionalInstallFlags}" != "" ]]; then
IFS=',' read -ra EXTRA_FLAGS <<< "${AdditionalInstallFlags}"
for extra_flag in "${!EXTRA_FLAGS[@]}"; do
# Trim whitespace around each flag
extra_flag="$(echo -e "${!extra_flag}" | sed -e 's/^[ \t]*//' -e 's/[ \t]*$//')"
if [[ "$extra_flag" != "" ]]; then
COMMON_ARGS+=("$extra_flag")
fi
done
fi
# Turn with TLS # Turn with TLS
if [[ "${TurnDomainName}" != '' ]]; then if [[ "${TurnDomainName}" != '' ]]; then
LIVEKIT_TURN_DOMAIN_NAME=$(/usr/local/bin/store_secret.sh save LIVEKIT_TURN_DOMAIN_NAME "${TurnDomainName}") LIVEKIT_TURN_DOMAIN_NAME=$(/usr/local/bin/store_secret.sh save LIVEKIT_TURN_DOMAIN_NAME "${TurnDomainName}")
@ -967,9 +947,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/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_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/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/CALL_USER=.*/CALL_USER=$(echo $SHARED_SECRET | jq -r .DEFAULT_APP_USERNAME)/" "${!CLUSTER_CONFIG_DIR}/master_node/app.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/CALL_SECRET=.*/CALL_SECRET=$(echo $SHARED_SECRET | jq -r .DEFAULT_APP_PASSWORD)/" "${!CLUSTER_CONFIG_DIR}/master_node/app.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_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" sed -i "s/ENABLED_MODULES=.*/ENABLED_MODULES=$(echo $SHARED_SECRET | jq -r .ENABLED_MODULES)/" "${!CLUSTER_CONFIG_DIR}/openvidu.env"
# Update URLs in secret # Update URLs in secret
@ -1025,9 +1006,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 '. + {"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_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 '. + {"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 '. + {"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 '. + {"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 '. + {"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 '. + {"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_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")"'"}')" 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 # Update shared secret
@ -1338,14 +1320,10 @@ Resources:
docker container kill --signal=SIGQUIT openvidu || true docker container kill --signal=SIGQUIT openvidu || true
docker container kill --signal=SIGQUIT ingress || true docker container kill --signal=SIGQUIT ingress || true
docker container kill --signal=SIGQUIT egress || true docker container kill --signal=SIGQUIT egress || true
for agent_container in $(docker ps --filter "label=openvidu-agent=true" --format '{{.Names}}'); do
docker container kill --signal=SIGQUIT "$agent_container"
done
TIME_PASSED=0 TIME_PASSED=0
HEARTBEAT_MAX=1800 HEARTBEAT_MAX=1800
# Wait for running containers to not be openvidu, ingress, egress or an openvidu agent # Wait for running containers to not be openvidu, ingress or egress
while [ $(docker ps --filter "label=openvidu-agent=true" -q | wc -l) -gt 0 ] || \ while [ $(docker inspect -f '{{.State.Running}}' openvidu 2>/dev/null) == "true" ] || \
[ $(docker inspect -f '{{.State.Running}}' openvidu 2>/dev/null) == "true" ] || \
[ $(docker inspect -f '{{.State.Running}}' ingress 2>/dev/null) == "true" ] || \ [ $(docker inspect -f '{{.State.Running}}' ingress 2>/dev/null) == "true" ] || \
[ $(docker inspect -f '{{.State.Running}}' egress 2>/dev/null) == "true" ]; do [ $(docker inspect -f '{{.State.Running}}' egress 2>/dev/null) == "true" ]; do
echo "Waiting for containers to stop..." echo "Waiting for containers to stop..."
@ -1691,7 +1669,7 @@ Resources:
ToPort: 4443 ToPort: 4443
SourceSecurityGroupId: !GetAtt OpenViduMediaNodeSG.GroupId SourceSecurityGroupId: !GetAtt OpenViduMediaNodeSG.GroupId
OpenViduMediaNodeToMasterMeetWebhookIngress: OpenViduMediaNodeToMasterDefaultAppWebhookIngress:
Type: AWS::EC2::SecurityGroupIngress Type: AWS::EC2::SecurityGroupIngress
Properties: Properties:
GroupId: !GetAtt OpenViduMasterNodeSG.GroupId GroupId: !GetAtt OpenViduMasterNodeSG.GroupId
@ -1723,14 +1701,6 @@ Resources:
FromPort: 443 FromPort: 443
ToPort: 443 ToPort: 443
CidrIpv6: ::/0 CidrIpv6: ::/0
- IpProtocol: tcp
FromPort: 7881
ToPort: 7881
CidrIp: 0.0.0.0/0
- IpProtocol: tcp
FromPort: 7881
ToPort: 7881
CidrIpv6: ::/0
- IpProtocol: udp - IpProtocol: udp
FromPort: 7885 FromPort: 7885
ToPort: 7885 ToPort: 7885
@ -1747,14 +1717,6 @@ Resources:
FromPort: 50000 FromPort: 50000
ToPort: 60000 ToPort: 60000
CidrIpv6: ::/0 CidrIpv6: ::/0
- IpProtocol: tcp
FromPort: 50000
ToPort: 60000
CidrIp: 0.0.0.0/0
- IpProtocol: tcp
FromPort: 50000
ToPort: 60000
CidrIpv6: ::/0
OpenViduMasterNodeToMediaNodeRTMPIngress: OpenViduMasterNodeToMediaNodeRTMPIngress:
Type: AWS::EC2::SecurityGroupIngress Type: AWS::EC2::SecurityGroupIngress
@ -1767,6 +1729,7 @@ Resources:
OpenViduMasterNodeTurnTLSToMediaNodeIngressSG: OpenViduMasterNodeTurnTLSToMediaNodeIngressSG:
Type: AWS::EC2::SecurityGroupIngress Type: AWS::EC2::SecurityGroupIngress
Condition: TurnTLSIsEnabled
Properties: Properties:
GroupId: !Ref OpenViduMediaNodeSG GroupId: !Ref OpenViduMediaNodeSG
IpProtocol: tcp IpProtocol: tcp

View File

@ -293,8 +293,6 @@ param maxNumberOfMediaNodes int = 5
@description('Target CPU percentage to scale up or down') @description('Target CPU percentage to scale up or down')
param scaleTargetCPU int = 50 param scaleTargetCPU int = 50
param additionalInstallFlags string = ''
/*------------------------------------------- VARIABLES AND VALIDATIONS -------------------------------------------*/ /*------------------------------------------- VARIABLES AND VALIDATIONS -------------------------------------------*/
var isEmptyIp = publicIpAddressObject.newOrExistingOrNone == 'none' var isEmptyIp = publicIpAddressObject.newOrExistingOrNone == 'none'
@ -422,12 +420,11 @@ var stringInterpolationParamsMaster = {
openviduLicense: openviduLicense openviduLicense: openviduLicense
rtcEngine: rtcEngine rtcEngine: rtcEngine
keyVaultName: keyVaultName keyVaultName: keyVaultName
additionalInstallFlags: additionalInstallFlags
} }
var installScriptTemplateMaster = ''' var installScriptTemplateMaster = '''
#!/bin/bash -x #!/bin/bash -x
OPENVIDU_VERSION=main OPENVIDU_VERSION=3.2.0
DOMAIN= DOMAIN=
# Assume azure cli is installed # Assume azure cli is installed
@ -490,13 +487,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)" 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_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)" 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")" DEFAULT_APP_USERNAME="$(/usr/local/bin/store_secret.sh save DEFAULT-APP-USERNAME "calluser")"
MEET_ADMIN_SECRET="$(/usr/local/bin/store_secret.sh generate MEET-ADMIN-SECRET)" DEFAULT_APP_PASSWORD="$(/usr/local/bin/store_secret.sh generate DEFAULT-APP-PASSWORD)"
MEET_API_KEY="$(/usr/local/bin/store_secret.sh generate MEET-API-KEY)" 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_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)" 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}")" 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")" ALL_SECRETS_GENERATED="$(/usr/local/bin/store_secret.sh save ALL-SECRETS-GENERATED "true")"
# Base command # Base command
@ -524,24 +522,14 @@ COMMON_ARGS=(
"--dashboard-admin-password=$DASHBOARD_ADMIN_PASSWORD" "--dashboard-admin-password=$DASHBOARD_ADMIN_PASSWORD"
"--grafana-admin-user=$GRAFANA_ADMIN_USERNAME" "--grafana-admin-user=$GRAFANA_ADMIN_USERNAME"
"--grafana-admin-password=$GRAFANA_ADMIN_PASSWORD" "--grafana-admin-password=$GRAFANA_ADMIN_PASSWORD"
"--meet-admin-user=$MEET_ADMIN_USER" "--default-app-user=$DEFAULT_APP_USERNAME"
"--meet-admin-password=$MEET_ADMIN_SECRET" "--default-app-password=$DEFAULT_APP_PASSWORD"
"--meet-api-key=$MEET_API_KEY" "--default-app-admin-user=$DEFAULT_APP_ADMIN_USERNAME"
"--default-app-admin-password=$DEFAULT_APP_ADMIN_PASSWORD"
"--livekit-api-key=$LIVEKIT_API_KEY" "--livekit-api-key=$LIVEKIT_API_KEY"
"--livekit-api-secret=$LIVEKIT_API_SECRET" "--livekit-api-secret=$LIVEKIT_API_SECRET"
) )
# Include additional installer flags provided by the user
if [[ "${additionalInstallFlags}" != "" ]]; then
IFS=',' read -ra EXTRA_FLAGS <<< "${additionalInstallFlags}"
for extra_flag in "${EXTRA_FLAGS[@]}"; do
# Trim whitespace around each flag
extra_flag="$(echo -e "${extra_flag}" | sed -e 's/^[ \t]*//' -e 's/[ \t]*$//')"
if [[ "$extra_flag" != "" ]]; then
COMMON_ARGS+=("$extra_flag")
fi
done
fi
# Turn with TLS # Turn with TLS
if [[ "${turnDomainName}" != '' ]]; then if [[ "${turnDomainName}" != '' ]]; then
@ -679,9 +667,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 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_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 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 DEFAULT_APP_USERNAME=$(az keyvault secret show --vault-name ${keyVaultName} --name DEFAULT-APP-USERNAME --query value -o tsv)
export MEET_ADMIN_SECRET=$(az keyvault secret show --vault-name ${keyVaultName} --name MEET-ADMIN-SECRET --query value -o tsv) export DEFAULT_APP_PASSWORD=$(az keyvault secret show --vault-name ${keyVaultName} --name DEFAULT-APP-PASSWORD --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_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) export ENABLED_MODULES=$(az keyvault secret show --vault-name ${keyVaultName} --name ENABLED-MODULES --query value -o tsv)
# Replace rest of the values # Replace rest of the values
@ -699,9 +688,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/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_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/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/CALL_USER=.*/CALL_USER=$DEFAULT_APP_USERNAME/" "${CLUSTER_CONFIG_DIR}/master_node/app.env"
sed -i "s/MEET_ADMIN_SECRET=.*/MEET_ADMIN_SECRET=$MEET_ADMIN_SECRET/" "${CLUSTER_CONFIG_DIR}/master_node/meet.env" sed -i "s/CALL_SECRET=.*/CALL_SECRET=$DEFAULT_APP_PASSWORD/" "${CLUSTER_CONFIG_DIR}/master_node/app.env"
sed -i "s/MEET_API_KEY=.*/MEET_API_KEY=$MEET_API_KEY/" "${CLUSTER_CONFIG_DIR}/master_node/meet.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" sed -i "s/ENABLED_MODULES=.*/ENABLED_MODULES=$ENABLED_MODULES/" "${CLUSTER_CONFIG_DIR}/openvidu.env"
# Update URLs in secret # Update URLs in secret
@ -749,9 +739,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")" 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_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")" 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")" DEFAULT_APP_USERNAME="$(/usr/local/bin/get_value_from_config.sh CALL_USER "${CLUSTER_CONFIG_DIR}/master_node/app.env")"
MEET_ADMIN_SECRET="$(/usr/local/bin/get_value_from_config.sh MEET_ADMIN_SECRET "${CLUSTER_CONFIG_DIR}/master_node/meet.env")" DEFAULT_APP_PASSWORD="$(/usr/local/bin/get_value_from_config.sh CALL_SECRET "${CLUSTER_CONFIG_DIR}/master_node/app.env")"
MEET_API_KEY="$(/usr/local/bin/get_value_from_config.sh MEET_API_KEY "${CLUSTER_CONFIG_DIR}/master_node/meet.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")" ENABLED_MODULES="$(/usr/local/bin/get_value_from_config.sh ENABLED_MODULES "${CLUSTER_CONFIG_DIR}/openvidu.env")"
# Update shared secret # Update shared secret
@ -771,9 +762,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 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-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 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 DEFAULT-APP-USERNAME --value $DEFAULT_APP_USERNAME
az keyvault secret set --vault-name ${keyVaultName} --name MEET-ADMIN-SECRET --value $MEET_ADMIN_SECRET az keyvault secret set --vault-name ${keyVaultName} --name DEFAULT-APP-PASSWORD --value $DEFAULT_APP_PASSWORD
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-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 az keyvault secret set --vault-name ${keyVaultName} --name ENABLED-MODULES --value $ENABLED_MODULES
''' '''
@ -1118,6 +1110,7 @@ while true; do
sleep $WAIT_INTERVAL sleep $WAIT_INTERVAL
done done
set -e set -e
# Get current shared secret # Get current shared secret
DOMAIN=$(az keyvault secret show --vault-name ${keyVaultName} --name DOMAIN-NAME --query value -o tsv) DOMAIN=$(az keyvault secret show --vault-name ${keyVaultName} --name DOMAIN-NAME --query value -o tsv)
OPENVIDU_PRO_LICENSE=$(az keyvault secret show --vault-name ${keyVaultName} --name OPENVIDU-PRO-LICENSE --query value -o tsv) OPENVIDU_PRO_LICENSE=$(az keyvault secret show --vault-name ${keyVaultName} --name OPENVIDU-PRO-LICENSE --query value -o tsv)
@ -1172,13 +1165,9 @@ if [ -x "$(command -v docker)" ]; then
docker container kill --signal=SIGQUIT openvidu || true docker container kill --signal=SIGQUIT openvidu || true
docker container kill --signal=SIGQUIT ingress || true docker container kill --signal=SIGQUIT ingress || true
docker container kill --signal=SIGQUIT egress || true docker container kill --signal=SIGQUIT egress || true
for agent_container in $(docker ps --filter "label=openvidu-agent=true" --format '{{.Names}}'); do
docker container kill --signal=SIGQUIT "$agent_container"
done
# Wait for running containers to not be openvidu, ingress, egress or an openvidu agent # Wait for running containers to not be openvidu, ingress or egress
while [ $(docker ps --filter "label=openvidu-agent=true" -q | wc -l) -gt 0 ] || \ while [ $(docker inspect -f '{{.State.Running}}' openvidu 2>/dev/null) == "true" ] || \
[ $(docker inspect -f '{{.State.Running}}' openvidu 2>/dev/null) == "true" ] || \
[ $(docker inspect -f '{{.State.Running}}' ingress 2>/dev/null) == "true" ] || \ [ $(docker inspect -f '{{.State.Running}}' ingress 2>/dev/null) == "true" ] || \
[ $(docker inspect -f '{{.State.Running}}' egress 2>/dev/null) == "true" ]; do [ $(docker inspect -f '{{.State.Running}}' egress 2>/dev/null) == "true" ]; do
echo "Waiting for containers to stop..." echo "Waiting for containers to stop..."
@ -1198,21 +1187,6 @@ TIMESTAMP=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
az tag update --resource-id $RESOURCE_ID --operation replace --tags "STATUS"="HEALTHY" "InstanceDeleteTime"="$TIMESTAMP" "storageAccount"="${storageAccountName}" az tag update --resource-id $RESOURCE_ID --operation replace --tags "STATUS"="HEALTHY" "InstanceDeleteTime"="$TIMESTAMP" "storageAccount"="${storageAccountName}"
az vmss delete-instances --resource-group $RESOURCE_GROUP_NAME --name $VM_SCALE_SET_NAME --instance-ids $INSTANCE_ID
'''
var delete_mediaNode_ScriptMediaTemplate = '''
#!/bin/bash
set -e
az login --identity
RESOURCE_GROUP_NAME=${resourceGroupName}
VM_SCALE_SET_NAME=${vmScaleSetName}
BEFORE_INSTANCE_ID=$(curl -H Metadata:true --noproxy "*" "http://169.254.169.254/metadata/instance?api-version=2021-02-01" | jq -r '.compute.resourceId')
INSTANCE_ID=$(echo $BEFORE_INSTANCE_ID | awk -F'/' '{print $NF}')
az vmss delete-instances --resource-group $RESOURCE_GROUP_NAME --name $VM_SCALE_SET_NAME --instance-ids $INSTANCE_ID az vmss delete-instances --resource-group $RESOURCE_GROUP_NAME --name $VM_SCALE_SET_NAME --instance-ids $INSTANCE_ID
''' '''
@ -1229,10 +1203,6 @@ chmod +x /usr/local/bin/install.sh
echo ${base64stop} | base64 -d > /usr/local/bin/stop_media_node.sh echo ${base64stop} | base64 -d > /usr/local/bin/stop_media_node.sh
chmod +x /usr/local/bin/stop_media_node.sh chmod +x /usr/local/bin/stop_media_node.sh
# delete_media_node.sh
echo ${base64delete} | base64 -d > /usr/local/bin/delete_media_node.sh
chmod +x /usr/local/bin/delete_media_node.sh
apt-get update && apt-get install -y apt-get update && apt-get install -y
apt-get install -y jq apt-get install -y jq
@ -1251,10 +1221,11 @@ az vmss update --resource-group $RESOURCE_GROUP_NAME --name $VM_SCALE_SET_NAME -
export HOME="/root" export HOME="/root"
# Install OpenVidu # Install OpenVidu
/usr/local/bin/install.sh || { echo "[OpenVidu] error installing OpenVidu"; /usr/local/bin/delete_media_node.sh; } /usr/local/bin/install.sh || { echo "[OpenVidu] error installing OpenVidu"; exit 1; }
# Start OpenVidu # Start OpenVidu
systemctl start openvidu || { echo "[OpenVidu] error starting OpenVidu"; /usr/local/bin/delete_media_node.sh; } systemctl start openvidu || { echo "[OpenVidu] error starting OpenVidu"; exit 1; }
#/usr/local/bin/set_as_unhealthy.sh
''' '''
var installScriptMedia = reduce( var installScriptMedia = reduce(
@ -1273,18 +1244,9 @@ var stop_media_nodesScriptMedia = reduce(
var base64stopMediaNode = base64(stop_media_nodesScriptMedia) var base64stopMediaNode = base64(stop_media_nodesScriptMedia)
var delete_mediaNode_ScriptMedia = reduce(
items(stopMediaNodeParams),
{ value: delete_mediaNode_ScriptMediaTemplate },
(curr, next) => { value: replace(curr.value, '\${${next.key}}', next.value) }
).value
var base64delete_mediaNode_ScriptMedia = base64(delete_mediaNode_ScriptMedia)
var userDataParamsMedia = { var userDataParamsMedia = {
base64install: base64installMedia base64install: base64installMedia
base64stop: base64stopMediaNode base64stop: base64stopMediaNode
base64delete: base64delete_mediaNode_ScriptMedia
resourceGroupName: resourceGroup().name resourceGroupName: resourceGroup().name
vmScaleSetName: '${stackName}-mediaNodeScaleSet' vmScaleSetName: '${stackName}-mediaNodeScaleSet'
} }
@ -1857,9 +1819,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 parent: openviduMasterNodeNSG
name: 'mediaNode_to_masterNode_MEET_WEBHOOK_INGRESS' name: 'mediaNode_to_masterNode_DEFAULTAPP_WEBHOOK_INGRESS'
properties: { properties: {
protocol: 'Tcp' protocol: 'Tcp'
sourceApplicationSecurityGroups: [ sourceApplicationSecurityGroups: [
@ -1953,22 +1915,6 @@ resource openviduMediaNodeNSG 'Microsoft.Network/networkSecurityGroups@2023-11-0
direction: 'Inbound' direction: 'Inbound'
} }
} }
{
name: 'WebRTC_traffic_TCP'
properties: {
protocol: 'Tcp'
sourceAddressPrefix: '*'
sourcePortRange: '*'
destinationAddressPrefix: '*'
destinationPortRanges: [
'50000'
'60000'
]
access: 'Allow'
priority: 150
direction: 'Inbound'
}
}
] ]
} }
} }
@ -1996,7 +1942,7 @@ resource masterToMediaRtmpIngress 'Microsoft.Network/networkSecurityGroups/secur
] ]
destinationPortRange: '1935' destinationPortRange: '1935'
access: 'Allow' access: 'Allow'
priority: 160 priority: 150
direction: 'Inbound' direction: 'Inbound'
} }
} }
@ -2019,7 +1965,7 @@ resource masterToMediaTurnTlsIngress 'Microsoft.Network/networkSecurityGroups/se
] ]
destinationPortRange: '5349' destinationPortRange: '5349'
access: 'Allow' access: 'Allow'
priority: 170 priority: 160
direction: 'Inbound' direction: 'Inbound'
} }
} }
@ -2042,7 +1988,7 @@ resource masterToMediaServerIngress 'Microsoft.Network/networkSecurityGroups/sec
] ]
destinationPortRange: '7880' destinationPortRange: '7880'
access: 'Allow' access: 'Allow'
priority: 180 priority: 170
direction: 'Inbound' direction: 'Inbound'
} }
} }
@ -2065,7 +2011,7 @@ resource masterToMediaHttpWhipIngress 'Microsoft.Network/networkSecurityGroups/s
] ]
destinationPortRange: '8080' destinationPortRange: '8080'
access: 'Allow' access: 'Allow'
priority: 190 priority: 180
direction: 'Inbound' direction: 'Inbound'
} }
} }

File diff suppressed because one or more lines are too long

View File

@ -458,28 +458,6 @@
} }
] ]
}, },
{
"name": "FLAGS",
"label": "(Optional) Additional Install Flags",
"elements": [
{
"name": "additionalInstallFlags",
"type": "Microsoft.Common.TextBox",
"label": "Additional Install Flags",
"subLabel": "Additional optional flags to pass to the OpenVidu installer (comma-separated, e.g., \"--flag1=value, --flag2\")",
"defaultValue": "",
"toolTip": "",
"constraints": {
"required": false,
"regex": "^[A-Za-z0-9, =_.\\-]*$",
"validationMessage": "Must be a comma-separated list of flags (for example, --flag=value, --bool-flag)",
"validations": []
},
"infoMessages": [],
"visible": true
}
]
},
{ {
"name": "parameters TURN", "name": "parameters TURN",
"label": "(Optional) TURN server configuration with TLS", "label": "(Optional) TURN server configuration with TLS",
@ -574,8 +552,7 @@
"datetime": "[steps('parameters SCALING').datetime]", "datetime": "[steps('parameters SCALING').datetime]",
"automationAccountName": "[steps('parameters SCALING').automationAccountName]", "automationAccountName": "[steps('parameters SCALING').automationAccountName]",
"storageAccountName": "[steps('parameters STORAGE').storageAccountName]", "storageAccountName": "[steps('parameters STORAGE').storageAccountName]",
"containerName": "[steps('parameters STORAGE').containerName]", "containerName": "[steps('parameters STORAGE').containerName]"
"additionalInstallFlags": "[steps('FLAGS').additionalInstallFlags]"
} }
} }
} }

View File

@ -3,9 +3,9 @@
set -eu set -eu
export DOCKER_VERSION="${DOCKER_VERSION:-28.1.1}" export DOCKER_VERSION="${DOCKER_VERSION:-28.1.1}"
export DOCKER_COMPOSE_VERSION="${DOCKER_COMPOSE_VERSION:-v2.35.1}" export DOCKER_COMPOSE_VERSION="${DOCKER_COMPOSE_VERSION:-v2.35.1}"
export OPENVIDU_VERSION="${OPENVIDU_VERSION:-main}" export OPENVIDU_VERSION="${OPENVIDU_VERSION:-3.2.0}"
export INSTALLER_IMAGE="${INSTALLER_IMAGE:-docker.io/openvidu/openvidu-installer:${OPENVIDU_VERSION}}" 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 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 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}" export REDIS_SERVER_IMAGE="${REDIS_SERVER_IMAGE:-docker.io/redis:7.4.4-alpine}"
@ -15,17 +15,16 @@ 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_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_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_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_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_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_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_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.9.1}" 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 PROMETHEUS_IMAGE="${PROMETHEUS_IMAGE:-docker.io/prom/prometheus:v3.4.0}"
export PROMTAIL_IMAGE="${PROMTAIL_IMAGE:-docker.io/grafana/promtail:3.5.1}" 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 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}" export GRAFANA_IMAGE="${GRAFANA_IMAGE:-docker.io/grafana/grafana:11.6.2}"
# Function to compare two version strings # Function to compare two version strings
@ -181,11 +180,10 @@ COMMON_DOCKER_OPTIONS="--network=host -v ${TMP_DIR}:/output \
-e OPENVIDU_OPERATOR_IMAGE=$OPENVIDU_OPERATOR_IMAGE \ -e OPENVIDU_OPERATOR_IMAGE=$OPENVIDU_OPERATOR_IMAGE \
-e OPENVIDU_SERVER_PRO_IMAGE=$OPENVIDU_SERVER_PRO_IMAGE \ -e OPENVIDU_SERVER_PRO_IMAGE=$OPENVIDU_SERVER_PRO_IMAGE \
-e OPENVIDU_SERVER_IMAGE=$OPENVIDU_SERVER_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_PRO_IMAGE=$OPENVIDU_DASHBOARD_PRO_IMAGE \
-e OPENVIDU_DASHBOARD_IMAGE=$OPENVIDU_DASHBOARD_IMAGE \ -e OPENVIDU_DASHBOARD_IMAGE=$OPENVIDU_DASHBOARD_IMAGE \
-e OPENVIDU_V2COMPATIBILITY_IMAGE=$OPENVIDU_V2COMPATIBILITY_IMAGE \ -e OPENVIDU_V2COMPATIBILITY_IMAGE=$OPENVIDU_V2COMPATIBILITY_IMAGE \
-e OPENVIDU_AGENT_SPEECH_PROCESSING_IMAGE=$OPENVIDU_AGENT_SPEECH_PROCESSING_IMAGE \
-e LIVEKIT_INGRESS_SERVER_IMAGE=$LIVEKIT_INGRESS_SERVER_IMAGE \ -e LIVEKIT_INGRESS_SERVER_IMAGE=$LIVEKIT_INGRESS_SERVER_IMAGE \
-e LIVEKIT_EGRESS_SERVER_IMAGE=$LIVEKIT_EGRESS_SERVER_IMAGE \ -e LIVEKIT_EGRESS_SERVER_IMAGE=$LIVEKIT_EGRESS_SERVER_IMAGE \
-e PROMETHEUS_IMAGE=$PROMETHEUS_IMAGE \ -e PROMETHEUS_IMAGE=$PROMETHEUS_IMAGE \

View File

@ -3,9 +3,9 @@
set -eu set -eu
export DOCKER_VERSION="${DOCKER_VERSION:-28.1.1}" export DOCKER_VERSION="${DOCKER_VERSION:-28.1.1}"
export DOCKER_COMPOSE_VERSION="${DOCKER_COMPOSE_VERSION:-v2.35.1}" export DOCKER_COMPOSE_VERSION="${DOCKER_COMPOSE_VERSION:-v2.35.1}"
export OPENVIDU_VERSION="${OPENVIDU_VERSION:-main}" export OPENVIDU_VERSION="${OPENVIDU_VERSION:-3.2.0}"
export INSTALLER_IMAGE="${INSTALLER_IMAGE:-docker.io/openvidu/openvidu-installer:${OPENVIDU_VERSION}}" 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 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 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}" export REDIS_SERVER_IMAGE="${REDIS_SERVER_IMAGE:-docker.io/redis:7.4.4-alpine}"
@ -15,17 +15,16 @@ 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_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_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_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_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_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_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_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.9.1}" 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 PROMETHEUS_IMAGE="${PROMETHEUS_IMAGE:-docker.io/prom/prometheus:v3.4.0}"
export PROMTAIL_IMAGE="${PROMTAIL_IMAGE:-docker.io/grafana/promtail:3.5.1}" 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 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}" export GRAFANA_IMAGE="${GRAFANA_IMAGE:-docker.io/grafana/grafana:11.6.2}"
# Function to compare two version strings # Function to compare two version strings
@ -181,11 +180,10 @@ COMMON_DOCKER_OPTIONS="--network=host -v ${TMP_DIR}:/output \
-e OPENVIDU_OPERATOR_IMAGE=$OPENVIDU_OPERATOR_IMAGE \ -e OPENVIDU_OPERATOR_IMAGE=$OPENVIDU_OPERATOR_IMAGE \
-e OPENVIDU_SERVER_PRO_IMAGE=$OPENVIDU_SERVER_PRO_IMAGE \ -e OPENVIDU_SERVER_PRO_IMAGE=$OPENVIDU_SERVER_PRO_IMAGE \
-e OPENVIDU_SERVER_IMAGE=$OPENVIDU_SERVER_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_PRO_IMAGE=$OPENVIDU_DASHBOARD_PRO_IMAGE \
-e OPENVIDU_DASHBOARD_IMAGE=$OPENVIDU_DASHBOARD_IMAGE \ -e OPENVIDU_DASHBOARD_IMAGE=$OPENVIDU_DASHBOARD_IMAGE \
-e OPENVIDU_V2COMPATIBILITY_IMAGE=$OPENVIDU_V2COMPATIBILITY_IMAGE \ -e OPENVIDU_V2COMPATIBILITY_IMAGE=$OPENVIDU_V2COMPATIBILITY_IMAGE \
-e OPENVIDU_AGENT_SPEECH_PROCESSING_IMAGE=$OPENVIDU_AGENT_SPEECH_PROCESSING_IMAGE \
-e LIVEKIT_INGRESS_SERVER_IMAGE=$LIVEKIT_INGRESS_SERVER_IMAGE \ -e LIVEKIT_INGRESS_SERVER_IMAGE=$LIVEKIT_INGRESS_SERVER_IMAGE \
-e LIVEKIT_EGRESS_SERVER_IMAGE=$LIVEKIT_EGRESS_SERVER_IMAGE \ -e LIVEKIT_EGRESS_SERVER_IMAGE=$LIVEKIT_EGRESS_SERVER_IMAGE \
-e PROMETHEUS_IMAGE=$PROMETHEUS_IMAGE \ -e PROMETHEUS_IMAGE=$PROMETHEUS_IMAGE \

File diff suppressed because it is too large Load Diff

View File

@ -330,13 +330,6 @@ Parameters:
Type: String Type: String
Description: Name of the S3 bucket to store cluster data. If empty, a bucket will be created Description: Name of the S3 bucket to store cluster data. If empty, a bucket will be created
AdditionalInstallFlags:
Description: Additional optional flags to pass to the OpenVidu installer (comma-separated, e.g., "--flag1=value, --flag2").
Type: String
Default: ""
AllowedPattern: '^[A-Za-z0-9, =_.\-]*$' # Allows letters, numbers, comma, space, underscore, dot, equals, and hyphen
ConstraintDescription: Must be a comma-separated list of flags (for example, --flag=value, --bool-flag).
OpenViduVPC: OpenViduVPC:
Description: "Dedicated VPC for OpenVidu cluster" Description: "Dedicated VPC for OpenVidu cluster"
Type: AWS::EC2::VPC::Id Type: AWS::EC2::VPC::Id
@ -404,10 +397,6 @@ Metadata:
default: Volumes configuration default: Volumes configuration
Parameters: Parameters:
- MasterNodesDiskSize - MasterNodesDiskSize
- Label:
default: "(Optional) Additional Installer Flags"
Parameters:
- AdditionalInstallFlags
- Label: - Label:
default: (Optional) TURN server configuration with TLS default: (Optional) TURN server configuration with TLS
Parameters: Parameters:
@ -418,22 +407,6 @@ Conditions:
TurnTLSIsEnabled: !Or [!Not [!Equals [!Ref TurnDomainName, ""]], !Not [!Equals [!Ref TurnCertificateARN, ""]]] TurnTLSIsEnabled: !Or [!Not [!Equals [!Ref TurnDomainName, ""]], !Not [!Equals [!Ref TurnCertificateARN, ""]]]
CreateRecordingsBucket: !Equals [!Ref S3AppDataBucketName, ""] CreateRecordingsBucket: !Equals [!Ref S3AppDataBucketName, ""]
CreateClusterDataBucket: !Equals [!Ref S3ClusterDataBucketName, ""] CreateClusterDataBucket: !Equals [!Ref S3ClusterDataBucketName, ""]
# ---
# Experimental TURN TLS with main domain
ExperimentalTurnTLSWithMainDomain:
Fn::Not:
- Fn::Equals:
- !Ref AdditionalInstallFlags
- !Select [0, !Split ["--experimental-turn-tls-with-main-domain", !Ref AdditionalInstallFlags]]
NotExperimentalTurnTLSWithMainDomain:
Fn::Or:
- Fn::Equals:
- !Ref AdditionalInstallFlags
- !Select [0, !Split ["--experimental-turn-tls-with-main-domain", !Ref AdditionalInstallFlags]]
- Fn::Equals:
- !Ref AdditionalInstallFlags
- ""
# ---
Resources: Resources:
@ -465,9 +438,10 @@ Resources:
"GRAFANA_URL": "none", "GRAFANA_URL": "none",
"GRAFANA_ADMIN_USERNAME": "none", "GRAFANA_ADMIN_USERNAME": "none",
"GRAFANA_ADMIN_PASSWORD": "none", "GRAFANA_ADMIN_PASSWORD": "none",
"MEET_ADMIN_USER": "none", "DEFAULT_APP_USERNAME": "none",
"MEET_ADMIN_SECRET": "none", "DEFAULT_APP_PASSWORD": "none",
"MEET_API_KEY": "none", "DEFAULT_APP_ADMIN_USERNAME": "none",
"DEFAULT_APP_ADMIN_PASSWORD": "none",
"LIVEKIT_API_KEY": "none", "LIVEKIT_API_KEY": "none",
"LIVEKIT_API_SECRET": "none", "LIVEKIT_API_SECRET": "none",
"ENABLED_MODULES": "none", "ENABLED_MODULES": "none",
@ -772,7 +746,7 @@ Resources:
content: !Sub | content: !Sub |
#!/bin/bash -x #!/bin/bash -x
set -e set -e
OPENVIDU_VERSION=main OPENVIDU_VERSION=3.2.0
DOMAIN= DOMAIN=
YQ_VERSION=v4.44.5 YQ_VERSION=v4.44.5
@ -866,12 +840,13 @@ Resources:
DASHBOARD_ADMIN_PASSWORD="$(/usr/local/bin/store_secret.sh generate DASHBOARD_ADMIN_PASSWORD)" 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_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)" 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")" DEFAULT_APP_USERNAME="$(/usr/local/bin/store_secret.sh save DEFAULT_APP_USERNAME "calluser")"
MEET_ADMIN_SECRET="$(/usr/local/bin/store_secret.sh generate MEET_ADMIN_SECRET)" DEFAULT_APP_PASSWORD="$(/usr/local/bin/store_secret.sh generate DEFAULT_APP_PASSWORD)"
MEET_API_KEY="$(/usr/local/bin/store_secret.sh generate MEET_API_KEY)" 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_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)" 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")" ALL_SECRETS_GENERATED="$(/usr/local/bin/store_secret.sh save ALL_SECRETS_GENERATED "true")"
fi fi
@ -937,9 +912,10 @@ Resources:
DASHBOARD_ADMIN_PASSWORD=$(echo "$SHARED_SECRET" | jq -r '.DASHBOARD_ADMIN_PASSWORD') DASHBOARD_ADMIN_PASSWORD=$(echo "$SHARED_SECRET" | jq -r '.DASHBOARD_ADMIN_PASSWORD')
GRAFANA_ADMIN_USERNAME=$(echo "$SHARED_SECRET" | jq -r '.GRAFANA_ADMIN_USERNAME') GRAFANA_ADMIN_USERNAME=$(echo "$SHARED_SECRET" | jq -r '.GRAFANA_ADMIN_USERNAME')
GRAFANA_ADMIN_PASSWORD=$(echo "$SHARED_SECRET" | jq -r '.GRAFANA_ADMIN_PASSWORD') GRAFANA_ADMIN_PASSWORD=$(echo "$SHARED_SECRET" | jq -r '.GRAFANA_ADMIN_PASSWORD')
MEET_ADMIN_USER=$(echo "$SHARED_SECRET" | jq -r '.MEET_ADMIN_USER') DEFAULT_APP_USERNAME=$(echo "$SHARED_SECRET" | jq -r '.DEFAULT_APP_USERNAME')
MEET_ADMIN_SECRET=$(echo "$SHARED_SECRET" | jq -r '.MEET_ADMIN_SECRET') DEFAULT_APP_PASSWORD=$(echo "$SHARED_SECRET" | jq -r '.DEFAULT_APP_PASSWORD')
MEET_API_KEY=$(echo "$SHARED_SECRET" | jq -r '.MEET_API_KEY') DEFAULT_APP_ADMIN_USERNAME=$(echo "$SHARED_SECRET" | jq -r '.DEFAULT_APP_ADMIN_USERNAME')
DEFAULT_APP_ADMIN_PASSWORD=$(echo "$SHARED_SECRET" | jq -r '.DEFAULT_APP_ADMIN_PASSWORD')
LIVEKIT_API_KEY=$(echo "$SHARED_SECRET" | jq -r '.LIVEKIT_API_KEY') LIVEKIT_API_KEY=$(echo "$SHARED_SECRET" | jq -r '.LIVEKIT_API_KEY')
LIVEKIT_API_SECRET=$(echo "$SHARED_SECRET" | jq -r '.LIVEKIT_API_SECRET') LIVEKIT_API_SECRET=$(echo "$SHARED_SECRET" | jq -r '.LIVEKIT_API_SECRET')
ENABLED_MODULES=$(echo "$SHARED_SECRET" | jq -r '.ENABLED_MODULES') ENABLED_MODULES=$(echo "$SHARED_SECRET" | jq -r '.ENABLED_MODULES')
@ -970,25 +946,14 @@ Resources:
"--dashboard-admin-password=$DASHBOARD_ADMIN_PASSWORD" "--dashboard-admin-password=$DASHBOARD_ADMIN_PASSWORD"
"--grafana-admin-user=$GRAFANA_ADMIN_USERNAME" "--grafana-admin-user=$GRAFANA_ADMIN_USERNAME"
"--grafana-admin-password=$GRAFANA_ADMIN_PASSWORD" "--grafana-admin-password=$GRAFANA_ADMIN_PASSWORD"
"--meet-admin-user=$MEET_ADMIN_USER" "--default-app-user=$DEFAULT_APP_USERNAME"
"--meet-admin-password=$MEET_ADMIN_SECRET" "--default-app-password=$DEFAULT_APP_PASSWORD"
"--meet-api-key=$MEET_API_KEY" "--default-app-admin-user=$DEFAULT_APP_ADMIN_USERNAME"
"--default-app-admin-password=$DEFAULT_APP_ADMIN_PASSWORD"
"--livekit-api-key=$LIVEKIT_API_KEY" "--livekit-api-key=$LIVEKIT_API_KEY"
"--livekit-api-secret=$LIVEKIT_API_SECRET" "--livekit-api-secret=$LIVEKIT_API_SECRET"
) )
# Include additional installer flags provided by the user
if [[ "${AdditionalInstallFlags}" != "" ]]; then
IFS=',' read -ra EXTRA_FLAGS <<< "${AdditionalInstallFlags}"
for extra_flag in "${!EXTRA_FLAGS[@]}"; do
# Trim whitespace around each flag
extra_flag="$(echo -e "${!extra_flag}" | sed -e 's/^[ \t]*//' -e 's/[ \t]*$//')"
if [[ "$extra_flag" != "" ]]; then
COMMON_ARGS+=("$extra_flag")
fi
done
fi
if [[ "${!LIVEKIT_TURN_DOMAIN_NAME}" != "none" ]]; then if [[ "${!LIVEKIT_TURN_DOMAIN_NAME}" != "none" ]]; then
COMMON_ARGS+=("--turn-domain-name='${!LIVEKIT_TURN_DOMAIN_NAME}'") COMMON_ARGS+=("--turn-domain-name='${!LIVEKIT_TURN_DOMAIN_NAME}'")
fi fi
@ -1109,9 +1074,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/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_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/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/CALL_USER=.*/CALL_USER=$(echo $SHARED_SECRET | jq -r .DEFAULT_APP_USERNAME)/" "${!CLUSTER_CONFIG_DIR}/master_node/app.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/CALL_SECRET=.*/CALL_SECRET=$(echo $SHARED_SECRET | jq -r .DEFAULT_APP_PASSWORD)/" "${!CLUSTER_CONFIG_DIR}/master_node/app.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_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" sed -i "s/ENABLED_MODULES=.*/ENABLED_MODULES=$(echo $SHARED_SECRET | jq -r .ENABLED_MODULES)/" "${!CLUSTER_CONFIG_DIR}/openvidu.env"
# Update URLs in secret # Update URLs in secret
@ -1161,9 +1127,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 '. + {"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_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 '. + {"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 '. + {"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 '. + {"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 '. + {"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 '. + {"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_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")"'"}')" 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 # Update shared secret
@ -1594,14 +1561,10 @@ Resources:
docker container kill --signal=SIGQUIT openvidu || true docker container kill --signal=SIGQUIT openvidu || true
docker container kill --signal=SIGQUIT ingress || true docker container kill --signal=SIGQUIT ingress || true
docker container kill --signal=SIGQUIT egress || true docker container kill --signal=SIGQUIT egress || true
for agent_container in $(docker ps --filter "label=openvidu-agent=true" --format '{{.Names}}'); do
docker container kill --signal=SIGQUIT "$agent_container"
done
TIME_PASSED=0 TIME_PASSED=0
HEARTBEAT_MAX=1800 HEARTBEAT_MAX=1800
# Wait for running containers to not be openvidu, ingress, egress or an openvidu agent # Wait for running containers to not be openvidu, ingress or egress
while [ $(docker ps --filter "label=openvidu-agent=true" -q | wc -l) -gt 0 ] || \ while [ $(docker inspect -f '{{.State.Running}}' openvidu 2>/dev/null) == "true" ] || \
[ $(docker inspect -f '{{.State.Running}}' openvidu 2>/dev/null) == "true" ] || \
[ $(docker inspect -f '{{.State.Running}}' ingress 2>/dev/null) == "true" ] || \ [ $(docker inspect -f '{{.State.Running}}' ingress 2>/dev/null) == "true" ] || \
[ $(docker inspect -f '{{.State.Running}}' egress 2>/dev/null) == "true" ]; do [ $(docker inspect -f '{{.State.Running}}' egress 2>/dev/null) == "true" ]; do
echo "Waiting for containers to stop..." echo "Waiting for containers to stop..."
@ -2040,7 +2003,7 @@ Resources:
ToPort: 4443 ToPort: 4443
SourceSecurityGroupId: !GetAtt OpenViduMediaNodeSG.GroupId SourceSecurityGroupId: !GetAtt OpenViduMediaNodeSG.GroupId
OpenViduMasterToMasterMeetIngress: OpenViduMasterToMasterDefaultAppIngress:
Type: AWS::EC2::SecurityGroupIngress Type: AWS::EC2::SecurityGroupIngress
Properties: Properties:
GroupId: !Ref OpenViduMasterNodeSG GroupId: !Ref OpenViduMasterNodeSG
@ -2049,7 +2012,7 @@ Resources:
ToPort: 6080 ToPort: 6080
SourceSecurityGroupId: !Ref OpenViduMasterNodeSG SourceSecurityGroupId: !Ref OpenViduMasterNodeSG
OpenViduMediaNodeToMasterMeetWebhookIngress: OpenViduMediaNodeToMasterDefaultAppWebhookIngress:
Type: AWS::EC2::SecurityGroupIngress Type: AWS::EC2::SecurityGroupIngress
Properties: Properties:
GroupId: !GetAtt OpenViduMasterNodeSG.GroupId GroupId: !GetAtt OpenViduMasterNodeSG.GroupId
@ -2081,14 +2044,6 @@ Resources:
FromPort: 443 FromPort: 443
ToPort: 443 ToPort: 443
CidrIpv6: ::/0 CidrIpv6: ::/0
- IpProtocol: tcp
FromPort: 7881
ToPort: 7881
CidrIp: 0.0.0.0/0
- IpProtocol: tcp
FromPort: 7881
ToPort: 7881
CidrIpv6: ::/0
- IpProtocol: udp - IpProtocol: udp
FromPort: 7885 FromPort: 7885
ToPort: 7885 ToPort: 7885
@ -2105,14 +2060,6 @@ Resources:
FromPort: 50000 FromPort: 50000
ToPort: 60000 ToPort: 60000
CidrIpv6: ::/0 CidrIpv6: ::/0
- IpProtocol: tcp
FromPort: 50000
ToPort: 60000
CidrIp: 0.0.0.0/0
- IpProtocol: tcp
FromPort: 50000
ToPort: 60000
CidrIpv6: ::/0
OpenViduLoadBalancerToMediaNodeRTMPIngressSG: OpenViduLoadBalancerToMediaNodeRTMPIngressSG:
Type: AWS::EC2::SecurityGroupIngress Type: AWS::EC2::SecurityGroupIngress
@ -2171,29 +2118,6 @@ Resources:
ToPort: 8080 ToPort: 8080
SourceSecurityGroupId: !Ref OpenViduMasterNodeSG SourceSecurityGroupId: !Ref OpenViduMasterNodeSG
# ---
# Experimental TURN TLS with main domain
OpenViduTurnTLSMasterNodeToMediaNodeIngressSG:
Type: AWS::EC2::SecurityGroupIngress
Condition: ExperimentalTurnTLSWithMainDomain
Properties:
GroupId: !Ref OpenViduMediaNodeSG
IpProtocol: tcp
FromPort: 5349
ToPort: 5349
SourceSecurityGroupId: !Ref OpenViduMasterNodeSG
OpenViduTurnTLSLoadBalancerToMediaNodeIngressSG:
Type: AWS::EC2::SecurityGroupIngress
Condition: ExperimentalTurnTLSWithMainDomain
Properties:
GroupId: !Ref OpenViduMasterNodeSG
IpProtocol: tcp
FromPort: 443
ToPort: 443
SourceSecurityGroupId: !Ref OpenViduLoadBalancerSG
# ---
OpenViduLoadBalancerSG: OpenViduLoadBalancerSG:
Type: AWS::EC2::SecurityGroup Type: AWS::EC2::SecurityGroup
Properties: Properties:
@ -2284,7 +2208,6 @@ Resources:
OpenViduMasterNodeListener: OpenViduMasterNodeListener:
Type: 'AWS::ElasticLoadBalancingV2::Listener' Type: 'AWS::ElasticLoadBalancingV2::Listener'
Condition: NotExperimentalTurnTLSWithMainDomain
Properties: Properties:
DefaultActions: DefaultActions:
- Type: forward - Type: forward
@ -2295,22 +2218,6 @@ Resources:
Certificates: Certificates:
- CertificateArn: !Ref OpenViduCertificateARN - CertificateArn: !Ref OpenViduCertificateARN
# ---
# Experimental TURN TLS with main domain
OpenViduMasterNodeWithTurnTLSListener:
Type: 'AWS::ElasticLoadBalancingV2::Listener'
Condition: ExperimentalTurnTLSWithMainDomain
Properties:
DefaultActions:
- Type: forward
TargetGroupArn: !Ref OpenViduMasterNodeWithTurnTLSTG
LoadBalancerArn: !Ref LoadBalancer
Port: 443
Protocol: TLS
Certificates:
- CertificateArn: !Ref OpenViduCertificateARN
# ---
OpenViduRTMPMediaNodeListener: OpenViduRTMPMediaNodeListener:
Type: 'AWS::ElasticLoadBalancingV2::Listener' Type: 'AWS::ElasticLoadBalancingV2::Listener'
Properties: Properties:
@ -2338,7 +2245,6 @@ Resources:
OpenViduMasterNodeTG: OpenViduMasterNodeTG:
Type: AWS::ElasticLoadBalancingV2::TargetGroup Type: AWS::ElasticLoadBalancingV2::TargetGroup
Condition: NotExperimentalTurnTLSWithMainDomain
Properties: Properties:
Name: Name:
Fn::Join: Fn::Join:
@ -2372,45 +2278,6 @@ Resources:
- Key: Name - Key: Name
Value: !Sub ${AWS::StackName} - OpenVidu HA - Master Target Group Value: !Sub ${AWS::StackName} - OpenVidu HA - Master Target Group
# ---
# Experimental TURN TLS with main domain
OpenViduMasterNodeWithTurnTLSTG:
Type: AWS::ElasticLoadBalancingV2::TargetGroup
Condition: ExperimentalTurnTLSWithMainDomain
Properties:
Name:
Fn::Join:
# Generate a not too long and unique target id
# Getting a unique identifier from the stack id
- ''
- - OVTurnTLSMaster-
- !Select [4, !Split ['-', !Select [2, !Split ['/', !Ref AWS::StackId]]]]
TargetType: instance
Targets:
- Id: !Ref OpenViduMasterNode1
- Id: !Ref OpenViduMasterNode2
- Id: !Ref OpenViduMasterNode3
- Id: !Ref OpenViduMasterNode4
VpcId: !Ref OpenViduVPC
Port: 443
Protocol: TCP
Matcher:
HttpCode: '200'
HealthCheckIntervalSeconds: 10
HealthCheckPath: /health/caddy
HealthCheckProtocol: HTTP
HealthCheckPort: '7880'
HealthCheckTimeoutSeconds: 5
HealthyThresholdCount: 3
UnhealthyThresholdCount: 4
TargetGroupAttributes:
- Key: deregistration_delay.timeout_seconds
Value: 60
Tags:
- Key: Name
Value: !Sub ${AWS::StackName} - OpenVidu HA - TURN TLS Master Target Group
# ---
OpenViduMediaNodeRTMPTG: OpenViduMediaNodeRTMPTG:
Type: AWS::ElasticLoadBalancingV2::TargetGroup Type: AWS::ElasticLoadBalancingV2::TargetGroup
Properties: Properties:

View File

@ -39,6 +39,12 @@ param turnOwnPublicCertificate string = ''
@description('(Optional) This setting is applicable if the certificate type is set to \'owncert\' and the TurnDomainName is specified.') @description('(Optional) This setting is applicable if the certificate type is set to \'owncert\' and the TurnDomainName is specified.')
param turnOwnPrivateCertificate string = '' param turnOwnPrivateCertificate string = ''
@description('Name of the PublicIPAddress resource in Azure when using TURN server with TLS')
param turnPublicIpAddressObject object = {
name: ''
id: ''
}
@description('Visit https://openvidu.io/account') @description('Visit https://openvidu.io/account')
@secure() @secure()
param openviduLicense string param openviduLicense string
@ -296,8 +302,6 @@ param maxNumberOfMediaNodes int = 5
@description('Target CPU percentage to scale up or down') @description('Target CPU percentage to scale up or down')
param scaleTargetCPU int = 50 param scaleTargetCPU int = 50
param additionalInstallFlags string = ''
/*------------------------------------------- VARIABLES AND VALIDATIONS -------------------------------------------*/ /*------------------------------------------- VARIABLES AND VALIDATIONS -------------------------------------------*/
var masterNodeVMSettings = { var masterNodeVMSettings = {
@ -438,7 +442,6 @@ var stringInterpolationParamsMaster1 = {
rtcEngine: rtcEngine rtcEngine: rtcEngine
keyVaultName: keyVaultName keyVaultName: keyVaultName
masterNodeNum: '1' masterNodeNum: '1'
additionalInstallFlags: additionalInstallFlags
} }
var stringInterpolationParamsMaster2 = { var stringInterpolationParamsMaster2 = {
@ -455,7 +458,6 @@ var stringInterpolationParamsMaster2 = {
rtcEngine: rtcEngine rtcEngine: rtcEngine
keyVaultName: keyVaultName keyVaultName: keyVaultName
masterNodeNum: '2' masterNodeNum: '2'
additionalInstallFlags: additionalInstallFlags
} }
var stringInterpolationParamsMaster3 = { var stringInterpolationParamsMaster3 = {
@ -472,7 +474,6 @@ var stringInterpolationParamsMaster3 = {
rtcEngine: rtcEngine rtcEngine: rtcEngine
keyVaultName: keyVaultName keyVaultName: keyVaultName
masterNodeNum: '3' masterNodeNum: '3'
additionalInstallFlags: additionalInstallFlags
} }
var stringInterpolationParamsMaster4 = { var stringInterpolationParamsMaster4 = {
@ -489,13 +490,12 @@ var stringInterpolationParamsMaster4 = {
rtcEngine: rtcEngine rtcEngine: rtcEngine
keyVaultName: keyVaultName keyVaultName: keyVaultName
masterNodeNum: '4' masterNodeNum: '4'
additionalInstallFlags: additionalInstallFlags
} }
var installScriptTemplateMaster = ''' var installScriptTemplateMaster = '''
#!/bin/bash -x #!/bin/bash -x
set -e set -e
OPENVIDU_VERSION=main OPENVIDU_VERSION=3.2.0
DOMAIN= DOMAIN=
# Assume azure cli is installed # Assume azure cli is installed
@ -571,13 +571,14 @@ if [[ $MASTER_NODE_NUM -eq 1 ]] && [[ "$ALL_SECRETS_GENERATED" == "" || "$ALL_SE
DASHBOARD_ADMIN_PASSWORD="$(/usr/local/bin/store_secret.sh generate DASHBOARD-ADMIN-PASSWORD)" 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_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)" 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")" DEFAULT_APP_USERNAME="$(/usr/local/bin/store_secret.sh save DEFAULT-APP-USERNAME "calluser")"
MEET_ADMIN_SECRET="$(/usr/local/bin/store_secret.sh generate MEET-ADMIN-SECRET)" DEFAULT_APP_PASSWORD="$(/usr/local/bin/store_secret.sh generate DEFAULT-APP-PASSWORD)"
MEET_API_KEY="$(/usr/local/bin/store_secret.sh generate MEET-API-KEY)" 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_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)" 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}")" 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")" ALL_SECRETS_GENERATED="$(/usr/local/bin/store_secret.sh save ALL-SECRETS-GENERATED "true")"
fi fi
@ -625,9 +626,10 @@ GRAFANA_ADMIN_USERNAME=$(az keyvault secret show --vault-name ${keyVaultName} --
GRAFANA_ADMIN_PASSWORD=$(az keyvault secret show --vault-name ${keyVaultName} --name GRAFANA-ADMIN-PASSWORD --query value -o tsv) GRAFANA_ADMIN_PASSWORD=$(az keyvault secret show --vault-name ${keyVaultName} --name GRAFANA-ADMIN-PASSWORD --query value -o tsv)
LIVEKIT_API_KEY=$(az keyvault secret show --vault-name ${keyVaultName} --name LIVEKIT-API-KEY --query value -o tsv) LIVEKIT_API_KEY=$(az keyvault secret show --vault-name ${keyVaultName} --name LIVEKIT-API-KEY --query value -o tsv)
LIVEKIT_API_SECRET=$(az keyvault secret show --vault-name ${keyVaultName} --name LIVEKIT-API-SECRET --query value -o tsv) LIVEKIT_API_SECRET=$(az keyvault secret show --vault-name ${keyVaultName} --name LIVEKIT-API-SECRET --query value -o tsv)
MEET_ADMIN_USER=$(az keyvault secret show --vault-name ${keyVaultName} --name MEET-ADMIN-USER --query value -o tsv) DEFAULT_APP_USERNAME=$(az keyvault secret show --vault-name ${keyVaultName} --name DEFAULT-APP-USERNAME --query value -o tsv)
MEET_ADMIN_SECRET=$(az keyvault secret show --vault-name ${keyVaultName} --name MEET-ADMIN-SECRET --query value -o tsv) DEFAULT_APP_PASSWORD=$(az keyvault secret show --vault-name ${keyVaultName} --name DEFAULT-APP-PASSWORD --query value -o tsv)
MEET_API_KEY=$(az keyvault secret show --vault-name ${keyVaultName} --name MEET-API-KEY --query value -o tsv) DEFAULT_APP_ADMIN_USERNAME=$(az keyvault secret show --vault-name ${keyVaultName} --name DEFAULT-APP-ADMIN-USERNAME --query value -o tsv)
DEFAULT_APP_ADMIN_PASSWORD=$(az keyvault secret show --vault-name ${keyVaultName} --name DEFAULT-APP-ADMIN-PASSWORD --query value -o tsv)
ENABLED_MODULES=$(az keyvault secret show --vault-name ${keyVaultName} --name ENABLED-MODULES --query value -o tsv) ENABLED_MODULES=$(az keyvault secret show --vault-name ${keyVaultName} --name ENABLED-MODULES --query value -o tsv)
@ -658,27 +660,16 @@ COMMON_ARGS=(
"--dashboard-admin-password=$DASHBOARD_ADMIN_PASSWORD" "--dashboard-admin-password=$DASHBOARD_ADMIN_PASSWORD"
"--grafana-admin-user=$GRAFANA_ADMIN_USERNAME" "--grafana-admin-user=$GRAFANA_ADMIN_USERNAME"
"--grafana-admin-password=$GRAFANA_ADMIN_PASSWORD" "--grafana-admin-password=$GRAFANA_ADMIN_PASSWORD"
"--meet-admin-user=$MEET_ADMIN_USER" "--default-app-user=$DEFAULT_APP_USERNAME"
"--meet-admin-password=$MEET_ADMIN_SECRET" "--default-app-password=$DEFAULT_APP_PASSWORD"
"--meet-api-key=$MEET_API_KEY" "--default-app-admin-user=$DEFAULT_APP_ADMIN_USERNAME"
"--default-app-admin-password=$DEFAULT_APP_ADMIN_PASSWORD"
"--livekit-api-key=$LIVEKIT_API_KEY" "--livekit-api-key=$LIVEKIT_API_KEY"
"--livekit-api-secret=$LIVEKIT_API_SECRET" "--livekit-api-secret=$LIVEKIT_API_SECRET"
) )
# Include additional installer flags provided by the user
if [[ "${additionalInstallFlags}" != "" ]]; then
IFS=',' read -ra EXTRA_FLAGS <<< "${additionalInstallFlags}"
for extra_flag in "${EXTRA_FLAGS[@]}"; do
# Trim whitespace around each flag
extra_flag="$(echo -e "${extra_flag}" | sed -e 's/^[ \t]*//' -e 's/[ \t]*$//')"
if [[ "$extra_flag" != "" ]]; then
COMMON_ARGS+=("$extra_flag")
fi
done
fi
if [[ $LIVEKIT_TURN_DOMAIN_NAME != "" ]]; then if [[ $LIVEKIT_TURN_DOMAIN_NAME != "" ]]; then
COMMON_ARGS+=("--turn-domain-name=$LIVEKIT_TURN_DOMAIN_NAME") COMMON_ARGS+=("--turn-domain-name=$LIVEKIT_TURN_DOMAIN_NAME}")
fi fi
# Certificate arguments # Certificate arguments
@ -801,9 +792,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 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_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 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 DEFAULT_APP_USERNAME=$(az keyvault secret show --vault-name ${keyVaultName} --name DEFAULT-APP-USERNAME --query value -o tsv)
export MEET_ADMIN_SECRET=$(az keyvault secret show --vault-name ${keyVaultName} --name MEET-ADMIN-SECRET --query value -o tsv) export DEFAULT_APP_PASSWORD=$(az keyvault secret show --vault-name ${keyVaultName} --name DEFAULT-APP-PASSWORD --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_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) export ENABLED_MODULES=$(az keyvault secret show --vault-name ${keyVaultName} --name ENABLED-MODULES --query value -o tsv)
# Replace rest of the values # Replace rest of the values
@ -821,9 +813,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/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_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/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/CALL_USER=.*/CALL_USER=$DEFAULT_APP_USERNAME/" "${CLUSTER_CONFIG_DIR}/master_node/app.env"
sed -i "s/MEET_ADMIN_SECRET=.*/MEET_ADMIN_SECRET=$MEET_ADMIN_SECRET/" "${CLUSTER_CONFIG_DIR}/master_node/meet.env" sed -i "s/CALL_SECRET=.*/CALL_SECRET=$DEFAULT_APP_PASSWORD/" "${CLUSTER_CONFIG_DIR}/master_node/app.env"
sed -i "s/MEET_API_KEY=.*/MEET_API_KEY=$MEET_API_KEY/" "${CLUSTER_CONFIG_DIR}/master_node/meet.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" sed -i "s/ENABLED_MODULES=.*/ENABLED_MODULES=$ENABLED_MODULES/" "${CLUSTER_CONFIG_DIR}/openvidu.env"
# Update URLs in secret # Update URLs in secret
@ -871,9 +864,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")" 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_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")" 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")" DEFAULT_APP_USERNAME="$(/usr/local/bin/get_value_from_config.sh CALL_USER "${CLUSTER_CONFIG_DIR}/master_node/app.env")"
MEET_ADMIN_SECRET="$(/usr/local/bin/get_value_from_config.sh MEET_ADMIN_SECRET "${CLUSTER_CONFIG_DIR}/master_node/meet.env")" DEFAULT_APP_PASSWORD="$(/usr/local/bin/get_value_from_config.sh CALL_SECRET "${CLUSTER_CONFIG_DIR}/master_node/app.env")"
MEET_API_KEY="$(/usr/local/bin/get_value_from_config.sh MEET_API_KEY "${CLUSTER_CONFIG_DIR}/master_node/meet.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")" ENABLED_MODULES="$(/usr/local/bin/get_value_from_config.sh ENABLED_MODULES "${CLUSTER_CONFIG_DIR}/openvidu.env")"
# Update shared secret # Update shared secret
@ -893,9 +887,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 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-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 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 DEFAULT-APP-USERNAME --value $DEFAULT_APP_USERNAME
az keyvault secret set --vault-name ${keyVaultName} --name MEET-ADMIN-SECRET --value $MEET_ADMIN_SECRET az keyvault secret set --vault-name ${keyVaultName} --name DEFAULT-APP-PASSWORD --value $DEFAULT_APP_PASSWORD
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-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 az keyvault secret set --vault-name ${keyVaultName} --name ENABLED-MODULES --value $ENABLED_MODULES
''' '''
@ -1065,10 +1060,7 @@ var store_secretScriptMaster = reduce(
var blobStorageParams = { var blobStorageParams = {
storageAccountName: isEmptyStorageAccountName ? storageAccount.name : existingStorageAccount.name storageAccountName: isEmptyStorageAccountName ? storageAccount.name : existingStorageAccount.name
storageAccountKey: listKeys(storageAccount.id, '2021-04-01').keys[0].value storageAccountKey: listKeys(storageAccount.id, '2021-04-01').keys[0].value
storageAccountContainerName: isEmptyAppDataContainerName ? 'openvidu-appdata' : '${appDataContainerName}' storageAccountContainerName: isEmptyContainerName ? 'openvidu-appdata' : '${containerName}'
storageAccountClusterContainerName: isEmptyClusterContainerName
? 'openvidu-clusterdata'
: '${clusterDataContainerName}'
} }
var config_blobStorageScript = reduce( var config_blobStorageScript = reduce(
@ -1497,13 +1489,9 @@ if [ -x "$(command -v docker)" ]; then
docker container kill --signal=SIGQUIT openvidu || true docker container kill --signal=SIGQUIT openvidu || true
docker container kill --signal=SIGQUIT ingress || true docker container kill --signal=SIGQUIT ingress || true
docker container kill --signal=SIGQUIT egress || true docker container kill --signal=SIGQUIT egress || true
for agent_container in $(docker ps --filter "label=openvidu-agent=true" --format '{{.Names}}'); do
docker container kill --signal=SIGQUIT "$agent_container"
done
# Wait for running containers to not be openvidu, ingress or egress # Wait for running containers to not be openvidu, ingress or egress
while [ $(docker ps --filter "label=openvidu-agent=true" -q | wc -l) -gt 0 ] || \ while [ $(docker inspect -f '{{.State.Running}}' openvidu 2>/dev/null) == "true" ] || \
[ $(docker inspect -f '{{.State.Running}}' openvidu 2>/dev/null) == "true" ] || \
[ $(docker inspect -f '{{.State.Running}}' ingress 2>/dev/null) == "true" ] || \ [ $(docker inspect -f '{{.State.Running}}' ingress 2>/dev/null) == "true" ] || \
[ $(docker inspect -f '{{.State.Running}}' egress 2>/dev/null) == "true" ]; do [ $(docker inspect -f '{{.State.Running}}' egress 2>/dev/null) == "true" ]; do
echo "Waiting for containers to stop..." echo "Waiting for containers to stop..."
@ -1523,21 +1511,6 @@ RESOURCE_ID=/subscriptions/$SUBSCRIPTION_ID/resourcegroups/$RESOURCE_GROUP_NAME/
TIMESTAMP=$(date -u +"%Y-%m-%dT%H:%M:%SZ") TIMESTAMP=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
az tag update --resource-id $RESOURCE_ID --operation replace --tags "STATUS"="HEALTHY" "InstanceDeleteTime"="$TIMESTAMP" "storageAccount"="${storageAccountName}" az tag update --resource-id $RESOURCE_ID --operation replace --tags "STATUS"="HEALTHY" "InstanceDeleteTime"="$TIMESTAMP" "storageAccount"="${storageAccountName}"
az vmss delete-instances --resource-group $RESOURCE_GROUP_NAME --name $VM_SCALE_SET_NAME --instance-ids $INSTANCE_ID
'''
var delete_mediaNode_ScriptMediaTemplate = '''
#!/bin/bash
set -e
az login --identity
RESOURCE_GROUP_NAME=${resourceGroupName}
VM_SCALE_SET_NAME=${vmScaleSetName}
BEFORE_INSTANCE_ID=$(curl -H Metadata:true --noproxy "*" "http://169.254.169.254/metadata/instance?api-version=2021-02-01" | jq -r '.compute.resourceId')
INSTANCE_ID=$(echo $BEFORE_INSTANCE_ID | awk -F'/' '{print $NF}')
az vmss delete-instances --resource-group $RESOURCE_GROUP_NAME --name $VM_SCALE_SET_NAME --instance-ids $INSTANCE_ID az vmss delete-instances --resource-group $RESOURCE_GROUP_NAME --name $VM_SCALE_SET_NAME --instance-ids $INSTANCE_ID
''' '''
@ -1572,10 +1545,10 @@ az vmss update --resource-group $RESOURCE_GROUP_NAME --name $VM_SCALE_SET_NAME -
export HOME="/root" export HOME="/root"
# Install OpenVidu # Install OpenVidu
/usr/local/bin/install.sh || { echo "[OpenVidu] error installing OpenVidu"; /usr/local/bin/delete_media_node.sh; } /usr/local/bin/install.sh || { echo "[OpenVidu] error installing OpenVidu"; exit 1; }
# Start OpenVidu # Start OpenVidu
systemctl start openvidu || { echo "[OpenVidu] error starting OpenVidu"; /usr/local/bin/delete_media_node.sh; } systemctl start openvidu || { echo "[OpenVidu] error starting OpenVidu"; exit 1; }
''' '''
var installScriptMedia = reduce( var installScriptMedia = reduce(
@ -1590,20 +1563,12 @@ var stop_media_nodesScriptMedia = reduce(
(curr, next) => { value: replace(curr.value, '\${${next.key}}', next.value) } (curr, next) => { value: replace(curr.value, '\${${next.key}}', next.value) }
).value ).value
var delete_mediaNode_ScriptMedia = reduce(
items(stopMediaNodeParams),
{ value: delete_mediaNode_ScriptMediaTemplate },
(curr, next) => { value: replace(curr.value, '\${${next.key}}', next.value) }
).value
var base64installMedia = base64(installScriptMedia) var base64installMedia = base64(installScriptMedia)
var base64stopMediaNode = base64(stop_media_nodesScriptMedia) var base64stopMediaNode = base64(stop_media_nodesScriptMedia)
var base64delete_mediaNode_ScriptMedia = base64(delete_mediaNode_ScriptMedia)
var userDataParamsMedia = { var userDataParamsMedia = {
base64install: base64installMedia base64install: base64installMedia
base64stop: base64stopMediaNode base64stop: base64stopMediaNode
base64delete_mediaNode: base64delete_mediaNode_ScriptMedia
resourceGroupName: resourceGroup().name resourceGroupName: resourceGroup().name
vmScaleSetName: '${stackName}-mediaNodeScaleSet' vmScaleSetName: '${stackName}-mediaNodeScaleSet'
} }
@ -1900,6 +1865,7 @@ resource scaleInActivityLogRule 'Microsoft.Insights/activityLogAlerts@2020-10-01
/*------------------------------------------- NETWORK -------------------------------------------*/ /*------------------------------------------- NETWORK -------------------------------------------*/
var isEmptyIp = publicIpAddressObject.newOrExistingOrNone == 'none' var isEmptyIp = publicIpAddressObject.newOrExistingOrNone == 'none'
var turnIsEmptyIp = turnPublicIpAddressObject.newOrExistingOrNone == 'none'
var lbName = '${stackName}-loadBalancer' var lbName = '${stackName}-loadBalancer'
var lbFrontEndName = 'LoadBalancerFrontEnd' var lbFrontEndName = 'LoadBalancerFrontEnd'
var lbBackendPoolNameMasterNode = 'LoadBalancerBackEndMasterNode' var lbBackendPoolNameMasterNode = 'LoadBalancerBackEndMasterNode'
@ -1928,6 +1894,32 @@ resource publicIP_LoadBalancer_ifNew 'Microsoft.Network/publicIPAddresses@2023-1
name: publicIpAddressObject.name name: publicIpAddressObject.name
} }
var ipTURNEmpty = turnPublicIpAddressObject.newOrExistingOrNone == 'none'
resource publicIPAddressTurnTLSLoadBalancer 'Microsoft.Network/publicIPAddresses@2024-05-01' = if (ipTURNEmpty && turnTLSIsEnabled == true) {
name: '${stackName}-publicIPAddressTurnTLSLoadBalancer'
location: location
sku: {
name: 'Standard'
}
properties: {
publicIPAddressVersion: 'IPv4'
publicIPAllocationMethod: 'Static'
}
}
var ipTURNExists = turnPublicIpAddressObject.newOrExistingOrNone == 'existing'
resource publicIP_TurnTLSLoadBalancer_ifExisting 'Microsoft.Network/publicIPAddresses@2023-11-01' existing = if (ipTURNExists && turnTLSIsEnabled == true) {
name: turnPublicIpAddressObject.name
}
var ipTURNNew = turnPublicIpAddressObject.newOrExistingOrNone == 'new'
resource publicIP_TurnTLSLoadBalancer_ifNew 'Microsoft.Network/publicIPAddresses@2023-11-01' existing = if (ipTURNNew && turnTLSIsEnabled == true) {
name: turnPublicIpAddressObject.name
}
resource LoadBalancer 'Microsoft.Network/loadBalancers@2024-05-01' = { resource LoadBalancer 'Microsoft.Network/loadBalancers@2024-05-01' = {
name: lbName name: lbName
location: location location: location
@ -2043,6 +2035,74 @@ resource LoadBalancer 'Microsoft.Network/loadBalancers@2024-05-01' = {
} }
} }
var tlbName = '${stackName}-turnloadBalancer'
var tlbFrontEndName = 'TurnLoadBalancerFrontEnd'
resource TurnTLSLoadbalancer 'Microsoft.Network/loadBalancers@2024-05-01' = if (turnTLSIsEnabled == true) {
name: tlbName
location: location
sku: {
name: 'Standard'
}
properties: {
frontendIPConfigurations: [
{
name: tlbFrontEndName
properties: {
privateIPAllocationMethod: 'Dynamic'
privateIPAddressVersion: 'IPv4'
publicIPAddress: {
id: turnIsEmptyIp
? publicIPAddressTurnTLSLoadBalancer.id
: ipTURNNew ? publicIP_TurnTLSLoadBalancer_ifNew.id : publicIP_TurnTLSLoadBalancer_ifExisting.id
}
}
}
]
backendAddressPools: [
{
name: lbBackendPoolNameMasterNode
}
]
loadBalancingRules: [
{
name: 'TURNTLSRuleforMasterNode'
properties: {
frontendIPConfiguration: {
id: resourceId('Microsoft.Network/loadBalancers/frontendIPConfigurations', tlbName, tlbFrontEndName)
}
backendAddressPool: {
id: resourceId('Microsoft.Network/loadBalancers/backendAddressPools', tlbName, lbBackendPoolNameMasterNode)
}
frontendPort: 443
backendPort: 443
enableFloatingIP: false
protocol: 'Tcp'
enableTcpReset: true
loadDistribution: 'Default'
disableOutboundSnat: true
probe: {
id: resourceId('Microsoft.Network/loadBalancers/probes', tlbName, 'probeForHTTPSRuleMasterNode')
}
}
}
]
probes: [
{
name: 'probeForTURNTLSRuleMasterNode'
properties: {
protocol: 'Http'
requestPath: '/'
port: 443
probeThreshold: 3
intervalInSeconds: 10
numberOfProbes: 5
}
}
]
}
}
resource natGateway 'Microsoft.Network/natGateways@2021-05-01' = { resource natGateway 'Microsoft.Network/natGateways@2021-05-01' = {
name: '${stackName}-natGateway' name: '${stackName}-natGateway'
location: location location: location
@ -2129,9 +2189,6 @@ resource subnetMasterNode2 'Microsoft.Network/virtualNetworks/subnets@2023-11-01
id: natGateway.id id: natGateway.id
} }
} }
dependsOn: [
subnetMasterNode1
]
} }
resource netInterfaceMasterNode1 'Microsoft.Network/networkInterfaces@2023-11-01' = { resource netInterfaceMasterNode1 'Microsoft.Network/networkInterfaces@2023-11-01' = {
@ -2721,9 +2778,9 @@ resource mediaToMasterV2CompatibilityWebhookIngress 'Microsoft.Network/networkSe
} }
} }
resource masterToMasterMeet 'Microsoft.Network/networkSecurityGroups/securityRules@2023-11-01' = { resource masterToMasterDefaultApp 'Microsoft.Network/networkSecurityGroups/securityRules@2023-11-01' = {
parent: openviduMasterNodeNSG parent: openviduMasterNodeNSG
name: 'masterNode_to_masterNode_MEET_INGRESS' name: 'masterNode_to_masterNode_DEFAULTAPP_INGRESS'
properties: { properties: {
protocol: 'Tcp' protocol: 'Tcp'
sourceApplicationSecurityGroups: [ sourceApplicationSecurityGroups: [
@ -2744,9 +2801,9 @@ resource masterToMasterMeet 'Microsoft.Network/networkSecurityGroups/securityRul
} }
} }
resource mediaToMasterMeetWebhookIngress 'Microsoft.Network/networkSecurityGroups/securityRules@2023-11-01' = { resource mediaToMasterDefaultAppWebhookIngress 'Microsoft.Network/networkSecurityGroups/securityRules@2023-11-01' = {
parent: openviduMasterNodeNSG parent: openviduMasterNodeNSG
name: 'mediaNode_to_masterNode_MEET_WEBHOOK_INGRESS' name: 'mediaNode_to_masterNode_DEFAULTAPP_WEBHOOK_INGRESS'
properties: { properties: {
protocol: 'Tcp' protocol: 'Tcp'
sourceApplicationSecurityGroups: [ sourceApplicationSecurityGroups: [
@ -2840,22 +2897,6 @@ resource openviduMediaNodeNSG 'Microsoft.Network/networkSecurityGroups@2023-11-0
direction: 'Inbound' direction: 'Inbound'
} }
} }
{
name: 'WebRTC_traffic_TCP'
properties: {
protocol: 'Tcp'
sourceAddressPrefix: '*'
sourcePortRange: '*'
destinationAddressPrefix: '*'
destinationPortRanges: [
'50000'
'60000'
]
access: 'Allow'
priority: 150
direction: 'Inbound'
}
}
] ]
} }
} }
@ -2879,7 +2920,7 @@ resource loadBalancerToMediaRtmpIngress 'Microsoft.Network/networkSecurityGroups
] ]
destinationPortRange: '1945' destinationPortRange: '1945'
access: 'Allow' access: 'Allow'
priority: 160 priority: 150
direction: 'Inbound' direction: 'Inbound'
} }
} }
@ -2898,7 +2939,7 @@ resource loadBalancerToMediaHealthcheckIngress 'Microsoft.Network/networkSecurit
] ]
destinationPortRange: '9092' destinationPortRange: '9092'
access: 'Allow' access: 'Allow'
priority: 170 priority: 160
direction: 'Inbound' direction: 'Inbound'
} }
} }
@ -2917,7 +2958,7 @@ resource loadBalancerToMediaTurnTlsIngress 'Microsoft.Network/networkSecurityGro
] ]
destinationPortRange: '5349' destinationPortRange: '5349'
access: 'Allow' access: 'Allow'
priority: 180 priority: 170
direction: 'Inbound' direction: 'Inbound'
} }
} }
@ -2936,7 +2977,7 @@ resource loadBalancerToMediaTurnTlsHealthCheckIngress 'Microsoft.Network/network
] ]
destinationPortRange: '7880' destinationPortRange: '7880'
access: 'Allow' access: 'Allow'
priority: 190 priority: 180
direction: 'Inbound' direction: 'Inbound'
} }
} }
@ -2959,7 +3000,7 @@ resource masterToMediaServerIngress 'Microsoft.Network/networkSecurityGroups/sec
] ]
destinationPortRange: '7880' destinationPortRange: '7880'
access: 'Allow' access: 'Allow'
priority: 200 priority: 190
direction: 'Inbound' direction: 'Inbound'
} }
} }
@ -2982,7 +3023,7 @@ resource masterToMediaClientIngress 'Microsoft.Network/networkSecurityGroups/sec
] ]
destinationPortRange: '8080' destinationPortRange: '8080'
access: 'Allow' access: 'Allow'
priority: 210 priority: 200
direction: 'Inbound' direction: 'Inbound'
} }
} }
@ -3019,28 +3060,14 @@ resource blobContainerScaleIn 'Microsoft.Storage/storageAccounts/blobServices/co
} }
@description('Name of the bucket where OpenVidu will store the recordings if a new Storage account is being creating. If not specified, a default bucket will be created. If you want to use an existing storage account, fill this parameter with the name of the container where the recordings are stored.') @description('Name of the bucket where OpenVidu will store the recordings if a new Storage account is being creating. If not specified, a default bucket will be created. If you want to use an existing storage account, fill this parameter with the name of the container where the recordings are stored.')
param appDataContainerName string = '' param containerName string = ''
var isEmptyAppDataContainerName = appDataContainerName == '' var isEmptyContainerName = containerName == ''
resource blobContainer 'Microsoft.Storage/storageAccounts/blobServices/containers@2023-01-01' = if (isEmptyStorageAccountName == true) { resource blobContainer 'Microsoft.Storage/storageAccounts/blobServices/containers@2023-01-01' = if (isEmptyStorageAccountName == true) {
name: isEmptyAppDataContainerName name: isEmptyContainerName
? '${storageAccount.name}/default/openvidu-appdata' ? '${storageAccount.name}/default/openvidu-appdata'
: '${storageAccount.name}/default/${appDataContainerName}' : '${storageAccount.name}/default/${containerName}'
properties: {
publicAccess: 'None'
}
}
@description('Name of the bucket where OpenVidu will store the recordings if a new Storage account is being creating. If not specified, a default bucket will be created. If you want to use an existing storage account, fill this parameter with the name of the container where the recordings are stored.')
param clusterDataContainerName string = ''
var isEmptyClusterContainerName = clusterDataContainerName == ''
resource clusterDatablobContainer 'Microsoft.Storage/storageAccounts/blobServices/containers@2023-01-01' = if (isEmptyStorageAccountName == true) {
name: isEmptyClusterContainerName
? '${storageAccount.name}/default/openvidu-clusterdata'
: '${storageAccount.name}/default/${clusterDataContainerName}'
properties: { properties: {
publicAccess: 'None' publicAccess: 'None'
} }

File diff suppressed because one or more lines are too long

View File

@ -473,28 +473,6 @@
} }
] ]
}, },
{
"name": "FLAGS",
"label": "(Optional) Additional Install Flags",
"elements": [
{
"name": "additionalInstallFlags",
"type": "Microsoft.Common.TextBox",
"label": "Additional Install Flags",
"subLabel": "Additional optional flags to pass to the OpenVidu installer (comma-separated, e.g., \"--flag1=value, --flag2\")",
"defaultValue": "",
"toolTip": "",
"constraints": {
"required": false,
"regex": "^[A-Za-z0-9, =_.\\-]*$",
"validationMessage": "Must be a comma-separated list of flags (for example, --flag=value, --bool-flag)",
"validations": []
},
"infoMessages": [],
"visible": true
}
]
},
{ {
"name": "parameters TURN", "name": "parameters TURN",
"label": "(Optional) TURN server configuration with TLS", "label": "(Optional) TURN server configuration with TLS",
@ -556,6 +534,25 @@
}, },
"infoMessages": [], "infoMessages": [],
"visible": true "visible": true
},
{
"name": "turnPublicIpAddressObject",
"type": "Microsoft.Network.PublicIpAddressCombo",
"label": {
"publicIpAddress": "Turn Public Ip Address"
},
"toolTip": {
"publicIpAddress": "Name of the PublicIPAddress resource in Azure when using TURN server with TLS"
},
"defaultValue": {
"publicIpAddressName": "defaultName"
},
"options": {
"hideNone": false,
"hideDomainNameLabel": true,
"hideExisting": false
},
"visible": true
} }
] ]
} }
@ -576,6 +573,7 @@
"turnDomainName": "[steps('parameters TURN').turnDomainName]", "turnDomainName": "[steps('parameters TURN').turnDomainName]",
"turnOwnPublicCertificate": "[steps('parameters TURN').turnOwnPublicCertificate]", "turnOwnPublicCertificate": "[steps('parameters TURN').turnOwnPublicCertificate]",
"turnOwnPrivateCertificate": "[steps('parameters TURN').turnOwnPrivateCertificate]", "turnOwnPrivateCertificate": "[steps('parameters TURN').turnOwnPrivateCertificate]",
"turnPublicIpAddressObject": "[steps('parameters TURN').turnPublicIpAddressObject]",
"openviduLicense": "[steps('parameters OPENVIDU').openviduLicense]", "openviduLicense": "[steps('parameters OPENVIDU').openviduLicense]",
"rtcEngine": "[steps('parameters OPENVIDU').rtcEngine]", "rtcEngine": "[steps('parameters OPENVIDU').rtcEngine]",
"masterNodeInstanceType": "[steps('parameters INSTANCE').masterNodeInstanceType]", "masterNodeInstanceType": "[steps('parameters INSTANCE').masterNodeInstanceType]",
@ -590,8 +588,7 @@
"datetime": "[steps('parameters SCALING').datetime]", "datetime": "[steps('parameters SCALING').datetime]",
"automationAccountName": "[steps('parameters SCALING').automationAccountName]", "automationAccountName": "[steps('parameters SCALING').automationAccountName]",
"storageAccountName": "[steps('parameters STORAGE').storageAccountName]", "storageAccountName": "[steps('parameters STORAGE').storageAccountName]",
"containerName": "[steps('parameters STORAGE').containerName]", "containerName": "[steps('parameters STORAGE').containerName]"
"additionalInstallFlags": "[steps('FLAGS').additionalInstallFlags]"
} }
} }
} }

View File

@ -3,9 +3,9 @@
set -eu set -eu
export DOCKER_VERSION="${DOCKER_VERSION:-28.1.1}" export DOCKER_VERSION="${DOCKER_VERSION:-28.1.1}"
export DOCKER_COMPOSE_VERSION="${DOCKER_COMPOSE_VERSION:-v2.35.1}" export DOCKER_COMPOSE_VERSION="${DOCKER_COMPOSE_VERSION:-v2.35.1}"
export OPENVIDU_VERSION="${OPENVIDU_VERSION:-main}" export OPENVIDU_VERSION="${OPENVIDU_VERSION:-3.2.0}"
export INSTALLER_IMAGE="${INSTALLER_IMAGE:-docker.io/openvidu/openvidu-installer:${OPENVIDU_VERSION}}" 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 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 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}" export REDIS_SERVER_IMAGE="${REDIS_SERVER_IMAGE:-docker.io/redis:7.4.4-alpine}"
@ -15,17 +15,16 @@ 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_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_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_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_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_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_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_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.9.1}" 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 PROMETHEUS_IMAGE="${PROMETHEUS_IMAGE:-docker.io/prom/prometheus:v3.4.0}"
export PROMTAIL_IMAGE="${PROMTAIL_IMAGE:-docker.io/grafana/promtail:3.5.1}" 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 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}" export GRAFANA_IMAGE="${GRAFANA_IMAGE:-docker.io/grafana/grafana:11.6.2}"
# Function to compare two version strings # Function to compare two version strings
@ -181,11 +180,10 @@ COMMON_DOCKER_OPTIONS="--network=host -v ${TMP_DIR}:/output \
-e OPENVIDU_OPERATOR_IMAGE=$OPENVIDU_OPERATOR_IMAGE \ -e OPENVIDU_OPERATOR_IMAGE=$OPENVIDU_OPERATOR_IMAGE \
-e OPENVIDU_SERVER_PRO_IMAGE=$OPENVIDU_SERVER_PRO_IMAGE \ -e OPENVIDU_SERVER_PRO_IMAGE=$OPENVIDU_SERVER_PRO_IMAGE \
-e OPENVIDU_SERVER_IMAGE=$OPENVIDU_SERVER_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_PRO_IMAGE=$OPENVIDU_DASHBOARD_PRO_IMAGE \
-e OPENVIDU_DASHBOARD_IMAGE=$OPENVIDU_DASHBOARD_IMAGE \ -e OPENVIDU_DASHBOARD_IMAGE=$OPENVIDU_DASHBOARD_IMAGE \
-e OPENVIDU_V2COMPATIBILITY_IMAGE=$OPENVIDU_V2COMPATIBILITY_IMAGE \ -e OPENVIDU_V2COMPATIBILITY_IMAGE=$OPENVIDU_V2COMPATIBILITY_IMAGE \
-e OPENVIDU_AGENT_SPEECH_PROCESSING_IMAGE=$OPENVIDU_AGENT_SPEECH_PROCESSING_IMAGE \
-e LIVEKIT_INGRESS_SERVER_IMAGE=$LIVEKIT_INGRESS_SERVER_IMAGE \ -e LIVEKIT_INGRESS_SERVER_IMAGE=$LIVEKIT_INGRESS_SERVER_IMAGE \
-e LIVEKIT_EGRESS_SERVER_IMAGE=$LIVEKIT_EGRESS_SERVER_IMAGE \ -e LIVEKIT_EGRESS_SERVER_IMAGE=$LIVEKIT_EGRESS_SERVER_IMAGE \
-e PROMETHEUS_IMAGE=$PROMETHEUS_IMAGE \ -e PROMETHEUS_IMAGE=$PROMETHEUS_IMAGE \

View File

@ -3,9 +3,9 @@
set -eu set -eu
export DOCKER_VERSION="${DOCKER_VERSION:-28.1.1}" export DOCKER_VERSION="${DOCKER_VERSION:-28.1.1}"
export DOCKER_COMPOSE_VERSION="${DOCKER_COMPOSE_VERSION:-v2.35.1}" export DOCKER_COMPOSE_VERSION="${DOCKER_COMPOSE_VERSION:-v2.35.1}"
export OPENVIDU_VERSION="${OPENVIDU_VERSION:-main}" export OPENVIDU_VERSION="${OPENVIDU_VERSION:-3.2.0}"
export INSTALLER_IMAGE="${INSTALLER_IMAGE:-docker.io/openvidu/openvidu-installer:${OPENVIDU_VERSION}}" 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 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 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}" export REDIS_SERVER_IMAGE="${REDIS_SERVER_IMAGE:-docker.io/redis:7.4.4-alpine}"
@ -15,17 +15,16 @@ 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_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_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_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_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_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_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_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.9.1}" 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 PROMETHEUS_IMAGE="${PROMETHEUS_IMAGE:-docker.io/prom/prometheus:v3.4.0}"
export PROMTAIL_IMAGE="${PROMTAIL_IMAGE:-docker.io/grafana/promtail:3.5.1}" 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 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}" export GRAFANA_IMAGE="${GRAFANA_IMAGE:-docker.io/grafana/grafana:11.6.2}"
# Function to compare two version strings # Function to compare two version strings
@ -181,11 +180,10 @@ COMMON_DOCKER_OPTIONS="--network=host -v ${TMP_DIR}:/output \
-e OPENVIDU_OPERATOR_IMAGE=$OPENVIDU_OPERATOR_IMAGE \ -e OPENVIDU_OPERATOR_IMAGE=$OPENVIDU_OPERATOR_IMAGE \
-e OPENVIDU_SERVER_PRO_IMAGE=$OPENVIDU_SERVER_PRO_IMAGE \ -e OPENVIDU_SERVER_PRO_IMAGE=$OPENVIDU_SERVER_PRO_IMAGE \
-e OPENVIDU_SERVER_IMAGE=$OPENVIDU_SERVER_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_PRO_IMAGE=$OPENVIDU_DASHBOARD_PRO_IMAGE \
-e OPENVIDU_DASHBOARD_IMAGE=$OPENVIDU_DASHBOARD_IMAGE \ -e OPENVIDU_DASHBOARD_IMAGE=$OPENVIDU_DASHBOARD_IMAGE \
-e OPENVIDU_V2COMPATIBILITY_IMAGE=$OPENVIDU_V2COMPATIBILITY_IMAGE \ -e OPENVIDU_V2COMPATIBILITY_IMAGE=$OPENVIDU_V2COMPATIBILITY_IMAGE \
-e OPENVIDU_AGENT_SPEECH_PROCESSING_IMAGE=$OPENVIDU_AGENT_SPEECH_PROCESSING_IMAGE \
-e LIVEKIT_INGRESS_SERVER_IMAGE=$LIVEKIT_INGRESS_SERVER_IMAGE \ -e LIVEKIT_INGRESS_SERVER_IMAGE=$LIVEKIT_INGRESS_SERVER_IMAGE \
-e LIVEKIT_EGRESS_SERVER_IMAGE=$LIVEKIT_EGRESS_SERVER_IMAGE \ -e LIVEKIT_EGRESS_SERVER_IMAGE=$LIVEKIT_EGRESS_SERVER_IMAGE \
-e PROMETHEUS_IMAGE=$PROMETHEUS_IMAGE \ -e PROMETHEUS_IMAGE=$PROMETHEUS_IMAGE \

View File

@ -41,13 +41,6 @@ Parameters:
Description: "If certificate type is 'letsencrypt', this email will be used for Let's Encrypt notifications" Description: "If certificate type is 'letsencrypt', this email will be used for Let's Encrypt notifications"
Type: String Type: String
AdditionalInstallFlags:
Description: Additional optional flags to pass to the OpenVidu installer (comma-separated, e.g., "--flag1=value, --flag2").
Type: String
Default: ""
AllowedPattern: '^[A-Za-z0-9, =_.\-]*$' # Allows letters, numbers, comma, space, underscore, dot, equals, and hyphen
ConstraintDescription: Must be a comma-separated list of flags (for example, --flag=value, --bool-flag).
TurnDomainName: TurnDomainName:
Description: '(Optional) Domain name for the TURN server with TLS. Only needed if your users are behind restrictive firewalls' Description: '(Optional) Domain name for the TURN server with TLS. Only needed if your users are behind restrictive firewalls'
Type: String Type: String
@ -247,10 +240,6 @@ Metadata:
default: S3 bucket for application data and recordings default: S3 bucket for application data and recordings
Parameters: Parameters:
- S3AppDataBucketName - S3AppDataBucketName
- Label:
default: "(Optional) Additional Installer Flags"
Parameters:
- AdditionalInstallFlags
- Label: - Label:
default: (Optional) TURN server configuration with TLS default: (Optional) TURN server configuration with TLS
Parameters: Parameters:
@ -293,9 +282,10 @@ Resources:
"GRAFANA_ADMIN_PASSWORD": "none", "GRAFANA_ADMIN_PASSWORD": "none",
"LIVEKIT_API_KEY": "none", "LIVEKIT_API_KEY": "none",
"LIVEKIT_API_SECRET": "none", "LIVEKIT_API_SECRET": "none",
"MEET_ADMIN_USER": "none", "DEFAULT_APP_USERNAME": "none",
"MEET_ADMIN_SECRET": "none", "DEFAULT_APP_PASSWORD": "none",
"MEET_API_KEY": "none", "DEFAULT_APP_ADMIN_USERNAME": "none",
"DEFAULT_APP_ADMIN_PASSWORD": "none",
"ENABLED_MODULES": "none" "ENABLED_MODULES": "none"
} }
@ -388,7 +378,7 @@ Resources:
'/usr/local/bin/install.sh': '/usr/local/bin/install.sh':
content: !Sub | content: !Sub |
#!/bin/bash -x #!/bin/bash -x
OPENVIDU_VERSION=main OPENVIDU_VERSION=3.2.0
DOMAIN= DOMAIN=
YQ_VERSION=v4.44.5 YQ_VERSION=v4.44.5
@ -433,10 +423,11 @@ Resources:
DASHBOARD_ADMIN_PASSWORD="$(/usr/local/bin/store_secret.sh generate DASHBOARD_ADMIN_PASSWORD)" 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_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)" 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")" DEFAULT_APP_USERNAME="$(/usr/local/bin/store_secret.sh save DEFAULT_APP_USERNAME "calluser")"
MEET_ADMIN_SECRET="$(/usr/local/bin/store_secret.sh generate MEET_ADMIN_SECRET)" DEFAULT_APP_PASSWORD="$(/usr/local/bin/store_secret.sh generate DEFAULT_APP_PASSWORD)"
MEET_API_KEY="$(/usr/local/bin/store_secret.sh generate MEET_API_KEY)" DEFAULT_APP_ADMIN_USERNAME="$(/usr/local/bin/store_secret.sh save DEFAULT_APP_ADMIN_USERNAME "calladmin")"
ENABLED_MODULES="$(/usr/local/bin/store_secret.sh save ENABLED_MODULES "observability,openviduMeet,v2compatibility")" 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,v2compatibility")"
LIVEKIT_API_KEY="$(/usr/local/bin/store_secret.sh generate LIVEKIT_API_KEY "API" 12)" 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)" LIVEKIT_API_SECRET="$(/usr/local/bin/store_secret.sh generate LIVEKIT_API_SECRET)"
@ -463,25 +454,14 @@ Resources:
"--dashboard-admin-password=$DASHBOARD_ADMIN_PASSWORD" "--dashboard-admin-password=$DASHBOARD_ADMIN_PASSWORD"
"--grafana-admin-user=$GRAFANA_ADMIN_USERNAME" "--grafana-admin-user=$GRAFANA_ADMIN_USERNAME"
"--grafana-admin-password=$GRAFANA_ADMIN_PASSWORD" "--grafana-admin-password=$GRAFANA_ADMIN_PASSWORD"
"--meet-admin-user=$MEET_ADMIN_USER" "--default-app-user=$DEFAULT_APP_USERNAME"
"--meet-admin-password=$MEET_ADMIN_SECRET" "--default-app-password=$DEFAULT_APP_PASSWORD"
"--meet-api-key=$MEET_API_KEY" "--default-app-admin-user=$DEFAULT_APP_ADMIN_USERNAME"
"--default-app-admin-password=$DEFAULT_APP_ADMIN_PASSWORD"
"--livekit-api-key=$LIVEKIT_API_KEY" "--livekit-api-key=$LIVEKIT_API_KEY"
"--livekit-api-secret=$LIVEKIT_API_SECRET" "--livekit-api-secret=$LIVEKIT_API_SECRET"
) )
# Include additional installer flags provided by the user
if [[ "${AdditionalInstallFlags}" != "" ]]; then
IFS=',' read -ra EXTRA_FLAGS <<< "${AdditionalInstallFlags}"
for extra_flag in "${!EXTRA_FLAGS[@]}"; do
# Trim whitespace around each flag
extra_flag="$(echo -e "${!extra_flag}" | sed -e 's/^[ \t]*//' -e 's/[ \t]*$//')"
if [[ "$extra_flag" != "" ]]; then
COMMON_ARGS+=("$extra_flag")
fi
done
fi
# Turn with TLS # Turn with TLS
if [[ "${TurnDomainName}" != '' ]]; then if [[ "${TurnDomainName}" != '' ]]; then
LIVEKIT_TURN_DOMAIN_NAME=$(/usr/local/bin/store_secret.sh save LIVEKIT_TURN_DOMAIN_NAME "${TurnDomainName}") LIVEKIT_TURN_DOMAIN_NAME=$(/usr/local/bin/store_secret.sh save LIVEKIT_TURN_DOMAIN_NAME "${TurnDomainName}")
@ -663,9 +643,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/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_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/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/CALL_USER=.*/CALL_USER=$(echo $SHARED_SECRET | jq -r .DEFAULT_APP_USERNAME)/" "${!CONFIG_DIR}/app.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/CALL_SECRET=.*/CALL_SECRET=$(echo $SHARED_SECRET | jq -r .DEFAULT_APP_PASSWORD)/" "${!CONFIG_DIR}/app.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_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" sed -i "s/ENABLED_MODULES=.*/ENABLED_MODULES=$(echo $SHARED_SECRET | jq -r .ENABLED_MODULES)/" "${!CONFIG_DIR}/openvidu.env"
# Update URLs in secret # Update URLs in secret
@ -718,9 +699,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 '. + {"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_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 '. + {"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 '. + {"DEFAULT_APP_USERNAME": "'"$(/usr/local/bin/get_value_from_config.sh CALL_USER "${!CONFIG_DIR}/app.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 '. + {"DEFAULT_APP_PASSWORD": "'"$(/usr/local/bin/get_value_from_config.sh CALL_SECRET "${!CONFIG_DIR}/app.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_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")"'"}')" 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 # Update shared secret
@ -969,14 +951,6 @@ Resources:
FromPort: 7885 FromPort: 7885
ToPort: 7885 ToPort: 7885
CidrIpv6: ::/0 CidrIpv6: ::/0
- IpProtocol: tcp
FromPort: 7881
ToPort: 7881
CidrIp: 0.0.0.0/0
- IpProtocol: tcp
FromPort: 7881
ToPort: 7881
CidrIpv6: ::/0
- IpProtocol: udp - IpProtocol: udp
FromPort: 50000 FromPort: 50000
ToPort: 60000 ToPort: 60000

View File

@ -170,8 +170,6 @@ param adminUsername string
@secure() @secure()
param adminSshKey object param adminSshKey object
param additionalInstallFlags string = ''
/*------------------------------------------- VARIABLES AND VALIDATIONS -------------------------------------------*/ /*------------------------------------------- VARIABLES AND VALIDATIONS -------------------------------------------*/
//Condition for ipValid if is filled //Condition for ipValid if is filled
@ -278,12 +276,11 @@ var stringInterpolationParams = {
keyVaultName: keyVaultName keyVaultName: keyVaultName
openviduLicense: openviduLicense openviduLicense: openviduLicense
rtcEngine: rtcEngine rtcEngine: rtcEngine
additionalInstallFlags: additionalInstallFlags
} }
var installScriptTemplate = ''' var installScriptTemplate = '''
#!/bin/bash -x #!/bin/bash -x
OPENVIDU_VERSION=main OPENVIDU_VERSION=3.2.0
DOMAIN= DOMAIN=
apt-get update && apt-get install -y \ apt-get update && apt-get install -y \
@ -314,12 +311,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)" 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_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)" 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")" DEFAULT_APP_USERNAME="$(/usr/local/bin/store_secret.sh save DEFAULT-APP-USERNAME "calluser")"
MEET_ADMIN_SECRET="$(/usr/local/bin/store_secret.sh generate MEET-ADMIN-SECRET)" DEFAULT_APP_PASSWORD="$(/usr/local/bin/store_secret.sh generate DEFAULT-APP-PASSWORD)"
MEET_API_KEY="$(/usr/local/bin/store_secret.sh generate MEET-API-KEY)" 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_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)" 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,v2compatibility")" ENABLED_MODULES="$(/usr/local/bin/store_secret.sh save ENABLED-MODULES "observability,app,v2compatibility")"
# Base command # Base command
INSTALL_COMMAND="sh <(curl -fsSL http://get.openvidu.io/community/singlenode/$OPENVIDU_VERSION/install.sh)" INSTALL_COMMAND="sh <(curl -fsSL http://get.openvidu.io/community/singlenode/$OPENVIDU_VERSION/install.sh)"
@ -344,25 +342,14 @@ COMMON_ARGS=(
"--dashboard-admin-password=$DASHBOARD_ADMIN_PASSWORD" "--dashboard-admin-password=$DASHBOARD_ADMIN_PASSWORD"
"--grafana-admin-user=$GRAFANA_ADMIN_USERNAME" "--grafana-admin-user=$GRAFANA_ADMIN_USERNAME"
"--grafana-admin-password=$GRAFANA_ADMIN_PASSWORD" "--grafana-admin-password=$GRAFANA_ADMIN_PASSWORD"
"--meet-admin-user=$MEET_ADMIN_USER" "--default-app-user=$DEFAULT_APP_USERNAME"
"--meet-admin-password=$MEET_ADMIN_SECRET" "--default-app-password=$DEFAULT_APP_PASSWORD"
"--meet-api-key=$MEET_API_KEY" "--default-app-admin-user=$DEFAULT_APP_ADMIN_USERNAME"
"--default-app-admin-password=$DEFAULT_APP_ADMIN_PASSWORD"
"--livekit-api-key=$LIVEKIT_API_KEY" "--livekit-api-key=$LIVEKIT_API_KEY"
"--livekit-api-secret=$LIVEKIT_API_SECRET" "--livekit-api-secret=$LIVEKIT_API_SECRET"
) )
# Include additional installer flags provided by the user
if [[ "${additionalInstallFlags}" != "" ]]; then
IFS=',' read -ra EXTRA_FLAGS <<< "${additionalInstallFlags}"
for extra_flag in "${EXTRA_FLAGS[@]}"; do
# Trim whitespace around each flag
extra_flag="$(echo -e "${extra_flag}" | sed -e 's/^[ \t]*//' -e 's/[ \t]*$//')"
if [[ "$extra_flag" != "" ]]; then
COMMON_ARGS+=("$extra_flag")
fi
done
fi
# Turn with TLS # Turn with TLS
if [[ "${turnDomainName}" != '' ]]; then if [[ "${turnDomainName}" != '' ]]; then
LIVEKIT_TURN_DOMAIN_NAME=$(/usr/local/bin/store_secret.sh save LIVEKIT-TURN-DOMAIN-NAME "${turnDomainName}") LIVEKIT_TURN_DOMAIN_NAME=$(/usr/local/bin/store_secret.sh save LIVEKIT-TURN-DOMAIN-NAME "${turnDomainName}")
@ -493,16 +480,17 @@ 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 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_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 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 DEFAULT_APP_USERNAME=$(az keyvault secret show --vault-name ${keyVaultName} --name DEFAULT-APP-USERNAME --query value -o tsv)
export MEET_ADMIN_SECRET=$(az keyvault secret show --vault-name ${keyVaultName} --name MEET-ADMIN-SECRET --query value -o tsv) export DEFAULT_APP_PASSWORD=$(az keyvault secret show --vault-name ${keyVaultName} --name DEFAULT-APP-PASSWORD --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_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) export ENABLED_MODULES=$(az keyvault secret show --vault-name ${keyVaultName} --name ENABLED-MODULES --query value -o tsv)
# Replace rest of the values # Replace rest of the values
sed -i "s/REDIS_PASSWORD=.*/REDIS_PASSWORD=$REDIS_PASSWORD/" "${CONFIG_DIR}/openvidu.env" sed -i "s/REDIS_PASSWORD=.*/REDIS_PASSWORD=$REDIS_PASSWORD/" "${CONFIG_DIR}/openvidu.env"
sed -i "s/OPENVIDU_RTC_ENGINE=.*/OPENVIDU_RTC_ENGINE=$OPENVIDU_RTC_ENGINE/" "${CONFIG_DIR}/openvidu.env" sed -i "s/OPENVIDU_RTC_ENGINE=.*/OPENVIDU_RTC_ENGINE=$OPENVIDU_RTC_ENGINE/" "${CLUSTER_CONFIG_DIR}/openvidu.env"
sed -i "s/OPENVIDU_PRO_LICENSE=.*/OPENVIDU_PRO_LICENSE=$OPENVIDU_PRO_LICENSE/" "${CONFIG_DIR}/openvidu.env" sed -i "s/OPENVIDU_PRO_LICENSE=.*/OPENVIDU_PRO_LICENSE=$OPENVIDU_PRO_LICENSE/" "${CLUSTER_CONFIG_DIR}/openvidu.env"
sed -i "s/MONGO_ADMIN_USERNAME=.*/MONGO_ADMIN_USERNAME=$MONGO_ADMIN_USERNAME/" "${CONFIG_DIR}/openvidu.env" sed -i "s/MONGO_ADMIN_USERNAME=.*/MONGO_ADMIN_USERNAME=$MONGO_ADMIN_USERNAME/" "${CONFIG_DIR}/openvidu.env"
sed -i "s/MONGO_ADMIN_PASSWORD=.*/MONGO_ADMIN_PASSWORD=$MONGO_ADMIN_PASSWORD/" "${CONFIG_DIR}/openvidu.env" sed -i "s/MONGO_ADMIN_PASSWORD=.*/MONGO_ADMIN_PASSWORD=$MONGO_ADMIN_PASSWORD/" "${CONFIG_DIR}/openvidu.env"
sed -i "s/MONGO_REPLICA_SET_KEY=.*/MONGO_REPLICA_SET_KEY=$MONGO_REPLICA_SET_KEY/" "${CONFIG_DIR}/openvidu.env" sed -i "s/MONGO_REPLICA_SET_KEY=.*/MONGO_REPLICA_SET_KEY=$MONGO_REPLICA_SET_KEY/" "${CONFIG_DIR}/openvidu.env"
@ -514,9 +502,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/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_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/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/CALL_USER=.*/CALL_USER=$DEFAULT_APP_USERNAME/" "${CONFIG_DIR}/app.env"
sed -i "s/MEET_ADMIN_SECRET=.*/MEET_ADMIN_SECRET=$MEET_ADMIN_SECRET/" "${CONFIG_DIR}/meet.env" sed -i "s/CALL_SECRET=.*/CALL_SECRET=$DEFAULT_APP_PASSWORD/" "${CONFIG_DIR}/app.env"
sed -i "s/MEET_API_KEY=.*/MEET_API_KEY=$MEET_API_KEY/" "${CONFIG_DIR}/meet.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" sed -i "s/ENABLED_MODULES=.*/ENABLED_MODULES=$ENABLED_MODULES/" "${CONFIG_DIR}/openvidu.env"
@ -552,8 +541,8 @@ fi
REDIS_PASSWORD="$(/usr/local/bin/get_value_from_config.sh REDIS_PASSWORD "${CONFIG_DIR}/openvidu.env")" REDIS_PASSWORD="$(/usr/local/bin/get_value_from_config.sh REDIS_PASSWORD "${CONFIG_DIR}/openvidu.env")"
DOMAIN_NAME="$(/usr/local/bin/get_value_from_config.sh DOMAIN_NAME "${CONFIG_DIR}/openvidu.env")" DOMAIN_NAME="$(/usr/local/bin/get_value_from_config.sh DOMAIN_NAME "${CONFIG_DIR}/openvidu.env")"
LIVEKIT_TURN_DOMAIN_NAME="$(/usr/local/bin/get_value_from_config.sh LIVEKIT_TURN_DOMAIN_NAME "${CONFIG_DIR}/openvidu.env")" LIVEKIT_TURN_DOMAIN_NAME="$(/usr/local/bin/get_value_from_config.sh LIVEKIT_TURN_DOMAIN_NAME "${CONFIG_DIR}/openvidu.env")"
OPENVIDU_RTC_ENGINE="$(/usr/local/bin/get_value_from_config.sh OPENVIDU_RTC_ENGINE "${CONFIG_DIR}/openvidu.env")" OPENVIDU_RTC_ENGINE="$(/usr/local/bin/get_value_from_config.sh OPENVIDU_RTC_ENGINE "${CLUSTER_CONFIG_DIR}/openvidu.env")"
OPENVIDU_PRO_LICENSE="$(/usr/local/bin/get_value_from_config.sh OPENVIDU_PRO_LICENSE "${CONFIG_DIR}/openvidu.env")" OPENVIDU_PRO_LICENSE="$(/usr/local/bin/get_value_from_config.sh OPENVIDU_PRO_LICENSE "${CLUSTER_CONFIG_DIR}/openvidu.env")"
MONGO_ADMIN_USERNAME="$(/usr/local/bin/get_value_from_config.sh MONGO_ADMIN_USERNAME "${CONFIG_DIR}/openvidu.env")" MONGO_ADMIN_USERNAME="$(/usr/local/bin/get_value_from_config.sh MONGO_ADMIN_USERNAME "${CONFIG_DIR}/openvidu.env")"
MONGO_ADMIN_PASSWORD="$(/usr/local/bin/get_value_from_config.sh MONGO_ADMIN_PASSWORD "${CONFIG_DIR}/openvidu.env")" MONGO_ADMIN_PASSWORD="$(/usr/local/bin/get_value_from_config.sh MONGO_ADMIN_PASSWORD "${CONFIG_DIR}/openvidu.env")"
MONGO_REPLICA_SET_KEY="$(/usr/local/bin/get_value_from_config.sh MONGO_REPLICA_SET_KEY "${CONFIG_DIR}/openvidu.env")" MONGO_REPLICA_SET_KEY="$(/usr/local/bin/get_value_from_config.sh MONGO_REPLICA_SET_KEY "${CONFIG_DIR}/openvidu.env")"
@ -565,9 +554,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")" 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_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")" 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")" DEFAULT_APP_USERNAME="$(/usr/local/bin/get_value_from_config.sh CALL_USER "${CONFIG_DIR}/app.env")"
MEET_ADMIN_SECRET="$(/usr/local/bin/get_value_from_config.sh MEET_ADMIN_SECRET "${CONFIG_DIR}/meet.env")" DEFAULT_APP_PASSWORD="$(/usr/local/bin/get_value_from_config.sh CALL_SECRET "${CONFIG_DIR}/app.env")"
MEET_API_KEY="$(/usr/local/bin/get_value_from_config.sh MEET_API_KEY "${CONFIG_DIR}/meet.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")" ENABLED_MODULES="$(/usr/local/bin/get_value_from_config.sh ENABLED_MODULES "${CONFIG_DIR}/openvidu.env")"
@ -588,9 +578,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 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-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 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 DEFAULT-APP-USERNAME --value $DEFAULT_APP_USERNAME
az keyvault secret set --vault-name ${keyVaultName} --name MEET-ADMIN-SECRET --value $MEET_ADMIN_SECRET az keyvault secret set --vault-name ${keyVaultName} --name DEFAULT-APP-PASSWORD --value $DEFAULT_APP_PASSWORD
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-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 az keyvault secret set --vault-name ${keyVaultName} --name ENABLED-MODULES --value $ENABLED_MODULES
''' '''

File diff suppressed because one or more lines are too long

View File

@ -335,28 +335,6 @@
} }
] ]
}, },
{
"name": "FLAGS",
"label": "(Optional) Additional Install Flags",
"elements": [
{
"name": "additionalInstallFlags",
"type": "Microsoft.Common.TextBox",
"label": "Additional Install Flags",
"subLabel": "Additional optional flags to pass to the OpenVidu installer (comma-separated, e.g., \"--flag1=value, --flag2\")",
"defaultValue": "",
"toolTip": "",
"constraints": {
"required": false,
"regex": "^[A-Za-z0-9, =_.\\-]*$",
"validationMessage": "Must be a comma-separated list of flags (for example, --flag=value, --bool-flag)",
"validations": []
},
"infoMessages": [],
"visible": true
}
]
},
{ {
"name": "parameters TURN", "name": "parameters TURN",
"label": "(Optional) TURN server configuration with TLS", "label": "(Optional) TURN server configuration with TLS",
@ -444,8 +422,7 @@
"adminUsername": "[steps('parameters INSTANCE').adminUsername]", "adminUsername": "[steps('parameters INSTANCE').adminUsername]",
"adminSshKey": "[steps('parameters INSTANCE').adminSshKey]", "adminSshKey": "[steps('parameters INSTANCE').adminSshKey]",
"storageAccountName": "[steps('parameters STORAGE').storageAccountName]", "storageAccountName": "[steps('parameters STORAGE').storageAccountName]",
"containerName": "[steps('parameters STORAGE').containerName]", "containerName": "[steps('parameters STORAGE').containerName]"
"additionalInstallFlags": "[steps('FLAGS').additionalInstallFlags]"
} }
} }
} }

View File

@ -3,9 +3,9 @@
set -eu set -eu
export DOCKER_VERSION="${DOCKER_VERSION:-28.1.1}" export DOCKER_VERSION="${DOCKER_VERSION:-28.1.1}"
export DOCKER_COMPOSE_VERSION="${DOCKER_COMPOSE_VERSION:-v2.35.1}" export DOCKER_COMPOSE_VERSION="${DOCKER_COMPOSE_VERSION:-v2.35.1}"
export OPENVIDU_VERSION="${OPENVIDU_VERSION:-main}" export OPENVIDU_VERSION="${OPENVIDU_VERSION:-3.2.0}"
export INSTALLER_IMAGE="${INSTALLER_IMAGE:-docker.io/openvidu/openvidu-installer:${OPENVIDU_VERSION}}" 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 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 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}" export REDIS_SERVER_IMAGE="${REDIS_SERVER_IMAGE:-docker.io/redis:7.4.4-alpine}"
@ -15,17 +15,16 @@ 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_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_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_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_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_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_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_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.9.1}" 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 PROMETHEUS_IMAGE="${PROMETHEUS_IMAGE:-docker.io/prom/prometheus:v3.4.0}"
export PROMTAIL_IMAGE="${PROMTAIL_IMAGE:-docker.io/grafana/promtail:3.5.1}" 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 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}" export GRAFANA_IMAGE="${GRAFANA_IMAGE:-docker.io/grafana/grafana:11.6.2}"
# Function to compare two version strings # Function to compare two version strings
@ -181,11 +180,10 @@ COMMON_DOCKER_OPTIONS="--network=host -v ${TMP_DIR}:/output \
-e OPENVIDU_OPERATOR_IMAGE=$OPENVIDU_OPERATOR_IMAGE \ -e OPENVIDU_OPERATOR_IMAGE=$OPENVIDU_OPERATOR_IMAGE \
-e OPENVIDU_SERVER_PRO_IMAGE=$OPENVIDU_SERVER_PRO_IMAGE \ -e OPENVIDU_SERVER_PRO_IMAGE=$OPENVIDU_SERVER_PRO_IMAGE \
-e OPENVIDU_SERVER_IMAGE=$OPENVIDU_SERVER_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_PRO_IMAGE=$OPENVIDU_DASHBOARD_PRO_IMAGE \
-e OPENVIDU_DASHBOARD_IMAGE=$OPENVIDU_DASHBOARD_IMAGE \ -e OPENVIDU_DASHBOARD_IMAGE=$OPENVIDU_DASHBOARD_IMAGE \
-e OPENVIDU_V2COMPATIBILITY_IMAGE=$OPENVIDU_V2COMPATIBILITY_IMAGE \ -e OPENVIDU_V2COMPATIBILITY_IMAGE=$OPENVIDU_V2COMPATIBILITY_IMAGE \
-e OPENVIDU_AGENT_SPEECH_PROCESSING_IMAGE=$OPENVIDU_AGENT_SPEECH_PROCESSING_IMAGE \
-e LIVEKIT_INGRESS_SERVER_IMAGE=$LIVEKIT_INGRESS_SERVER_IMAGE \ -e LIVEKIT_INGRESS_SERVER_IMAGE=$LIVEKIT_INGRESS_SERVER_IMAGE \
-e LIVEKIT_EGRESS_SERVER_IMAGE=$LIVEKIT_EGRESS_SERVER_IMAGE \ -e LIVEKIT_EGRESS_SERVER_IMAGE=$LIVEKIT_EGRESS_SERVER_IMAGE \
-e PROMETHEUS_IMAGE=$PROMETHEUS_IMAGE \ -e PROMETHEUS_IMAGE=$PROMETHEUS_IMAGE \

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