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
# run: docker run -d -p 3000:3000 --network host browserless/chrome:1.57-chrome-stable
- name: Run Chrome
run: docker run --network=host -d -p 4444:4444 selenium/standalone-chrome:138.0
run: docker run --network=host -d -p 4444:4444 selenium/standalone-chrome:127.0
- name: Run openvidu-local-deployment
uses: OpenVidu/actions/start-openvidu-local-deployment@main
- name: Start OpenVidu Call backend
@ -100,7 +100,7 @@ jobs:
# - name: Run Browserless Chrome
# run: docker run -d -p 3000:3000 --network host browserless/chrome:1.57-chrome-stable
- name: Run Chrome
run: docker run --network=host -d -p 4444:4444 selenium/standalone-chrome:138.0
run: docker run --network=host -d -p 4444:4444 selenium/standalone-chrome:127.0
- name: Run openvidu-local-deployment
uses: OpenVidu/actions/start-openvidu-local-deployment@main
- name: Start OpenVidu Call backend
@ -133,7 +133,7 @@ jobs:
# - name: Run Browserless Chrome
# run: docker run -d -p 3000:3000 --network host browserless/chrome:1.57-chrome-stable
- name: Run Chrome
run: docker run --network=host -d -p 4444:4444 selenium/standalone-chrome:138.0
run: docker run --network=host -d -p 4444:4444 selenium/standalone-chrome:127.0
- name: Run openvidu-local-deployment
uses: OpenVidu/actions/start-openvidu-local-deployment@main
- name: Start OpenVidu Call backend
@ -166,7 +166,7 @@ jobs:
# - name: Run Browserless Chrome
# run: docker run -d -p 3000:3000 --network host browserless/chrome:1.57-chrome-stable
- name: Run Chrome
run: docker run --network=host -d -p 4444:4444 selenium/standalone-chrome:138.0
run: docker run --network=host -d -p 4444:4444 selenium/standalone-chrome:127.0
- name: Run openvidu-local-deployment
uses: OpenVidu/actions/start-openvidu-local-deployment@main
- name: Start OpenVidu Call backend
@ -180,38 +180,6 @@ jobs:
- name: Cleanup
if: always()
uses: OpenVidu/actions/cleanup@main
e2e_internal_directives:
needs: test_setup
name: Internal Directives Tests
runs-on: ubuntu-latest
steps:
- name: Checkout Repository
uses: actions/checkout@v4
with:
ref: ${{ inputs.commit_sha || github.sha }}
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
- name: Install wait-on package
run: npm install -g wait-on
# - name: Run Browserless Chrome
# run: docker run -d -p 3000:3000 --network host browserless/chrome:1.57-chrome-stable
- name: Run Chrome
run: docker run --network=host -d -p 4444:4444 selenium/standalone-chrome:138.0
- name: Run openvidu-local-deployment
uses: OpenVidu/actions/start-openvidu-local-deployment@main
- name: Start OpenVidu Call backend
uses: OpenVidu/actions/start-openvidu-call@main
- name: Build and Serve openvidu-components-angular Testapp
uses: OpenVidu/actions/start-openvidu-components-testapp@main
- name: Run Tests
env:
LAUNCH_MODE: CI
run: npm run e2e:lib-internal-directives --prefix openvidu-components-angular
- name: Cleanup
if: always()
uses: OpenVidu/actions/cleanup@main
e2e_chat:
needs: test_setup
@ -231,7 +199,7 @@ jobs:
# - name: Run Browserless Chrome
# run: docker run -d -p 3000:3000 --network host browserless/chrome:1.57-chrome-stable
- name: Run Chrome
run: docker run --network=host -d -p 4444:4444 selenium/standalone-chrome:138.0
run: docker run --network=host -d -p 4444:4444 selenium/standalone-chrome:127.0
- name: Run openvidu-local-deployment
uses: OpenVidu/actions/start-openvidu-local-deployment@main
- name: Start OpenVidu Call backend
@ -264,7 +232,7 @@ jobs:
# - name: Run Browserless Chrome
# run: docker run -d -p 3000:3000 --network host browserless/chrome:1.57-chrome-stable
- name: Run Chrome
run: docker run --network=host -d -p 4444:4444 selenium/standalone-chrome:138.0
run: docker run --network=host -d -p 4444:4444 selenium/standalone-chrome:127.0
- name: Run openvidu-local-deployment
uses: OpenVidu/actions/start-openvidu-local-deployment@main
- name: Start OpenVidu Call backend
@ -297,7 +265,7 @@ jobs:
# - name: Run Browserless Chrome
# run: docker run -d -p 3000:3000 --network host browserless/chrome:1.57-chrome-stable
- name: Run Chrome
run: docker run --network=host -d -p 4444:4444 selenium/standalone-chrome:138.0
run: docker run --network=host -d -p 4444:4444 selenium/standalone-chrome:127.0
- name: Run openvidu-local-deployment
uses: OpenVidu/actions/start-openvidu-local-deployment@main
- name: Start OpenVidu Call backend
@ -330,7 +298,7 @@ jobs:
# - name: Run Browserless Chrome
# run: docker run -d -p 3000:3000 --network host browserless/chrome:1.57-chrome-stable
- name: Run Chrome
run: docker run --network=host -d -p 4444:4444 selenium/standalone-chrome:138.0
run: docker run --network=host -d -p 4444:4444 selenium/standalone-chrome:127.0
- name: Run openvidu-local-deployment
uses: OpenVidu/actions/start-openvidu-local-deployment@main
- name: Start OpenVidu Call backend
@ -363,7 +331,7 @@ jobs:
# - name: Run Browserless Chrome
# run: docker run -d -p 3000:3000 --network host browserless/chrome:1.57-chrome-stable
- name: Run Chrome
run: docker run --network=host -d -p 4444:4444 selenium/standalone-chrome:138.0
run: docker run --network=host -d -p 4444:4444 selenium/standalone-chrome:127.0
- name: Run openvidu-local-deployment
uses: OpenVidu/actions/start-openvidu-local-deployment@main
- name: Start OpenVidu Call backend
@ -396,7 +364,7 @@ jobs:
# - name: Run Browserless Chrome
# run: docker run -d -p 3000:3000 --network host browserless/chrome:1.57-chrome-stable
- name: Run Chrome
run: docker run --network=host -d -v $(pwd)/openvidu-components-angular/e2e/assets:/e2e-assets selenium/standalone-chrome:138.0
run: docker run --network=host -d -v $(pwd)/openvidu-components-angular/e2e/assets:/e2e-assets selenium/standalone-chrome:127.0
- name: Run openvidu-local-deployment
uses: OpenVidu/actions/start-openvidu-local-deployment@main
- name: Start OpenVidu Call backend
@ -429,7 +397,7 @@ jobs:
# - name: Run Browserless Chrome
# run: docker run -d -p 3000:3000 --network host browserless/chrome:1.57-chrome-stable
- name: Run Chrome
run: docker run --network=host -d -p 4444:4444 selenium/standalone-chrome:138.0
run: docker run --network=host -d -p 4444:4444 selenium/standalone-chrome:127.0
- name: Run openvidu-local-deployment
uses: OpenVidu/actions/start-openvidu-local-deployment@main
- name: Start OpenVidu Call backend

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);
const tabs = await browser.getAllWindowHandles();
await browser.switchTo().window(tabs[1]);
await utils.clickOn('#mic-btn');
await browser.switchTo().window(tabs[0]);
await utils.waitForElement('.OV_stream.remote.speaking');
expect(await utils.getNumberOfElements('.OV_stream.remote.speaking')).toEqual(1);
// Check only one element is marked as speaker due to the local participant is muted
await utils.waitForElement('.OV_stream.speaking');
expect(await utils.getNumberOfElements('.OV_stream.speaking')).toEqual(1);
});
});

View File

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

View File

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

View File

@ -27,7 +27,7 @@
"@types/node": "20.12.14",
"@types/selenium-webdriver": "4.1.16",
"@types/ws": "^8.5.12",
"chromedriver": "138.0.0",
"chromedriver": "136.0.2",
"concat": "^1.0.3",
"cross-env": "^7.0.3",
"eslint-config-prettier": "^9.1.0",
@ -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-attribute-directives": "tsc --project ./e2e && npx jasmine --fail-fast ./e2e/dist/nested-components/attribute-directives.test.js",
"e2e:lib-directives": "tsc --project ./e2e && npx jasmine --fail-fast ./e2e/dist/api-directives.test.js",
"e2e:lib-internal-directives": "tsc --project ./e2e && npx jasmine --fail-fast ./e2e/dist/internal-directives.test.js",
"e2e:lib-chat": "tsc --project ./e2e && npx jasmine --fail-fast ./e2e/dist/chat.test.js",
"e2e:lib-events": "tsc --project ./e2e && npx jasmine ./e2e/dist/events.test.js",
"e2e:lib-media-devices": "tsc --project ./e2e && npx jasmine --fail-fast ./e2e/dist/media-devices.test.js",
@ -100,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",
"husky": "cd .. && husky install"
},
"version": "3.3.0"
"version": "3.2.0"
}

View File

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

View File

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

View File

@ -15,5 +15,5 @@
"livekit-client": "^2.1.0",
"@livekit/track-processors": "^0.3.2"
},
"version": "3.3.0"
"version": "3.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 -->
| **Parameter** | **Type** | **Reference** |
|:--------------------------------: | :-------: | :---------------------------------------------: |
| **navbarTitle** | `string` | [AdminDashboardTitleDirective](../directives/AdminDashboardTitleDirective.html) |
| **recordingsList** | `RecordingInfo[]` | [AdminDashboardRecordingsListDirective](../directives/AdminDashboardRecordingsListDirective.html) |
| **recordingsList** | `RecordingInfo` | [AdminDashboardRecordingsListDirective](../directives/AdminDashboardRecordingsListDirective.html) |
<!-- end-dynamic-api-directives-content -->

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -49,7 +49,9 @@ export class ActivitiesPanelRecordingActivityDirective implements AfterViewInit,
}
update(value: boolean) {
this.libService.updateRecordingActivityConfig({ enabled: value });
if (this.libService.showRecordingActivity() !== value) {
this.libService.setRecordingActivity(value);
}
}
}
@ -101,6 +103,8 @@ export class ActivitiesPanelBroadcastingActivityDirective implements AfterViewIn
}
update(value: boolean) {
if (this.libService.showBroadcastingActivity() !== value) {
this.libService.setBroadcastingActivity(value);
}
}
}

View File

@ -16,17 +16,15 @@ import { OpenViduComponentsConfigService } from '../../services/config/directive
standalone: false
})
export class AdminDashboardRecordingsListDirective implements AfterViewInit, OnDestroy {
@Input() set recordingsList(value: RecordingInfo[]) {
this.recordingsValue = value;
this.update(this.recordingsValue);
}
recordingsValue: RecordingInfo[] = [];
recordingsValue: RecordingInfo [] = [];
constructor(
public elementRef: ElementRef,
private libService: OpenViduComponentsConfigService
) {}
constructor(public elementRef: ElementRef, private libService: OpenViduComponentsConfigService) {}
ngAfterViewInit() {
this.update(this.recordingsValue);
@ -40,7 +38,9 @@ export class AdminDashboardRecordingsListDirective implements AfterViewInit, OnD
}
update(value: RecordingInfo[]) {
this.libService.updateAdminConfig({ recordingsList: value });
if (this.libService.getAdminRecordingsList() !== value) {
this.libService.setAdminRecordingsList(value);
}
}
}
@ -58,17 +58,15 @@ export class AdminDashboardRecordingsListDirective implements AfterViewInit, OnD
standalone: false
})
export class AdminDashboardTitleDirective implements AfterViewInit, OnDestroy {
@Input() set navbarTitle(value: string) {
@Input() set navbarTitle(value: any) {
this.navbarTitleValue = value;
this.update(this.navbarTitleValue);
}
navbarTitleValue: string = 'OpenVidu Dashboard';
navbarTitleValue: any = null;
constructor(
public elementRef: ElementRef,
private libService: OpenViduComponentsConfigService
) {}
constructor(public elementRef: ElementRef, private libService: OpenViduComponentsConfigService) {}
ngAfterViewInit() {
this.update(this.navbarTitleValue);
@ -77,15 +75,18 @@ export class AdminDashboardTitleDirective implements AfterViewInit, OnDestroy {
this.clear();
}
clear() {
this.navbarTitleValue = 'OpenVidu Dashboard';
this.navbarTitleValue = null;
this.update(null);
}
update(value: any) {
this.libService.updateAdminConfig({ dashboardTitle: value });
if (this.libService.getAdminDashboardTitle() !== value) {
this.libService.setAdminDashboardTitle(value);
}
}
}
/**
* The **navbarTitle** directive allows customize the title of the navbar in {@link AdminLoginComponent}.
*
@ -100,6 +101,7 @@ export class AdminDashboardTitleDirective implements AfterViewInit, OnDestroy {
standalone: false
})
export class AdminLoginTitleDirective implements AfterViewInit, OnDestroy {
@Input() set navbarTitle(value: any) {
this.navbarTitleValue = value;
this.update(this.navbarTitleValue);
@ -107,10 +109,7 @@ export class AdminLoginTitleDirective implements AfterViewInit, OnDestroy {
navbarTitleValue: any = null;
constructor(
public elementRef: ElementRef,
private libService: OpenViduComponentsConfigService
) {}
constructor(public elementRef: ElementRef, private libService: OpenViduComponentsConfigService) {}
ngAfterViewInit() {
this.update(this.navbarTitleValue);
@ -124,10 +123,14 @@ export class AdminLoginTitleDirective implements AfterViewInit, OnDestroy {
}
update(value: any) {
this.libService.updateAdminConfig({ loginTitle: value });
if (this.libService.getAdminLoginTitle() !== value) {
this.libService.setAdminLoginTitle(value);
}
}
}
/**
* The **error** directive allows show the authentication error in {@link AdminLoginComponent}.
*
@ -137,11 +140,12 @@ export class AdminLoginTitleDirective implements AfterViewInit, OnDestroy {
* <ov-admin-login [error]="error"></ov-admin-login>
*
*/
@Directive({
@Directive({
selector: 'ov-admin-login[error]',
standalone: false
})
export class AdminLoginErrorDirective implements AfterViewInit, OnDestroy {
@Input() set error(value: any) {
this.errorValue = value;
this.update(this.errorValue);
@ -149,10 +153,7 @@ export class AdminLoginErrorDirective implements AfterViewInit, OnDestroy {
errorValue: any = null;
constructor(
public elementRef: ElementRef,
private libService: OpenViduComponentsConfigService
) {}
constructor(public elementRef: ElementRef, private libService: OpenViduComponentsConfigService) {}
ngAfterViewInit() {
this.update(this.errorValue);
@ -166,6 +167,9 @@ export class AdminLoginErrorDirective implements AfterViewInit, OnDestroy {
}
update(value: any) {
this.libService.updateAdminConfig({ loginError: value });
if (this.libService.getAdminLoginError() !== value) {
this.libService.setAdminLoginError(value);
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

@ -62,7 +62,9 @@ export class ToolbarCameraButtonDirective implements AfterViewInit, OnDestroy {
}
private update(value: boolean) {
this.libService.updateToolbarConfig({ camera: value });
if (this.libService.showCameraButton() !== value) {
this.libService.setCameraButton(value);
}
}
}
@ -126,7 +128,9 @@ export class ToolbarMicrophoneButtonDirective implements AfterViewInit, OnDestro
}
private update(value: boolean) {
this.libService.updateToolbarConfig({ microphone: value });
if (this.libService.showMicrophoneButton() !== value) {
this.libService.setMicrophoneButton(value);
}
}
}
@ -190,7 +194,9 @@ export class ToolbarScreenshareButtonDirective implements AfterViewInit, OnDestr
}
private update(value: boolean) {
this.libService.updateToolbarConfig({ screenshare: value });
if (this.libService.showScreenshareButton() !== value) {
this.libService.setScreenshareButton(value);
}
}
}
@ -251,7 +257,9 @@ export class ToolbarRecordingButtonDirective implements AfterViewInit, OnDestroy
}
private update(value: boolean) {
this.libService.updateToolbarConfig({ recording: value });
if (this.libService.showRecordingButton() !== value) {
this.libService.setRecordingButton(value);
}
}
}
@ -313,8 +321,10 @@ export class ToolbarBroadcastingButtonDirective implements AfterViewInit, OnDest
}
private update(value: boolean) {
if (this.libService.showBroadcastingButton() !== value) {
this.libService.setBroadcastingButton(value);
}
}
}
/**
@ -374,7 +384,9 @@ export class ToolbarFullscreenButtonDirective implements AfterViewInit, OnDestro
}
private update(value: boolean) {
this.libService.updateToolbarConfig({ fullscreen: value });
if (this.libService.showFullscreenButton() !== value) {
this.libService.setFullscreenButton(value);
}
}
}
@ -435,7 +447,9 @@ export class ToolbarBackgroundEffectsButtonDirective implements AfterViewInit, O
}
private update(value: boolean) {
this.libService.updateToolbarConfig({ backgroundEffects: value });
if (this.libService.showBackgroundEffectsButton() !== value) {
this.libService.setBackgroundEffectsButton(value);
}
}
}
@ -555,7 +569,9 @@ export class ToolbarSettingsButtonDirective implements AfterViewInit, OnDestroy
}
private update(value: boolean) {
this.libService.updateToolbarConfig({ settings: value });
if (this.libService.showToolbarSettingsButton() !== value) {
this.libService.setToolbarSettingsButton(value);
}
}
}
@ -617,7 +633,9 @@ export class ToolbarLeaveButtonDirective implements AfterViewInit, OnDestroy {
}
private update(value: boolean) {
this.libService.updateToolbarConfig({ leave: value });
if (this.libService.showLeaveButton() !== value) {
this.libService.setLeaveButton(value);
}
}
}
@ -680,7 +698,9 @@ export class ToolbarParticipantsPanelButtonDirective implements AfterViewInit, O
}
private update(value: boolean) {
this.libService.updateToolbarConfig({ participantsPanel: value });
if (this.libService.showParticipantsPanelButton() !== value) {
this.libService.setParticipantsPanelButton(value);
}
}
}
@ -741,7 +761,9 @@ export class ToolbarChatPanelButtonDirective implements AfterViewInit, OnDestroy
}
private update(value: boolean) {
this.libService.updateToolbarConfig({ chatPanel: value });
if (this.libService.showChatPanelButton() !== value) {
this.libService.setChatPanelButton(value);
}
}
}
@ -802,7 +824,9 @@ export class ToolbarActivitiesPanelButtonDirective implements AfterViewInit, OnD
}
private update(value: boolean) {
this.libService.updateToolbarConfig({ activitiesPanel: value });
if (this.libService.showActivitiesPanelButton() !== value) {
this.libService.setActivitiesPanelButton(value);
}
}
}
@ -864,7 +888,9 @@ export class ToolbarDisplayRoomNameDirective implements AfterViewInit, OnDestroy
}
private update(value: boolean) {
this.libService.updateToolbarConfig({ displayRoomName: value });
if (this.libService.showRoomName() !== value) {
this.libService.setDisplayRoomName(value);
}
}
}
@ -926,7 +952,9 @@ export class ToolbarDisplayLogoDirective implements AfterViewInit, OnDestroy {
}
private update(value: boolean) {
this.libService.updateToolbarConfig({ displayLogo: value });
if (this.libService.showLogo() !== value) {
this.libService.setDisplayLogo(value);
}
}
}
@ -981,6 +1009,8 @@ export class ToolbarAdditionalButtonsPossitionDirective implements AfterViewInit
}
private update(value: ToolbarAdditionalButtonsPosition) {
this.libService.updateToolbarConfig({ additionalButtonsPosition: value });
if (this.libService.getToolbarAdditionalButtonsPosition() !== value) {
this.libService.setToolbarAdditionalButtonsPosition(value);
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -33,8 +33,8 @@
"AUDIO_DEVICE": "Dispositivo audio",
"NO_VIDEO_DEVICE": "Dispositivo video non trovato",
"NO_AUDIO_DEVICE": "Dispositivo audio non trovato",
"JOIN": "Unisciti alla stanza",
"PREPARING": "Preparazione della stanza in corso..."
"JOIN": "Unisciti alla sessione",
"PREPARING": "Preparazione della sessione in corso..."
},
"TOOLBAR": {
"MUTE_AUDIO": "Disattiva l'audio",
@ -52,12 +52,10 @@
"START_RECORDING": "Avvia registrazione",
"STOP_RECORDING": "Interrompi registrazione",
"SETTINGS": "Impostazioni",
"LEAVE": "Abbandona la stanza",
"LEAVE": "Abbandona la sessione",
"PARTICIPANTS": "Partecipanti",
"CHAT": "Chat",
"ACTIVITIES": "Attività",
"NO_TRACKS_PUBLISHED": "Condividi audio o video per iniziare la registrazione.",
"VIEW_RECORDINGS": "Visualizza registrazioni"
"ACTIVITIES": "Attività"
},
"STREAM": {
"SETTINGS": "Impostazioni",
@ -79,7 +77,7 @@
"CHAT": {
"TITLE": "Chat",
"YOU": "Tu",
"SUBTITLE": "I messaggi verranno rimossi alla fine della stanza",
"SUBTITLE": "I messaggi verranno rimossi alla fine della sessione",
"PLACEHOLDER": "Invia un messaggio...",
"SEND": "Invia",
"MESSAGE_SENT_NOTIFICATION": "messaggio inviato",
@ -91,9 +89,7 @@
"MICROPHONE": "MICROFONO",
"SCREEN": "SCREEN",
"NO_STREAMS": "NESSUNO",
"YOU": "Tu",
"MUTE": "Disattiva l'audio",
"UNMUTE": "Attiva l'audio"
"YOU": "Tu"
},
"SETTINGS": {
"TITLE": "Impostazioni",
@ -104,7 +100,7 @@
"CAPTIONS": "Sottotitoli",
"DISABLED_AUDIO": "Disattiva l'audio",
"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": {
"TITLE": "Effetti di sfondo",
@ -118,10 +114,6 @@
"SUBTITLE": "Registra la tua riunione per i posteri",
"CONTENT_TITLE": "Registra la tua videochiamata",
"CONTENT_SUBTITLE": "Al termine della registrazione potrete scaricarla con facilità",
"VIEW_ONLY_SUBTITLE": "Visualizza e accedi alle registrazioni della sala",
"VIEW_ONLY_CONTENT_TITLE": "Registrazioni di videochiamate",
"VIEW_ONLY_CONTENT_SUBTITLE": "Qui puoi accedere a tutte le registrazioni disponibili",
"WATCH": "Guardare",
"STARTING": "Avvio della registrazione",
"STOPPING": "Interruzione della registrazione",
"IN_PROGRESS": "Registrazione in corso",
@ -132,10 +124,7 @@
"DELETE_QUESTION": "Sei sicuro di voler eliminare la registrazione?",
"DOWNLOAD": "Scarica",
"RECORDINGS": "REGISTRAZIONI",
"NO_MODERATOR": "Solo il MODERATORE può avviare la registrazione",
"NO_TRACKS_PUBLISHED": "Condividi audio o video per iniziare la registrazione.",
"NO_RECORDINGS_AVAILABLE": "Nessuna registrazione disponibile al momento",
"ERROR_STARTING": "Errore di avvio"
"NO_MODERATOR": "Solo il MODERATORE può avviare la registrazione"
},
"STREAMING": {
"TITLE": "Streaming",
@ -150,9 +139,9 @@
}
},
"ERRORS": {
"SESSION": "Si è verificato un errore di connessione alla stanza",
"SESSION": "Si è verificato un errore di connessione alla sessione",
"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",
"NETWORK_DISCONNECT": "Sei stato disconnesso a causa di un problema di connettività di rete",
"SIGNAL_CLOSE": "La connessione al server è stata chiusa inaspettatamente",

View File

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

View File

@ -33,8 +33,8 @@
"AUDIO_DEVICE": "Audiospeler",
"NO_VIDEO_DEVICE": "Videoapparaat niet gevonden",
"NO_AUDIO_DEVICE": "Audioapparaat niet gevonden",
"JOIN": "Deelnemen aan kamer",
"PREPARING": "Kamer voorbereiden ..."
"JOIN": "Deelnemen aan sessie",
"PREPARING": "Sessie voorbereiden ..."
},
"TOOLBAR": {
"MUTE_AUDIO": "Audio dempen",
@ -52,12 +52,10 @@
"START_RECORDING": "Start opname",
"STOP_RECORDING": "Stop opname",
"SETTINGS": "Instellingen",
"LEAVE": "Verlaat de kamer",
"LEAVE": "Verlaat de sessie",
"PARTICIPANTS": "Deelnemers",
"CHAT": "Chat",
"ACTIVITIES": "Activiteiten",
"NO_TRACKS_PUBLISHED": "Deel audio of video om met opnemen te beginnen.",
"VIEW_RECORDINGS": "Opnames bekijken"
"ACTIVITIES": "Activiteiten"
},
"STREAM": {
"SETTINGS": "Instellingen",
@ -79,7 +77,7 @@
"CHAT": {
"TITLE": "Chat",
"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 ...",
"SEND": "Versturen",
"MESSAGE_SENT_NOTIFICATION": "bericht verzonden",
@ -91,9 +89,7 @@
"MICROPHONE": "MICROFOON",
"SCREEN": "SCHERM",
"NO_STREAMS": "GEEN",
"YOU": "Jij",
"MUTE": "Dempen",
"UNMUTE": "Dempen opheffen"
"YOU": "Jij"
},
"SETTINGS": {
"TITLE": "Instellingen",
@ -104,7 +100,7 @@
"CAPTIONS": "Ondertitels",
"DISABLED_AUDIO": "Geen audio",
"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": {
"TITLE": "Achtergrondeffecten",
@ -118,10 +114,6 @@
"SUBTITLE": "Neem uw vergadering op voor het nageslacht",
"CONTENT_TITLE": "Neem uw videogesprek op",
"CONTENT_SUBTITLE": "Als de opname klaar is kunt u deze met gemak downloaden",
"VIEW_ONLY_SUBTITLE": "Bekijk en toegang tot kameropnames",
"VIEW_ONLY_CONTENT_TITLE": "Videogesprek opnames",
"VIEW_ONLY_CONTENT_SUBTITLE": "Hier heeft u toegang tot alle beschikbare opnames",
"WATCH": "Bekijken",
"STARTING": "Beginnen met opnemen",
"STOPPING": "Opname stoppen",
"IN_PROGRESS": "Opname in uitvoering",
@ -132,10 +124,7 @@
"DELETE_QUESTION": "Weet je zeker dat je de opname wilt verwijderen?",
"DOWNLOAD": "Downloaden",
"RECORDINGS": "OPNAME",
"NO_MODERATOR": "Alleen de MOEDERATOR kan de opname starten",
"NO_TRACKS_PUBLISHED": "Deel audio of video om met opnemen te beginnen.",
"NO_RECORDINGS_AVAILABLE": "Momenteel zijn er geen opnames beschikbaar",
"ERROR_STARTING": "Fout bij starten opname"
"NO_MODERATOR": "Alleen de MOEDERATOR kan de opname starten"
},
"STREAMING": {
"TITLE": "Streaming",
@ -150,9 +139,9 @@
}
},
"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",
"RECONNECT": "Proberen opnieuw verbinding te maken met de kamer...",
"RECONNECT": "Proberen opnieuw verbinding te maken met de sessie...",
"DISCONNECT": "Je bent losgekoppeld",
"NETWORK_DISCONNECT": "Je bent losgekoppeld vanwege een netwerkprobleem",
"SIGNAL_CLOSE": "De verbinding met de server werd onverwacht verbroken",

View File

@ -33,8 +33,8 @@
"AUDIO_DEVICE": "Dispositivo de áudio",
"NO_VIDEO_DEVICE": "Dispositivo de vídeo não encontrado",
"NO_AUDIO_DEVICE": "Dispositivo de áudio não encontrado",
"JOIN": "Entrar na sala",
"PREPARING": "Preparando sala..."
"JOIN": "Entrar na sessão",
"PREPARING": "Preparando sessão..."
},
"TOOLBAR": {
"MUTE_AUDIO": "Mute seu áudio",
@ -52,12 +52,10 @@
"START_RECORDING": "Iniciar_gravação",
"STOP_RECORDING": "Parar de gravar",
"SETTINGS": "Configurações",
"LEAVE": "Sair da sala",
"LEAVE": "Sair da sessão",
"PARTICIPANTS": "Participantes",
"CHAT": "Chat",
"ACTIVITIES": "Actividades",
"NO_TRACKS_PUBLISHED": "Compartilhe áudio ou vídeo para começar a gravar.",
"VIEW_RECORDINGS": "Ver gravações"
"ACTIVITIES": "Actividades"
},
"STREAM": {
"SETTINGS": "Configurações",
@ -79,7 +77,7 @@
"CHAT": {
"TITLE": "Chat",
"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...",
"SEND": "Enviar",
"MESSAGE_SENT_NOTIFICATION": "mensagem enviada",
@ -91,9 +89,7 @@
"MICROPHONE": "MICROFONE",
"SCREEN": "TELA",
"NO_STREAMS": "NENHUM",
"YOU": "Você (eu)",
"MUTE": "Silenciar",
"UNMUTE": "Ativar som"
"YOU": "Você (eu)"
},
"SETTINGS": {
"TITLE": "Configurações",
@ -104,7 +100,7 @@
"CAPTIONS": "Legendas",
"DISABLED_AUDIO": "Áudio 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": {
"TITLE": "Efeitos de fundo",
@ -118,10 +114,6 @@
"SUBTITLE": "Grave a sua reunião para a posteridade",
"CONTENT_TITLE": "Grave a sua videochamada",
"CONTENT_SUBTITLE": "Quando a gravação tiver terminado, poderá descarregá-la com facilidade",
"VIEW_ONLY_SUBTITLE": "Visualize e acesse gravações da sala",
"VIEW_ONLY_CONTENT_TITLE": "Gravações de videochamada",
"VIEW_ONLY_CONTENT_SUBTITLE": "Aqui você pode acessar todas as gravações disponíveis",
"WATCH": "Assistir",
"STARTING": "Começar a gravação",
"STOPPING": "Parando a gravação",
"IN_PROGRESS": "Gravação em andamento",
@ -132,10 +124,7 @@
"DELETE_QUESTION": "Tem certeza de que deseja excluir a gravação?",
"DOWNLOAD": "Download",
"RECORDINGS": "GRAVAÇÕES",
"NO_MODERATOR": "Só o MODERADOR pode iniciar a gravação",
"NO_TRACKS_PUBLISHED": "Compartilhe áudio ou vídeo para começar a gravar.",
"NO_RECORDINGS_AVAILABLE": "Nenhuma gravação disponível no momento",
"ERROR_STARTING": "Erro ao iniciar gravação"
"NO_MODERATOR": "Só o MODERADOR pode iniciar a gravação"
},
"STREAMING": {
"TITLE": "Streaming",
@ -150,9 +139,9 @@
}
},
"ERRORS": {
"SESSION": "Houve um erro de ligação à sala",
"SESSION": "Houve um erro de ligação à sessão",
"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",
"NETWORK_DISCONNECT": "Você foi desconectado devido a um problema de conectividade de rede",
"SIGNAL_CLOSE": "A conexão com o servidor foi encerrada inesperadamente",

View File

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

View File

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

View File

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

View File

@ -9,36 +9,7 @@ export enum StorageKeys {
CAMERA_ENABLED = 'cameraEnabled',
LANG = 'lang',
CAPTION_LANG = 'captionLang',
BACKGROUND = 'virtualBg',
TAB_ID = 'tabId',
ACTIVE_TABS = 'activeTabs'
BACKGROUND = "virtualBg"
}
export const PERSISTENT_KEYS: StorageKeys[] = [
StorageKeys.VIDEO_DEVICE,
StorageKeys.AUDIO_DEVICE,
StorageKeys.LANG,
StorageKeys.CAPTION_LANG,
StorageKeys.BACKGROUND
];
export const SESSION_KEYS: StorageKeys[] = [StorageKeys.TAB_ID];
export const TAB_MANAGEMENT_KEYS: StorageKeys[] = [StorageKeys.TAB_ID, StorageKeys.ACTIVE_TABS];
// Data that should be unique per tab (stored in localStorage with tabId prefix)
export const TAB_SPECIFIC_KEYS: StorageKeys[] = [
StorageKeys.PARTICIPANT_NAME,
StorageKeys.MICROPHONE_ENABLED,
StorageKeys.CAMERA_ENABLED,
StorageKeys.LANG,
StorageKeys.CAPTION_LANG,
StorageKeys.BACKGROUND,
StorageKeys.VIDEO_DEVICE,
StorageKeys.AUDIO_DEVICE
];
// Data that should be truly persistent and shared between tabs
export const SHARED_PERSISTENT_KEYS: StorageKeys[] = [];
export const STORAGE_PREFIX = 'ovComponents-';

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 { BehaviorSubject, Observable } from 'rxjs';
import { distinctUntilChanged, shareReplay, map } from 'rxjs/operators';
import { RecordingInfo } from '../../models/recording.model';
import { ToolbarAdditionalButtonsPosition } from '../../models/toolbar.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
*/
@ -104,483 +11,450 @@ interface GeneralConfig {
providedIn: 'root'
})
export class OpenViduComponentsConfigService {
/**
* Helper method to create a configuration item with BehaviorSubject and Observable
*/
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 token = <BehaviorSubject<string>>new BehaviorSubject('');
token$: Observable<string>;
/**
* Helper method for array configurations with optimized comparison
*/
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)
private livekitUrl = <BehaviorSubject<string>>new BehaviorSubject('');
livekitUrl$: Observable<string>;
private tokenError = <BehaviorSubject<any>>new BehaviorSubject(null);
tokenError$: Observable<any>;
private minimal = <BehaviorSubject<boolean>>new BehaviorSubject(false);
minimal$: Observable<boolean>;
private participantName = <BehaviorSubject<string>>new BehaviorSubject('');
participantName$: Observable<string>;
private prejoin = <BehaviorSubject<boolean>>new BehaviorSubject(true);
prejoin$: Observable<boolean>;
private prejoinDisplayParticipantName = <BehaviorSubject<boolean>>new BehaviorSubject(true);
prejoinDisplayParticipantName$: Observable<boolean>;
private videoEnabled = <BehaviorSubject<boolean>>new BehaviorSubject(true);
videoEnabled$: Observable<boolean>;
private audioEnabled = <BehaviorSubject<boolean>>new BehaviorSubject(true);
audioEnabled$: Observable<boolean>;
private recordingStreamBaseUrl = <BehaviorSubject<string>>new BehaviorSubject('call/api/recordings');
recordingStreamBaseUrl$: Observable<string>;
//Toolbar settings
private cameraButton = <BehaviorSubject<boolean>>new BehaviorSubject(true);
cameraButton$: Observable<boolean>;
private microphoneButton = <BehaviorSubject<boolean>>new BehaviorSubject(true);
microphoneButton$: Observable<boolean>;
private screenshareButton = <BehaviorSubject<boolean>>new BehaviorSubject(true);
screenshareButton$: Observable<boolean>;
private fullscreenButton = <BehaviorSubject<boolean>>new BehaviorSubject(true);
fullscreenButton$: Observable<boolean>;
private captionsButton = <BehaviorSubject<boolean>>new BehaviorSubject(true);
captionsButton$: Observable<boolean>;
private toolbarSettingsButton = <BehaviorSubject<boolean>>new BehaviorSubject(true);
toolbarSettingsButton$: Observable<boolean>;
private leaveButton = <BehaviorSubject<boolean>>new BehaviorSubject(true);
leaveButton$: Observable<boolean>;
private participantsPanelButton = <BehaviorSubject<boolean>>new BehaviorSubject(true);
participantsPanelButton$: Observable<boolean>;
private chatPanelButton = <BehaviorSubject<boolean>>new BehaviorSubject(true);
chatPanelButton$: Observable<boolean>;
private activitiesPanelButton = <BehaviorSubject<boolean>>new BehaviorSubject(true);
activitiesPanelButton$: Observable<boolean>;
private displayRoomName = <BehaviorSubject<boolean>>new BehaviorSubject(true);
displayRoomName$: Observable<boolean>;
private brandingLogo = <BehaviorSubject<string>>new BehaviorSubject('');
brandingLogo$: Observable<string>;
private displayLogo = <BehaviorSubject<boolean>>new BehaviorSubject(true);
displayLogo$: Observable<boolean>;
private toolbarAdditionalButtonsPosition = <BehaviorSubject<ToolbarAdditionalButtonsPosition>>(
new BehaviorSubject(ToolbarAdditionalButtonsPosition.AFTER_MENU)
);
return { subject, observable$ };
}
toolbarAdditionalButtonsPosition$: Observable<ToolbarAdditionalButtonsPosition>;
/**
* Helper method for RecordingControls with specific comparison
*/
private createRecordingControlsConfigItem(initialValue: RecordingControls): ConfigItem<RecordingControls> {
const subject = new BehaviorSubject<RecordingControls>(initialValue);
const observable$ = subject.asObservable().pipe(
distinctUntilChanged(
(prev, curr) =>
prev.play === curr.play &&
prev.download === curr.download &&
prev.delete === curr.delete &&
prev.externalView === curr.externalView
),
shareReplay(1)
);
return { subject, observable$ };
}
private displayParticipantName = <BehaviorSubject<boolean>>new BehaviorSubject(true);
displayParticipantName$: Observable<boolean>;
private displayAudioDetection = <BehaviorSubject<boolean>>new BehaviorSubject(true);
displayAudioDetection$: Observable<boolean>;
private streamVideoControls = <BehaviorSubject<boolean>>new BehaviorSubject(true);
streamVideoControls$: Observable<boolean>;
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>;
/**
* Helper method for ToolbarConfig with specific comparison
*/
private createToolbarConfigItem(initialValue: ToolbarConfig): ConfigItem<ToolbarConfig> {
const subject = new BehaviorSubject<ToolbarConfig>(initialValue);
const observable$ = subject.asObservable().pipe(
distinctUntilChanged((prev, curr) => this.compareToolbarConfig(prev, curr)),
shareReplay(1)
);
return { subject, observable$ };
}
// Admin
private adminRecordingsList: BehaviorSubject<RecordingInfo[]> = new BehaviorSubject(<RecordingInfo[]>[]);
adminRecordingsList$: Observable<RecordingInfo[]>;
private adminLoginError = <BehaviorSubject<any>>new BehaviorSubject(null);
private adminLoginTitle = <BehaviorSubject<string>>new BehaviorSubject('');
private adminDashboardTitle = <BehaviorSubject<string>>new BehaviorSubject('');
adminLoginTitle$: Observable<string>;
adminDashboardTitle$: Observable<string>;
adminLoginError$: Observable<any>;
/**
* Helper method for StreamConfig with specific comparison
*/
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$ };
}
/**
* Helper method for RecordingActivityConfig with specific comparison
*/
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$ };
}
/**
* Helper method for AdminConfig with specific comparison
*/
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$ };
}
/**
* Helper method for GeneralConfig with specific comparison
*/
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$ };
}
/**
* Optimized deep equality check
*/
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);
const keysB = Object.keys(b);
if (keysA.length !== keysB.length) return false;
return keysA.every((key) => this.deepEqual(a[key], b[key]));
}
/**
* Compare ToolbarConfig efficiently
*/
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
);
}
/**
* Compare StreamConfig efficiently
*/
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
);
}
/**
* Compare RecordingActivityConfig efficiently
*/
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
);
}
/**
* Compare AdminConfig efficiently
*/
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]))
);
}
/**
* Compare GeneralConfig efficiently
*/
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 generalConfig = this.createGeneralConfigItem({
token: '',
livekitUrl: '',
tokenError: null,
minimal: false,
participantName: '',
prejoin: true,
prejoinDisplayParticipantName: true,
showDisconnectionDialog: true,
recordingStreamBaseUrl: 'call/api/recordings'
});
private toolbarConfig = this.createToolbarConfigItem({
camera: true,
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({
videoEnabled: true,
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));
recordingStreamBaseUrl$: Observable<string> = this.generalConfig.observable$.pipe(map((config) => config.recordingStreamBaseUrl));
// Stream observables
videoEnabled$: Observable<boolean> = this.streamConfig.observable$.pipe(map((config) => config.videoEnabled));
audioEnabled$: Observable<boolean> = this.streamConfig.observable$.pipe(map((config) => config.audioEnabled));
displayParticipantName$: Observable<boolean> = this.streamConfig.observable$.pipe(map((config) => config.displayParticipantName));
displayAudioDetection$: Observable<boolean> = this.streamConfig.observable$.pipe(map((config) => config.displayAudioDetection));
streamVideoControls$: Observable<boolean> = this.streamConfig.observable$.pipe(map((config) => config.videoControls));
participantItemMuteButton$: Observable<boolean> = this.streamConfig.observable$.pipe(map((config) => config.participantItemMuteButton));
// Toolbar observables
cameraButton$: Observable<boolean> = this.toolbarConfig.observable$.pipe(map((config) => config.camera));
microphoneButton$: Observable<boolean> = this.toolbarConfig.observable$.pipe(map((config) => config.microphone));
screenshareButton$: Observable<boolean> = this.toolbarConfig.observable$.pipe(map((config) => config.screenshare));
fullscreenButton$: Observable<boolean> = this.toolbarConfig.observable$.pipe(map((config) => config.fullscreen));
captionsButton$: Observable<boolean> = this.toolbarConfig.observable$.pipe(map((config) => config.captions));
toolbarSettingsButton$: Observable<boolean> = this.toolbarConfig.observable$.pipe(map((config) => config.settings));
leaveButton$: Observable<boolean> = this.toolbarConfig.observable$.pipe(map((config) => config.leave));
participantsPanelButton$: Observable<boolean> = this.toolbarConfig.observable$.pipe(map((config) => config.participantsPanel));
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
recordingActivity$: Observable<boolean> = this.recordingActivityConfig.observable$.pipe(map((config) => config.enabled));
recordingActivityReadOnly$: Observable<boolean> = this.recordingActivityConfig.observable$.pipe(map((config) => config.readOnly));
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$;
// Internals
private layoutRemoteParticipants: BehaviorSubject<ParticipantModel[] | undefined> = new BehaviorSubject(<any>undefined);
layoutRemoteParticipants$: Observable<ParticipantModel[] | undefined>;
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();
}
// ============================================
// BATCH UPDATE METHODS
// ============================================
/**
* Update multiple general configuration properties at once
*/
updateGeneralConfig(partialConfig: Partial<GeneralConfig>): void {
const current = this.generalConfig.subject.getValue();
this.generalConfig.subject.next({ ...current, ...partialConfig });
setToken(token: string) {
this.token.next(token);
}
/**
* Update multiple toolbar configuration properties at once
*/
updateToolbarConfig(partialConfig: Partial<ToolbarConfig>): void {
const current = this.toolbarConfig.subject.getValue();
this.toolbarConfig.subject.next({ ...current, ...partialConfig });
setLivekitUrl(livekitUrl: string) {
this.livekitUrl.next(livekitUrl);
}
/**
* 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 {
return this.generalConfig.subject.getValue().livekitUrl;
return this.livekitUrl.getValue();
}
showPrejoin(): boolean {
return this.generalConfig.subject.getValue().prejoin;
setTokenError(error: any) {
this.tokenError.next(error);
}
getShowDisconnectionDialog(): boolean {
return this.generalConfig.subject.getValue().showDisconnectionDialog;
setMinimal(minimal: boolean) {
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 {
let baseUrl = this.generalConfig.subject.getValue().recordingStreamBaseUrl;
let baseUrl = this.recordingStreamBaseUrl.getValue();
// Add trailing slash if not present
baseUrl += baseUrl.endsWith('/') ? '' : '/';
return baseUrl;
}
// Stream configuration methods
//Toolbar settings
isVideoEnabled(): boolean {
return this.streamConfig.subject.getValue().videoEnabled;
setCameraButton(cameraButton: boolean) {
this.cameraButton.next(cameraButton);
}
isAudioEnabled(): boolean {
return this.streamConfig.subject.getValue().audioEnabled;
showCameraButton(): boolean {
return this.cameraButton.getValue();
}
// Toolbar configuration methods
setMicrophoneButton(microphoneButton: boolean) {
this.microphoneButton.next(microphoneButton);
}
getRoomName(): string {
return this.toolbarConfig.subject.getValue().roomName;
showMicrophoneButton(): boolean {
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) {
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 {
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) {
this.broadcastingActivityConfig.subject.next(broadcastingActivity);
setAdminRecordingsList(adminRecordingsList: RecordingInfo[]) {
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
setLayoutRemoteParticipants(participants: ParticipantModel[] | undefined) {
this.layoutRemoteParticipantsConfig.subject.next(participants);
}
// Recording Activity Configuration methods
showRecordingActivityRecordingsList(): boolean {
return this.recordingActivityConfig.subject.getValue().showRecordingsList;
this.layoutRemoteParticipants.next(participants);
}
}

View File

@ -1,5 +1,6 @@
import { Injectable } from '@angular/core';
import { ILogService, ILogger } from '../../models/logger.model';
import { ILogService } from '../../models/logger.model';
import { GlobalConfigService } from '../config/global-config.service';
/**
@ -9,87 +10,42 @@ import { GlobalConfigService } from '../config/global-config.service';
providedIn: 'root'
})
export class LoggerService implements ILogService {
private log: Console;
private LOG_FNS: Function[] = [];
private MSG_PREFIXES: string[][] = [
['[', '] DEBUG: '],
['[', '] VERBOSE: '],
public log;
public LOG_FNS = [];
public MSG_PREFIXES = [
['[', ']'],
['[', '] WARN: '],
['[', '] ERROR: ']
];
private loggerCache: Map<string, ILogger> = new Map();
constructor(private globalService: GlobalConfigService) {
this.initializeLogger();
}
private initializeLogger(): void {
private getLoggerFns(prefix: string) {
this.log = window.console;
this.LOG_FNS = [
this.log.log.bind(this.log),
this.log.debug.bind(this.log),
this.log.warn.bind(this.log),
this.log.error.bind(this.log)
];
this.LOG_FNS = [this.log.log, this.log.warn, this.log.error];
const loggerFns = this.LOG_FNS.map((logTemplFn, i) => {
return logTemplFn.bind(this.log, this.MSG_PREFIXES[i][0] + prefix + this.MSG_PREFIXES[i][1]);
});
return loggerFns;
}
private createLoggerFunctions(
prefix: string
): [(...args: any[]) => void, (...args: any[]) => void, (...args: any[]) => void, (...args: any[]) => void] {
public get(prefix: string) {
const prodMode = this.globalService.isProduction();
const debugFn = (...args: any[]): void => {
const loggerService = this;
return {
d: function(...args: any[]) {
if (!prodMode) {
// Only log debug messages in non-production mode
this.LOG_FNS[0](this.MSG_PREFIXES[0][0] + prefix + this.MSG_PREFIXES[0][1], ...args);
loggerService.getLoggerFns(prefix)[0].apply(this.log, arguments);
}
},
w: function(...args: any[]) {
loggerService.getLoggerFns(prefix)[1].apply(this.log, arguments);
},
e: function(...args: any[]) {
loggerService.getLoggerFns(prefix)[2].apply(this.log, arguments);
}
};
const verboseFn = (...args: any[]): void => {
if (!prodMode) {
// Only log verbose messages in non-production mode and when verbose is enabled
this.LOG_FNS[1](this.MSG_PREFIXES[1][0] + prefix + this.MSG_PREFIXES[1][1], ...args);
}
};
const warnFn = (...args: any[]): void => {
this.LOG_FNS[2](this.MSG_PREFIXES[2][0] + prefix + this.MSG_PREFIXES[2][1], ...args);
};
const errorFn = (...args: any[]): void => {
this.LOG_FNS[3](this.MSG_PREFIXES[3][0] + prefix + this.MSG_PREFIXES[3][1], ...args);
};
return [debugFn, verboseFn, warnFn, errorFn];
}
public get(prefix: string): ILogger {
// Check cache first
if (this.loggerCache.has(prefix)) {
return this.loggerCache.get(prefix)!;
}
// Create new logger functions
const [debugFn, verboseFn, warnFn, errorFn] = this.createLoggerFunctions(prefix);
const logger: ILogger = {
d: debugFn,
v: verboseFn,
w: warnFn,
e: errorFn
};
// Cache the logger
this.loggerCache.set(prefix, logger);
return logger;
}
/**
* Clears the logger cache. Useful for testing or when configuration changes.
* @internal
*/
public clearCache(): void {
this.loggerCache.clear();
}
}

View File

@ -64,12 +64,6 @@ export class OpenViduService {
* @internal
*/
initRoom(): void {
// If room already exists, don't recreate it
if (this.room) {
this.log.d('Room already initialized, skipping re-initialization');
return;
}
const videoDeviceId = this.deviceService.getCameraSelected()?.device ?? undefined;
const audioDeviceId = this.deviceService.getMicrophoneSelected()?.device ?? undefined;
const roomOptions: RoomOptions = {
@ -94,7 +88,6 @@ export class OpenViduService {
disconnectOnPageLeave: true
};
this.room = new Room(roomOptions);
this.log.d('Room initialized successfully');
}
/**
@ -137,20 +130,12 @@ export class OpenViduService {
*/
getRoom(): Room {
if (!this.room) {
this.log.e('Room is not initialized. Make sure token is set before accessing the room.');
throw new Error('Room is not initialized. Make sure token is set before accessing the room.');
this.log.e('Room is not initialized');
throw new Error('Room is not initialized');
}
return this.room;
}
/**
* Checks if room is initialized without throwing an error
* @returns true if room is initialized, false otherwise
*/
isRoomInitialized(): boolean {
return !!this.room;
}
/**
* Returns the room name
*/
@ -166,14 +151,6 @@ export class OpenViduService {
return this.room?.state === ConnectionState.Connected;
}
hasRoomTracksPublished(): boolean {
const { localParticipant, remoteParticipants } = this.getRoom();
const localTracks = localParticipant.getTrackPublications();
const remoteTracks = Array.from(remoteParticipants.values()).flatMap((p) => p.getTrackPublications());
return localTracks.length > 0 || remoteTracks.length > 0;
}
/**
* @internal
*/
@ -186,13 +163,6 @@ export class OpenViduService {
this.log.e('LiveKit URL is not defined. Please, check the livekitUrl parameter of the VideoConferenceComponent');
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);
}

View File

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

View File

@ -1,6 +1,6 @@
import { Injectable, OnDestroy } from '@angular/core';
import { Injectable } from '@angular/core';
import { ILogger } from '../../models/logger.model';
import { STORAGE_PREFIX, StorageKeys, SESSION_KEYS, TAB_MANAGEMENT_KEYS, TAB_SPECIFIC_KEYS, SHARED_PERSISTENT_KEYS } from '../../models/storage.model';
import { STORAGE_PREFIX, StorageKeys } from '../../models/storage.model';
import { LoggerService } from '../logger/logger.service';
import { CustomDevice } from '../../models/device.model';
@ -10,125 +10,13 @@ import { CustomDevice } from '../../models/device.model';
@Injectable({
providedIn: 'root'
})
export class StorageService implements OnDestroy {
public localStorage = window.localStorage;
public sessionStorage = window.sessionStorage;
export class StorageService {
public storage = window.localStorage;
public log: ILogger;
protected PREFIX_KEY = STORAGE_PREFIX;
private tabId: string;
private readonly TAB_CLEANUP_INTERVAL = 30000; // 30 seconds
private cleanupInterval: any;
constructor(protected loggerSrv: LoggerService) {
this.log = this.loggerSrv.get('StorageService');
this.initializeTabManagement();
}
/**
* Initializes tab management system
* Creates unique tab ID and sets up cleanup mechanism
*/
private initializeTabManagement(): void {
// Generate unique tab ID
this.tabId = this.generateTabId();
this.setSessionValue(StorageKeys.TAB_ID, this.tabId);
// Register this tab as active
this.registerActiveTab();
// Set up periodic cleanup of inactive tabs
this.setupTabCleanup();
// Listen for page unload to clean up this tab
window.addEventListener('beforeunload', () => {
this.unregisterActiveTab();
});
this.log.d(`Tab initialized with ID: ${this.tabId}`);
}
/**
* Generates a unique tab identifier
*/
private generateTabId(): string {
return `tab_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`;
}
/**
* Registers current tab as active
*/
private registerActiveTab(): void {
const activeTabs = this.getActiveTabsFromStorage() || {};
activeTabs[this.tabId] = Date.now();
this.setLocalValue(StorageKeys.ACTIVE_TABS, activeTabs);
}
/**
* Unregisters current tab from active tabs
*/
private unregisterActiveTab(): void {
const activeTabs = this.getActiveTabsFromStorage() || {};
delete activeTabs[this.tabId];
this.setLocalValue(StorageKeys.ACTIVE_TABS, activeTabs);
this.cleanupTabData(this.tabId);
}
/**
* Sets up periodic cleanup of inactive tabs
*/
private setupTabCleanup(): void {
this.cleanupInterval = setInterval(() => {
this.cleanupInactiveTabs();
}, this.TAB_CLEANUP_INTERVAL);
}
/**
* Cleans up data from inactive tabs
*/
private cleanupInactiveTabs(): void {
const activeTabs = this.getActiveTabsFromStorage() || {};
const currentTime = Date.now();
const timeoutThreshold = this.TAB_CLEANUP_INTERVAL * 2; // 60 seconds
Object.keys(activeTabs).forEach(tabId => {
const lastActivity = activeTabs[tabId];
if (currentTime - lastActivity > timeoutThreshold) {
this.log.d(`Cleaning up inactive tab: ${tabId}`);
delete activeTabs[tabId];
this.cleanupTabData(tabId);
}
});
// Update heartbeat for current tab
activeTabs[this.tabId] = currentTime;
this.setLocalValue(StorageKeys.ACTIVE_TABS, activeTabs);
}
/**
* Cleans up data associated with a specific tab
*/
private cleanupTabData(tabId: string): void {
// Clean up tab-specific data from localStorage
TAB_SPECIFIC_KEYS.forEach(key => {
const storageKey = `${this.PREFIX_KEY}${tabId}_${key}`;
this.localStorage.removeItem(storageKey);
});
this.log.d(`Cleaned up data for tab: ${tabId}`);
}
/**
* Gets active tabs from localStorage
*/
private getActiveTabsFromStorage(): { [key: string]: number } | null {
return this.getLocalValue(StorageKeys.ACTIVE_TABS);
}
/**
* Gets the current tab ID
*/
public getTabId(): string {
return this.tabId;
}
getParticipantName(): string | null {
@ -218,164 +106,24 @@ export class StorageService implements OnDestroy {
}
protected set(key: string, item: any) {
if (SESSION_KEYS.includes(key as StorageKeys)) {
this.setSessionValue(key, item);
} else {
this.setLocalValue(key, item);
}
const value = JSON.stringify({ item: item });
this.storage.setItem(this.PREFIX_KEY + key, value);
}
protected get(key: string): any {
if (SESSION_KEYS.includes(key as StorageKeys)) {
return this.getSessionValue(key);
} else {
return this.getLocalValue(key);
const str = this.storage.getItem(this.PREFIX_KEY + key);
if (!!str) {
return JSON.parse(str).item;
}
return null;
}
protected remove(key: string) {
if (SESSION_KEYS.includes(key as StorageKeys)) {
this.removeSessionValue(key);
} else {
this.removeLocalValue(key);
}
}
/**
* Determines if a key should use tab-specific storage in localStorage
*/
private shouldUseTabSpecificKey(key: string): boolean {
return TAB_SPECIFIC_KEYS.includes(key as StorageKeys);
}
/**
* Sets value in localStorage with tab-specific key if needed
*/
private setLocalValue(key: string, item: any): void {
const value = JSON.stringify({ item: item });
const storageKey = this.shouldUseTabSpecificKey(key)
? `${this.PREFIX_KEY}${this.tabId}_${key}`
: `${this.PREFIX_KEY}${key}`;
this.localStorage.setItem(storageKey, value);
}
/**
* Gets value from localStorage with tab-specific key if needed
*/
private getLocalValue(key: string): any {
const storageKey = this.shouldUseTabSpecificKey(key)
? `${this.PREFIX_KEY}${this.tabId}_${key}`
: `${this.PREFIX_KEY}${key}`;
const str = this.localStorage.getItem(storageKey);
if (!!str) {
return JSON.parse(str).item;
}
return null;
}
/**
* Removes value from localStorage with tab-specific key if needed
*/
private removeLocalValue(key: string): void {
const storageKey = this.shouldUseTabSpecificKey(key)
? `${this.PREFIX_KEY}${this.tabId}_${key}`
: `${this.PREFIX_KEY}${key}`;
this.localStorage.removeItem(storageKey);
}
/**
* Sets value in sessionStorage
*/
private setSessionValue(key: string, item: any): void {
const value = JSON.stringify({ item: item });
this.sessionStorage.setItem(this.PREFIX_KEY + key, value);
}
/**
* Gets value from sessionStorage
*/
private getSessionValue(key: string): any {
const str = this.sessionStorage.getItem(this.PREFIX_KEY + key);
if (!!str) {
return JSON.parse(str).item;
}
return null;
}
/**
* Removes value from sessionStorage
*/
private removeSessionValue(key: string): void {
this.sessionStorage.removeItem(this.PREFIX_KEY + key);
this.storage.removeItem(this.PREFIX_KEY + key);
}
public clear() {
this.log.d('Clearing localStorage and sessionStorage');
// Clear only our prefixed keys from localStorage
Object.keys(this.localStorage).forEach(key => {
if (key.startsWith(this.PREFIX_KEY)) {
this.localStorage.removeItem(key);
}
});
// Clear only our prefixed keys from sessionStorage
Object.keys(this.sessionStorage).forEach(key => {
if (key.startsWith(this.PREFIX_KEY)) {
this.sessionStorage.removeItem(key);
}
});
}
/**
* Clears only session data (tab-specific data)
*/
public clearSessionData(): void {
this.log.d('Clearing session data');
Object.keys(this.sessionStorage).forEach(key => {
if (key.startsWith(this.PREFIX_KEY)) {
this.sessionStorage.removeItem(key);
}
});
}
/**
* Clears only tab-specific data for current tab
*/
public clearTabSpecificData(): void {
this.log.d('Clearing tab-specific data');
TAB_SPECIFIC_KEYS.forEach(key => {
this.removeLocalValue(key);
});
}
/**
* Clears only persistent data
*/
public clearPersistentData(): void {
this.log.d('Clearing persistent data');
SHARED_PERSISTENT_KEYS.forEach(key => {
this.removeLocalValue(key);
});
TAB_MANAGEMENT_KEYS.forEach(key => {
this.removeLocalValue(key);
});
}
/**
* Cleanup method to be called when service is destroyed
*/
public destroy(): void {
if (this.cleanupInterval) {
clearInterval(this.cleanupInterval);
}
this.unregisterActiveTab();
}
/**
* Angular lifecycle hook - called when service is destroyed
*/
ngOnDestroy(): void {
this.destroy();
this.log.d('Clearing localStorage');
this.storage.clear();
}
}

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/config/openvidu-components-angular.config';
// Directives
export * from './lib/directives/template/internals.directive';
export * from './lib/directives/api/activities-panel.directive';
export * from './lib/directives/api/admin.directive';
export * from './lib/directives/api/api.directive.module';

View File

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

View File

@ -34,6 +34,7 @@ export class CallComponent implements OnInit {
{ name: 'custom', lang: 'cus' }
];
prejoin: boolean = true;
prejoinDisplayParticipantName: boolean = true;
participantName: string = `Participant${Math.floor(Math.random() * 1000)}`;
videoEnabled: boolean = true;
audioEnabled: boolean = true;
@ -58,13 +59,6 @@ export class CallComponent implements OnInit {
activitiesPanelBroadcastingActivity: boolean = true;
toolbarSettingsButton: boolean = true;
fakeDevices: boolean = false;
// Internal directive inputs (public for E2E)
prejoinDisplayParticipantName: boolean = true;
public recordingActivityViewRecordingsButton: boolean = false;
public recordingActivityStartStopRecordingButton: boolean = true;
toolbarViewRecordingsButton: boolean = false;
private redirectToHomeOnLeaves: boolean = true;
private staticVideos = [
@ -110,6 +104,8 @@ export class CallComponent implements OnInit {
} catch {}
}
if (params['prejoin'] !== undefined) this.prejoin = params['prejoin'] === 'true';
if (params['displayParticipantName'] !== undefined)
this.prejoinDisplayParticipantName = params['displayParticipantName'] === 'true';
if (params['participantName']) this.participantName = params['participantName'];
if (params['videoEnabled'] !== undefined) this.videoEnabled = params['videoEnabled'] === 'true';
if (params['audioEnabled'] !== undefined) this.audioEnabled = params['audioEnabled'] === 'true';
@ -145,15 +141,6 @@ export class CallComponent implements OnInit {
if (params['fakeDevices'] !== undefined) this.fakeDevices = params['fakeDevices'] === 'true';
// Internal/private directive params
if (params['prejoinDisplayParticipantName'] !== undefined)
this.prejoinDisplayParticipantName = params['prejoinDisplayParticipantName'] === 'true';
if (params['recordingActivityViewRecordingsButton'] !== undefined)
this.recordingActivityViewRecordingsButton = params['recordingActivityViewRecordingsButton'] === 'true';
if (params['recordingActivityStartStopRecordingButton'] !== undefined)
this.recordingActivityStartStopRecordingButton = params['recordingActivityStartStopRecordingButton'] === 'true';
if (params['toolbarViewRecordingsButton'] !== undefined)
this.toolbarViewRecordingsButton = params['toolbarViewRecordingsButton'] === 'true';
if (params['redirectToHome'] === undefined) {
this.redirectToHomeOnLeaves = true;
} else {
@ -211,10 +198,8 @@ export class CallComponent implements OnInit {
if (publication.videoTrack?.attachedElements) {
this.replaceWithStaticVideos(publication.videoTrack?.attachedElements);
const firstVideo = this.staticVideos.shift();
if (firstVideo) {
this.staticVideos.push(firstVideo);
}
}
}, 2000);
}
});

View File

@ -41,13 +41,6 @@ Parameters:
Description: "If certificate type is 'letsencrypt', this email will be used for Let's Encrypt notifications"
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:
Description: '(Optional) Domain name for the TURN server with TLS. Only needed if your users are behind restrictive firewalls'
Type: String
@ -227,10 +220,6 @@ Metadata:
default: S3 bucket for application data and recordings
Parameters:
- S3AppDataBucketName
- Label:
default: "(Optional) Additional Installer Flags"
Parameters:
- AdditionalInstallFlags
- Label:
default: (Optional) TURN server configuration with TLS
Parameters:
@ -271,9 +260,10 @@ Resources:
"GRAFANA_ADMIN_PASSWORD": "none",
"LIVEKIT_API_KEY": "none",
"LIVEKIT_API_SECRET": "none",
"MEET_ADMIN_USER": "none",
"MEET_ADMIN_SECRET": "none",
"MEET_API_KEY": "none",
"DEFAULT_APP_USERNAME": "none",
"DEFAULT_APP_PASSWORD": "none",
"DEFAULT_APP_ADMIN_USERNAME": "none",
"DEFAULT_APP_ADMIN_PASSWORD": "none",
"ENABLED_MODULES": "none"
}
@ -366,7 +356,7 @@ Resources:
'/usr/local/bin/install.sh':
content: !Sub |
#!/bin/bash -x
OPENVIDU_VERSION=main
OPENVIDU_VERSION=3.2.0
DOMAIN=
YQ_VERSION=v4.44.5
@ -409,10 +399,11 @@ Resources:
DASHBOARD_ADMIN_PASSWORD="$(/usr/local/bin/store_secret.sh generate DASHBOARD_ADMIN_PASSWORD)"
GRAFANA_ADMIN_USERNAME="$(/usr/local/bin/store_secret.sh save GRAFANA_ADMIN_USERNAME "grafanaadmin")"
GRAFANA_ADMIN_PASSWORD="$(/usr/local/bin/store_secret.sh generate GRAFANA_ADMIN_PASSWORD)"
MEET_ADMIN_USER="$(/usr/local/bin/store_secret.sh save MEET_ADMIN_USER "meetadmin")"
MEET_ADMIN_SECRET="$(/usr/local/bin/store_secret.sh generate MEET_ADMIN_SECRET)"
MEET_API_KEY="$(/usr/local/bin/store_secret.sh generate MEET_API_KEY)"
ENABLED_MODULES="$(/usr/local/bin/store_secret.sh save ENABLED_MODULES "observability,openviduMeet")"
DEFAULT_APP_USERNAME="$(/usr/local/bin/store_secret.sh save DEFAULT_APP_USERNAME "calluser")"
DEFAULT_APP_PASSWORD="$(/usr/local/bin/store_secret.sh generate DEFAULT_APP_PASSWORD)"
DEFAULT_APP_ADMIN_USERNAME="$(/usr/local/bin/store_secret.sh save DEFAULT_APP_ADMIN_USERNAME "calladmin")"
DEFAULT_APP_ADMIN_PASSWORD="$(/usr/local/bin/store_secret.sh generate DEFAULT_APP_ADMIN_PASSWORD)"
ENABLED_MODULES="$(/usr/local/bin/store_secret.sh save ENABLED_MODULES "observability,app")"
LIVEKIT_API_KEY="$(/usr/local/bin/store_secret.sh generate LIVEKIT_API_KEY "API" 12)"
LIVEKIT_API_SECRET="$(/usr/local/bin/store_secret.sh generate LIVEKIT_API_SECRET)"
@ -437,25 +428,14 @@ Resources:
"--dashboard-admin-password=$DASHBOARD_ADMIN_PASSWORD"
"--grafana-admin-user=$GRAFANA_ADMIN_USERNAME"
"--grafana-admin-password=$GRAFANA_ADMIN_PASSWORD"
"--meet-admin-user=$MEET_ADMIN_USER"
"--meet-admin-password=$MEET_ADMIN_SECRET"
"--meet-api-key=$MEET_API_KEY"
"--default-app-user=$DEFAULT_APP_USERNAME"
"--default-app-password=$DEFAULT_APP_PASSWORD"
"--default-app-admin-user=$DEFAULT_APP_ADMIN_USERNAME"
"--default-app-admin-password=$DEFAULT_APP_ADMIN_PASSWORD"
"--livekit-api-key=$LIVEKIT_API_KEY"
"--livekit-api-secret=$LIVEKIT_API_SECRET"
)
# 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
if [[ "${TurnDomainName}" != '' ]]; then
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/LIVEKIT_API_KEY=.*/LIVEKIT_API_KEY=$(echo $SHARED_SECRET | jq -r .LIVEKIT_API_KEY)/" "${!CONFIG_DIR}/openvidu.env"
sed -i "s/LIVEKIT_API_SECRET=.*/LIVEKIT_API_SECRET=$(echo $SHARED_SECRET | jq -r .LIVEKIT_API_SECRET)/" "${!CONFIG_DIR}/openvidu.env"
sed -i "s/MEET_ADMIN_USER=.*/MEET_ADMIN_USER=$(echo $SHARED_SECRET | jq -r .MEET_ADMIN_USER)/" "${!CONFIG_DIR}/meet.env"
sed -i "s/MEET_ADMIN_SECRET=.*/MEET_ADMIN_SECRET=$(echo $SHARED_SECRET | jq -r .MEET_ADMIN_SECRET)/" "${!CONFIG_DIR}/meet.env"
sed -i "s/MEET_API_KEY=.*/MEET_API_KEY=$(echo $SHARED_SECRET | jq -r .MEET_API_KEY)/" "${!CONFIG_DIR}/meet.env"
sed -i "s/CALL_USER=.*/CALL_USER=$(echo $SHARED_SECRET | jq -r .DEFAULT_APP_USERNAME)/" "${!CONFIG_DIR}/app.env"
sed -i "s/CALL_SECRET=.*/CALL_SECRET=$(echo $SHARED_SECRET | jq -r .DEFAULT_APP_PASSWORD)/" "${!CONFIG_DIR}/app.env"
sed -i "s/CALL_ADMIN_USER=.*/CALL_ADMIN_USER=$(echo $SHARED_SECRET | jq -r .DEFAULT_APP_ADMIN_USERNAME)/" "${!CONFIG_DIR}/app.env"
sed -i "s/CALL_ADMIN_SECRET=.*/CALL_ADMIN_SECRET=$(echo $SHARED_SECRET | jq -r .DEFAULT_APP_ADMIN_PASSWORD)/" "${!CONFIG_DIR}/app.env"
sed -i "s/ENABLED_MODULES=.*/ENABLED_MODULES=$(echo $SHARED_SECRET | jq -r .ENABLED_MODULES)/" "${!CONFIG_DIR}/openvidu.env"
# Update URLs in secret
@ -688,9 +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 '. + {"LIVEKIT_API_KEY": "'"$(/usr/local/bin/get_value_from_config.sh LIVEKIT_API_KEY "${!CONFIG_DIR}/openvidu.env")"'"}')"
SHARED_SECRET="$(echo "$SHARED_SECRET" | jq '. + {"LIVEKIT_API_SECRET": "'"$(/usr/local/bin/get_value_from_config.sh LIVEKIT_API_SECRET "${!CONFIG_DIR}/openvidu.env")"'"}')"
SHARED_SECRET="$(echo "$SHARED_SECRET" | jq '. + {"MEET_ADMIN_USER": "'"$(/usr/local/bin/get_value_from_config.sh MEET_ADMIN_USER "${!CONFIG_DIR}/meet.env")"'"}')"
SHARED_SECRET="$(echo "$SHARED_SECRET" | jq '. + {"MEET_ADMIN_SECRET": "'"$(/usr/local/bin/get_value_from_config.sh MEET_ADMIN_SECRET "${!CONFIG_DIR}/meet.env")"'"}')"
SHARED_SECRET="$(echo "$SHARED_SECRET" | jq '. + {"MEET_API_KEY": "'"$(/usr/local/bin/get_value_from_config.sh MEET_API_KEY "${!CONFIG_DIR}/meet.env")"'"}')"
SHARED_SECRET="$(echo "$SHARED_SECRET" | jq '. + {"DEFAULT_APP_USERNAME": "'"$(/usr/local/bin/get_value_from_config.sh CALL_USER "${!CONFIG_DIR}/app.env")"'"}')"
SHARED_SECRET="$(echo "$SHARED_SECRET" | jq '. + {"DEFAULT_APP_PASSWORD": "'"$(/usr/local/bin/get_value_from_config.sh CALL_SECRET "${!CONFIG_DIR}/app.env")"'"}')"
SHARED_SECRET="$(echo "$SHARED_SECRET" | jq '. + {"DEFAULT_APP_ADMIN_USERNAME": "'"$(/usr/local/bin/get_value_from_config.sh CALL_ADMIN_USER "${!CONFIG_DIR}/app.env")"'"}')"
SHARED_SECRET="$(echo "$SHARED_SECRET" | jq '. + {"DEFAULT_APP_ADMIN_PASSWORD": "'"$(/usr/local/bin/get_value_from_config.sh CALL_ADMIN_SECRET "${!CONFIG_DIR}/app.env")"'"}')"
SHARED_SECRET="$(echo "$SHARED_SECRET" | jq '. + {"ENABLED_MODULES": "'"$(/usr/local/bin/get_value_from_config.sh ENABLED_MODULES "${!CONFIG_DIR}/openvidu.env")"'"}')"
# Update shared secret
@ -931,14 +913,6 @@ Resources:
FromPort: 1935
ToPort: 1935
CidrIpv6: ::/0
- IpProtocol: tcp
FromPort: 7881
ToPort: 7881
CidrIp: 0.0.0.0/0
- IpProtocol: tcp
FromPort: 7881
ToPort: 7881
CidrIpv6: ::/0
- IpProtocol: udp
FromPort: 7885
ToPort: 7885
@ -955,6 +929,14 @@ Resources:
FromPort: 50000
ToPort: 60000
CidrIpv6: ::/0
- IpProtocol: tcp
FromPort: 50000
ToPort: 60000
CidrIp: 0.0.0.0/0
- IpProtocol: tcp
FromPort: 50000
ToPort: 60000
CidrIpv6: ::/0
Outputs:
ServicesAndCredentials:

View File

@ -159,8 +159,6 @@ param adminUsername string
@secure()
param adminSshKey object
param additionalInstallFlags string = ''
/*------------------------------------------- VARIABLES AND VALIDATIONS -------------------------------------------*/
//Condition for ipValid if is filled
@ -265,12 +263,11 @@ var stringInterpolationParams = {
turnOwnPublicCertificate: turnOwnPublicCertificate
turnOwnPrivateCertificate: turnOwnPrivateCertificate
keyVaultName: keyVaultName
additionalInstallFlags: additionalInstallFlags
}
var installScriptTemplate = '''
#!/bin/bash -x
OPENVIDU_VERSION=main
OPENVIDU_VERSION=3.2.0
DOMAIN=
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)"
GRAFANA_ADMIN_USERNAME="$(/usr/local/bin/store_secret.sh save GRAFANA-ADMIN-USERNAME "grafanaadmin")"
GRAFANA_ADMIN_PASSWORD="$(/usr/local/bin/store_secret.sh generate GRAFANA-ADMIN-PASSWORD)"
MEET_ADMIN_USER="$(/usr/local/bin/store_secret.sh save MEET-ADMIN-USER "meetadmin")"
MEET_ADMIN_SECRET="$(/usr/local/bin/store_secret.sh generate MEET-ADMIN-SECRET)"
MEET_API_KEY="$(/usr/local/bin/store_secret.sh generate MEET-API-KEY)"
DEFAULT_APP_USERNAME="$(/usr/local/bin/store_secret.sh save DEFAULT-APP-USERNAME "calluser")"
DEFAULT_APP_PASSWORD="$(/usr/local/bin/store_secret.sh generate DEFAULT-APP-PASSWORD)"
DEFAULT_APP_ADMIN_USERNAME="$(/usr/local/bin/store_secret.sh save DEFAULT-APP-ADMIN-USERNAME "calladmin")"
DEFAULT_APP_ADMIN_PASSWORD="$(/usr/local/bin/store_secret.sh generate DEFAULT-APP-ADMIN-PASSWORD)"
LIVEKIT_API_KEY="$(/usr/local/bin/store_secret.sh generate LIVEKIT-API-KEY "API" 12)"
LIVEKIT_API_SECRET="$(/usr/local/bin/store_secret.sh generate LIVEKIT-API-SECRET)"
ENABLED_MODULES="$(/usr/local/bin/store_secret.sh save ENABLED-MODULES "observability,openviduMeet")"
ENABLED_MODULES="$(/usr/local/bin/store_secret.sh save ENABLED-MODULES "observability,app")"
# Base command
INSTALL_COMMAND="sh <(curl -fsSL http://get.openvidu.io/community/singlenode/$OPENVIDU_VERSION/install.sh)"
@ -328,25 +326,14 @@ COMMON_ARGS=(
"--dashboard-admin-password=$DASHBOARD_ADMIN_PASSWORD"
"--grafana-admin-user=$GRAFANA_ADMIN_USERNAME"
"--grafana-admin-password=$GRAFANA_ADMIN_PASSWORD"
"--meet-admin-user=$MEET_ADMIN_USER"
"--meet-admin-password=$MEET_ADMIN_SECRET"
"--meet-api-key=$MEET_API_KEY"
"--default-app-user=$DEFAULT_APP_USERNAME"
"--default-app-password=$DEFAULT_APP_PASSWORD"
"--default-app-admin-user=$DEFAULT_APP_ADMIN_USERNAME"
"--default-app-admin-password=$DEFAULT_APP_ADMIN_PASSWORD"
"--livekit-api-key=$LIVEKIT_API_KEY"
"--livekit-api-secret=$LIVEKIT_API_SECRET"
)
# 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
if [[ "${turnDomainName}" != '' ]]; then
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 LIVEKIT_API_KEY=$(az keyvault secret show --vault-name ${keyVaultName} --name LIVEKIT-API-KEY --query value -o tsv)
export LIVEKIT_API_SECRET=$(az keyvault secret show --vault-name ${keyVaultName} --name LIVEKIT-API-SECRET --query value -o tsv)
export MEET_ADMIN_USER=$(az keyvault secret show --vault-name ${keyVaultName} --name MEET-ADMIN-USER --query value -o tsv)
export MEET_ADMIN_SECRET=$(az keyvault secret show --vault-name ${keyVaultName} --name MEET-ADMIN-SECRET --query value -o tsv)
export MEET_API_KEY=$(az keyvault secret show --vault-name ${keyVaultName} --name MEET-API-KEY --query value -o tsv)
export DEFAULT_APP_USERNAME=$(az keyvault secret show --vault-name ${keyVaultName} --name DEFAULT-APP-USERNAME --query value -o tsv)
export DEFAULT_APP_PASSWORD=$(az keyvault secret show --vault-name ${keyVaultName} --name DEFAULT-APP-PASSWORD --query value -o tsv)
export DEFAULT_APP_ADMIN_USERNAME=$(az keyvault secret show --vault-name ${keyVaultName} --name DEFAULT-APP-ADMIN-USERNAME --query value -o tsv)
export DEFAULT_APP_ADMIN_PASSWORD=$(az keyvault secret show --vault-name ${keyVaultName} --name DEFAULT-APP-ADMIN-PASSWORD --query value -o tsv)
export ENABLED_MODULES=$(az keyvault secret show --vault-name ${keyVaultName} --name ENABLED-MODULES --query value -o tsv)
@ -494,9 +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/LIVEKIT_API_KEY=.*/LIVEKIT_API_KEY=$LIVEKIT_API_KEY/" "${CONFIG_DIR}/openvidu.env"
sed -i "s/LIVEKIT_API_SECRET=.*/LIVEKIT_API_SECRET=$LIVEKIT_API_SECRET/" "${CONFIG_DIR}/openvidu.env"
sed -i "s/MEET_ADMIN_USER=.*/MEET_ADMIN_USER=$MEET_ADMIN_USER/" "${CONFIG_DIR}/meet.env"
sed -i "s/MEET_ADMIN_SECRET=.*/MEET_ADMIN_SECRET=$MEET_ADMIN_SECRET/" "${CONFIG_DIR}/meet.env"
sed -i "s/MEET_API_KEY=.*/MEET_API_KEY=$MEET_API_KEY/" "${CONFIG_DIR}/meet.env"
sed -i "s/CALL_USER=.*/CALL_USER=$DEFAULT_APP_USERNAME/" "${CONFIG_DIR}/app.env"
sed -i "s/CALL_SECRET=.*/CALL_SECRET=$DEFAULT_APP_PASSWORD/" "${CONFIG_DIR}/app.env"
sed -i "s/CALL_ADMIN_USER=.*/CALL_ADMIN_USER=$DEFAULT_APP_ADMIN_USERNAME/" "${CONFIG_DIR}/app.env"
sed -i "s/CALL_ADMIN_SECRET=.*/CALL_ADMIN_SECRET=$DEFAULT_APP_ADMIN_PASSWORD/" "${CONFIG_DIR}/app.env"
sed -i "s/ENABLED_MODULES=.*/ENABLED_MODULES=$ENABLED_MODULES/" "${CONFIG_DIR}/openvidu.env"
@ -543,9 +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")"
LIVEKIT_API_KEY="$(/usr/local/bin/get_value_from_config.sh LIVEKIT_API_KEY "${CONFIG_DIR}/openvidu.env")"
LIVEKIT_API_SECRET="$(/usr/local/bin/get_value_from_config.sh LIVEKIT_API_SECRET "${CONFIG_DIR}/openvidu.env")"
MEET_ADMIN_USER="$(/usr/local/bin/get_value_from_config.sh MEET_ADMIN_USER "${CONFIG_DIR}/meet.env")"
MEET_ADMIN_SECRET="$(/usr/local/bin/get_value_from_config.sh MEET_ADMIN_SECRET "${CONFIG_DIR}/meet.env")"
MEET_API_KEY="$(/usr/local/bin/get_value_from_config.sh MEET_API_KEY "${CONFIG_DIR}/meet.env")"
DEFAULT_APP_USERNAME="$(/usr/local/bin/get_value_from_config.sh CALL_USER "${CONFIG_DIR}/app.env")"
DEFAULT_APP_PASSWORD="$(/usr/local/bin/get_value_from_config.sh CALL_SECRET "${CONFIG_DIR}/app.env")"
DEFAULT_APP_ADMIN_USERNAME="$(/usr/local/bin/get_value_from_config.sh CALL_ADMIN_USER "${CONFIG_DIR}/app.env")"
DEFAULT_APP_ADMIN_PASSWORD="$(/usr/local/bin/get_value_from_config.sh CALL_ADMIN_SECRET "${CONFIG_DIR}/app.env")"
ENABLED_MODULES="$(/usr/local/bin/get_value_from_config.sh ENABLED_MODULES "${CONFIG_DIR}/openvidu.env")"
@ -564,9 +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 LIVEKIT-API-KEY --value $LIVEKIT_API_KEY
az keyvault secret set --vault-name ${keyVaultName} --name LIVEKIT-API-SECRET --value $LIVEKIT_API_SECRET
az keyvault secret set --vault-name ${keyVaultName} --name MEET-ADMIN-USER --value $MEET_ADMIN_USER
az keyvault secret set --vault-name ${keyVaultName} --name MEET-ADMIN-SECRET --value $MEET_ADMIN_SECRET
az keyvault secret set --vault-name ${keyVaultName} --name MEET-API-KEY --value $MEET_API_KEY
az keyvault secret set --vault-name ${keyVaultName} --name DEFAULT-APP-USERNAME --value $DEFAULT_APP_USERNAME
az keyvault secret set --vault-name ${keyVaultName} --name DEFAULT-APP-PASSWORD --value $DEFAULT_APP_PASSWORD
az keyvault secret set --vault-name ${keyVaultName} --name DEFAULT-APP-ADMIN-USERNAME --value $DEFAULT_APP_ADMIN_USERNAME
az keyvault secret set --vault-name ${keyVaultName} --name DEFAULT-APP-ADMIN-PASSWORD --value $DEFAULT_APP_ADMIN_PASSWORD
az keyvault secret set --vault-name ${keyVaultName} --name ENABLED-MODULES --value $ENABLED_MODULES
'''
@ -1074,6 +1065,22 @@ resource webServerSecurityGroup 'Microsoft.Network/networkSecurityGroups@2023-11
direction: 'Inbound'
}
}
{
name: 'WebRTC_traffic_TCP'
properties: {
protocol: 'Tcp'
sourceAddressPrefix: '*'
sourcePortRange: '*'
destinationAddressPrefix: '*'
destinationPortRanges: [
'50000'
'60000'
]
access: 'Allow'
priority: 190
direction: 'Inbound'
}
}
]
}
}

File diff suppressed because one or more lines are too long

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",
"label": "(Optional) TURN server configuration with TLS",
@ -392,8 +370,7 @@
"adminUsername": "[steps('parameters INSTANCE').adminUsername]",
"adminSshKey": "[steps('parameters INSTANCE').adminSshKey]",
"storageAccountName": "[steps('parameters STORAGE').storageAccountName]",
"containerName": "[steps('parameters STORAGE').containerName]",
"additionalInstallFlags": "[steps('FLAGS').additionalInstallFlags]"
"containerName": "[steps('parameters STORAGE').containerName]"
}
}
}

View File

@ -3,9 +3,9 @@
set -eu
export DOCKER_VERSION="${DOCKER_VERSION:-28.1.1}"
export DOCKER_COMPOSE_VERSION="${DOCKER_COMPOSE_VERSION:-v2.35.1}"
export OPENVIDU_VERSION="${OPENVIDU_VERSION:-main}"
export OPENVIDU_VERSION="${OPENVIDU_VERSION:-3.2.0}"
export INSTALLER_IMAGE="${INSTALLER_IMAGE:-docker.io/openvidu/openvidu-installer:${OPENVIDU_VERSION}}"
export MINIO_SERVER_IMAGE="${MINIO_SERVER_IMAGE:-docker.io/openvidu/minio:2025.5.24-debian-12-r1}"
export MINIO_SERVER_IMAGE="${MINIO_SERVER_IMAGE:-docker.io/bitnami/minio:2025.5.24-debian-12-r1}"
export MINIO_CLIENT_IMAGE="${MINIO_CLIENT_IMAGE:-docker.io/minio/mc:RELEASE.2025-05-21T01-59-54Z}"
export MONGO_SERVER_IMAGE="${MONGO_SERVER_IMAGE:-docker.io/mongo:8.0.9}"
export REDIS_SERVER_IMAGE="${REDIS_SERVER_IMAGE:-docker.io/redis:7.4.4-alpine}"
@ -15,17 +15,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_SERVER_PRO_IMAGE="${OPENVIDU_SERVER_PRO_IMAGE:-docker.io/openvidu/openvidu-server-pro:${OPENVIDU_VERSION}}"
export OPENVIDU_SERVER_IMAGE="${OPENVIDU_SERVER_IMAGE:-docker.io/openvidu/openvidu-server:${OPENVIDU_VERSION}}"
export OPENVIDU_MEET_SERVER_IMAGE="${OPENVIDU_MEET_SERVER_IMAGE:-docker.io/openvidu/openvidu-meet:${OPENVIDU_VERSION}}"
export OPENVIDU_CALL_SERVER_IMAGE="${OPENVIDU_CALL_SERVER_IMAGE:-docker.io/openvidu/openvidu-call:${OPENVIDU_VERSION}}"
export OPENVIDU_DASHBOARD_PRO_IMAGE="${OPENVIDU_DASHBOARD_PRO_IMAGE:-docker.io/openvidu/openvidu-pro-dashboard:${OPENVIDU_VERSION}}"
export OPENVIDU_DASHBOARD_IMAGE="${OPENVIDU_DASHBOARD_IMAGE:-docker.io/openvidu/openvidu-dashboard:${OPENVIDU_VERSION}}"
export OPENVIDU_V2COMPATIBILITY_IMAGE="${OPENVIDU_V2COMPATIBILITY_IMAGE:-docker.io/openvidu/openvidu-v2compatibility:${OPENVIDU_VERSION}}"
export OPENVIDU_AGENT_SPEECH_PROCESSING_IMAGE="${OPENVIDU_AGENT_SPEECH_PROCESSING_IMAGE:-docker.io/openvidu/agent-speech-processing:${OPENVIDU_VERSION}}"
export LIVEKIT_INGRESS_SERVER_IMAGE="${LIVEKIT_INGRESS_SERVER_IMAGE:-docker.io/openvidu/ingress:${OPENVIDU_VERSION}}"
export LIVEKIT_EGRESS_SERVER_IMAGE="${LIVEKIT_EGRESS_SERVER_IMAGE:-docker.io/livekit/egress:v1.9.1}"
export PROMETHEUS_IMAGE="${PROMETHEUS_IMAGE:-docker.io/prom/prometheus:v3.4.0}"
export PROMTAIL_IMAGE="${PROMTAIL_IMAGE:-docker.io/grafana/promtail:3.5.1}"
export LOKI_IMAGE="${LOKI_IMAGE:-docker.io/grafana/loki:3.5.1}"
export MIMIR_IMAGE="${MIMIR_IMAGE:-docker.io/openvidu/grafana-mimir:2.16.0}"
export MIMIR_IMAGE="${MIMIR_IMAGE:-docker.io/bitnami/grafana-mimir:2.16.0}"
export GRAFANA_IMAGE="${GRAFANA_IMAGE:-docker.io/grafana/grafana:11.6.2}"
# Function to compare two version strings
@ -181,11 +180,10 @@ COMMON_DOCKER_OPTIONS="--network=host -v ${TMP_DIR}:/output \
-e OPENVIDU_OPERATOR_IMAGE=$OPENVIDU_OPERATOR_IMAGE \
-e OPENVIDU_SERVER_PRO_IMAGE=$OPENVIDU_SERVER_PRO_IMAGE \
-e OPENVIDU_SERVER_IMAGE=$OPENVIDU_SERVER_IMAGE \
-e OPENVIDU_MEET_SERVER_IMAGE=$OPENVIDU_MEET_SERVER_IMAGE \
-e OPENVIDU_CALL_SERVER_IMAGE=$OPENVIDU_CALL_SERVER_IMAGE \
-e OPENVIDU_DASHBOARD_PRO_IMAGE=$OPENVIDU_DASHBOARD_PRO_IMAGE \
-e OPENVIDU_DASHBOARD_IMAGE=$OPENVIDU_DASHBOARD_IMAGE \
-e OPENVIDU_V2COMPATIBILITY_IMAGE=$OPENVIDU_V2COMPATIBILITY_IMAGE \
-e OPENVIDU_AGENT_SPEECH_PROCESSING_IMAGE=$OPENVIDU_AGENT_SPEECH_PROCESSING_IMAGE \
-e LIVEKIT_INGRESS_SERVER_IMAGE=$LIVEKIT_INGRESS_SERVER_IMAGE \
-e LIVEKIT_EGRESS_SERVER_IMAGE=$LIVEKIT_EGRESS_SERVER_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"
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:
Description: '(Optional) Domain name for the TURN server with TLS. Only needed if your users are behind restrictive firewalls'
Type: String
@ -424,10 +417,6 @@ Metadata:
- OpenViduVPC
- OpenViduMasterNodeSubnet
- OpenViduMediaNodeSubnets
- Label:
default: "(Optional) Additional Installer Flags"
Parameters:
- AdditionalInstallFlags
- Label:
default: (Optional) TURN server configuration with TLS
Parameters:
@ -473,9 +462,10 @@ Resources:
"GRAFANA_ADMIN_PASSWORD": "none",
"LIVEKIT_API_KEY": "none",
"LIVEKIT_API_SECRET": "none",
"MEET_ADMIN_USER": "none",
"MEET_ADMIN_SECRET": "none",
"MEET_API_KEY": "none",
"DEFAULT_APP_USERNAME": "none",
"DEFAULT_APP_PASSWORD": "none",
"DEFAULT_APP_ADMIN_USERNAME": "none",
"DEFAULT_APP_ADMIN_PASSWORD": "none",
"OPENVIDU_VERSION": "none",
"ENABLED_MODULES": "none"
}
@ -661,7 +651,7 @@ Resources:
content: !Sub |
#!/bin/bash
set -e
OPENVIDU_VERSION=main
OPENVIDU_VERSION=3.2.0
DOMAIN=
YQ_VERSION=v4.44.5
@ -733,12 +723,13 @@ Resources:
DASHBOARD_ADMIN_PASSWORD="$(/usr/local/bin/store_secret.sh generate DASHBOARD_ADMIN_PASSWORD)"
GRAFANA_ADMIN_USERNAME="$(/usr/local/bin/store_secret.sh save GRAFANA_ADMIN_USERNAME "grafanaadmin")"
GRAFANA_ADMIN_PASSWORD="$(/usr/local/bin/store_secret.sh generate GRAFANA_ADMIN_PASSWORD)"
MEET_ADMIN_USER="$(/usr/local/bin/store_secret.sh save MEET_ADMIN_USER "meetadmin")"
MEET_ADMIN_SECRET="$(/usr/local/bin/store_secret.sh generate MEET_ADMIN_SECRET)"
MEET_API_KEY="$(/usr/local/bin/store_secret.sh generate MEET_API_KEY)"
DEFAULT_APP_USERNAME="$(/usr/local/bin/store_secret.sh save DEFAULT_APP_USERNAME "calluser")"
DEFAULT_APP_PASSWORD="$(/usr/local/bin/store_secret.sh generate DEFAULT_APP_PASSWORD)"
DEFAULT_APP_ADMIN_USERNAME="$(/usr/local/bin/store_secret.sh save DEFAULT_APP_ADMIN_USERNAME "calladmin")"
DEFAULT_APP_ADMIN_PASSWORD="$(/usr/local/bin/store_secret.sh generate DEFAULT_APP_ADMIN_PASSWORD)"
LIVEKIT_API_KEY="$(/usr/local/bin/store_secret.sh generate LIVEKIT_API_KEY "API" 12)"
LIVEKIT_API_SECRET="$(/usr/local/bin/store_secret.sh generate LIVEKIT_API_SECRET)"
ENABLED_MODULES="$(/usr/local/bin/store_secret.sh save ENABLED_MODULES "observability,v2compatibility,openviduMeet")"
ENABLED_MODULES="$(/usr/local/bin/store_secret.sh save ENABLED_MODULES "observability,v2compatibility,app")"
ALL_SECRETS_GENERATED="$(/usr/local/bin/store_secret.sh save ALL_SECRETS_GENERATED "true")"
# Base command
@ -766,25 +757,14 @@ Resources:
"--dashboard-admin-password=$DASHBOARD_ADMIN_PASSWORD"
"--grafana-admin-user=$GRAFANA_ADMIN_USERNAME"
"--grafana-admin-password=$GRAFANA_ADMIN_PASSWORD"
"--meet-admin-user=$MEET_ADMIN_USER"
"--meet-admin-password=$MEET_ADMIN_SECRET"
"--meet-api-key=$MEET_API_KEY"
"--default-app-user=$DEFAULT_APP_USERNAME"
"--default-app-password=$DEFAULT_APP_PASSWORD"
"--default-app-admin-user=$DEFAULT_APP_ADMIN_USERNAME"
"--default-app-admin-password=$DEFAULT_APP_ADMIN_PASSWORD"
"--livekit-api-key=$LIVEKIT_API_KEY"
"--livekit-api-secret=$LIVEKIT_API_SECRET"
)
# 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
if [[ "${TurnDomainName}" != '' ]]; then
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/LIVEKIT_API_KEY=.*/LIVEKIT_API_KEY=$(echo $SHARED_SECRET | jq -r .LIVEKIT_API_KEY)/" "${!CLUSTER_CONFIG_DIR}/openvidu.env"
sed -i "s/LIVEKIT_API_SECRET=.*/LIVEKIT_API_SECRET=$(echo $SHARED_SECRET | jq -r .LIVEKIT_API_SECRET)/" "${!CLUSTER_CONFIG_DIR}/openvidu.env"
sed -i "s/MEET_ADMIN_USER=.*/MEET_ADMIN_USER=$(echo $SHARED_SECRET | jq -r .MEET_ADMIN_USER)/" "${!CLUSTER_CONFIG_DIR}/master_node/meet.env"
sed -i "s/MEET_ADMIN_SECRET=.*/MEET_ADMIN_SECRET=$(echo $SHARED_SECRET | jq -r .MEET_ADMIN_SECRET)/" "${!CLUSTER_CONFIG_DIR}/master_node/meet.env"
sed -i "s/MEET_API_KEY=.*/MEET_API_KEY=$(echo $SHARED_SECRET | jq -r .MEET_API_KEY)/" "${!CLUSTER_CONFIG_DIR}/master_node/meet.env"
sed -i "s/CALL_USER=.*/CALL_USER=$(echo $SHARED_SECRET | jq -r .DEFAULT_APP_USERNAME)/" "${!CLUSTER_CONFIG_DIR}/master_node/app.env"
sed -i "s/CALL_SECRET=.*/CALL_SECRET=$(echo $SHARED_SECRET | jq -r .DEFAULT_APP_PASSWORD)/" "${!CLUSTER_CONFIG_DIR}/master_node/app.env"
sed -i "s/CALL_ADMIN_USER=.*/CALL_ADMIN_USER=$(echo $SHARED_SECRET | jq -r .DEFAULT_APP_ADMIN_USERNAME)/" "${!CLUSTER_CONFIG_DIR}/master_node/app.env"
sed -i "s/CALL_ADMIN_SECRET=.*/CALL_ADMIN_SECRET=$(echo $SHARED_SECRET | jq -r .DEFAULT_APP_ADMIN_PASSWORD)/" "${!CLUSTER_CONFIG_DIR}/master_node/app.env"
sed -i "s/ENABLED_MODULES=.*/ENABLED_MODULES=$(echo $SHARED_SECRET | jq -r .ENABLED_MODULES)/" "${!CLUSTER_CONFIG_DIR}/openvidu.env"
# Update URLs in secret
@ -1025,9 +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 '. + {"LIVEKIT_API_KEY": "'"$(/usr/local/bin/get_value_from_config.sh LIVEKIT_API_KEY "${!CLUSTER_CONFIG_DIR}/openvidu.env")"'"}')"
SHARED_SECRET="$(echo "$SHARED_SECRET" | jq '. + {"LIVEKIT_API_SECRET": "'"$(/usr/local/bin/get_value_from_config.sh LIVEKIT_API_SECRET "${!CLUSTER_CONFIG_DIR}/openvidu.env")"'"}')"
SHARED_SECRET="$(echo "$SHARED_SECRET" | jq '. + {"MEET_ADMIN_USER": "'"$(/usr/local/bin/get_value_from_config.sh MEET_ADMIN_USER "${!CLUSTER_CONFIG_DIR}/master_node/meet.env")"'"}')"
SHARED_SECRET="$(echo "$SHARED_SECRET" | jq '. + {"MEET_ADMIN_SECRET": "'"$(/usr/local/bin/get_value_from_config.sh MEET_ADMIN_SECRET "${!CLUSTER_CONFIG_DIR}/master_node/meet.env")"'"}')"
SHARED_SECRET="$(echo "$SHARED_SECRET" | jq '. + {"MEET_API_KEY": "'"$(/usr/local/bin/get_value_from_config.sh MEET_API_KEY "${!CLUSTER_CONFIG_DIR}/master_node/meet.env")"'"}')"
SHARED_SECRET="$(echo "$SHARED_SECRET" | jq '. + {"DEFAULT_APP_USERNAME": "'"$(/usr/local/bin/get_value_from_config.sh CALL_USER "${!CLUSTER_CONFIG_DIR}/master_node/app.env")"'"}')"
SHARED_SECRET="$(echo "$SHARED_SECRET" | jq '. + {"DEFAULT_APP_PASSWORD": "'"$(/usr/local/bin/get_value_from_config.sh CALL_SECRET "${!CLUSTER_CONFIG_DIR}/master_node/app.env")"'"}')"
SHARED_SECRET="$(echo "$SHARED_SECRET" | jq '. + {"DEFAULT_APP_ADMIN_USERNAME": "'"$(/usr/local/bin/get_value_from_config.sh CALL_ADMIN_USER "${!CLUSTER_CONFIG_DIR}/master_node/app.env")"'"}')"
SHARED_SECRET="$(echo "$SHARED_SECRET" | jq '. + {"DEFAULT_APP_ADMIN_PASSWORD": "'"$(/usr/local/bin/get_value_from_config.sh CALL_ADMIN_SECRET "${!CLUSTER_CONFIG_DIR}/master_node/app.env")"'"}')"
SHARED_SECRET="$(echo "$SHARED_SECRET" | jq '. + {"ENABLED_MODULES": "'"$(/usr/local/bin/get_value_from_config.sh ENABLED_MODULES "${!CLUSTER_CONFIG_DIR}/openvidu.env")"'"}')"
# Update shared secret
@ -1338,14 +1320,10 @@ Resources:
docker container kill --signal=SIGQUIT openvidu || true
docker container kill --signal=SIGQUIT ingress || 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
HEARTBEAT_MAX=1800
# Wait for running containers to not be openvidu, ingress, egress or an openvidu agent
while [ $(docker ps --filter "label=openvidu-agent=true" -q | wc -l) -gt 0 ] || \
[ $(docker inspect -f '{{.State.Running}}' openvidu 2>/dev/null) == "true" ] || \
# Wait for running containers to not be openvidu, ingress or egress
while [ $(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}}' egress 2>/dev/null) == "true" ]; do
echo "Waiting for containers to stop..."
@ -1691,7 +1669,7 @@ Resources:
ToPort: 4443
SourceSecurityGroupId: !GetAtt OpenViduMediaNodeSG.GroupId
OpenViduMediaNodeToMasterMeetWebhookIngress:
OpenViduMediaNodeToMasterDefaultAppWebhookIngress:
Type: AWS::EC2::SecurityGroupIngress
Properties:
GroupId: !GetAtt OpenViduMasterNodeSG.GroupId
@ -1723,14 +1701,6 @@ Resources:
FromPort: 443
ToPort: 443
CidrIpv6: ::/0
- IpProtocol: tcp
FromPort: 7881
ToPort: 7881
CidrIp: 0.0.0.0/0
- IpProtocol: tcp
FromPort: 7881
ToPort: 7881
CidrIpv6: ::/0
- IpProtocol: udp
FromPort: 7885
ToPort: 7885
@ -1747,14 +1717,6 @@ Resources:
FromPort: 50000
ToPort: 60000
CidrIpv6: ::/0
- IpProtocol: tcp
FromPort: 50000
ToPort: 60000
CidrIp: 0.0.0.0/0
- IpProtocol: tcp
FromPort: 50000
ToPort: 60000
CidrIpv6: ::/0
OpenViduMasterNodeToMediaNodeRTMPIngress:
Type: AWS::EC2::SecurityGroupIngress
@ -1767,6 +1729,7 @@ Resources:
OpenViduMasterNodeTurnTLSToMediaNodeIngressSG:
Type: AWS::EC2::SecurityGroupIngress
Condition: TurnTLSIsEnabled
Properties:
GroupId: !Ref OpenViduMediaNodeSG
IpProtocol: tcp

View File

@ -293,8 +293,6 @@ param maxNumberOfMediaNodes int = 5
@description('Target CPU percentage to scale up or down')
param scaleTargetCPU int = 50
param additionalInstallFlags string = ''
/*------------------------------------------- VARIABLES AND VALIDATIONS -------------------------------------------*/
var isEmptyIp = publicIpAddressObject.newOrExistingOrNone == 'none'
@ -422,12 +420,11 @@ var stringInterpolationParamsMaster = {
openviduLicense: openviduLicense
rtcEngine: rtcEngine
keyVaultName: keyVaultName
additionalInstallFlags: additionalInstallFlags
}
var installScriptTemplateMaster = '''
#!/bin/bash -x
OPENVIDU_VERSION=main
OPENVIDU_VERSION=3.2.0
DOMAIN=
# 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)"
GRAFANA_ADMIN_USERNAME="$(/usr/local/bin/store_secret.sh save GRAFANA-ADMIN-USERNAME "grafanaadmin")"
GRAFANA_ADMIN_PASSWORD="$(/usr/local/bin/store_secret.sh generate GRAFANA-ADMIN-PASSWORD)"
MEET_ADMIN_USER="$(/usr/local/bin/store_secret.sh save MEET-ADMIN-USER "meetadmin")"
MEET_ADMIN_SECRET="$(/usr/local/bin/store_secret.sh generate MEET-ADMIN-SECRET)"
MEET_API_KEY="$(/usr/local/bin/store_secret.sh generate MEET-API-KEY)"
DEFAULT_APP_USERNAME="$(/usr/local/bin/store_secret.sh save DEFAULT-APP-USERNAME "calluser")"
DEFAULT_APP_PASSWORD="$(/usr/local/bin/store_secret.sh generate DEFAULT-APP-PASSWORD)"
DEFAULT_APP_ADMIN_USERNAME="$(/usr/local/bin/store_secret.sh save DEFAULT-APP-ADMIN-USERNAME "calladmin")"
DEFAULT_APP_ADMIN_PASSWORD="$(/usr/local/bin/store_secret.sh generate DEFAULT-APP-ADMIN-PASSWORD)"
LIVEKIT_API_KEY="$(/usr/local/bin/store_secret.sh generate LIVEKIT-API-KEY "API" 12)"
LIVEKIT_API_SECRET="$(/usr/local/bin/store_secret.sh generate LIVEKIT-API-SECRET)"
OPENVIDU_VERSION="$(/usr/local/bin/store_secret.sh save OPENVIDU-VERSION "${OPENVIDU_VERSION}")"
ENABLED_MODULES="$(/usr/local/bin/store_secret.sh save ENABLED-MODULES "observability,openviduMeet,v2compatibility")"
ENABLED_MODULES="$(/usr/local/bin/store_secret.sh save ENABLED-MODULES "observability,app,v2compatibility")"
ALL_SECRETS_GENERATED="$(/usr/local/bin/store_secret.sh save ALL-SECRETS-GENERATED "true")"
# Base command
@ -524,24 +522,14 @@ COMMON_ARGS=(
"--dashboard-admin-password=$DASHBOARD_ADMIN_PASSWORD"
"--grafana-admin-user=$GRAFANA_ADMIN_USERNAME"
"--grafana-admin-password=$GRAFANA_ADMIN_PASSWORD"
"--meet-admin-user=$MEET_ADMIN_USER"
"--meet-admin-password=$MEET_ADMIN_SECRET"
"--meet-api-key=$MEET_API_KEY"
"--default-app-user=$DEFAULT_APP_USERNAME"
"--default-app-password=$DEFAULT_APP_PASSWORD"
"--default-app-admin-user=$DEFAULT_APP_ADMIN_USERNAME"
"--default-app-admin-password=$DEFAULT_APP_ADMIN_PASSWORD"
"--livekit-api-key=$LIVEKIT_API_KEY"
"--livekit-api-secret=$LIVEKIT_API_SECRET"
)
# 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
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 LIVEKIT_API_KEY=$(az keyvault secret show --vault-name ${keyVaultName} --name LIVEKIT-API-KEY --query value -o tsv)
export LIVEKIT_API_SECRET=$(az keyvault secret show --vault-name ${keyVaultName} --name LIVEKIT-API-SECRET --query value -o tsv)
export MEET_ADMIN_USER=$(az keyvault secret show --vault-name ${keyVaultName} --name MEET-ADMIN-USER --query value -o tsv)
export MEET_ADMIN_SECRET=$(az keyvault secret show --vault-name ${keyVaultName} --name MEET-ADMIN-SECRET --query value -o tsv)
export MEET_API_KEY=$(az keyvault secret show --vault-name ${keyVaultName} --name MEET-API-KEY --query value -o tsv)
export DEFAULT_APP_USERNAME=$(az keyvault secret show --vault-name ${keyVaultName} --name DEFAULT-APP-USERNAME --query value -o tsv)
export DEFAULT_APP_PASSWORD=$(az keyvault secret show --vault-name ${keyVaultName} --name DEFAULT-APP-PASSWORD --query value -o tsv)
export DEFAULT_APP_ADMIN_USERNAME=$(az keyvault secret show --vault-name ${keyVaultName} --name DEFAULT-APP-ADMIN-USERNAME --query value -o tsv)
export DEFAULT_APP_ADMIN_PASSWORD=$(az keyvault secret show --vault-name ${keyVaultName} --name DEFAULT-APP-ADMIN-PASSWORD --query value -o tsv)
export ENABLED_MODULES=$(az keyvault secret show --vault-name ${keyVaultName} --name ENABLED-MODULES --query value -o tsv)
# Replace rest of the values
@ -699,9 +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/LIVEKIT_API_KEY=.*/LIVEKIT_API_KEY=$LIVEKIT_API_KEY/" "${CLUSTER_CONFIG_DIR}/openvidu.env"
sed -i "s/LIVEKIT_API_SECRET=.*/LIVEKIT_API_SECRET=$LIVEKIT_API_SECRET/" "${CLUSTER_CONFIG_DIR}/openvidu.env"
sed -i "s/MEET_ADMIN_USER=.*/MEET_ADMIN_USER=$MEET_ADMIN_USER/" "${CLUSTER_CONFIG_DIR}/master_node/meet.env"
sed -i "s/MEET_ADMIN_SECRET=.*/MEET_ADMIN_SECRET=$MEET_ADMIN_SECRET/" "${CLUSTER_CONFIG_DIR}/master_node/meet.env"
sed -i "s/MEET_API_KEY=.*/MEET_API_KEY=$MEET_API_KEY/" "${CLUSTER_CONFIG_DIR}/master_node/meet.env"
sed -i "s/CALL_USER=.*/CALL_USER=$DEFAULT_APP_USERNAME/" "${CLUSTER_CONFIG_DIR}/master_node/app.env"
sed -i "s/CALL_SECRET=.*/CALL_SECRET=$DEFAULT_APP_PASSWORD/" "${CLUSTER_CONFIG_DIR}/master_node/app.env"
sed -i "s/CALL_ADMIN_USER=.*/CALL_ADMIN_USER=$DEFAULT_APP_ADMIN_USERNAME/" "${CLUSTER_CONFIG_DIR}/master_node/app.env"
sed -i "s/CALL_ADMIN_SECRET=.*/CALL_ADMIN_SECRET=$DEFAULT_APP_ADMIN_PASSWORD/" "${CLUSTER_CONFIG_DIR}/master_node/app.env"
sed -i "s/ENABLED_MODULES=.*/ENABLED_MODULES=$ENABLED_MODULES/" "${CLUSTER_CONFIG_DIR}/openvidu.env"
# Update URLs in secret
@ -749,9 +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")"
LIVEKIT_API_KEY="$(/usr/local/bin/get_value_from_config.sh LIVEKIT_API_KEY "${CLUSTER_CONFIG_DIR}/openvidu.env")"
LIVEKIT_API_SECRET="$(/usr/local/bin/get_value_from_config.sh LIVEKIT_API_SECRET "${CLUSTER_CONFIG_DIR}/openvidu.env")"
MEET_ADMIN_USER="$(/usr/local/bin/get_value_from_config.sh MEET_ADMIN_USER "${CLUSTER_CONFIG_DIR}/master_node/meet.env")"
MEET_ADMIN_SECRET="$(/usr/local/bin/get_value_from_config.sh MEET_ADMIN_SECRET "${CLUSTER_CONFIG_DIR}/master_node/meet.env")"
MEET_API_KEY="$(/usr/local/bin/get_value_from_config.sh MEET_API_KEY "${CLUSTER_CONFIG_DIR}/master_node/meet.env")"
DEFAULT_APP_USERNAME="$(/usr/local/bin/get_value_from_config.sh CALL_USER "${CLUSTER_CONFIG_DIR}/master_node/app.env")"
DEFAULT_APP_PASSWORD="$(/usr/local/bin/get_value_from_config.sh CALL_SECRET "${CLUSTER_CONFIG_DIR}/master_node/app.env")"
DEFAULT_APP_ADMIN_USERNAME="$(/usr/local/bin/get_value_from_config.sh CALL_ADMIN_USER "${CLUSTER_CONFIG_DIR}/master_node/app.env")"
DEFAULT_APP_ADMIN_PASSWORD="$(/usr/local/bin/get_value_from_config.sh CALL_ADMIN_SECRET "${CLUSTER_CONFIG_DIR}/master_node/app.env")"
ENABLED_MODULES="$(/usr/local/bin/get_value_from_config.sh ENABLED_MODULES "${CLUSTER_CONFIG_DIR}/openvidu.env")"
# Update shared secret
@ -771,9 +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 LIVEKIT-API-KEY --value $LIVEKIT_API_KEY
az keyvault secret set --vault-name ${keyVaultName} --name LIVEKIT-API-SECRET --value $LIVEKIT_API_SECRET
az keyvault secret set --vault-name ${keyVaultName} --name MEET-ADMIN-USER --value $MEET_ADMIN_USER
az keyvault secret set --vault-name ${keyVaultName} --name MEET-ADMIN-SECRET --value $MEET_ADMIN_SECRET
az keyvault secret set --vault-name ${keyVaultName} --name MEET-API-KEY --value $MEET_API_KEY
az keyvault secret set --vault-name ${keyVaultName} --name DEFAULT-APP-USERNAME --value $DEFAULT_APP_USERNAME
az keyvault secret set --vault-name ${keyVaultName} --name DEFAULT-APP-PASSWORD --value $DEFAULT_APP_PASSWORD
az keyvault secret set --vault-name ${keyVaultName} --name DEFAULT-APP-ADMIN-USERNAME --value $DEFAULT_APP_ADMIN_USERNAME
az keyvault secret set --vault-name ${keyVaultName} --name DEFAULT-APP-ADMIN-PASSWORD --value $DEFAULT_APP_ADMIN_PASSWORD
az keyvault secret set --vault-name ${keyVaultName} --name ENABLED-MODULES --value $ENABLED_MODULES
'''
@ -1118,6 +1110,7 @@ while true; do
sleep $WAIT_INTERVAL
done
set -e
# Get current shared secret
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)
@ -1172,13 +1165,9 @@ if [ -x "$(command -v docker)" ]; then
docker container kill --signal=SIGQUIT openvidu || true
docker container kill --signal=SIGQUIT ingress || 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
while [ $(docker ps --filter "label=openvidu-agent=true" -q | wc -l) -gt 0 ] || \
[ $(docker inspect -f '{{.State.Running}}' openvidu 2>/dev/null) == "true" ] || \
# Wait for running containers to not be openvidu, ingress or egress
while [ $(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}}' egress 2>/dev/null) == "true" ]; do
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 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
'''
@ -1229,10 +1203,6 @@ chmod +x /usr/local/bin/install.sh
echo ${base64stop} | base64 -d > /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 install -y jq
@ -1251,10 +1221,11 @@ az vmss update --resource-group $RESOURCE_GROUP_NAME --name $VM_SCALE_SET_NAME -
export HOME="/root"
# 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
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(
@ -1273,18 +1244,9 @@ var stop_media_nodesScriptMedia = reduce(
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 = {
base64install: base64installMedia
base64stop: base64stopMediaNode
base64delete: base64delete_mediaNode_ScriptMedia
resourceGroupName: resourceGroup().name
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
name: 'mediaNode_to_masterNode_MEET_WEBHOOK_INGRESS'
name: 'mediaNode_to_masterNode_DEFAULTAPP_WEBHOOK_INGRESS'
properties: {
protocol: 'Tcp'
sourceApplicationSecurityGroups: [
@ -1953,22 +1915,6 @@ resource openviduMediaNodeNSG 'Microsoft.Network/networkSecurityGroups@2023-11-0
direction: 'Inbound'
}
}
{
name: 'WebRTC_traffic_TCP'
properties: {
protocol: 'Tcp'
sourceAddressPrefix: '*'
sourcePortRange: '*'
destinationAddressPrefix: '*'
destinationPortRanges: [
'50000'
'60000'
]
access: 'Allow'
priority: 150
direction: 'Inbound'
}
}
]
}
}
@ -1996,7 +1942,7 @@ resource masterToMediaRtmpIngress 'Microsoft.Network/networkSecurityGroups/secur
]
destinationPortRange: '1935'
access: 'Allow'
priority: 160
priority: 150
direction: 'Inbound'
}
}
@ -2019,7 +1965,7 @@ resource masterToMediaTurnTlsIngress 'Microsoft.Network/networkSecurityGroups/se
]
destinationPortRange: '5349'
access: 'Allow'
priority: 170
priority: 160
direction: 'Inbound'
}
}
@ -2042,7 +1988,7 @@ resource masterToMediaServerIngress 'Microsoft.Network/networkSecurityGroups/sec
]
destinationPortRange: '7880'
access: 'Allow'
priority: 180
priority: 170
direction: 'Inbound'
}
}
@ -2065,7 +2011,7 @@ resource masterToMediaHttpWhipIngress 'Microsoft.Network/networkSecurityGroups/s
]
destinationPortRange: '8080'
access: 'Allow'
priority: 190
priority: 180
direction: 'Inbound'
}
}

File diff suppressed because one or more lines are too long

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",
"label": "(Optional) TURN server configuration with TLS",
@ -574,8 +552,7 @@
"datetime": "[steps('parameters SCALING').datetime]",
"automationAccountName": "[steps('parameters SCALING').automationAccountName]",
"storageAccountName": "[steps('parameters STORAGE').storageAccountName]",
"containerName": "[steps('parameters STORAGE').containerName]",
"additionalInstallFlags": "[steps('FLAGS').additionalInstallFlags]"
"containerName": "[steps('parameters STORAGE').containerName]"
}
}
}

View File

@ -3,9 +3,9 @@
set -eu
export DOCKER_VERSION="${DOCKER_VERSION:-28.1.1}"
export DOCKER_COMPOSE_VERSION="${DOCKER_COMPOSE_VERSION:-v2.35.1}"
export OPENVIDU_VERSION="${OPENVIDU_VERSION:-main}"
export OPENVIDU_VERSION="${OPENVIDU_VERSION:-3.2.0}"
export INSTALLER_IMAGE="${INSTALLER_IMAGE:-docker.io/openvidu/openvidu-installer:${OPENVIDU_VERSION}}"
export MINIO_SERVER_IMAGE="${MINIO_SERVER_IMAGE:-docker.io/openvidu/minio:2025.5.24-debian-12-r1}"
export MINIO_SERVER_IMAGE="${MINIO_SERVER_IMAGE:-docker.io/bitnami/minio:2025.5.24-debian-12-r1}"
export MINIO_CLIENT_IMAGE="${MINIO_CLIENT_IMAGE:-docker.io/minio/mc:RELEASE.2025-05-21T01-59-54Z}"
export MONGO_SERVER_IMAGE="${MONGO_SERVER_IMAGE:-docker.io/mongo:8.0.9}"
export REDIS_SERVER_IMAGE="${REDIS_SERVER_IMAGE:-docker.io/redis:7.4.4-alpine}"
@ -15,17 +15,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_SERVER_PRO_IMAGE="${OPENVIDU_SERVER_PRO_IMAGE:-docker.io/openvidu/openvidu-server-pro:${OPENVIDU_VERSION}}"
export OPENVIDU_SERVER_IMAGE="${OPENVIDU_SERVER_IMAGE:-docker.io/openvidu/openvidu-server:${OPENVIDU_VERSION}}"
export OPENVIDU_MEET_SERVER_IMAGE="${OPENVIDU_MEET_SERVER_IMAGE:-docker.io/openvidu/openvidu-meet:${OPENVIDU_VERSION}}"
export OPENVIDU_CALL_SERVER_IMAGE="${OPENVIDU_CALL_SERVER_IMAGE:-docker.io/openvidu/openvidu-call:${OPENVIDU_VERSION}}"
export OPENVIDU_DASHBOARD_PRO_IMAGE="${OPENVIDU_DASHBOARD_PRO_IMAGE:-docker.io/openvidu/openvidu-pro-dashboard:${OPENVIDU_VERSION}}"
export OPENVIDU_DASHBOARD_IMAGE="${OPENVIDU_DASHBOARD_IMAGE:-docker.io/openvidu/openvidu-dashboard:${OPENVIDU_VERSION}}"
export OPENVIDU_V2COMPATIBILITY_IMAGE="${OPENVIDU_V2COMPATIBILITY_IMAGE:-docker.io/openvidu/openvidu-v2compatibility:${OPENVIDU_VERSION}}"
export OPENVIDU_AGENT_SPEECH_PROCESSING_IMAGE="${OPENVIDU_AGENT_SPEECH_PROCESSING_IMAGE:-docker.io/openvidu/agent-speech-processing:${OPENVIDU_VERSION}}"
export LIVEKIT_INGRESS_SERVER_IMAGE="${LIVEKIT_INGRESS_SERVER_IMAGE:-docker.io/openvidu/ingress:${OPENVIDU_VERSION}}"
export LIVEKIT_EGRESS_SERVER_IMAGE="${LIVEKIT_EGRESS_SERVER_IMAGE:-docker.io/livekit/egress:v1.9.1}"
export PROMETHEUS_IMAGE="${PROMETHEUS_IMAGE:-docker.io/prom/prometheus:v3.4.0}"
export PROMTAIL_IMAGE="${PROMTAIL_IMAGE:-docker.io/grafana/promtail:3.5.1}"
export LOKI_IMAGE="${LOKI_IMAGE:-docker.io/grafana/loki:3.5.1}"
export MIMIR_IMAGE="${MIMIR_IMAGE:-docker.io/openvidu/grafana-mimir:2.16.0}"
export MIMIR_IMAGE="${MIMIR_IMAGE:-docker.io/bitnami/grafana-mimir:2.16.0}"
export GRAFANA_IMAGE="${GRAFANA_IMAGE:-docker.io/grafana/grafana:11.6.2}"
# Function to compare two version strings
@ -181,11 +180,10 @@ COMMON_DOCKER_OPTIONS="--network=host -v ${TMP_DIR}:/output \
-e OPENVIDU_OPERATOR_IMAGE=$OPENVIDU_OPERATOR_IMAGE \
-e OPENVIDU_SERVER_PRO_IMAGE=$OPENVIDU_SERVER_PRO_IMAGE \
-e OPENVIDU_SERVER_IMAGE=$OPENVIDU_SERVER_IMAGE \
-e OPENVIDU_MEET_SERVER_IMAGE=$OPENVIDU_MEET_SERVER_IMAGE \
-e OPENVIDU_CALL_SERVER_IMAGE=$OPENVIDU_CALL_SERVER_IMAGE \
-e OPENVIDU_DASHBOARD_PRO_IMAGE=$OPENVIDU_DASHBOARD_PRO_IMAGE \
-e OPENVIDU_DASHBOARD_IMAGE=$OPENVIDU_DASHBOARD_IMAGE \
-e OPENVIDU_V2COMPATIBILITY_IMAGE=$OPENVIDU_V2COMPATIBILITY_IMAGE \
-e OPENVIDU_AGENT_SPEECH_PROCESSING_IMAGE=$OPENVIDU_AGENT_SPEECH_PROCESSING_IMAGE \
-e LIVEKIT_INGRESS_SERVER_IMAGE=$LIVEKIT_INGRESS_SERVER_IMAGE \
-e LIVEKIT_EGRESS_SERVER_IMAGE=$LIVEKIT_EGRESS_SERVER_IMAGE \
-e PROMETHEUS_IMAGE=$PROMETHEUS_IMAGE \

View File

@ -3,9 +3,9 @@
set -eu
export DOCKER_VERSION="${DOCKER_VERSION:-28.1.1}"
export DOCKER_COMPOSE_VERSION="${DOCKER_COMPOSE_VERSION:-v2.35.1}"
export OPENVIDU_VERSION="${OPENVIDU_VERSION:-main}"
export OPENVIDU_VERSION="${OPENVIDU_VERSION:-3.2.0}"
export INSTALLER_IMAGE="${INSTALLER_IMAGE:-docker.io/openvidu/openvidu-installer:${OPENVIDU_VERSION}}"
export MINIO_SERVER_IMAGE="${MINIO_SERVER_IMAGE:-docker.io/openvidu/minio:2025.5.24-debian-12-r1}"
export MINIO_SERVER_IMAGE="${MINIO_SERVER_IMAGE:-docker.io/bitnami/minio:2025.5.24-debian-12-r1}"
export MINIO_CLIENT_IMAGE="${MINIO_CLIENT_IMAGE:-docker.io/minio/mc:RELEASE.2025-05-21T01-59-54Z}"
export MONGO_SERVER_IMAGE="${MONGO_SERVER_IMAGE:-docker.io/mongo:8.0.9}"
export REDIS_SERVER_IMAGE="${REDIS_SERVER_IMAGE:-docker.io/redis:7.4.4-alpine}"
@ -15,17 +15,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_SERVER_PRO_IMAGE="${OPENVIDU_SERVER_PRO_IMAGE:-docker.io/openvidu/openvidu-server-pro:${OPENVIDU_VERSION}}"
export OPENVIDU_SERVER_IMAGE="${OPENVIDU_SERVER_IMAGE:-docker.io/openvidu/openvidu-server:${OPENVIDU_VERSION}}"
export OPENVIDU_MEET_SERVER_IMAGE="${OPENVIDU_MEET_SERVER_IMAGE:-docker.io/openvidu/openvidu-meet:${OPENVIDU_VERSION}}"
export OPENVIDU_CALL_SERVER_IMAGE="${OPENVIDU_CALL_SERVER_IMAGE:-docker.io/openvidu/openvidu-call:${OPENVIDU_VERSION}}"
export OPENVIDU_DASHBOARD_PRO_IMAGE="${OPENVIDU_DASHBOARD_PRO_IMAGE:-docker.io/openvidu/openvidu-pro-dashboard:${OPENVIDU_VERSION}}"
export OPENVIDU_DASHBOARD_IMAGE="${OPENVIDU_DASHBOARD_IMAGE:-docker.io/openvidu/openvidu-dashboard:${OPENVIDU_VERSION}}"
export OPENVIDU_V2COMPATIBILITY_IMAGE="${OPENVIDU_V2COMPATIBILITY_IMAGE:-docker.io/openvidu/openvidu-v2compatibility:${OPENVIDU_VERSION}}"
export OPENVIDU_AGENT_SPEECH_PROCESSING_IMAGE="${OPENVIDU_AGENT_SPEECH_PROCESSING_IMAGE:-docker.io/openvidu/agent-speech-processing:${OPENVIDU_VERSION}}"
export LIVEKIT_INGRESS_SERVER_IMAGE="${LIVEKIT_INGRESS_SERVER_IMAGE:-docker.io/openvidu/ingress:${OPENVIDU_VERSION}}"
export LIVEKIT_EGRESS_SERVER_IMAGE="${LIVEKIT_EGRESS_SERVER_IMAGE:-docker.io/livekit/egress:v1.9.1}"
export PROMETHEUS_IMAGE="${PROMETHEUS_IMAGE:-docker.io/prom/prometheus:v3.4.0}"
export PROMTAIL_IMAGE="${PROMTAIL_IMAGE:-docker.io/grafana/promtail:3.5.1}"
export LOKI_IMAGE="${LOKI_IMAGE:-docker.io/grafana/loki:3.5.1}"
export MIMIR_IMAGE="${MIMIR_IMAGE:-docker.io/openvidu/grafana-mimir:2.16.0}"
export MIMIR_IMAGE="${MIMIR_IMAGE:-docker.io/bitnami/grafana-mimir:2.16.0}"
export GRAFANA_IMAGE="${GRAFANA_IMAGE:-docker.io/grafana/grafana:11.6.2}"
# Function to compare two version strings
@ -181,11 +180,10 @@ COMMON_DOCKER_OPTIONS="--network=host -v ${TMP_DIR}:/output \
-e OPENVIDU_OPERATOR_IMAGE=$OPENVIDU_OPERATOR_IMAGE \
-e OPENVIDU_SERVER_PRO_IMAGE=$OPENVIDU_SERVER_PRO_IMAGE \
-e OPENVIDU_SERVER_IMAGE=$OPENVIDU_SERVER_IMAGE \
-e OPENVIDU_MEET_SERVER_IMAGE=$OPENVIDU_MEET_SERVER_IMAGE \
-e OPENVIDU_CALL_SERVER_IMAGE=$OPENVIDU_CALL_SERVER_IMAGE \
-e OPENVIDU_DASHBOARD_PRO_IMAGE=$OPENVIDU_DASHBOARD_PRO_IMAGE \
-e OPENVIDU_DASHBOARD_IMAGE=$OPENVIDU_DASHBOARD_IMAGE \
-e OPENVIDU_V2COMPATIBILITY_IMAGE=$OPENVIDU_V2COMPATIBILITY_IMAGE \
-e OPENVIDU_AGENT_SPEECH_PROCESSING_IMAGE=$OPENVIDU_AGENT_SPEECH_PROCESSING_IMAGE \
-e LIVEKIT_INGRESS_SERVER_IMAGE=$LIVEKIT_INGRESS_SERVER_IMAGE \
-e LIVEKIT_EGRESS_SERVER_IMAGE=$LIVEKIT_EGRESS_SERVER_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
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:
Description: "Dedicated VPC for OpenVidu cluster"
Type: AWS::EC2::VPC::Id
@ -404,10 +397,6 @@ Metadata:
default: Volumes configuration
Parameters:
- MasterNodesDiskSize
- Label:
default: "(Optional) Additional Installer Flags"
Parameters:
- AdditionalInstallFlags
- Label:
default: (Optional) TURN server configuration with TLS
Parameters:
@ -418,22 +407,6 @@ Conditions:
TurnTLSIsEnabled: !Or [!Not [!Equals [!Ref TurnDomainName, ""]], !Not [!Equals [!Ref TurnCertificateARN, ""]]]
CreateRecordingsBucket: !Equals [!Ref S3AppDataBucketName, ""]
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:
@ -465,9 +438,10 @@ Resources:
"GRAFANA_URL": "none",
"GRAFANA_ADMIN_USERNAME": "none",
"GRAFANA_ADMIN_PASSWORD": "none",
"MEET_ADMIN_USER": "none",
"MEET_ADMIN_SECRET": "none",
"MEET_API_KEY": "none",
"DEFAULT_APP_USERNAME": "none",
"DEFAULT_APP_PASSWORD": "none",
"DEFAULT_APP_ADMIN_USERNAME": "none",
"DEFAULT_APP_ADMIN_PASSWORD": "none",
"LIVEKIT_API_KEY": "none",
"LIVEKIT_API_SECRET": "none",
"ENABLED_MODULES": "none",
@ -772,7 +746,7 @@ Resources:
content: !Sub |
#!/bin/bash -x
set -e
OPENVIDU_VERSION=main
OPENVIDU_VERSION=3.2.0
DOMAIN=
YQ_VERSION=v4.44.5
@ -866,12 +840,13 @@ Resources:
DASHBOARD_ADMIN_PASSWORD="$(/usr/local/bin/store_secret.sh generate DASHBOARD_ADMIN_PASSWORD)"
GRAFANA_ADMIN_USERNAME="$(/usr/local/bin/store_secret.sh save GRAFANA_ADMIN_USERNAME "grafanaadmin")"
GRAFANA_ADMIN_PASSWORD="$(/usr/local/bin/store_secret.sh generate GRAFANA_ADMIN_PASSWORD)"
MEET_ADMIN_USER="$(/usr/local/bin/store_secret.sh save MEET_ADMIN_USER "meetadmin")"
MEET_ADMIN_SECRET="$(/usr/local/bin/store_secret.sh generate MEET_ADMIN_SECRET)"
MEET_API_KEY="$(/usr/local/bin/store_secret.sh generate MEET_API_KEY)"
DEFAULT_APP_USERNAME="$(/usr/local/bin/store_secret.sh save DEFAULT_APP_USERNAME "calluser")"
DEFAULT_APP_PASSWORD="$(/usr/local/bin/store_secret.sh generate DEFAULT_APP_PASSWORD)"
DEFAULT_APP_ADMIN_USERNAME="$(/usr/local/bin/store_secret.sh save DEFAULT_APP_ADMIN_USERNAME "calladmin")"
DEFAULT_APP_ADMIN_PASSWORD="$(/usr/local/bin/store_secret.sh generate DEFAULT_APP_ADMIN_PASSWORD)"
LIVEKIT_API_KEY="$(/usr/local/bin/store_secret.sh generate LIVEKIT_API_KEY "API" 12)"
LIVEKIT_API_SECRET="$(/usr/local/bin/store_secret.sh generate LIVEKIT_API_SECRET)"
ENABLED_MODULES="$(/usr/local/bin/store_secret.sh save ENABLED_MODULES "observability,v2compatibility,openviduMeet")"
ENABLED_MODULES="$(/usr/local/bin/store_secret.sh save ENABLED_MODULES "observability,v2compatibility,app")"
ALL_SECRETS_GENERATED="$(/usr/local/bin/store_secret.sh save ALL_SECRETS_GENERATED "true")"
fi
@ -937,9 +912,10 @@ Resources:
DASHBOARD_ADMIN_PASSWORD=$(echo "$SHARED_SECRET" | jq -r '.DASHBOARD_ADMIN_PASSWORD')
GRAFANA_ADMIN_USERNAME=$(echo "$SHARED_SECRET" | jq -r '.GRAFANA_ADMIN_USERNAME')
GRAFANA_ADMIN_PASSWORD=$(echo "$SHARED_SECRET" | jq -r '.GRAFANA_ADMIN_PASSWORD')
MEET_ADMIN_USER=$(echo "$SHARED_SECRET" | jq -r '.MEET_ADMIN_USER')
MEET_ADMIN_SECRET=$(echo "$SHARED_SECRET" | jq -r '.MEET_ADMIN_SECRET')
MEET_API_KEY=$(echo "$SHARED_SECRET" | jq -r '.MEET_API_KEY')
DEFAULT_APP_USERNAME=$(echo "$SHARED_SECRET" | jq -r '.DEFAULT_APP_USERNAME')
DEFAULT_APP_PASSWORD=$(echo "$SHARED_SECRET" | jq -r '.DEFAULT_APP_PASSWORD')
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_SECRET=$(echo "$SHARED_SECRET" | jq -r '.LIVEKIT_API_SECRET')
ENABLED_MODULES=$(echo "$SHARED_SECRET" | jq -r '.ENABLED_MODULES')
@ -970,25 +946,14 @@ Resources:
"--dashboard-admin-password=$DASHBOARD_ADMIN_PASSWORD"
"--grafana-admin-user=$GRAFANA_ADMIN_USERNAME"
"--grafana-admin-password=$GRAFANA_ADMIN_PASSWORD"
"--meet-admin-user=$MEET_ADMIN_USER"
"--meet-admin-password=$MEET_ADMIN_SECRET"
"--meet-api-key=$MEET_API_KEY"
"--default-app-user=$DEFAULT_APP_USERNAME"
"--default-app-password=$DEFAULT_APP_PASSWORD"
"--default-app-admin-user=$DEFAULT_APP_ADMIN_USERNAME"
"--default-app-admin-password=$DEFAULT_APP_ADMIN_PASSWORD"
"--livekit-api-key=$LIVEKIT_API_KEY"
"--livekit-api-secret=$LIVEKIT_API_SECRET"
)
# 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
COMMON_ARGS+=("--turn-domain-name='${!LIVEKIT_TURN_DOMAIN_NAME}'")
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/LIVEKIT_API_KEY=.*/LIVEKIT_API_KEY=$(echo $SHARED_SECRET | jq -r .LIVEKIT_API_KEY)/" "${!CLUSTER_CONFIG_DIR}/openvidu.env"
sed -i "s/LIVEKIT_API_SECRET=.*/LIVEKIT_API_SECRET=$(echo $SHARED_SECRET | jq -r .LIVEKIT_API_SECRET)/" "${!CLUSTER_CONFIG_DIR}/openvidu.env"
sed -i "s/MEET_ADMIN_USER=.*/MEET_ADMIN_USER=$(echo $SHARED_SECRET | jq -r .MEET_ADMIN_USER)/" "${!CLUSTER_CONFIG_DIR}/master_node/meet.env"
sed -i "s/MEET_ADMIN_SECRET=.*/MEET_ADMIN_SECRET=$(echo $SHARED_SECRET | jq -r .MEET_ADMIN_SECRET)/" "${!CLUSTER_CONFIG_DIR}/master_node/meet.env"
sed -i "s/MEET_API_KEY=.*/MEET_API_KEY=$(echo $SHARED_SECRET | jq -r .MEET_API_KEY)/" "${!CLUSTER_CONFIG_DIR}/master_node/meet.env"
sed -i "s/CALL_USER=.*/CALL_USER=$(echo $SHARED_SECRET | jq -r .DEFAULT_APP_USERNAME)/" "${!CLUSTER_CONFIG_DIR}/master_node/app.env"
sed -i "s/CALL_SECRET=.*/CALL_SECRET=$(echo $SHARED_SECRET | jq -r .DEFAULT_APP_PASSWORD)/" "${!CLUSTER_CONFIG_DIR}/master_node/app.env"
sed -i "s/CALL_ADMIN_USER=.*/CALL_ADMIN_USER=$(echo $SHARED_SECRET | jq -r .DEFAULT_APP_ADMIN_USERNAME)/" "${!CLUSTER_CONFIG_DIR}/master_node/app.env"
sed -i "s/CALL_ADMIN_SECRET=.*/CALL_ADMIN_SECRET=$(echo $SHARED_SECRET | jq -r .DEFAULT_APP_ADMIN_PASSWORD)/" "${!CLUSTER_CONFIG_DIR}/master_node/app.env"
sed -i "s/ENABLED_MODULES=.*/ENABLED_MODULES=$(echo $SHARED_SECRET | jq -r .ENABLED_MODULES)/" "${!CLUSTER_CONFIG_DIR}/openvidu.env"
# Update URLs in secret
@ -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 '. + {"LIVEKIT_API_KEY": "'"$(/usr/local/bin/get_value_from_config.sh LIVEKIT_API_KEY "${!CLUSTER_CONFIG_DIR}/openvidu.env")"'"}')"
SHARED_SECRET="$(echo "$SHARED_SECRET" | jq '. + {"LIVEKIT_API_SECRET": "'"$(/usr/local/bin/get_value_from_config.sh LIVEKIT_API_SECRET "${!CLUSTER_CONFIG_DIR}/openvidu.env")"'"}')"
SHARED_SECRET="$(echo "$SHARED_SECRET" | jq '. + {"MEET_ADMIN_USER": "'"$(/usr/local/bin/get_value_from_config.sh MEET_ADMIN_USER "${!CLUSTER_CONFIG_DIR}/master_node/meet.env")"'"}')"
SHARED_SECRET="$(echo "$SHARED_SECRET" | jq '. + {"MEET_ADMIN_SECRET": "'"$(/usr/local/bin/get_value_from_config.sh MEET_ADMIN_SECRET "${!CLUSTER_CONFIG_DIR}/master_node/meet.env")"'"}')"
SHARED_SECRET="$(echo "$SHARED_SECRET" | jq '. + {"MEET_API_KEY": "'"$(/usr/local/bin/get_value_from_config.sh MEET_API_KEY "${!CLUSTER_CONFIG_DIR}/master_node/meet.env")"'"}')"
SHARED_SECRET="$(echo "$SHARED_SECRET" | jq '. + {"DEFAULT_APP_USERNAME": "'"$(/usr/local/bin/get_value_from_config.sh CALL_USER "${!CLUSTER_CONFIG_DIR}/master_node/app.env")"'"}')"
SHARED_SECRET="$(echo "$SHARED_SECRET" | jq '. + {"DEFAULT_APP_PASSWORD": "'"$(/usr/local/bin/get_value_from_config.sh CALL_SECRET "${!CLUSTER_CONFIG_DIR}/master_node/app.env")"'"}')"
SHARED_SECRET="$(echo "$SHARED_SECRET" | jq '. + {"DEFAULT_APP_ADMIN_USERNAME": "'"$(/usr/local/bin/get_value_from_config.sh CALL_ADMIN_USER "${!CLUSTER_CONFIG_DIR}/master_node/app.env")"'"}')"
SHARED_SECRET="$(echo "$SHARED_SECRET" | jq '. + {"DEFAULT_APP_ADMIN_PASSWORD": "'"$(/usr/local/bin/get_value_from_config.sh CALL_ADMIN_SECRET "${!CLUSTER_CONFIG_DIR}/master_node/app.env")"'"}')"
SHARED_SECRET="$(echo "$SHARED_SECRET" | jq '. + {"ENABLED_MODULES": "'"$(/usr/local/bin/get_value_from_config.sh ENABLED_MODULES "${!CLUSTER_CONFIG_DIR}/openvidu.env")"'"}')"
# Update shared secret
@ -1594,14 +1561,10 @@ Resources:
docker container kill --signal=SIGQUIT openvidu || true
docker container kill --signal=SIGQUIT ingress || 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
HEARTBEAT_MAX=1800
# Wait for running containers to not be openvidu, ingress, egress or an openvidu agent
while [ $(docker ps --filter "label=openvidu-agent=true" -q | wc -l) -gt 0 ] || \
[ $(docker inspect -f '{{.State.Running}}' openvidu 2>/dev/null) == "true" ] || \
# Wait for running containers to not be openvidu, ingress or egress
while [ $(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}}' egress 2>/dev/null) == "true" ]; do
echo "Waiting for containers to stop..."
@ -2040,7 +2003,7 @@ Resources:
ToPort: 4443
SourceSecurityGroupId: !GetAtt OpenViduMediaNodeSG.GroupId
OpenViduMasterToMasterMeetIngress:
OpenViduMasterToMasterDefaultAppIngress:
Type: AWS::EC2::SecurityGroupIngress
Properties:
GroupId: !Ref OpenViduMasterNodeSG
@ -2049,7 +2012,7 @@ Resources:
ToPort: 6080
SourceSecurityGroupId: !Ref OpenViduMasterNodeSG
OpenViduMediaNodeToMasterMeetWebhookIngress:
OpenViduMediaNodeToMasterDefaultAppWebhookIngress:
Type: AWS::EC2::SecurityGroupIngress
Properties:
GroupId: !GetAtt OpenViduMasterNodeSG.GroupId
@ -2081,14 +2044,6 @@ Resources:
FromPort: 443
ToPort: 443
CidrIpv6: ::/0
- IpProtocol: tcp
FromPort: 7881
ToPort: 7881
CidrIp: 0.0.0.0/0
- IpProtocol: tcp
FromPort: 7881
ToPort: 7881
CidrIpv6: ::/0
- IpProtocol: udp
FromPort: 7885
ToPort: 7885
@ -2105,14 +2060,6 @@ Resources:
FromPort: 50000
ToPort: 60000
CidrIpv6: ::/0
- IpProtocol: tcp
FromPort: 50000
ToPort: 60000
CidrIp: 0.0.0.0/0
- IpProtocol: tcp
FromPort: 50000
ToPort: 60000
CidrIpv6: ::/0
OpenViduLoadBalancerToMediaNodeRTMPIngressSG:
Type: AWS::EC2::SecurityGroupIngress
@ -2171,29 +2118,6 @@ Resources:
ToPort: 8080
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:
Type: AWS::EC2::SecurityGroup
Properties:
@ -2284,7 +2208,6 @@ Resources:
OpenViduMasterNodeListener:
Type: 'AWS::ElasticLoadBalancingV2::Listener'
Condition: NotExperimentalTurnTLSWithMainDomain
Properties:
DefaultActions:
- Type: forward
@ -2295,22 +2218,6 @@ Resources:
Certificates:
- 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:
Type: 'AWS::ElasticLoadBalancingV2::Listener'
Properties:
@ -2338,7 +2245,6 @@ Resources:
OpenViduMasterNodeTG:
Type: AWS::ElasticLoadBalancingV2::TargetGroup
Condition: NotExperimentalTurnTLSWithMainDomain
Properties:
Name:
Fn::Join:
@ -2372,45 +2278,6 @@ Resources:
- Key: Name
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:
Type: AWS::ElasticLoadBalancingV2::TargetGroup
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.')
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')
@secure()
param openviduLicense string
@ -296,8 +302,6 @@ param maxNumberOfMediaNodes int = 5
@description('Target CPU percentage to scale up or down')
param scaleTargetCPU int = 50
param additionalInstallFlags string = ''
/*------------------------------------------- VARIABLES AND VALIDATIONS -------------------------------------------*/
var masterNodeVMSettings = {
@ -438,7 +442,6 @@ var stringInterpolationParamsMaster1 = {
rtcEngine: rtcEngine
keyVaultName: keyVaultName
masterNodeNum: '1'
additionalInstallFlags: additionalInstallFlags
}
var stringInterpolationParamsMaster2 = {
@ -455,7 +458,6 @@ var stringInterpolationParamsMaster2 = {
rtcEngine: rtcEngine
keyVaultName: keyVaultName
masterNodeNum: '2'
additionalInstallFlags: additionalInstallFlags
}
var stringInterpolationParamsMaster3 = {
@ -472,7 +474,6 @@ var stringInterpolationParamsMaster3 = {
rtcEngine: rtcEngine
keyVaultName: keyVaultName
masterNodeNum: '3'
additionalInstallFlags: additionalInstallFlags
}
var stringInterpolationParamsMaster4 = {
@ -489,13 +490,12 @@ var stringInterpolationParamsMaster4 = {
rtcEngine: rtcEngine
keyVaultName: keyVaultName
masterNodeNum: '4'
additionalInstallFlags: additionalInstallFlags
}
var installScriptTemplateMaster = '''
#!/bin/bash -x
set -e
OPENVIDU_VERSION=main
OPENVIDU_VERSION=3.2.0
DOMAIN=
# 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)"
GRAFANA_ADMIN_USERNAME="$(/usr/local/bin/store_secret.sh save GRAFANA-ADMIN-USERNAME "grafanaadmin")"
GRAFANA_ADMIN_PASSWORD="$(/usr/local/bin/store_secret.sh generate GRAFANA-ADMIN-PASSWORD)"
MEET_ADMIN_USER="$(/usr/local/bin/store_secret.sh save MEET-ADMIN-USER "meetadmin")"
MEET_ADMIN_SECRET="$(/usr/local/bin/store_secret.sh generate MEET-ADMIN-SECRET)"
MEET_API_KEY="$(/usr/local/bin/store_secret.sh generate MEET-API-KEY)"
DEFAULT_APP_USERNAME="$(/usr/local/bin/store_secret.sh save DEFAULT-APP-USERNAME "calluser")"
DEFAULT_APP_PASSWORD="$(/usr/local/bin/store_secret.sh generate DEFAULT-APP-PASSWORD)"
DEFAULT_APP_ADMIN_USERNAME="$(/usr/local/bin/store_secret.sh save DEFAULT-APP-ADMIN-USERNAME "calladmin")"
DEFAULT_APP_ADMIN_PASSWORD="$(/usr/local/bin/store_secret.sh generate DEFAULT-APP-ADMIN-PASSWORD)"
LIVEKIT_API_KEY="$(/usr/local/bin/store_secret.sh generate LIVEKIT-API-KEY "API" 12)"
LIVEKIT_API_SECRET="$(/usr/local/bin/store_secret.sh generate LIVEKIT-API-SECRET)"
OPENVIDU_VERSION="$(/usr/local/bin/store_secret.sh save OPENVIDU-VERSION "${OPENVIDU_VERSION}")"
ENABLED_MODULES="$(/usr/local/bin/store_secret.sh save ENABLED-MODULES "observability,openviduMeet,v2compatibility")"
ENABLED_MODULES="$(/usr/local/bin/store_secret.sh save ENABLED-MODULES "observability,app,v2compatibility")"
ALL_SECRETS_GENERATED="$(/usr/local/bin/store_secret.sh save ALL-SECRETS-GENERATED "true")"
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)
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)
MEET_ADMIN_USER=$(az keyvault secret show --vault-name ${keyVaultName} --name MEET-ADMIN-USER --query value -o tsv)
MEET_ADMIN_SECRET=$(az keyvault secret show --vault-name ${keyVaultName} --name MEET-ADMIN-SECRET --query value -o tsv)
MEET_API_KEY=$(az keyvault secret show --vault-name ${keyVaultName} --name MEET-API-KEY --query value -o tsv)
DEFAULT_APP_USERNAME=$(az keyvault secret show --vault-name ${keyVaultName} --name DEFAULT-APP-USERNAME --query value -o tsv)
DEFAULT_APP_PASSWORD=$(az keyvault secret show --vault-name ${keyVaultName} --name DEFAULT-APP-PASSWORD --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)
@ -658,27 +660,16 @@ COMMON_ARGS=(
"--dashboard-admin-password=$DASHBOARD_ADMIN_PASSWORD"
"--grafana-admin-user=$GRAFANA_ADMIN_USERNAME"
"--grafana-admin-password=$GRAFANA_ADMIN_PASSWORD"
"--meet-admin-user=$MEET_ADMIN_USER"
"--meet-admin-password=$MEET_ADMIN_SECRET"
"--meet-api-key=$MEET_API_KEY"
"--default-app-user=$DEFAULT_APP_USERNAME"
"--default-app-password=$DEFAULT_APP_PASSWORD"
"--default-app-admin-user=$DEFAULT_APP_ADMIN_USERNAME"
"--default-app-admin-password=$DEFAULT_APP_ADMIN_PASSWORD"
"--livekit-api-key=$LIVEKIT_API_KEY"
"--livekit-api-secret=$LIVEKIT_API_SECRET"
)
# 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
COMMON_ARGS+=("--turn-domain-name=$LIVEKIT_TURN_DOMAIN_NAME")
COMMON_ARGS+=("--turn-domain-name=$LIVEKIT_TURN_DOMAIN_NAME}")
fi
# 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 LIVEKIT_API_KEY=$(az keyvault secret show --vault-name ${keyVaultName} --name LIVEKIT-API-KEY --query value -o tsv)
export LIVEKIT_API_SECRET=$(az keyvault secret show --vault-name ${keyVaultName} --name LIVEKIT-API-SECRET --query value -o tsv)
export MEET_ADMIN_USER=$(az keyvault secret show --vault-name ${keyVaultName} --name MEET-ADMIN-USER --query value -o tsv)
export MEET_ADMIN_SECRET=$(az keyvault secret show --vault-name ${keyVaultName} --name MEET-ADMIN-SECRET --query value -o tsv)
export MEET_API_KEY=$(az keyvault secret show --vault-name ${keyVaultName} --name MEET-API-KEY --query value -o tsv)
export DEFAULT_APP_USERNAME=$(az keyvault secret show --vault-name ${keyVaultName} --name DEFAULT-APP-USERNAME --query value -o tsv)
export DEFAULT_APP_PASSWORD=$(az keyvault secret show --vault-name ${keyVaultName} --name DEFAULT-APP-PASSWORD --query value -o tsv)
export DEFAULT_APP_ADMIN_USERNAME=$(az keyvault secret show --vault-name ${keyVaultName} --name DEFAULT-APP-ADMIN-USERNAME --query value -o tsv)
export DEFAULT_APP_ADMIN_PASSWORD=$(az keyvault secret show --vault-name ${keyVaultName} --name DEFAULT-APP-ADMIN-PASSWORD --query value -o tsv)
export ENABLED_MODULES=$(az keyvault secret show --vault-name ${keyVaultName} --name ENABLED-MODULES --query value -o tsv)
# Replace rest of the values
@ -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/LIVEKIT_API_KEY=.*/LIVEKIT_API_KEY=$LIVEKIT_API_KEY/" "${CLUSTER_CONFIG_DIR}/openvidu.env"
sed -i "s/LIVEKIT_API_SECRET=.*/LIVEKIT_API_SECRET=$LIVEKIT_API_SECRET/" "${CLUSTER_CONFIG_DIR}/openvidu.env"
sed -i "s/MEET_ADMIN_USER=.*/MEET_ADMIN_USER=$MEET_ADMIN_USER/" "${CLUSTER_CONFIG_DIR}/master_node/meet.env"
sed -i "s/MEET_ADMIN_SECRET=.*/MEET_ADMIN_SECRET=$MEET_ADMIN_SECRET/" "${CLUSTER_CONFIG_DIR}/master_node/meet.env"
sed -i "s/MEET_API_KEY=.*/MEET_API_KEY=$MEET_API_KEY/" "${CLUSTER_CONFIG_DIR}/master_node/meet.env"
sed -i "s/CALL_USER=.*/CALL_USER=$DEFAULT_APP_USERNAME/" "${CLUSTER_CONFIG_DIR}/master_node/app.env"
sed -i "s/CALL_SECRET=.*/CALL_SECRET=$DEFAULT_APP_PASSWORD/" "${CLUSTER_CONFIG_DIR}/master_node/app.env"
sed -i "s/CALL_ADMIN_USER=.*/CALL_ADMIN_USER=$DEFAULT_APP_ADMIN_USERNAME/" "${CLUSTER_CONFIG_DIR}/master_node/app.env"
sed -i "s/CALL_ADMIN_SECRET=.*/CALL_ADMIN_SECRET=$DEFAULT_APP_ADMIN_PASSWORD/" "${CLUSTER_CONFIG_DIR}/master_node/app.env"
sed -i "s/ENABLED_MODULES=.*/ENABLED_MODULES=$ENABLED_MODULES/" "${CLUSTER_CONFIG_DIR}/openvidu.env"
# Update URLs in secret
@ -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")"
LIVEKIT_API_KEY="$(/usr/local/bin/get_value_from_config.sh LIVEKIT_API_KEY "${CLUSTER_CONFIG_DIR}/openvidu.env")"
LIVEKIT_API_SECRET="$(/usr/local/bin/get_value_from_config.sh LIVEKIT_API_SECRET "${CLUSTER_CONFIG_DIR}/openvidu.env")"
MEET_ADMIN_USER="$(/usr/local/bin/get_value_from_config.sh MEET_ADMIN_USER "${CLUSTER_CONFIG_DIR}/master_node/meet.env")"
MEET_ADMIN_SECRET="$(/usr/local/bin/get_value_from_config.sh MEET_ADMIN_SECRET "${CLUSTER_CONFIG_DIR}/master_node/meet.env")"
MEET_API_KEY="$(/usr/local/bin/get_value_from_config.sh MEET_API_KEY "${CLUSTER_CONFIG_DIR}/master_node/meet.env")"
DEFAULT_APP_USERNAME="$(/usr/local/bin/get_value_from_config.sh CALL_USER "${CLUSTER_CONFIG_DIR}/master_node/app.env")"
DEFAULT_APP_PASSWORD="$(/usr/local/bin/get_value_from_config.sh CALL_SECRET "${CLUSTER_CONFIG_DIR}/master_node/app.env")"
DEFAULT_APP_ADMIN_USERNAME="$(/usr/local/bin/get_value_from_config.sh CALL_ADMIN_USER "${CLUSTER_CONFIG_DIR}/master_node/app.env")"
DEFAULT_APP_ADMIN_PASSWORD="$(/usr/local/bin/get_value_from_config.sh CALL_ADMIN_SECRET "${CLUSTER_CONFIG_DIR}/master_node/app.env")"
ENABLED_MODULES="$(/usr/local/bin/get_value_from_config.sh ENABLED_MODULES "${CLUSTER_CONFIG_DIR}/openvidu.env")"
# Update shared secret
@ -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 LIVEKIT-API-KEY --value $LIVEKIT_API_KEY
az keyvault secret set --vault-name ${keyVaultName} --name LIVEKIT-API-SECRET --value $LIVEKIT_API_SECRET
az keyvault secret set --vault-name ${keyVaultName} --name MEET-ADMIN-USER --value $MEET_ADMIN_USER
az keyvault secret set --vault-name ${keyVaultName} --name MEET-ADMIN-SECRET --value $MEET_ADMIN_SECRET
az keyvault secret set --vault-name ${keyVaultName} --name MEET-API-KEY --value $MEET_API_KEY
az keyvault secret set --vault-name ${keyVaultName} --name DEFAULT-APP-USERNAME --value $DEFAULT_APP_USERNAME
az keyvault secret set --vault-name ${keyVaultName} --name DEFAULT-APP-PASSWORD --value $DEFAULT_APP_PASSWORD
az keyvault secret set --vault-name ${keyVaultName} --name DEFAULT-APP-ADMIN-USERNAME --value $DEFAULT_APP_ADMIN_USERNAME
az keyvault secret set --vault-name ${keyVaultName} --name DEFAULT-APP-ADMIN-PASSWORD --value $DEFAULT_APP_ADMIN_PASSWORD
az keyvault secret set --vault-name ${keyVaultName} --name ENABLED-MODULES --value $ENABLED_MODULES
'''
@ -1065,10 +1060,7 @@ var store_secretScriptMaster = reduce(
var blobStorageParams = {
storageAccountName: isEmptyStorageAccountName ? storageAccount.name : existingStorageAccount.name
storageAccountKey: listKeys(storageAccount.id, '2021-04-01').keys[0].value
storageAccountContainerName: isEmptyAppDataContainerName ? 'openvidu-appdata' : '${appDataContainerName}'
storageAccountClusterContainerName: isEmptyClusterContainerName
? 'openvidu-clusterdata'
: '${clusterDataContainerName}'
storageAccountContainerName: isEmptyContainerName ? 'openvidu-appdata' : '${containerName}'
}
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 ingress || 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
while [ $(docker ps --filter "label=openvidu-agent=true" -q | wc -l) -gt 0 ] || \
[ $(docker inspect -f '{{.State.Running}}' openvidu 2>/dev/null) == "true" ] || \
while [ $(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}}' egress 2>/dev/null) == "true" ]; do
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")
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
'''
@ -1572,10 +1545,10 @@ az vmss update --resource-group $RESOURCE_GROUP_NAME --name $VM_SCALE_SET_NAME -
export HOME="/root"
# 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
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(
@ -1590,20 +1563,12 @@ var stop_media_nodesScriptMedia = reduce(
(curr, next) => { value: replace(curr.value, '\${${next.key}}', next.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 base64stopMediaNode = base64(stop_media_nodesScriptMedia)
var base64delete_mediaNode_ScriptMedia = base64(delete_mediaNode_ScriptMedia)
var userDataParamsMedia = {
base64install: base64installMedia
base64stop: base64stopMediaNode
base64delete_mediaNode: base64delete_mediaNode_ScriptMedia
resourceGroupName: resourceGroup().name
vmScaleSetName: '${stackName}-mediaNodeScaleSet'
}
@ -1900,6 +1865,7 @@ resource scaleInActivityLogRule 'Microsoft.Insights/activityLogAlerts@2020-10-01
/*------------------------------------------- NETWORK -------------------------------------------*/
var isEmptyIp = publicIpAddressObject.newOrExistingOrNone == 'none'
var turnIsEmptyIp = turnPublicIpAddressObject.newOrExistingOrNone == 'none'
var lbName = '${stackName}-loadBalancer'
var lbFrontEndName = 'LoadBalancerFrontEnd'
var lbBackendPoolNameMasterNode = 'LoadBalancerBackEndMasterNode'
@ -1928,6 +1894,32 @@ resource publicIP_LoadBalancer_ifNew 'Microsoft.Network/publicIPAddresses@2023-1
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' = {
name: lbName
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' = {
name: '${stackName}-natGateway'
location: location
@ -2129,9 +2189,6 @@ resource subnetMasterNode2 'Microsoft.Network/virtualNetworks/subnets@2023-11-01
id: natGateway.id
}
}
dependsOn: [
subnetMasterNode1
]
}
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
name: 'masterNode_to_masterNode_MEET_INGRESS'
name: 'masterNode_to_masterNode_DEFAULTAPP_INGRESS'
properties: {
protocol: 'Tcp'
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
name: 'mediaNode_to_masterNode_MEET_WEBHOOK_INGRESS'
name: 'mediaNode_to_masterNode_DEFAULTAPP_WEBHOOK_INGRESS'
properties: {
protocol: 'Tcp'
sourceApplicationSecurityGroups: [
@ -2840,22 +2897,6 @@ resource openviduMediaNodeNSG 'Microsoft.Network/networkSecurityGroups@2023-11-0
direction: 'Inbound'
}
}
{
name: 'WebRTC_traffic_TCP'
properties: {
protocol: 'Tcp'
sourceAddressPrefix: '*'
sourcePortRange: '*'
destinationAddressPrefix: '*'
destinationPortRanges: [
'50000'
'60000'
]
access: 'Allow'
priority: 150
direction: 'Inbound'
}
}
]
}
}
@ -2879,7 +2920,7 @@ resource loadBalancerToMediaRtmpIngress 'Microsoft.Network/networkSecurityGroups
]
destinationPortRange: '1945'
access: 'Allow'
priority: 160
priority: 150
direction: 'Inbound'
}
}
@ -2898,7 +2939,7 @@ resource loadBalancerToMediaHealthcheckIngress 'Microsoft.Network/networkSecurit
]
destinationPortRange: '9092'
access: 'Allow'
priority: 170
priority: 160
direction: 'Inbound'
}
}
@ -2917,7 +2958,7 @@ resource loadBalancerToMediaTurnTlsIngress 'Microsoft.Network/networkSecurityGro
]
destinationPortRange: '5349'
access: 'Allow'
priority: 180
priority: 170
direction: 'Inbound'
}
}
@ -2936,7 +2977,7 @@ resource loadBalancerToMediaTurnTlsHealthCheckIngress 'Microsoft.Network/network
]
destinationPortRange: '7880'
access: 'Allow'
priority: 190
priority: 180
direction: 'Inbound'
}
}
@ -2959,7 +3000,7 @@ resource masterToMediaServerIngress 'Microsoft.Network/networkSecurityGroups/sec
]
destinationPortRange: '7880'
access: 'Allow'
priority: 200
priority: 190
direction: 'Inbound'
}
}
@ -2982,7 +3023,7 @@ resource masterToMediaClientIngress 'Microsoft.Network/networkSecurityGroups/sec
]
destinationPortRange: '8080'
access: 'Allow'
priority: 210
priority: 200
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.')
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) {
name: isEmptyAppDataContainerName
name: isEmptyContainerName
? '${storageAccount.name}/default/openvidu-appdata'
: '${storageAccount.name}/default/${appDataContainerName}'
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}'
: '${storageAccount.name}/default/${containerName}'
properties: {
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",
"label": "(Optional) TURN server configuration with TLS",
@ -556,6 +534,25 @@
},
"infoMessages": [],
"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]",
"turnOwnPublicCertificate": "[steps('parameters TURN').turnOwnPublicCertificate]",
"turnOwnPrivateCertificate": "[steps('parameters TURN').turnOwnPrivateCertificate]",
"turnPublicIpAddressObject": "[steps('parameters TURN').turnPublicIpAddressObject]",
"openviduLicense": "[steps('parameters OPENVIDU').openviduLicense]",
"rtcEngine": "[steps('parameters OPENVIDU').rtcEngine]",
"masterNodeInstanceType": "[steps('parameters INSTANCE').masterNodeInstanceType]",
@ -590,8 +588,7 @@
"datetime": "[steps('parameters SCALING').datetime]",
"automationAccountName": "[steps('parameters SCALING').automationAccountName]",
"storageAccountName": "[steps('parameters STORAGE').storageAccountName]",
"containerName": "[steps('parameters STORAGE').containerName]",
"additionalInstallFlags": "[steps('FLAGS').additionalInstallFlags]"
"containerName": "[steps('parameters STORAGE').containerName]"
}
}
}

View File

@ -3,9 +3,9 @@
set -eu
export DOCKER_VERSION="${DOCKER_VERSION:-28.1.1}"
export DOCKER_COMPOSE_VERSION="${DOCKER_COMPOSE_VERSION:-v2.35.1}"
export OPENVIDU_VERSION="${OPENVIDU_VERSION:-main}"
export OPENVIDU_VERSION="${OPENVIDU_VERSION:-3.2.0}"
export INSTALLER_IMAGE="${INSTALLER_IMAGE:-docker.io/openvidu/openvidu-installer:${OPENVIDU_VERSION}}"
export MINIO_SERVER_IMAGE="${MINIO_SERVER_IMAGE:-docker.io/openvidu/minio:2025.5.24-debian-12-r1}"
export MINIO_SERVER_IMAGE="${MINIO_SERVER_IMAGE:-docker.io/bitnami/minio:2025.5.24-debian-12-r1}"
export MINIO_CLIENT_IMAGE="${MINIO_CLIENT_IMAGE:-docker.io/minio/mc:RELEASE.2025-05-21T01-59-54Z}"
export MONGO_SERVER_IMAGE="${MONGO_SERVER_IMAGE:-docker.io/mongo:8.0.9}"
export REDIS_SERVER_IMAGE="${REDIS_SERVER_IMAGE:-docker.io/redis:7.4.4-alpine}"
@ -15,17 +15,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_SERVER_PRO_IMAGE="${OPENVIDU_SERVER_PRO_IMAGE:-docker.io/openvidu/openvidu-server-pro:${OPENVIDU_VERSION}}"
export OPENVIDU_SERVER_IMAGE="${OPENVIDU_SERVER_IMAGE:-docker.io/openvidu/openvidu-server:${OPENVIDU_VERSION}}"
export OPENVIDU_MEET_SERVER_IMAGE="${OPENVIDU_MEET_SERVER_IMAGE:-docker.io/openvidu/openvidu-meet:${OPENVIDU_VERSION}}"
export OPENVIDU_CALL_SERVER_IMAGE="${OPENVIDU_CALL_SERVER_IMAGE:-docker.io/openvidu/openvidu-call:${OPENVIDU_VERSION}}"
export OPENVIDU_DASHBOARD_PRO_IMAGE="${OPENVIDU_DASHBOARD_PRO_IMAGE:-docker.io/openvidu/openvidu-pro-dashboard:${OPENVIDU_VERSION}}"
export OPENVIDU_DASHBOARD_IMAGE="${OPENVIDU_DASHBOARD_IMAGE:-docker.io/openvidu/openvidu-dashboard:${OPENVIDU_VERSION}}"
export OPENVIDU_V2COMPATIBILITY_IMAGE="${OPENVIDU_V2COMPATIBILITY_IMAGE:-docker.io/openvidu/openvidu-v2compatibility:${OPENVIDU_VERSION}}"
export OPENVIDU_AGENT_SPEECH_PROCESSING_IMAGE="${OPENVIDU_AGENT_SPEECH_PROCESSING_IMAGE:-docker.io/openvidu/agent-speech-processing:${OPENVIDU_VERSION}}"
export LIVEKIT_INGRESS_SERVER_IMAGE="${LIVEKIT_INGRESS_SERVER_IMAGE:-docker.io/openvidu/ingress:${OPENVIDU_VERSION}}"
export LIVEKIT_EGRESS_SERVER_IMAGE="${LIVEKIT_EGRESS_SERVER_IMAGE:-docker.io/livekit/egress:v1.9.1}"
export PROMETHEUS_IMAGE="${PROMETHEUS_IMAGE:-docker.io/prom/prometheus:v3.4.0}"
export PROMTAIL_IMAGE="${PROMTAIL_IMAGE:-docker.io/grafana/promtail:3.5.1}"
export LOKI_IMAGE="${LOKI_IMAGE:-docker.io/grafana/loki:3.5.1}"
export MIMIR_IMAGE="${MIMIR_IMAGE:-docker.io/openvidu/grafana-mimir:2.16.0}"
export MIMIR_IMAGE="${MIMIR_IMAGE:-docker.io/bitnami/grafana-mimir:2.16.0}"
export GRAFANA_IMAGE="${GRAFANA_IMAGE:-docker.io/grafana/grafana:11.6.2}"
# Function to compare two version strings
@ -181,11 +180,10 @@ COMMON_DOCKER_OPTIONS="--network=host -v ${TMP_DIR}:/output \
-e OPENVIDU_OPERATOR_IMAGE=$OPENVIDU_OPERATOR_IMAGE \
-e OPENVIDU_SERVER_PRO_IMAGE=$OPENVIDU_SERVER_PRO_IMAGE \
-e OPENVIDU_SERVER_IMAGE=$OPENVIDU_SERVER_IMAGE \
-e OPENVIDU_MEET_SERVER_IMAGE=$OPENVIDU_MEET_SERVER_IMAGE \
-e OPENVIDU_CALL_SERVER_IMAGE=$OPENVIDU_CALL_SERVER_IMAGE \
-e OPENVIDU_DASHBOARD_PRO_IMAGE=$OPENVIDU_DASHBOARD_PRO_IMAGE \
-e OPENVIDU_DASHBOARD_IMAGE=$OPENVIDU_DASHBOARD_IMAGE \
-e OPENVIDU_V2COMPATIBILITY_IMAGE=$OPENVIDU_V2COMPATIBILITY_IMAGE \
-e OPENVIDU_AGENT_SPEECH_PROCESSING_IMAGE=$OPENVIDU_AGENT_SPEECH_PROCESSING_IMAGE \
-e LIVEKIT_INGRESS_SERVER_IMAGE=$LIVEKIT_INGRESS_SERVER_IMAGE \
-e LIVEKIT_EGRESS_SERVER_IMAGE=$LIVEKIT_EGRESS_SERVER_IMAGE \
-e PROMETHEUS_IMAGE=$PROMETHEUS_IMAGE \

View File

@ -3,9 +3,9 @@
set -eu
export DOCKER_VERSION="${DOCKER_VERSION:-28.1.1}"
export DOCKER_COMPOSE_VERSION="${DOCKER_COMPOSE_VERSION:-v2.35.1}"
export OPENVIDU_VERSION="${OPENVIDU_VERSION:-main}"
export OPENVIDU_VERSION="${OPENVIDU_VERSION:-3.2.0}"
export INSTALLER_IMAGE="${INSTALLER_IMAGE:-docker.io/openvidu/openvidu-installer:${OPENVIDU_VERSION}}"
export MINIO_SERVER_IMAGE="${MINIO_SERVER_IMAGE:-docker.io/openvidu/minio:2025.5.24-debian-12-r1}"
export MINIO_SERVER_IMAGE="${MINIO_SERVER_IMAGE:-docker.io/bitnami/minio:2025.5.24-debian-12-r1}"
export MINIO_CLIENT_IMAGE="${MINIO_CLIENT_IMAGE:-docker.io/minio/mc:RELEASE.2025-05-21T01-59-54Z}"
export MONGO_SERVER_IMAGE="${MONGO_SERVER_IMAGE:-docker.io/mongo:8.0.9}"
export REDIS_SERVER_IMAGE="${REDIS_SERVER_IMAGE:-docker.io/redis:7.4.4-alpine}"
@ -15,17 +15,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_SERVER_PRO_IMAGE="${OPENVIDU_SERVER_PRO_IMAGE:-docker.io/openvidu/openvidu-server-pro:${OPENVIDU_VERSION}}"
export OPENVIDU_SERVER_IMAGE="${OPENVIDU_SERVER_IMAGE:-docker.io/openvidu/openvidu-server:${OPENVIDU_VERSION}}"
export OPENVIDU_MEET_SERVER_IMAGE="${OPENVIDU_MEET_SERVER_IMAGE:-docker.io/openvidu/openvidu-meet:${OPENVIDU_VERSION}}"
export OPENVIDU_CALL_SERVER_IMAGE="${OPENVIDU_CALL_SERVER_IMAGE:-docker.io/openvidu/openvidu-call:${OPENVIDU_VERSION}}"
export OPENVIDU_DASHBOARD_PRO_IMAGE="${OPENVIDU_DASHBOARD_PRO_IMAGE:-docker.io/openvidu/openvidu-pro-dashboard:${OPENVIDU_VERSION}}"
export OPENVIDU_DASHBOARD_IMAGE="${OPENVIDU_DASHBOARD_IMAGE:-docker.io/openvidu/openvidu-dashboard:${OPENVIDU_VERSION}}"
export OPENVIDU_V2COMPATIBILITY_IMAGE="${OPENVIDU_V2COMPATIBILITY_IMAGE:-docker.io/openvidu/openvidu-v2compatibility:${OPENVIDU_VERSION}}"
export OPENVIDU_AGENT_SPEECH_PROCESSING_IMAGE="${OPENVIDU_AGENT_SPEECH_PROCESSING_IMAGE:-docker.io/openvidu/agent-speech-processing:${OPENVIDU_VERSION}}"
export LIVEKIT_INGRESS_SERVER_IMAGE="${LIVEKIT_INGRESS_SERVER_IMAGE:-docker.io/openvidu/ingress:${OPENVIDU_VERSION}}"
export LIVEKIT_EGRESS_SERVER_IMAGE="${LIVEKIT_EGRESS_SERVER_IMAGE:-docker.io/livekit/egress:v1.9.1}"
export PROMETHEUS_IMAGE="${PROMETHEUS_IMAGE:-docker.io/prom/prometheus:v3.4.0}"
export PROMTAIL_IMAGE="${PROMTAIL_IMAGE:-docker.io/grafana/promtail:3.5.1}"
export LOKI_IMAGE="${LOKI_IMAGE:-docker.io/grafana/loki:3.5.1}"
export MIMIR_IMAGE="${MIMIR_IMAGE:-docker.io/openvidu/grafana-mimir:2.16.0}"
export MIMIR_IMAGE="${MIMIR_IMAGE:-docker.io/bitnami/grafana-mimir:2.16.0}"
export GRAFANA_IMAGE="${GRAFANA_IMAGE:-docker.io/grafana/grafana:11.6.2}"
# Function to compare two version strings
@ -181,11 +180,10 @@ COMMON_DOCKER_OPTIONS="--network=host -v ${TMP_DIR}:/output \
-e OPENVIDU_OPERATOR_IMAGE=$OPENVIDU_OPERATOR_IMAGE \
-e OPENVIDU_SERVER_PRO_IMAGE=$OPENVIDU_SERVER_PRO_IMAGE \
-e OPENVIDU_SERVER_IMAGE=$OPENVIDU_SERVER_IMAGE \
-e OPENVIDU_MEET_SERVER_IMAGE=$OPENVIDU_MEET_SERVER_IMAGE \
-e OPENVIDU_CALL_SERVER_IMAGE=$OPENVIDU_CALL_SERVER_IMAGE \
-e OPENVIDU_DASHBOARD_PRO_IMAGE=$OPENVIDU_DASHBOARD_PRO_IMAGE \
-e OPENVIDU_DASHBOARD_IMAGE=$OPENVIDU_DASHBOARD_IMAGE \
-e OPENVIDU_V2COMPATIBILITY_IMAGE=$OPENVIDU_V2COMPATIBILITY_IMAGE \
-e OPENVIDU_AGENT_SPEECH_PROCESSING_IMAGE=$OPENVIDU_AGENT_SPEECH_PROCESSING_IMAGE \
-e LIVEKIT_INGRESS_SERVER_IMAGE=$LIVEKIT_INGRESS_SERVER_IMAGE \
-e LIVEKIT_EGRESS_SERVER_IMAGE=$LIVEKIT_EGRESS_SERVER_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"
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:
Description: '(Optional) Domain name for the TURN server with TLS. Only needed if your users are behind restrictive firewalls'
Type: String
@ -247,10 +240,6 @@ Metadata:
default: S3 bucket for application data and recordings
Parameters:
- S3AppDataBucketName
- Label:
default: "(Optional) Additional Installer Flags"
Parameters:
- AdditionalInstallFlags
- Label:
default: (Optional) TURN server configuration with TLS
Parameters:
@ -293,9 +282,10 @@ Resources:
"GRAFANA_ADMIN_PASSWORD": "none",
"LIVEKIT_API_KEY": "none",
"LIVEKIT_API_SECRET": "none",
"MEET_ADMIN_USER": "none",
"MEET_ADMIN_SECRET": "none",
"MEET_API_KEY": "none",
"DEFAULT_APP_USERNAME": "none",
"DEFAULT_APP_PASSWORD": "none",
"DEFAULT_APP_ADMIN_USERNAME": "none",
"DEFAULT_APP_ADMIN_PASSWORD": "none",
"ENABLED_MODULES": "none"
}
@ -388,7 +378,7 @@ Resources:
'/usr/local/bin/install.sh':
content: !Sub |
#!/bin/bash -x
OPENVIDU_VERSION=main
OPENVIDU_VERSION=3.2.0
DOMAIN=
YQ_VERSION=v4.44.5
@ -433,10 +423,11 @@ Resources:
DASHBOARD_ADMIN_PASSWORD="$(/usr/local/bin/store_secret.sh generate DASHBOARD_ADMIN_PASSWORD)"
GRAFANA_ADMIN_USERNAME="$(/usr/local/bin/store_secret.sh save GRAFANA_ADMIN_USERNAME "grafanaadmin")"
GRAFANA_ADMIN_PASSWORD="$(/usr/local/bin/store_secret.sh generate GRAFANA_ADMIN_PASSWORD)"
MEET_ADMIN_USER="$(/usr/local/bin/store_secret.sh save MEET_ADMIN_USER "meetadmin")"
MEET_ADMIN_SECRET="$(/usr/local/bin/store_secret.sh generate MEET_ADMIN_SECRET)"
MEET_API_KEY="$(/usr/local/bin/store_secret.sh generate MEET_API_KEY)"
ENABLED_MODULES="$(/usr/local/bin/store_secret.sh save ENABLED_MODULES "observability,openviduMeet,v2compatibility")"
DEFAULT_APP_USERNAME="$(/usr/local/bin/store_secret.sh save DEFAULT_APP_USERNAME "calluser")"
DEFAULT_APP_PASSWORD="$(/usr/local/bin/store_secret.sh generate DEFAULT_APP_PASSWORD)"
DEFAULT_APP_ADMIN_USERNAME="$(/usr/local/bin/store_secret.sh save DEFAULT_APP_ADMIN_USERNAME "calladmin")"
DEFAULT_APP_ADMIN_PASSWORD="$(/usr/local/bin/store_secret.sh generate DEFAULT_APP_ADMIN_PASSWORD)"
ENABLED_MODULES="$(/usr/local/bin/store_secret.sh save ENABLED_MODULES "observability,app,v2compatibility")"
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)"
@ -463,25 +454,14 @@ Resources:
"--dashboard-admin-password=$DASHBOARD_ADMIN_PASSWORD"
"--grafana-admin-user=$GRAFANA_ADMIN_USERNAME"
"--grafana-admin-password=$GRAFANA_ADMIN_PASSWORD"
"--meet-admin-user=$MEET_ADMIN_USER"
"--meet-admin-password=$MEET_ADMIN_SECRET"
"--meet-api-key=$MEET_API_KEY"
"--default-app-user=$DEFAULT_APP_USERNAME"
"--default-app-password=$DEFAULT_APP_PASSWORD"
"--default-app-admin-user=$DEFAULT_APP_ADMIN_USERNAME"
"--default-app-admin-password=$DEFAULT_APP_ADMIN_PASSWORD"
"--livekit-api-key=$LIVEKIT_API_KEY"
"--livekit-api-secret=$LIVEKIT_API_SECRET"
)
# 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
if [[ "${TurnDomainName}" != '' ]]; then
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/LIVEKIT_API_KEY=.*/LIVEKIT_API_KEY=$(echo $SHARED_SECRET | jq -r .LIVEKIT_API_KEY)/" "${!CONFIG_DIR}/openvidu.env"
sed -i "s/LIVEKIT_API_SECRET=.*/LIVEKIT_API_SECRET=$(echo $SHARED_SECRET | jq -r .LIVEKIT_API_SECRET)/" "${!CONFIG_DIR}/openvidu.env"
sed -i "s/MEET_ADMIN_USER=.*/MEET_ADMIN_USER=$(echo $SHARED_SECRET | jq -r .MEET_ADMIN_USER)/" "${!CONFIG_DIR}/meet.env"
sed -i "s/MEET_ADMIN_SECRET=.*/MEET_ADMIN_SECRET=$(echo $SHARED_SECRET | jq -r .MEET_ADMIN_SECRET)/" "${!CONFIG_DIR}/meet.env"
sed -i "s/MEET_API_KEY=.*/MEET_API_KEY=$(echo $SHARED_SECRET | jq -r .MEET_API_KEY)/" "${!CONFIG_DIR}/meet.env"
sed -i "s/CALL_USER=.*/CALL_USER=$(echo $SHARED_SECRET | jq -r .DEFAULT_APP_USERNAME)/" "${!CONFIG_DIR}/app.env"
sed -i "s/CALL_SECRET=.*/CALL_SECRET=$(echo $SHARED_SECRET | jq -r .DEFAULT_APP_PASSWORD)/" "${!CONFIG_DIR}/app.env"
sed -i "s/CALL_ADMIN_USER=.*/CALL_ADMIN_USER=$(echo $SHARED_SECRET | jq -r .DEFAULT_APP_ADMIN_USERNAME)/" "${!CONFIG_DIR}/app.env"
sed -i "s/CALL_ADMIN_SECRET=.*/CALL_ADMIN_SECRET=$(echo $SHARED_SECRET | jq -r .DEFAULT_APP_ADMIN_PASSWORD)/" "${!CONFIG_DIR}/app.env"
sed -i "s/ENABLED_MODULES=.*/ENABLED_MODULES=$(echo $SHARED_SECRET | jq -r .ENABLED_MODULES)/" "${!CONFIG_DIR}/openvidu.env"
# Update URLs in secret
@ -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 '. + {"LIVEKIT_API_KEY": "'"$(/usr/local/bin/get_value_from_config.sh LIVEKIT_API_KEY "${!CONFIG_DIR}/openvidu.env")"'"}')"
SHARED_SECRET="$(echo "$SHARED_SECRET" | jq '. + {"LIVEKIT_API_SECRET": "'"$(/usr/local/bin/get_value_from_config.sh LIVEKIT_API_SECRET "${!CONFIG_DIR}/openvidu.env")"'"}')"
SHARED_SECRET="$(echo "$SHARED_SECRET" | jq '. + {"MEET_ADMIN_USER": "'"$(/usr/local/bin/get_value_from_config.sh MEET_ADMIN_USER "${!CONFIG_DIR}/meet.env")"'"}')"
SHARED_SECRET="$(echo "$SHARED_SECRET" | jq '. + {"MEET_ADMIN_SECRET": "'"$(/usr/local/bin/get_value_from_config.sh MEET_ADMIN_SECRET "${!CONFIG_DIR}/meet.env")"'"}')"
SHARED_SECRET="$(echo "$SHARED_SECRET" | jq '. + {"MEET_API_KEY": "'"$(/usr/local/bin/get_value_from_config.sh MEET_API_KEY "${!CONFIG_DIR}/meet.env")"'"}')"
SHARED_SECRET="$(echo "$SHARED_SECRET" | jq '. + {"DEFAULT_APP_USERNAME": "'"$(/usr/local/bin/get_value_from_config.sh CALL_USER "${!CONFIG_DIR}/app.env")"'"}')"
SHARED_SECRET="$(echo "$SHARED_SECRET" | jq '. + {"DEFAULT_APP_PASSWORD": "'"$(/usr/local/bin/get_value_from_config.sh CALL_SECRET "${!CONFIG_DIR}/app.env")"'"}')"
SHARED_SECRET="$(echo "$SHARED_SECRET" | jq '. + {"DEFAULT_APP_ADMIN_USERNAME": "'"$(/usr/local/bin/get_value_from_config.sh CALL_ADMIN_USER "${!CONFIG_DIR}/app.env")"'"}')"
SHARED_SECRET="$(echo "$SHARED_SECRET" | jq '. + {"DEFAULT_APP_ADMIN_PASSWORD": "'"$(/usr/local/bin/get_value_from_config.sh CALL_ADMIN_SECRET "${!CONFIG_DIR}/app.env")"'"}')"
SHARED_SECRET="$(echo "$SHARED_SECRET" | jq '. + {"ENABLED_MODULES": "'"$(/usr/local/bin/get_value_from_config.sh ENABLED_MODULES "${!CONFIG_DIR}/openvidu.env")"'"}')"
# Update shared secret
@ -969,14 +951,6 @@ Resources:
FromPort: 7885
ToPort: 7885
CidrIpv6: ::/0
- IpProtocol: tcp
FromPort: 7881
ToPort: 7881
CidrIp: 0.0.0.0/0
- IpProtocol: tcp
FromPort: 7881
ToPort: 7881
CidrIpv6: ::/0
- IpProtocol: udp
FromPort: 50000
ToPort: 60000

View File

@ -170,8 +170,6 @@ param adminUsername string
@secure()
param adminSshKey object
param additionalInstallFlags string = ''
/*------------------------------------------- VARIABLES AND VALIDATIONS -------------------------------------------*/
//Condition for ipValid if is filled
@ -278,12 +276,11 @@ var stringInterpolationParams = {
keyVaultName: keyVaultName
openviduLicense: openviduLicense
rtcEngine: rtcEngine
additionalInstallFlags: additionalInstallFlags
}
var installScriptTemplate = '''
#!/bin/bash -x
OPENVIDU_VERSION=main
OPENVIDU_VERSION=3.2.0
DOMAIN=
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)"
GRAFANA_ADMIN_USERNAME="$(/usr/local/bin/store_secret.sh save GRAFANA-ADMIN-USERNAME "grafanaadmin")"
GRAFANA_ADMIN_PASSWORD="$(/usr/local/bin/store_secret.sh generate GRAFANA-ADMIN-PASSWORD)"
MEET_ADMIN_USER="$(/usr/local/bin/store_secret.sh save MEET-ADMIN-USER "meetadmin")"
MEET_ADMIN_SECRET="$(/usr/local/bin/store_secret.sh generate MEET-ADMIN-SECRET)"
MEET_API_KEY="$(/usr/local/bin/store_secret.sh generate MEET-API-KEY)"
DEFAULT_APP_USERNAME="$(/usr/local/bin/store_secret.sh save DEFAULT-APP-USERNAME "calluser")"
DEFAULT_APP_PASSWORD="$(/usr/local/bin/store_secret.sh generate DEFAULT-APP-PASSWORD)"
DEFAULT_APP_ADMIN_USERNAME="$(/usr/local/bin/store_secret.sh save DEFAULT-APP-ADMIN-USERNAME "calladmin")"
DEFAULT_APP_ADMIN_PASSWORD="$(/usr/local/bin/store_secret.sh generate DEFAULT-APP-ADMIN-PASSWORD)"
LIVEKIT_API_KEY="$(/usr/local/bin/store_secret.sh generate LIVEKIT-API-KEY "API" 12)"
LIVEKIT_API_SECRET="$(/usr/local/bin/store_secret.sh generate LIVEKIT-API-SECRET)"
ENABLED_MODULES="$(/usr/local/bin/store_secret.sh save ENABLED-MODULES "observability,openviduMeet,v2compatibility")"
ENABLED_MODULES="$(/usr/local/bin/store_secret.sh save ENABLED-MODULES "observability,app,v2compatibility")"
# Base command
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"
"--grafana-admin-user=$GRAFANA_ADMIN_USERNAME"
"--grafana-admin-password=$GRAFANA_ADMIN_PASSWORD"
"--meet-admin-user=$MEET_ADMIN_USER"
"--meet-admin-password=$MEET_ADMIN_SECRET"
"--meet-api-key=$MEET_API_KEY"
"--default-app-user=$DEFAULT_APP_USERNAME"
"--default-app-password=$DEFAULT_APP_PASSWORD"
"--default-app-admin-user=$DEFAULT_APP_ADMIN_USERNAME"
"--default-app-admin-password=$DEFAULT_APP_ADMIN_PASSWORD"
"--livekit-api-key=$LIVEKIT_API_KEY"
"--livekit-api-secret=$LIVEKIT_API_SECRET"
)
# 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
if [[ "${turnDomainName}" != '' ]]; then
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 LIVEKIT_API_KEY=$(az keyvault secret show --vault-name ${keyVaultName} --name LIVEKIT-API-KEY --query value -o tsv)
export LIVEKIT_API_SECRET=$(az keyvault secret show --vault-name ${keyVaultName} --name LIVEKIT-API-SECRET --query value -o tsv)
export MEET_ADMIN_USER=$(az keyvault secret show --vault-name ${keyVaultName} --name MEET-ADMIN-USER --query value -o tsv)
export MEET_ADMIN_SECRET=$(az keyvault secret show --vault-name ${keyVaultName} --name MEET-ADMIN-SECRET --query value -o tsv)
export MEET_API_KEY=$(az keyvault secret show --vault-name ${keyVaultName} --name MEET-API-KEY --query value -o tsv)
export DEFAULT_APP_USERNAME=$(az keyvault secret show --vault-name ${keyVaultName} --name DEFAULT-APP-USERNAME --query value -o tsv)
export DEFAULT_APP_PASSWORD=$(az keyvault secret show --vault-name ${keyVaultName} --name DEFAULT-APP-PASSWORD --query value -o tsv)
export DEFAULT_APP_ADMIN_USERNAME=$(az keyvault secret show --vault-name ${keyVaultName} --name DEFAULT-APP-ADMIN-USERNAME --query value -o tsv)
export DEFAULT_APP_ADMIN_PASSWORD=$(az keyvault secret show --vault-name ${keyVaultName} --name DEFAULT-APP-ADMIN-PASSWORD --query value -o tsv)
export ENABLED_MODULES=$(az keyvault secret show --vault-name ${keyVaultName} --name ENABLED-MODULES --query value -o tsv)
# Replace rest of the values
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_PRO_LICENSE=.*/OPENVIDU_PRO_LICENSE=$OPENVIDU_PRO_LICENSE/" "${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/" "${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_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"
@ -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/LIVEKIT_API_KEY=.*/LIVEKIT_API_KEY=$LIVEKIT_API_KEY/" "${CONFIG_DIR}/openvidu.env"
sed -i "s/LIVEKIT_API_SECRET=.*/LIVEKIT_API_SECRET=$LIVEKIT_API_SECRET/" "${CONFIG_DIR}/openvidu.env"
sed -i "s/MEET_ADMIN_USER=.*/MEET_ADMIN_USER=$MEET_ADMIN_USER/" "${CONFIG_DIR}/meet.env"
sed -i "s/MEET_ADMIN_SECRET=.*/MEET_ADMIN_SECRET=$MEET_ADMIN_SECRET/" "${CONFIG_DIR}/meet.env"
sed -i "s/MEET_API_KEY=.*/MEET_API_KEY=$MEET_API_KEY/" "${CONFIG_DIR}/meet.env"
sed -i "s/CALL_USER=.*/CALL_USER=$DEFAULT_APP_USERNAME/" "${CONFIG_DIR}/app.env"
sed -i "s/CALL_SECRET=.*/CALL_SECRET=$DEFAULT_APP_PASSWORD/" "${CONFIG_DIR}/app.env"
sed -i "s/CALL_ADMIN_USER=.*/CALL_ADMIN_USER=$DEFAULT_APP_ADMIN_USERNAME/" "${CONFIG_DIR}/app.env"
sed -i "s/CALL_ADMIN_SECRET=.*/CALL_ADMIN_SECRET=$DEFAULT_APP_ADMIN_PASSWORD/" "${CONFIG_DIR}/app.env"
sed -i "s/ENABLED_MODULES=.*/ENABLED_MODULES=$ENABLED_MODULES/" "${CONFIG_DIR}/openvidu.env"
@ -552,8 +541,8 @@ fi
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")"
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_PRO_LICENSE="$(/usr/local/bin/get_value_from_config.sh OPENVIDU_PRO_LICENSE "${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 "${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_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")"
@ -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")"
LIVEKIT_API_KEY="$(/usr/local/bin/get_value_from_config.sh LIVEKIT_API_KEY "${CONFIG_DIR}/openvidu.env")"
LIVEKIT_API_SECRET="$(/usr/local/bin/get_value_from_config.sh LIVEKIT_API_SECRET "${CONFIG_DIR}/openvidu.env")"
MEET_ADMIN_USER="$(/usr/local/bin/get_value_from_config.sh MEET_ADMIN_USER "${CONFIG_DIR}/meet.env")"
MEET_ADMIN_SECRET="$(/usr/local/bin/get_value_from_config.sh MEET_ADMIN_SECRET "${CONFIG_DIR}/meet.env")"
MEET_API_KEY="$(/usr/local/bin/get_value_from_config.sh MEET_API_KEY "${CONFIG_DIR}/meet.env")"
DEFAULT_APP_USERNAME="$(/usr/local/bin/get_value_from_config.sh CALL_USER "${CONFIG_DIR}/app.env")"
DEFAULT_APP_PASSWORD="$(/usr/local/bin/get_value_from_config.sh CALL_SECRET "${CONFIG_DIR}/app.env")"
DEFAULT_APP_ADMIN_USERNAME="$(/usr/local/bin/get_value_from_config.sh CALL_ADMIN_USER "${CONFIG_DIR}/app.env")"
DEFAULT_APP_ADMIN_PASSWORD="$(/usr/local/bin/get_value_from_config.sh CALL_ADMIN_SECRET "${CONFIG_DIR}/app.env")"
ENABLED_MODULES="$(/usr/local/bin/get_value_from_config.sh ENABLED_MODULES "${CONFIG_DIR}/openvidu.env")"
@ -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 LIVEKIT-API-KEY --value $LIVEKIT_API_KEY
az keyvault secret set --vault-name ${keyVaultName} --name LIVEKIT-API-SECRET --value $LIVEKIT_API_SECRET
az keyvault secret set --vault-name ${keyVaultName} --name MEET-ADMIN-USER --value $MEET_ADMIN_USER
az keyvault secret set --vault-name ${keyVaultName} --name MEET-ADMIN-SECRET --value $MEET_ADMIN_SECRET
az keyvault secret set --vault-name ${keyVaultName} --name MEET-API-KEY --value $MEET_API_KEY
az keyvault secret set --vault-name ${keyVaultName} --name DEFAULT-APP-USERNAME --value $DEFAULT_APP_USERNAME
az keyvault secret set --vault-name ${keyVaultName} --name DEFAULT-APP-PASSWORD --value $DEFAULT_APP_PASSWORD
az keyvault secret set --vault-name ${keyVaultName} --name DEFAULT-APP-ADMIN-USERNAME --value $DEFAULT_APP_ADMIN_USERNAME
az keyvault secret set --vault-name ${keyVaultName} --name DEFAULT-APP-ADMIN-PASSWORD --value $DEFAULT_APP_ADMIN_PASSWORD
az keyvault secret set --vault-name ${keyVaultName} --name ENABLED-MODULES --value $ENABLED_MODULES
'''

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",
"label": "(Optional) TURN server configuration with TLS",
@ -444,8 +422,7 @@
"adminUsername": "[steps('parameters INSTANCE').adminUsername]",
"adminSshKey": "[steps('parameters INSTANCE').adminSshKey]",
"storageAccountName": "[steps('parameters STORAGE').storageAccountName]",
"containerName": "[steps('parameters STORAGE').containerName]",
"additionalInstallFlags": "[steps('FLAGS').additionalInstallFlags]"
"containerName": "[steps('parameters STORAGE').containerName]"
}
}
}

View File

@ -3,9 +3,9 @@
set -eu
export DOCKER_VERSION="${DOCKER_VERSION:-28.1.1}"
export DOCKER_COMPOSE_VERSION="${DOCKER_COMPOSE_VERSION:-v2.35.1}"
export OPENVIDU_VERSION="${OPENVIDU_VERSION:-main}"
export OPENVIDU_VERSION="${OPENVIDU_VERSION:-3.2.0}"
export INSTALLER_IMAGE="${INSTALLER_IMAGE:-docker.io/openvidu/openvidu-installer:${OPENVIDU_VERSION}}"
export MINIO_SERVER_IMAGE="${MINIO_SERVER_IMAGE:-docker.io/openvidu/minio:2025.5.24-debian-12-r1}"
export MINIO_SERVER_IMAGE="${MINIO_SERVER_IMAGE:-docker.io/bitnami/minio:2025.5.24-debian-12-r1}"
export MINIO_CLIENT_IMAGE="${MINIO_CLIENT_IMAGE:-docker.io/minio/mc:RELEASE.2025-05-21T01-59-54Z}"
export MONGO_SERVER_IMAGE="${MONGO_SERVER_IMAGE:-docker.io/mongo:8.0.9}"
export REDIS_SERVER_IMAGE="${REDIS_SERVER_IMAGE:-docker.io/redis:7.4.4-alpine}"
@ -15,17 +15,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_SERVER_PRO_IMAGE="${OPENVIDU_SERVER_PRO_IMAGE:-docker.io/openvidu/openvidu-server-pro:${OPENVIDU_VERSION}}"
export OPENVIDU_SERVER_IMAGE="${OPENVIDU_SERVER_IMAGE:-docker.io/openvidu/openvidu-server:${OPENVIDU_VERSION}}"
export OPENVIDU_MEET_SERVER_IMAGE="${OPENVIDU_MEET_SERVER_IMAGE:-docker.io/openvidu/openvidu-meet:${OPENVIDU_VERSION}}"
export OPENVIDU_CALL_SERVER_IMAGE="${OPENVIDU_CALL_SERVER_IMAGE:-docker.io/openvidu/openvidu-call:${OPENVIDU_VERSION}}"
export OPENVIDU_DASHBOARD_PRO_IMAGE="${OPENVIDU_DASHBOARD_PRO_IMAGE:-docker.io/openvidu/openvidu-pro-dashboard:${OPENVIDU_VERSION}}"
export OPENVIDU_DASHBOARD_IMAGE="${OPENVIDU_DASHBOARD_IMAGE:-docker.io/openvidu/openvidu-dashboard:${OPENVIDU_VERSION}}"
export OPENVIDU_V2COMPATIBILITY_IMAGE="${OPENVIDU_V2COMPATIBILITY_IMAGE:-docker.io/openvidu/openvidu-v2compatibility:${OPENVIDU_VERSION}}"
export OPENVIDU_AGENT_SPEECH_PROCESSING_IMAGE="${OPENVIDU_AGENT_SPEECH_PROCESSING_IMAGE:-docker.io/openvidu/agent-speech-processing:${OPENVIDU_VERSION}}"
export LIVEKIT_INGRESS_SERVER_IMAGE="${LIVEKIT_INGRESS_SERVER_IMAGE:-docker.io/openvidu/ingress:${OPENVIDU_VERSION}}"
export LIVEKIT_EGRESS_SERVER_IMAGE="${LIVEKIT_EGRESS_SERVER_IMAGE:-docker.io/livekit/egress:v1.9.1}"
export PROMETHEUS_IMAGE="${PROMETHEUS_IMAGE:-docker.io/prom/prometheus:v3.4.0}"
export PROMTAIL_IMAGE="${PROMTAIL_IMAGE:-docker.io/grafana/promtail:3.5.1}"
export LOKI_IMAGE="${LOKI_IMAGE:-docker.io/grafana/loki:3.5.1}"
export MIMIR_IMAGE="${MIMIR_IMAGE:-docker.io/openvidu/grafana-mimir:2.16.0}"
export MIMIR_IMAGE="${MIMIR_IMAGE:-docker.io/bitnami/grafana-mimir:2.16.0}"
export GRAFANA_IMAGE="${GRAFANA_IMAGE:-docker.io/grafana/grafana:11.6.2}"
# Function to compare two version strings
@ -181,11 +180,10 @@ COMMON_DOCKER_OPTIONS="--network=host -v ${TMP_DIR}:/output \
-e OPENVIDU_OPERATOR_IMAGE=$OPENVIDU_OPERATOR_IMAGE \
-e OPENVIDU_SERVER_PRO_IMAGE=$OPENVIDU_SERVER_PRO_IMAGE \
-e OPENVIDU_SERVER_IMAGE=$OPENVIDU_SERVER_IMAGE \
-e OPENVIDU_MEET_SERVER_IMAGE=$OPENVIDU_MEET_SERVER_IMAGE \
-e OPENVIDU_CALL_SERVER_IMAGE=$OPENVIDU_CALL_SERVER_IMAGE \
-e OPENVIDU_DASHBOARD_PRO_IMAGE=$OPENVIDU_DASHBOARD_PRO_IMAGE \
-e OPENVIDU_DASHBOARD_IMAGE=$OPENVIDU_DASHBOARD_IMAGE \
-e OPENVIDU_V2COMPATIBILITY_IMAGE=$OPENVIDU_V2COMPATIBILITY_IMAGE \
-e OPENVIDU_AGENT_SPEECH_PROCESSING_IMAGE=$OPENVIDU_AGENT_SPEECH_PROCESSING_IMAGE \
-e LIVEKIT_INGRESS_SERVER_IMAGE=$LIVEKIT_INGRESS_SERVER_IMAGE \
-e LIVEKIT_EGRESS_SERVER_IMAGE=$LIVEKIT_EGRESS_SERVER_IMAGE \
-e PROMETHEUS_IMAGE=$PROMETHEUS_IMAGE \

View File

@ -3,7 +3,7 @@ set -eu
export INSTALL_PREFIX="${INSTALL_PREFIX:-/opt/openvidu}"
export DOCKER_VERSION="${DOCKER_VERSION:-28.1.1}"
export DOCKER_COMPOSE_VERSION="${DOCKER_COMPOSE_VERSION:-v2.35.1}"
export OPENVIDU_VERSION="${OPENVIDU_VERSION:-main}"
export OPENVIDU_VERSION="${OPENVIDU_VERSION:-3.2.0}"
export REGISTRY="${REGISTRY:-docker.io}"
export UPDATER_IMAGE="${UPDATER_IMAGE:-${REGISTRY}/openvidu/openvidu-updater:${OPENVIDU_VERSION}}"

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