Merge branch 'ov-components-refactor'

pull/809/head
Carlos Santos 2023-05-31 10:55:51 +02:00
commit 12a34ca7cb
27 changed files with 16781 additions and 1319 deletions

View File

@ -71,7 +71,7 @@ jobs:
name: openvidu-browser name: openvidu-browser
path: openvidu-components-angular path: openvidu-components-angular
- name: Run Browserless Chrome - name: Run Browserless Chrome
run: docker run -d -p 3000:3000 --network host browserless/chrome:1.53-chrome-stable run: docker run -d -p 3000:3000 --network host browserless/chrome:1.57-chrome-stable
- name: Run openvidu-server-kms - name: Run openvidu-server-kms
run: | run: |
docker run -p 4443:4443 --rm -d \ docker run -p 4443:4443 --rm -d \
@ -106,7 +106,7 @@ jobs:
name: openvidu-browser name: openvidu-browser
path: openvidu-components-angular path: openvidu-components-angular
- name: Run Browserless Chrome - name: Run Browserless Chrome
run: docker run -d -p 3000:3000 --network host browserless/chrome:1.53-chrome-stable run: docker run -d -p 3000:3000 --network host browserless/chrome:1.57-chrome-stable
- name: Run openvidu-server-kms - name: Run openvidu-server-kms
run: | run: |
docker run -p 4443:4443 --rm -d \ docker run -p 4443:4443 --rm -d \
@ -140,7 +140,7 @@ jobs:
name: openvidu-browser name: openvidu-browser
path: openvidu-components-angular path: openvidu-components-angular
- name: Run Browserless Chrome - name: Run Browserless Chrome
run: docker run -d -p 3000:3000 --network host browserless/chrome:1.53-chrome-stable run: docker run -d -p 3000:3000 --network host browserless/chrome:1.57-chrome-stable
- name: Install openvidu-browser and dependencies - name: Install openvidu-browser and dependencies
run: | run: |
cd openvidu-components-angular cd openvidu-components-angular

View File

@ -2,7 +2,7 @@ import { expect } from 'chai';
import { By, until, WebDriver, WebElement } from 'selenium-webdriver'; import { By, until, WebDriver, WebElement } from 'selenium-webdriver';
export class OpenViduComponentsPO { export class OpenViduComponentsPO {
private TIMEOUT = 30 * 1000; private TIMEOUT = 10 * 1000;
private POLL_TIMEOUT = 1 * 1000; private POLL_TIMEOUT = 1 * 1000;
constructor(private browser: WebDriver) {} constructor(private browser: WebDriver) {}
@ -16,7 +16,7 @@ export class OpenViduComponentsPO {
); );
} }
async getNumberOfElements(selector: string){ async getNumberOfElements(selector: string): Promise<number> {
return (await this.browser.findElements(By.css(selector))).length; return (await this.browser.findElements(By.css(selector))).length;
} }

View File

@ -1,3 +1,5 @@
import monkeyPatchMediaDevices from './utils/media-devices.js';
var MINIMAL; var MINIMAL;
var LANG; var LANG;
var CAPTIONS_LANG; var CAPTIONS_LANG;
@ -29,6 +31,7 @@ var CAPTIONS_BUTTON;
var SINGLE_TOKEN; var SINGLE_TOKEN;
var SESSION_NAME; var SESSION_NAME;
var FAKE_DEVICES;
var PARTICIPANT_NAME; var PARTICIPANT_NAME;
@ -43,6 +46,8 @@ $(document).ready(() => {
SINGLE_TOKEN = url.searchParams.get('singleToken') === null ? false : url.searchParams.get('singleToken') === 'true'; SINGLE_TOKEN = url.searchParams.get('singleToken') === null ? false : url.searchParams.get('singleToken') === 'true';
FAKE_DEVICES = url.searchParams.get('fakeDevices') === null ? false : url.searchParams.get('fakeDevices') === 'true';
// Directives // Directives
MINIMAL = url.searchParams.get('minimal') === null ? false : url.searchParams.get('minimal') === 'true'; MINIMAL = url.searchParams.get('minimal') === null ? false : url.searchParams.get('minimal') === 'true';
LANG = url.searchParams.get('lang') || 'en'; LANG = url.searchParams.get('lang') || 'en';
@ -197,6 +202,11 @@ function appendElement(id) {
async function joinSession(sessionName, participantName) { async function joinSession(sessionName, participantName) {
var webComponent = document.querySelector('openvidu-webcomponent'); var webComponent = document.querySelector('openvidu-webcomponent');
var tokens; var tokens;
if (FAKE_DEVICES) {
monkeyPatchMediaDevices();
}
if (SINGLE_TOKEN) { if (SINGLE_TOKEN) {
tokens = await getToken(sessionName); tokens = await getToken(sessionName);
} else { } else {

View File

@ -8,7 +8,10 @@
crossorigin="anonymous" crossorigin="anonymous"
></script> ></script>
<script src="app.js"></script> <script type="module" src="utils/filter-stream.js"></script>
<script type="module" src="utils/shader-renderer.js"></script>
<script type="module" src="utils/media-devices.js"></script>
<script type="module" src="app.js"></script>
<script src="openvidu-webcomponent-dev.js"></script> <script src="openvidu-webcomponent-dev.js"></script>
<link rel="stylesheet" href="openvidu-webcomponent-dev.css" /> <link rel="stylesheet" href="openvidu-webcomponent-dev.css" />

View File

@ -0,0 +1,30 @@
class FilterStream {
constructor(stream, label) {
const videoTrack = stream.getVideoTracks()[0];
const { width, height } = videoTrack.getSettings();
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
const video = document.createElement('video');
video.srcObject = new MediaStream([videoTrack]);
video.play();
video.addEventListener('play', () => {
const loop = () => {
if (!video.paused && !video.ended) {
ctx.filter = 'grayscale(100%)';
ctx.drawImage(video, 0, 0, video.videoWidth, video.videoHeight, 0, 0, video.videoWidth, video.videoHeight);
setTimeout(loop, 33);
}
};
loop();
});
this.outputStream = canvas.captureStream();
Object.defineProperty(this.outputStream.getVideoTracks()[0], 'label', {
writable: true,
value: label
});
}
}
export { FilterStream };

View File

@ -0,0 +1,62 @@
// Ideally we'd use an editor or import shaders directly from the API.
import { FilterStream } from './filter-stream.js';
export default function monkeyPatchMediaDevices() {
const enumerateDevicesFn = MediaDevices.prototype.enumerateDevices;
const getUserMediaFn = MediaDevices.prototype.getUserMedia;
const getDisplayMediaFn = MediaDevices.prototype.getDisplayMedia;
const fakeDevice = {
deviceId: 'virtual',
groupID: '',
kind: 'videoinput',
label: 'custom_fake_video_1'
};
MediaDevices.prototype.enumerateDevices = async function () {
const res = await enumerateDevicesFn.call(navigator.mediaDevices);
res.push(fakeDevice);
return res;
};
MediaDevices.prototype.getUserMedia = async function () {
const args = arguments[0];
const { deviceId, advanced, width, height } = args.video;
if (deviceId === 'virtual' || deviceId?.exact === 'virtual') {
const constraints = {
video: {
facingMode: args.facingMode,
advanced,
width,
height
},
audio: false
};
const res = await getUserMediaFn.call(navigator.mediaDevices, constraints);
if (res) {
const filter = new FilterStream(res, fakeDevice.label);
return filter.outputStream;
}
return res;
}
return getUserMediaFn.call(navigator.mediaDevices, ...arguments);
};
MediaDevices.prototype.getDisplayMedia = async function () {
const { video, audio } = arguments[0];
const screenVideoElement = document.getElementsByClassName("OT_video-element screen-type")[0];
const currentTrackLabel = screenVideoElement?.srcObject?.getVideoTracks()[0]?.label;
const res = await getDisplayMediaFn.call(navigator.mediaDevices, { video, audio });
if (res && currentTrackLabel && currentTrackLabel !== 'custom_fake_screen') {
const filter = new FilterStream(res, 'custom_fake_screen');
return filter.outputStream;
}
return res;
};
}

View File

@ -1,5 +1,5 @@
import { expect } from 'chai'; import { expect } from 'chai';
import { Builder, By, Key, WebDriver } from 'selenium-webdriver'; import { Builder, Key, WebDriver } from 'selenium-webdriver';
import { OPENVIDU_SECRET, OPENVIDU_SERVER_URL } from './config'; import { OPENVIDU_SECRET, OPENVIDU_SERVER_URL } from './config';
import { getBrowserOptionsWithoutDevices, WebComponentConfig } from './selenium.conf'; import { getBrowserOptionsWithoutDevices, WebComponentConfig } from './selenium.conf';
import { OpenViduComponentsPO } from './utils.po.test'; import { OpenViduComponentsPO } from './utils.po.test';
@ -88,8 +88,7 @@ describe('Testing API Directives', () => {
expect(await utils.isPresent('#session-name')).to.be.false; expect(await utils.isPresent('#session-name')).to.be.false;
// Checking if nickname is not displayed // Checking if nickname is not displayed
await browser.findElements(By.id('nickname-container')); expect(await utils.getNumberOfElements('#nickname-container')).equals(0);
expect(await utils.isPresent('#nickname-container')).to.be.false;
// Checking if audio detection is not displayed // Checking if audio detection is not displayed
expect(await utils.isPresent('#audio-wave-container')).to.be.false; expect(await utils.isPresent('#audio-wave-container')).to.be.false;
@ -123,6 +122,7 @@ describe('Testing API Directives', () => {
}); });
it('should run the app with VIDEO MUTED in prejoin page', async () => { it('should run the app with VIDEO MUTED in prejoin page', async () => {
try {
let idVideoEnabled; let idVideoEnabled;
const script = 'return document.getElementsByTagName("video")[0].srcObject.getVideoTracks()[0].enabled;'; const script = 'return document.getElementsByTagName("video")[0].srcObject.getVideoTracks()[0].enabled;';
@ -152,6 +152,10 @@ describe('Testing API Directives', () => {
await utils.waitForElement('#videocam_off'); await utils.waitForElement('#videocam_off');
expect(await utils.isPresent('#videocam_off')).to.be.true; expect(await utils.isPresent('#videocam_off')).to.be.true;
} catch (error) {
console.log(error);
console.log(await browser.takeScreenshot());
}
}); });
it('should run the app with VIDEO MUTED and WITHOUT PREJOIN page', async () => { it('should run the app with VIDEO MUTED and WITHOUT PREJOIN page', async () => {
@ -160,6 +164,8 @@ describe('Testing API Directives', () => {
await browser.get(`${url}&prejoin=false&videoMuted=true`); await browser.get(`${url}&prejoin=false&videoMuted=true`);
await browser.sleep(2000);
await utils.checkSessionIsPresent(); await utils.checkSessionIsPresent();
await utils.checkLayoutPresent(); await utils.checkLayoutPresent();
@ -251,9 +257,8 @@ describe('Testing API Directives', () => {
// Checking if fullscreen button is not present // Checking if fullscreen button is not present
await utils.waitForElement('.mat-menu-content'); await utils.waitForElement('.mat-menu-content');
expect(await utils.isPresent('.mat-menu-content')).to.be.true; expect(await utils.isPresent('.mat-menu-content')).to.be.true;
expect(await utils.getNumberOfElements('#fullscreen-btn')).equals(0);
await browser.findElements(By.id('fullscreen-btn'));
expect(await utils.isPresent('#fullscreen-btn')).to.be.false;
}); });
it('should HIDE the CAPTIONS button', async () => { it('should HIDE the CAPTIONS button', async () => {
@ -357,8 +362,7 @@ describe('Testing API Directives', () => {
await utils.checkToolbarIsPresent(); await utils.checkToolbarIsPresent();
// Checking if leave button is not present // Checking if leave button is not present
await browser.findElements(By.id('leave-btn')); expect(await utils.getNumberOfElements('#leave-btn')).equals(0);
expect(await utils.isPresent('#leave-btn')).to.be.false;
}); });
it('should HIDE the ACTIVITIES PANEL button', async () => { it('should HIDE the ACTIVITIES PANEL button', async () => {
@ -500,7 +504,7 @@ describe('Testing API Directives', () => {
// Go to first tab // Go to first tab
const tabs = await browser.getAllWindowHandles(); const tabs = await browser.getAllWindowHandles();
await browser.switchTo().window(tabs[0]); browser.switchTo().window(tabs[0]);
// Checking if mute button is not displayed in participant item // Checking if mute button is not displayed in participant item
await utils.waitForElement('#remote-participant-item'); await utils.waitForElement('#remote-participant-item');
@ -614,7 +618,7 @@ describe('Testing API Directives', () => {
await utils.checkToolbarIsPresent(); await utils.checkToolbarIsPresent();
await utils.waitForElement('#activities-panel-btn'); await utils.waitForElement('#activities-panel-btn');
await utils.clickOn('#activities-panel-btn') await utils.clickOn('#activities-panel-btn');
// Checking if participatns panel is displayed // Checking if participatns panel is displayed
await utils.waitForElement('#default-activities-panel'); await utils.waitForElement('#default-activities-panel');
@ -964,7 +968,6 @@ describe('Testing videoconference EVENTS', () => {
const button = await utils.waitForElement('#broadcasting-btn'); const button = await utils.waitForElement('#broadcasting-btn');
expect(await button.isEnabled()).to.be.false; expect(await button.isEnabled()).to.be.false;
const input = await utils.waitForElement('#broadcast-url-input'); const input = await utils.waitForElement('#broadcast-url-input');
await input.sendKeys('BroadcastUrl'); await input.sendKeys('BroadcastUrl');
@ -974,7 +977,6 @@ describe('Testing videoconference EVENTS', () => {
await utils.waitForElement('#onActivitiesPanelStartBroadcastingClicked'); await utils.waitForElement('#onActivitiesPanelStartBroadcastingClicked');
expect(await utils.isPresent('#onActivitiesPanelStartBroadcastingClicked')).to.be.true; expect(await utils.isPresent('#onActivitiesPanelStartBroadcastingClicked')).to.be.true;
// TODO: it needs an OpenVidu PRO (onActivitiesPanelStopBroadcastingClicked event) // TODO: it needs an OpenVidu PRO (onActivitiesPanelStopBroadcastingClicked event)
// expect(await utils.isPresent('#broadcasting-tag')).to.be.true; // expect(await utils.isPresent('#broadcasting-tag')).to.be.true;
@ -1037,6 +1039,133 @@ describe('Testing videoconference EVENTS', () => {
}); });
}); });
describe('Testing replace track with emulated devices', () => {
let browser: WebDriver;
let utils: OpenViduComponentsPO;
async function createChromeBrowser(): Promise<WebDriver> {
return await new Builder()
.forBrowser(WebComponentConfig.browserName)
.withCapabilities(WebComponentConfig.browserCapabilities)
.setChromeOptions(WebComponentConfig.browserOptions)
.usingServer(WebComponentConfig.seleniumAddress)
.build();
}
beforeEach(async () => {
browser = await createChromeBrowser();
utils = new OpenViduComponentsPO(browser);
});
afterEach(async () => {
// console.log('data:image/png;base64,' + await browser.takeScreenshot());
await browser.quit();
});
it('should replace the video track in prejoin page', async () => {
const script = 'return document.getElementsByTagName("video")[0].srcObject.getVideoTracks()[0].label;';
await browser.get(`${url}&fakeDevices=true`);
let videoDevices = await utils.waitForElement('#video-devices-form');
await videoDevices.click();
let element = await utils.waitForElement('#option-custom_fake_video_1');
await element.click();
let videoLabel;
await browser.sleep(1000);
videoLabel = await browser.executeScript<string>(script);
expect(videoLabel).to.be.equal('custom_fake_video_1');
await videoDevices.click();
element = await utils.waitForElement('#option-fake_device_0');
await element.click();
await browser.sleep(1000);
videoLabel = await browser.executeScript<string>(script);
expect(videoLabel).to.be.equal('fake_device_0');
});
it('should replace the video track in videoconference page', async () => {
const script = 'return document.getElementsByTagName("video")[0].srcObject.getVideoTracks()[0].label;';
await browser.get(`${url}&prejoin=false&fakeDevices=true`);
await utils.checkSessionIsPresent();
// Checking if toolbar is present
await utils.checkToolbarIsPresent();
// Open more options menu
await utils.clickOn('#more-options-btn');
// Checking if button panel is present
await utils.waitForElement('.mat-menu-content');
expect(await utils.isPresent('.mat-menu-content')).to.be.true;
await utils.clickOn('#toolbar-settings-btn');
await utils.waitForElement('.settings-container');
expect(await utils.isPresent('.settings-container')).to.be.true;
await utils.clickOn('#video-opt');
expect(await utils.isPresent('ov-video-devices-select')).to.be.true;
let videoDevices = await utils.waitForElement('#video-devices-form');
await videoDevices.click();
let element = await utils.waitForElement('#option-custom_fake_video_1');
await element.click();
let videoLabel;
await browser.sleep(1000);
videoLabel = await browser.executeScript<string>(script);
expect(videoLabel).to.be.equal('custom_fake_video_1');
await videoDevices.click();
element = await utils.waitForElement('#option-fake_device_0');
await element.click();
await browser.sleep(1000);
videoLabel = await browser.executeScript<string>(script);
expect(videoLabel).to.be.equal('fake_device_0');
});
it('should replace the screen track', async () => {
const script = 'return document.getElementsByClassName("OT_video-element screen-type")[0].srcObject.getVideoTracks()[0].label;';
await browser.get(`${url}&prejoin=false&fakeDevices=true`);
await utils.checkLayoutPresent();
await utils.checkToolbarIsPresent();
await utils.clickOn('#screenshare-btn');
await browser.sleep(500);
let screenLabel = await browser.executeScript<string>(script);
expect(screenLabel).not.equal('custom_fake_screen');
await utils.clickOn('#video-settings-btn-SCREEN');
await browser.sleep(500);
await utils.waitForElement('.video-settings-menu');
const replaceBtn = await utils.waitForElement('#replace-screen-button');
await replaceBtn.sendKeys(Key.ENTER);
await browser.sleep(1000);
screenLabel = await browser.executeScript<string>(script);
expect(screenLabel).to.be.equal('custom_fake_screen');
});
});
describe('Testing stream video menu features', () => { describe('Testing stream video menu features', () => {
let browser: WebDriver; let browser: WebDriver;
let utils: OpenViduComponentsPO; let utils: OpenViduComponentsPO;
@ -1063,8 +1192,8 @@ describe('Testing stream video menu features', () => {
await utils.checkLayoutPresent(); await utils.checkLayoutPresent();
await utils.waitForElement('#stream-menu-btn'); await utils.waitForElement('#video-settings-btn-CAMERA');
await utils.clickOn('#stream-menu-btn'); await utils.clickOn('#video-settings-btn-CAMERA');
await browser.sleep(500); await browser.sleep(500);
@ -1073,7 +1202,6 @@ describe('Testing stream video menu features', () => {
}); });
}); });
describe('Testing screenshare features', () => { describe('Testing screenshare features', () => {
let browser: WebDriver; let browser: WebDriver;
let utils: OpenViduComponentsPO; let utils: OpenViduComponentsPO;
@ -1095,10 +1223,10 @@ describe('Testing screenshare features', () => {
await browser.quit(); await browser.quit();
}); });
it('should toggle screensharing', async () => {
let element;
await browser.get(`${url}&prejoin=false`);
it('should toggle screensharing twice', async () => {
await browser.get(`${url}&prejoin=false`);
await utils.checkLayoutPresent(); await utils.checkLayoutPresent();
// Clicking to screensharing button // Clicking to screensharing button
@ -1107,18 +1235,54 @@ describe('Testing screenshare features', () => {
await screenshareButton.click(); await screenshareButton.click();
await utils.waitForElement('.OV_big'); await utils.waitForElement('.OV_big');
element = await browser.findElements(By.css('video')); expect(await utils.getNumberOfElements('video')).equals(2);
expect(element.length).equals(2);
// Clicking to screensharing button // Clicking to screensharing button
await screenshareButton.click(); await screenshareButton.click();
expect(await utils.getNumberOfElements('video')).equals(1);
// toggle screenshare again
await screenshareButton.click();
await utils.waitForElement('.OV_big');
expect(await utils.getNumberOfElements('video')).equals(2);
await screenshareButton.click();
expect(await utils.getNumberOfElements('video')).equals(1);
});
it('should show only screen if toggle screensharing with video muted', async () => {
await browser.get(`${url}&prejoin=false`);
await utils.checkLayoutPresent();
const camButton = await utils.waitForElement('#camera-btn');
await camButton.click();
// Clicking to screensharing button
const screenshareButton = await utils.waitForElement('#screenshare-btn');
expect(await screenshareButton.isDisplayed()).to.be.true;
await screenshareButton.click();
await browser.sleep(1000);
await utils.waitForElement('.OV_big');
expect(await utils.getNumberOfElements('video')).equals(1);
await screenshareButton.click();
await browser.sleep(1000);
expect(await utils.getNumberOfElements('video')).equals(1);
element = await browser.findElements(By.css('video'));
expect(element.length).equals(1);
}); });
it('should screensharing with audio muted', async () => { it('should screensharing with audio muted', async () => {
let element, isAudioEnabled; let isAudioEnabled;
const getAudioScript = (className: string) => { const getAudioScript = (className: string) => {
return `return document.getElementsByClassName('${className}')[0].srcObject.getAudioTracks()[0].enabled;`; return `return document.getElementsByClassName('${className}')[0].srcObject.getAudioTracks()[0].enabled;`;
}; };
@ -1135,25 +1299,22 @@ describe('Testing screenshare features', () => {
await screenshareButton.click(); await screenshareButton.click();
await utils.waitForElement('.screen-type'); await utils.waitForElement('.screen-type');
element = await browser.findElements(By.css('video')); expect(await utils.getNumberOfElements('video')).equals(2);
expect(element.length).equals(2);
isAudioEnabled = await browser.executeScript(getAudioScript('screen-type')); isAudioEnabled = await browser.executeScript(getAudioScript('screen-type'));
expect(isAudioEnabled).to.be.false; expect(isAudioEnabled).to.be.false;
await utils.waitForElement('#statusMic'); await utils.waitForElement('#statusMic');
element = await browser.findElements(By.id('statusMic')); expect(await utils.getNumberOfElements('#statusMic')).equals(2);
expect(element.length).equals(2);
// Clicking to screensharing button // Clicking to screensharing button
await screenshareButton.click(); await screenshareButton.click();
expect(await utils.getNumberOfElements('video')).equals(1);
element = await browser.findElements(By.css('video'));
expect(element.length).equals(1);
}); });
it('should show and hide CAMERA stream when muting video with screensharing', async () => { it('should show and hide CAMERA stream when muting video with screensharing', async () => {
let element;
await browser.get(`${url}&prejoin=false`); await browser.get(`${url}&prejoin=false`);
await utils.checkLayoutPresent(); await utils.checkLayoutPresent();
@ -1164,18 +1325,16 @@ describe('Testing screenshare features', () => {
await screenshareButton.click(); await screenshareButton.click();
await utils.waitForElement('.OV_big'); await utils.waitForElement('.OV_big');
element = await browser.findElements(By.css('video')); expect(await utils.getNumberOfElements('video')).equals(2);
expect(element.length).equals(2);
const muteVideoButton = await utils.waitForElement('#camera-btn'); const muteVideoButton = await utils.waitForElement('#camera-btn');
await muteVideoButton.click(); await muteVideoButton.click();
element = await browser.findElements(By.css('video')); expect(await utils.getNumberOfElements('video')).equals(1);
expect(element.length).equals(1);
}); });
it('should screenshare has audio active when camera is muted', async () => { it('should screenshare has audio active when camera is muted', async () => {
let element, isAudioEnabled; let isAudioEnabled;
const audioEnableScript = 'return document.getElementsByTagName("video")[0].srcObject.getAudioTracks()[0].enabled;'; const audioEnableScript = 'return document.getElementsByTagName("video")[0].srcObject.getAudioTracks()[0].enabled;';
await browser.get(`${url}&prejoin=false`); await browser.get(`${url}&prejoin=false`);
@ -1187,19 +1346,15 @@ describe('Testing screenshare features', () => {
expect(await utils.isPresent('#screenshare-btn')).to.be.true; expect(await utils.isPresent('#screenshare-btn')).to.be.true;
await screenshareButton.click(); await screenshareButton.click();
element = await utils.waitForElement('.OV_big'); await utils.waitForElement('.OV_big');
element = await browser.findElements(By.css('video')); expect(await utils.getNumberOfElements('video')).equals(2);
expect(element.length).equals(2); expect(await utils.getNumberOfElements('#statusMic')).equals(1);
element = await browser.findElements(By.id('statusMic'));
expect(element.length).equals(1);
// Muting camera video // Muting camera video
const muteVideoButton = await utils.waitForElement('#camera-btn'); const muteVideoButton = await utils.waitForElement('#camera-btn');
await muteVideoButton.click(); await muteVideoButton.click();
element = await browser.findElements(By.css('video')); expect(await utils.getNumberOfElements('video')).equals(1);
expect(element.length).equals(1);
await browser.sleep(500); await browser.sleep(500);
expect(await utils.isPresent('#statusMic')).to.be.false; expect(await utils.isPresent('#statusMic')).to.be.false;
@ -1210,13 +1365,11 @@ describe('Testing screenshare features', () => {
// Unmuting camera // Unmuting camera
await muteVideoButton.click(); await muteVideoButton.click();
await browser.sleep(500);
element = await utils.waitForElement('.camera-type'); await utils.waitForElement('.camera-type');
element = await browser.findElements(By.css('video')); expect(await utils.getNumberOfElements('video')).equals(2);
expect(element.length).equals(2); expect(await utils.getNumberOfElements('#statusMic')).equals(1);
element = await browser.findElements(By.id('statusMic'));
expect(element.length).equals(1);
}); });
it('should camera come back with audio muted when screensharing', async () => { it('should camera come back with audio muted when screensharing', async () => {
@ -1235,19 +1388,16 @@ describe('Testing screenshare features', () => {
await screenshareButton.click(); await screenshareButton.click();
await utils.waitForElement('.screen-type'); await utils.waitForElement('.screen-type');
element = await browser.findElements(By.css('video')); expect(await utils.getNumberOfElements('video')).equals(2);
expect(element.length).equals(2); expect(await utils.getNumberOfElements('#statusMic')).equals(1);
element = await browser.findElements(By.id('statusMic'));
expect(element.length).equals(1);
// Mute camera // Mute camera
const muteVideoButton = await utils.waitForElement('#camera-btn'); const muteVideoButton = await utils.waitForElement('#camera-btn');
await muteVideoButton.click(); await muteVideoButton.click();
element = await browser.findElements(By.css('video'));
expect(element.length).equals(1);
expect(await utils.getNumberOfElements('video')).equals(1);
expect(await utils.isPresent('#statusMic')).to.be.false; expect(await utils.isPresent('#statusMic')).to.be.false;
// Checking if audio is muted after join the room // Checking if audio is muted after join the room
@ -1259,8 +1409,8 @@ describe('Testing screenshare features', () => {
await muteAudioButton.click(); await muteAudioButton.click();
await utils.waitForElement('#statusMic'); await utils.waitForElement('#statusMic');
element = await browser.findElements(By.id('statusMic')); expect(await utils.getNumberOfElements('#statusMic')).equals(1);
expect(element.length).equals(1);
isAudioEnabled = await browser.executeScript(getAudioScript('screen-type')); isAudioEnabled = await browser.executeScript(getAudioScript('screen-type'));
expect(isAudioEnabled).to.be.false; expect(isAudioEnabled).to.be.false;
@ -1269,11 +1419,9 @@ describe('Testing screenshare features', () => {
await muteVideoButton.click(); await muteVideoButton.click();
await utils.waitForElement('.camera-type'); await utils.waitForElement('.camera-type');
element = await browser.findElements(By.css('video')); expect(await utils.getNumberOfElements('video')).equals(2);
expect(element.length).equals(2); expect(await utils.getNumberOfElements('#statusMic')).equals(2);
element = await browser.findElements(By.id('statusMic'));
expect(element.length).equals(2);
isAudioEnabled = await browser.executeScript(getAudioScript('camera-type')); isAudioEnabled = await browser.executeScript(getAudioScript('camera-type'));
expect(isAudioEnabled).to.be.false; expect(isAudioEnabled).to.be.false;
@ -1471,9 +1619,7 @@ describe('Testing panels', () => {
// Close chat panel // Close chat panel
await chatButton.click(); await chatButton.click();
await browser.findElements(By.className('input-container')); expect(await utils.getNumberOfElements('.input-container')).equals(0);
expect(await utils.isPresent('.input-container')).to.be.false;
expect(await utils.isPresent('messages-container')).to.be.false; expect(await utils.isPresent('messages-container')).to.be.false;
}); });
@ -1557,7 +1703,6 @@ describe('Testing CHAT features', () => {
await utils.waitForElement('.input-container'); await utils.waitForElement('.input-container');
expect(await utils.isPresent('.input-container')).to.be.true; expect(await utils.isPresent('.input-container')).to.be.true;
const input = await utils.waitForElement('#chat-input'); const input = await utils.waitForElement('#chat-input');
await input.sendKeys('demos.openvidu.io'); await input.sendKeys('demos.openvidu.io');
@ -1568,6 +1713,62 @@ describe('Testing CHAT features', () => {
}); });
}); });
describe('Testing TOOLBAR features', () => {
let browser: WebDriver;
let utils: OpenViduComponentsPO;
async function createChromeBrowser(): Promise<WebDriver> {
return await new Builder()
.forBrowser(WebComponentConfig.browserName)
.withCapabilities(WebComponentConfig.browserCapabilities)
.setChromeOptions(WebComponentConfig.browserOptions)
.usingServer(WebComponentConfig.seleniumAddress)
.build();
}
beforeEach(async () => {
browser = await createChromeBrowser();
utils = new OpenViduComponentsPO(browser);
});
afterEach(async () => {
await browser.quit();
});
it('should mute and unmute the local microphone', async () => {
await browser.get(`${url}&prejoin=false`);
await utils.checkLayoutPresent();
const micButton = await utils.waitForElement('#mic-btn');
await micButton.click();
await utils.waitForElement('#mic-btn #mic_off');
expect(await utils.isPresent('#mic-btn #mic_off')).to.be.true;
await micButton.click();
await utils.waitForElement('#mic-btn #mic');
expect(await utils.isPresent('#mic-btn #mic')).to.be.true;
});
it('should mute and unmute the local camera', async () => {
await browser.get(`${url}&prejoin=false`);
await utils.checkLayoutPresent();
const cameraButton = await utils.waitForElement('#camera-btn');
await cameraButton.click();
await utils.waitForElement('#camera-btn #videocam_off');
expect(await utils.isPresent('#camera-btn #videocam_off')).to.be.true;
await cameraButton.click();
await utils.waitForElement('#camera-btn #videocam');
expect(await utils.isPresent('#camera-btn #videocam')).to.be.true;
});
});
describe('Testing video is playing', () => { describe('Testing video is playing', () => {
let browser: WebDriver; let browser: WebDriver;
@ -1805,7 +2006,6 @@ describe('Testing PRO features with OpenVidu CE', () => {
// Expect it shows the pro feature alert // Expect it shows the pro feature alert
await utils.chceckProFeatureAlertIsPresent(); await utils.chceckProFeatureAlertIsPresent();
}); });
it('should SHOW the CAPTIONS PRO feature dialog', async () => { it('should SHOW the CAPTIONS PRO feature dialog', async () => {
@ -1853,7 +2053,6 @@ describe('Testing PRO features with OpenVidu CE', () => {
}); });
}); });
/** /**
* TODO: * TODO:
* The following E2E TESTS only work with OpenVidu PRO. * The following E2E TESTS only work with OpenVidu PRO.
@ -2031,3 +2230,103 @@ describe('Testing PRO features with OpenVidu CE', () => {
// }); // });
// }); // });
describe('Testing WITHOUT MEDIA DEVICES permissions', () => {
let browser: WebDriver;
let utils: OpenViduComponentsPO;
async function createChromeBrowser(): Promise<WebDriver> {
return await new Builder()
.forBrowser(WebComponentConfig.browserName)
.withCapabilities(WebComponentConfig.browserCapabilities)
.setChromeOptions(getBrowserOptionsWithoutDevices())
.usingServer(WebComponentConfig.seleniumAddress)
.build();
}
beforeEach(async () => {
browser = await createChromeBrowser();
utils = new OpenViduComponentsPO(browser);
});
afterEach(async () => {
await browser.quit();
});
it('should be able to ACCESS to PREJOIN page', async () => {
await browser.get(`${url}`);
await utils.checkPrejoinIsPresent();
let button = await utils.waitForElement('#camera-button');
expect(await button.isEnabled()).to.be.false;
button = await utils.waitForElement('#microphone-button');
expect(await button.isEnabled()).to.be.false;
});
it('should be able to ACCESS to ROOM page', async () => {
await browser.get(`${url}`);
await utils.checkPrejoinIsPresent();
await utils.clickOn('#join-button');
await utils.checkSessionIsPresent();
await utils.checkToolbarIsPresent();
let button = await utils.waitForElement('#camera-btn');
expect(await button.isEnabled()).to.be.false;
button = await utils.waitForElement('#mic-btn');
expect(await button.isEnabled()).to.be.false;
});
it('should be able to ACCESS to ROOM page without prejoin', async () => {
await browser.get(`${url}&prejoin=false`);
await utils.checkSessionIsPresent();
await utils.checkToolbarIsPresent();
let button = await utils.waitForElement('#camera-btn');
expect(await button.isEnabled()).to.be.false;
button = await utils.waitForElement('#mic-btn');
expect(await button.isEnabled()).to.be.false;
});
it('should the settings buttons be disabled', async () => {
await browser.get(`${url}&prejoin=false`);
await utils.checkToolbarIsPresent();
// Open more options menu
await utils.clickOn('#more-options-btn');
await browser.sleep(500);
// Checking if fullscreen button is not present
await utils.waitForElement('.mat-menu-content');
expect(await utils.isPresent('.mat-menu-content')).to.be.true;
await utils.clickOn('#toolbar-settings-btn');
await browser.sleep(500);
await utils.waitForElement('.settings-container');
expect(await utils.isPresent('.settings-container')).to.be.true;
await utils.clickOn('#video-opt');
expect(await utils.isPresent('ov-video-devices-select')).to.be.true;
let button = await utils.waitForElement('#camera-button');
expect(await button.isEnabled()).to.be.false;
await utils.clickOn('#audio-opt');
expect(await utils.isPresent('ov-audio-devices-select')).to.be.true;
button = await utils.waitForElement('#microphone-button');
expect(await button.isEnabled()).to.be.false;
});
});

File diff suppressed because it is too large Load Diff

View File

@ -22,17 +22,6 @@
height: -moz-available; height: -moz-available;
} }
/*!
* Copyright (c) 2017 TokBox, Inc.
* Released under the MIT license
* http://opensource.org/licenses/MIT
*/
/**
* OT Base styles
*/
/* Root OT object, this is where our CSS reset happens */
.OT_root, .OT_root,
.OT_root * { .OT_root * {
color: #ffffff; color: #ffffff;
@ -43,179 +32,6 @@
vertical-align: baseline; vertical-align: baseline;
} }
.OT_dialog-centering {
display: table;
width: 100%;
height: 100%;
}
.OT_dialog-centering-child {
display: table-cell;
vertical-align: middle;
}
.OT_dialog {
position: relative;
-webkit-box-sizing: border-box;
box-sizing: border-box;
margin-right: auto;
margin-left: auto;
color: #fff;
font-family: 'Ubuntu', sans-serif;
font-size: 13px;
line-height: 1.4;
}
.OT_dialog * {
font-family: inherit;
-webkit-box-sizing: inherit;
box-sizing: inherit;
}
.OT_closeButton {
color: #999999;
cursor: pointer;
font-size: 32px;
line-height: 36px;
position: absolute;
right: 18px;
top: 0;
}
.OT_dialog-messages {
text-align: center;
}
.OT_dialog-messages-main {
margin-bottom: 36px;
line-height: 36px;
font-weight: 300;
font-size: 24px;
}
.OT_dialog-messages-minor {
margin-bottom: 18px;
font-size: 13px;
line-height: 18px;
color: #a4a4a4;
}
.OT_dialog-messages-minor strong {
color: #ffffff;
}
.OT_dialog-actions-card {
display: inline-block;
}
.OT_dialog-button-title {
margin-bottom: 18px;
line-height: 18px;
font-weight: 300;
text-align: center;
font-size: 14px;
color: #999999;
}
.OT_dialog-button-title label {
color: #999999;
}
.OT_dialog-button-title a,
.OT_dialog-button-title a:link,
.OT_dialog-button-title a:active {
color: #02a1de;
}
.OT_dialog-button-title strong {
color: #ffffff;
font-weight: 100;
display: block;
}
.OT_dialog-button {
display: inline-block;
margin-bottom: 18px;
padding: 0 1em;
background-color: #1ca3dc;
text-align: center;
cursor: pointer;
}
.OT_dialog-button:disabled {
cursor: not-allowed;
opacity: 0.5;
}
.OT_dialog-button-large {
line-height: 36px;
padding-top: 9px;
padding-bottom: 9px;
font-weight: 100;
font-size: 24px;
}
.OT_dialog-button-small {
line-height: 18px;
padding-top: 9px;
padding-bottom: 9px;
background-color: #444444;
color: #999999;
font-size: 16px;
}
.OT_dialog-progress-bar {
display: inline-block; /* prevents margin collapse */
width: 100%;
margin-top: 5px;
margin-bottom: 41px;
border: 1px solid #4e4e4e;
height: 8px;
}
.OT_dialog-progress-bar-fill {
height: 100%;
background-color: #29a4da;
}
.OT_dialog-plugin-upgrading .OT_dialog-plugin-upgrade-percentage {
line-height: 54px;
font-size: 48px;
font-weight: 100;
}
/* Helpers */
.OT_centered {
position: fixed;
left: 50%;
top: 50%;
margin: 0;
}
.OT_dialog-hidden {
display: none;
}
.OT_dialog-button-block {
display: block;
}
.OT_dialog-no-natural-margin {
margin-bottom: 0;
}
/* Publisher and Subscriber styles */
.OT_publisher, .OT_publisher,
.OT_subscriber { .OT_subscriber {
@ -236,604 +52,3 @@
-webkit-transform-origin: 0 0; -webkit-transform-origin: 0 0;
transform-origin: 0 0; transform-origin: 0 0;
} }
.OT_subscriber_error {
background-color: #000;
color: #fff;
text-align: center;
}
.OT_subscriber_error > p {
padding: 20px;
}
/* The publisher/subscriber name/mute background */
.OT_publisher .OT_bar,
.OT_subscriber .OT_bar,
.OT_publisher .OT_name,
.OT_subscriber .OT_name,
.OT_publisher .OT_archiving,
.OT_subscriber .OT_archiving,
.OT_publisher .OT_archiving-status,
.OT_subscriber .OT_archiving-status,
.OT_publisher .OT_archiving-light-box,
.OT_subscriber .OT_archiving-light-box {
-webkit-box-sizing: border-box;
-ms-box-sizing: border-box;
box-sizing: border-box;
top: 0;
left: 0;
right: 0;
display: block;
height: 34px;
position: absolute;
}
.OT_publisher .OT_bar,
.OT_subscriber .OT_bar {
background: rgba(0, 0, 0, 0.4);
}
.OT_publisher .OT_edge-bar-item,
.OT_subscriber .OT_edge-bar-item {
z-index: 1; /* required to get audio level meter underneath */
}
/* The publisher/subscriber name panel/archiving status bar */
.OT_publisher .OT_name,
.OT_subscriber .OT_name {
background-color: transparent;
color: #ffffff;
font-size: 15px;
line-height: 34px;
font-weight: normal;
padding: 0 4px 0 36px;
}
.OT_publisher .OT_archiving-status,
.OT_subscriber .OT_archiving-status {
background: rgba(0, 0, 0, 0.4);
top: auto;
bottom: 0;
left: 34px;
padding: 0 4px;
color: rgba(255, 255, 255, 0.8);
font-size: 15px;
line-height: 34px;
font-weight: normal;
}
.OT_micro .OT_archiving-status,
.OT_micro:hover .OT_archiving-status,
.OT_mini .OT_archiving-status,
.OT_mini:hover .OT_archiving-status {
display: none;
}
.OT_publisher .OT_archiving-light-box,
.OT_subscriber .OT_archiving-light-box {
background: rgba(0, 0, 0, 0.4);
top: auto;
bottom: 0;
right: auto;
width: 34px;
height: 34px;
}
.OT_archiving-light {
width: 7px;
height: 7px;
border-radius: 30px;
position: absolute;
top: 14px;
left: 14px;
background-color: #575757;
-webkit-box-shadow: 0 0 5px 1px #575757;
box-shadow: 0 0 5px 1px #575757;
}
.OT_archiving-light.OT_active {
background-color: #970d13;
animation: OT_pulse 1.3s ease-in;
-webkit-animation: OT_pulse 1.3s ease-in;
-moz-animation: OT_pulse 1.3s ease-in;
-webkit-animation: OT_pulse 1.3s ease-in;
animation-iteration-count: infinite;
-webkit-animation-iteration-count: infinite;
-moz-animation-iteration-count: infinite;
-webkit-animation-iteration-count: infinite;
}
@-webkit-keyframes OT_pulse {
0% {
-webkit-box-shadow: 0 0 0px 0px #c70019;
box-shadow: 0 0 0px 0px #c70019;
}
30% {
-webkit-box-shadow: 0 0 5px 1px #c70019;
box-shadow: 0 0 5px 1px #c70019;
}
50% {
-webkit-box-shadow: 0 0 5px 1px #c70019;
box-shadow: 0 0 5px 1px #c70019;
}
80% {
-webkit-box-shadow: 0 0 0px 0px #c70019;
box-shadow: 0 0 0px 0px #c70019;
}
100% {
-webkit-box-shadow: 0 0 0px 0px #c70019;
box-shadow: 0 0 0px 0px #c70019;
}
}
@-webkit-keyframes OT_pulse {
0% {
-webkit-box-shadow: 0 0 0px 0px #c70019;
box-shadow: 0 0 0px 0px #c70019;
}
30% {
-webkit-box-shadow: 0 0 5px 1px #c70019;
box-shadow: 0 0 5px 1px #c70019;
}
50% {
-webkit-box-shadow: 0 0 5px 1px #c70019;
box-shadow: 0 0 5px 1px #c70019;
}
80% {
-webkit-box-shadow: 0 0 0px 0px #c70019;
box-shadow: 0 0 0px 0px #c70019;
}
100% {
-webkit-box-shadow: 0 0 0px 0px #c70019;
box-shadow: 0 0 0px 0px #c70019;
}
}
.OT_mini .OT_bar,
.OT_bar.OT_mode-mini,
.OT_bar.OT_mode-mini-auto {
bottom: 0;
height: auto;
}
.OT_mini .OT_name.OT_mode-off,
.OT_mini .OT_name.OT_mode-on,
.OT_mini .OT_name.OT_mode-auto,
.OT_mini:hover .OT_name.OT_mode-auto {
display: none;
}
.OT_publisher .OT_name,
.OT_subscriber .OT_name {
left: 10px;
right: 37px;
height: 34px;
padding-left: 0;
}
.OT_publisher .OT_mute,
.OT_subscriber .OT_mute {
border: none;
cursor: pointer;
display: block;
position: absolute;
text-align: center;
text-indent: -9999em;
background-color: transparent;
background-repeat: no-repeat;
}
.OT_publisher .OT_mute,
.OT_subscriber .OT_mute {
right: 0;
top: 0;
border-left: 1px solid rgba(255, 255, 255, 0.2);
height: 36px;
width: 37px;
}
.OT_mini .OT_mute,
.OT_publisher.OT_mini .OT_mute.OT_mode-auto.OT_mode-on-hold,
.OT_subscriber.OT_mini .OT_mute.OT_mode-auto.OT_mode-on-hold {
top: 50%;
left: 50%;
right: auto;
margin-top: -18px;
margin-left: -18.5px;
border-left: none;
}
.OT_publisher .OT_mute {
background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABMAAAAcCAMAAAC02HQrAAAA1VBMVEUAAAD3+Pj3+Pj3+Pj3+Pj3+Pj3+Pj3+Pj3+Pj3+Pn3+Pj3+Pj3+Pj3+Pj3+Pj3+Pj3+Pj3+Pj3+Pj3+Pj3+Pj3+Pj3+Pj3+Pj3+Pj3+Pj3+Pj3+Pj3+Pj3+Pj3+Pj3+Pj3+Pj3+Pj3+Pj3+Pj3+Pj3+Pj3+Pj3+Pj3+Pj3+Pj3+Pj3+Pj3+Pj3+Pj3+Pj3+Pj3+Pj3+Pn3+Pj3+Pj3+Pj3+Pj3+Pj3+Pj3+Pj3+Pj3+Pj3+Pj3+Pj3+Pj3+Pj3+Pj3+Pj3+Pj3+Pj39/j3+Pj3+Pn4+Pk/JRMlAAAAQ3RSTlMABAUHCQoLDhAQERwdHiAjLjAxOD9ASFBRVl1mbnZ6fH2LjI+QkaWqrrC1uLzAwcXJycrL1NXj5Ofo6u3w9fr7/P3+d4M3+QAAAQBJREFUGBlVwYdCglAABdCLlr5Unijm3hMUtBzlBLSr//9JgUToOQgVJgceJgU8aHgMeA38K50ZOpcQmTPwcyXn+JM8M3JJIqQypiIkeXelTyIkGZPwKS1NMia1lgKTVkaE3oQQGYsmHNqSMWnTgUFbMiZtGlD2dpaxrL1XgM0i4ZK8MeAmFhsAs29MGZniawagS63oMOQUNXYB5D0D1RMDpyoMLw/fiE2og/V+PVDR5AiBl0/2Uwik+vx4xV3a5G5Ye68Nd1czjUjZckm6VhmPciRzeCZICjwTJAViQq+3e+St167rAoHK8sLYZVkBYPCZAZ/eGa+2R5LH7Wrc0YFf/O9J3yBDFaoAAAAASUVORK5CYII=);
background-position: 9px 5px;
}
.OT_publisher .OT_mute.OT_active {
background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABUAAAAdCAYAAABFRCf7AAADcElEQVRIiaWVXWhcRRTHf7NNd2aDtUKMIjTpg4ufFIuiUOmDEWm0Vi3VYhXRqIggQh4sWJFSig9+oOhTKSpIRUWMIBIr2kptoTbgU6ooxCiIjR+14kcJmf9sNceHnd3ebnc3Uv9wuXfOzPzmnDMz5zozGwdWAbc65w5RUJQ8cC2wDJgFJioh/MJCMrNxq2vOzK4HmIvRRemxKP0RJWt53o7S+d2Yzsx6gQ+AIUDAnUqpBLzXZd4RYFUlhB/bdZacc3PAOmAcCMC7wfvFwLNdoAPAyx09bXyYWRl4E7gDmAdGlNKFwLYu8GolhO9O87RJd64GbMrgEvB68P4osMWdXLtVV7czlooNpVRWSs8DO7NpR/B+3rBHsvetCgtCMTxwQCm9BbyQrc8F7/uBex3uRCeXO0PrUZ4NfKyUPgWeyj3bg/crDNsIRGwBaJQGorQ3Svdn2wHgc2BUKb0DPJHtjwfvbwRucc7tz+N+i9LFUdoXpfVN36I0CVwBTFI/q9e1LPxT8P4qYEdu70q12mYzWw1MYQzjeJF6zq+shHC4B7jklOBPP/TzSunh4P0DwKvAfb5c9krpe+CcwsEoZdbhEvBM9wxRAl5RShcA9wAngE3B+8tLpdLuwrhp4MNmK0pfRWkySr7NXS8+L5nZbWZWy/Vin1IaitJnUTqvwevJ71lgSSWEFKUfHG7Q2m/xqFJaGry/GXgfGPLl8mJgrXPur2JoUC8Qy3OpG+sAbGhEKT0ErAWOA6uBPWbW1wr9BOgFbgKezot0kAPYqJQA1gC/A9cA+82svzksSn1R+jNKX0SpnM/e1x3yqig92JhrZivM7FjO8bSZLSuCR/Ok16K0KMNHojQWpYko7Y7S1igN5PE3ROl4lNaZ2UVmNpPBU01orvZvZPCeKFXbBR+lEKVtUapFaSZKg9njqpl9aWYTrmXCImA7sCWb9lK/jj9TrwkrgA1AH3AQuKsSwkzbrLfxpgpsBtYDxf/R3xm2ExirhNCuHHZXTsmRwiat+S/zSt06eysVA/4pmGr/G3qm6ik28v29FKgCg8BS6pvS0KNRGgZ+Bb4FpsxsOkfUlMuwDcBWYOUZOHYM2AU8WQmhBifDv70O7PjX7KZ+4G7g3FM8zd6uBIaBy4AqxnIcZwFLCovPAhE4Sj38b4BDwEeVEFKD9S94Khjn486v3QAAAABJRU5ErkJggg==);
background-position: 9px 4px;
}
.OT_subscriber .OT_mute {
background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABcAAAATCAYAAAB7u5a2AAABx0lEQVQ4jaWUv48NURiGn3ONmCs32ZBd28ht1gqyZAkF21ylQkEiSp2ehpDlD1BoFGqqVdJohYKI7MaPxMoVNghCWMF+7ybLUewnOXfcMWO9yeQ857zne8+XmZOBGjJpr0kvTIomvTZpS526UCO4DUwD64FjwCFgqZnnR+oc8LfgzKQ73vGsr42ZtGjSQFV9o8KfBCacZwCaef4YmAf2rzjcpN3A2WSpm/AssKcqPDNpDBjs410CViXzTwk/A7b1C4wxDgOngAsZcAXY2buDfp/6S4F3lDS8DjgBzDWAjX/Y/e/QgYS/AhsKHa+OMQ6GEJ4Cj4BOAxgq6aCowyZtdf4OtAr+FHDO+R4wWnVbihr3cQnICt4boO38GWj9a/icjwOACt4m4K3zEPA+AxaAtTWCnwN3lzHkEL8V/OPAGud9wK2GF9XR1Wae/1zG2AI+pGYI4VUIoRtjHAc2A9cz4LRPevYCZ+i9/4sJt4GXJU10gaPAzdI2TTro/5Tfz8XEe2LSZGmxq/SDNvP8BnA5WRrx4BwYBe6vONx1EnjovGvBLAAd4Adwuyq8UiaNmDTvr+a8SQ9MuvbfwckBHZPe+QEfTdpep+4XZmPBHiHgz74AAAAASUVORK5CYII=);
background-position: 8px 7px;
}
.OT_subscriber .OT_mute.OT_active {
background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAUCAYAAACXtf2DAAACtklEQVQ4jZ2VSYiURxTHf+/T9Nc9iRrBuYySmIsXUU9iFMEFERRBvAjJLUQi5ioiHvSScfTmgqC4XAT1ZIgLuJHkICaaQAgKI2hAUBT30bjUq7bbv4eukXK029F3+eqtv/fqK6qQdEnSNUmT6CDB/bvgfjO4N9zj2RD8007xg1IABkwEzkma0qb4PGAPMBZYLtSD8eNwAEjqTlNI0gNJM4YU7w7ut4O7gvuhZFsR3C8NC5BBLiTIY0mzM8AvqbiC++pk+zLpE95XuwAws3vAQuBPYDRwWtL84P4tsDSLv5oaug4EYOawAMF9jMdoLxqNZcDvQA04UVYqL4G/svj7AF21mhJscrvCksYBFO7xc2AAGGg2mrdjvf4rcAyomNn+slLZmUEGBgsYdh945xZJmgvckDSrEJpK6ySBgV6q12O8ABwGPjGzfWWlsjdN9rpjoSfA+DYDXARGAksK4Is3XC1Ub4z1f4CDQGFmu6tleQSYk0U+p7WVeefLJc00s4fAeWB6Qeunvj0m2ugx9gO7kmlrtSxvBfcy6fXUZS6rgG/S+jLQUwCVNmMC9HqM14EtSe+rluWazN8YEv8IqKZ1E1qnaIDO0ucx3gX6kv6TpM3AM+D/IbGjgP60/gq4WQA33gMA2OQxPgHWJX1ttSwL4FAeZGYLgB2SasBs4A8L7qOBf9M0uXQB3a+TMYSmVctyDrA9mfcBK82smSdKWgCcAaa1bTm4fxbc/8uuCQX3RanAD5Ka6Wo5IGnE0HxJPZ03pQX5Org3MsD3AO5xXLPZXJ9BjkrqdFg6QjZkgG3Jtsw93pG0VFI9QU5K6voYQBHcTydAfwheBI9HgvvPAJIWS3qeIL9JGvUxkO7gfi1BrqTvwkG/pPmSnibIqTzXPgAyEVgBjAEu1qrVPbk/PVTHgb/NbPGg/RVIzOQqzSTBaQAAAABJRU5ErkJggg==);
background-position: 7px 7px;
}
/**
* Styles for display modes
*
* Note: It's important that these completely control the display and opacity
* attributes, no other selectors should atempt to change them.
*/
/* Default display mode transitions for various chrome elements */
.OT_publisher .OT_edge-bar-item,
.OT_subscriber .OT_edge-bar-item {
-webkit-transition-property: top, bottom, opacity;
transition-property: top, bottom, opacity;
-webkit-transition-duration: 0.5s;
transition-duration: 0.5s;
-webkit-transition-timing-function: ease-in;
transition-timing-function: ease-in;
}
.OT_publisher .OT_edge-bar-item.OT_mode-off,
.OT_subscriber .OT_edge-bar-item.OT_mode-off,
.OT_publisher .OT_edge-bar-item.OT_mode-auto,
.OT_subscriber .OT_edge-bar-item.OT_mode-auto,
.OT_publisher .OT_edge-bar-item.OT_mode-mini-auto,
.OT_subscriber .OT_edge-bar-item.OT_mode-mini-auto {
top: -25px;
opacity: 0;
}
.OT_publisher .OT_edge-bar-item.OT_mode-off,
.OT_subscriber .OT_edge-bar-item.OT_mode-off {
display: none;
}
.OT_mini .OT_mute.OT_mode-auto,
.OT_publisher .OT_mute.OT_mode-mini-auto,
.OT_subscriber .OT_mute.OT_mode-mini-auto {
top: 50%;
}
.OT_publisher .OT_edge-bar-item.OT_edge-bottom.OT_mode-off,
.OT_subscriber .OT_edge-bar-item.OT_edge-bottom.OT_mode-off,
.OT_publisher .OT_edge-bar-item.OT_edge-bottom.OT_mode-auto,
.OT_subscriber .OT_edge-bar-item.OT_edge-bottom.OT_mode-auto,
.OT_publisher .OT_edge-bar-item.OT_edge-bottom.OT_mode-mini-auto,
.OT_subscriber .OT_edge-bar-item.OT_edge-bottom.OT_mode-mini-auto {
top: auto;
bottom: -25px;
}
.OT_publisher .OT_edge-bar-item.OT_mode-on,
.OT_subscriber .OT_edge-bar-item.OT_mode-on,
.OT_publisher .OT_edge-bar-item.OT_mode-auto.OT_mode-on-hold,
.OT_subscriber .OT_edge-bar-item.OT_mode-auto.OT_mode-on-hold,
.OT_publisher:hover .OT_edge-bar-item.OT_mode-auto,
.OT_subscriber:hover .OT_edge-bar-item.OT_mode-auto,
.OT_publisher:hover .OT_edge-bar-item.OT_mode-mini-auto,
.OT_subscriber:hover .OT_edge-bar-item.OT_mode-mini-auto {
top: 0;
opacity: 1;
}
.OT_mini .OT_mute.OT_mode-on,
.OT_mini:hover .OT_mute.OT_mode-auto,
.OT_mute.OT_mode-mini,
.OT_root:hover .OT_mute.OT_mode-mini-auto {
top: 50%;
}
.OT_publisher .OT_edge-bar-item.OT_edge-bottom.OT_mode-on,
.OT_subscriber .OT_edge-bar-item.OT_edge-bottom.OT_mode-on,
.OT_publisher:hover .OT_edge-bar-item.OT_edge-bottom.OT_mode-auto,
.OT_subscriber:hover .OT_edge-bar-item.OT_edge-bottom.OT_mode-auto {
top: auto;
bottom: 0;
opacity: 1;
}
/* Load animation */
.OT_root .OT_video-loading {
position: absolute;
z-index: 1;
width: 100%;
height: 100%;
display: none;
background-color: rgba(0, 0, 0, 0.75);
}
.OT_root .OT_video-loading .OT_video-loading-spinner {
background: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9Ii0yMCAtMjAgMjQwIDI0MCI+PGRlZnM+PGxpbmVhckdyYWRpZW50IGlkPSJhIiB4Mj0iMCIgeTI9IjEiPjxzdG9wIG9mZnNldD0iMCIgc3RvcC1jb2xvcj0iI2ZmZiIgc3RvcC1vcGFjaXR5PSIwIi8+PHN0b3Agb2Zmc2V0PSIxIiBzdG9wLWNvbG9yPSIjZmZmIiBzdG9wLW9wYWNpdHk9IjAiLz48L2xpbmVhckdyYWRpZW50PjxsaW5lYXJHcmFkaWVudCBpZD0iYiIgeDE9IjEiIHgyPSIwIiB5Mj0iMSI+PHN0b3Agb2Zmc2V0PSIwIiBzdG9wLWNvbG9yPSIjZmZmIiBzdG9wLW9wYWNpdHk9IjAiLz48c3RvcCBvZmZzZXQ9IjEiIHN0b3AtY29sb3I9IiNmZmYiIHN0b3Atb3BhY2l0eT0iLjA4Ii8+PC9saW5lYXJHcmFkaWVudD48bGluZWFyR3JhZGllbnQgaWQ9ImMiIHgxPSIxIiB4Mj0iMCIgeTE9IjEiPjxzdG9wIG9mZnNldD0iMCIgc3RvcC1jb2xvcj0iI2ZmZiIgc3RvcC1vcGFjaXR5PSIuMDgiLz48c3RvcCBvZmZzZXQ9IjEiIHN0b3AtY29sb3I9IiNmZmYiIHN0b3Atb3BhY2l0eT0iLjE2Ii8+PC9saW5lYXJHcmFkaWVudD48bGluZWFyR3JhZGllbnQgaWQ9ImQiIHgyPSIwIiB5MT0iMSI+PHN0b3Agb2Zmc2V0PSIwIiBzdG9wLWNvbG9yPSIjZmZmIiBzdG9wLW9wYWNpdHk9Ii4xNiIvPjxzdG9wIG9mZnNldD0iMSIgc3RvcC1jb2xvcj0iI2ZmZiIgc3RvcC1vcGFjaXR5PSIuMzMiLz48L2xpbmVhckdyYWRpZW50PjxsaW5lYXJHcmFkaWVudCBpZD0iZSIgeDI9IjEiIHkxPSIxIj48c3RvcCBvZmZzZXQ9IjAiIHN0b3AtY29sb3I9IiNmZmYiIHN0b3Atb3BhY2l0eT0iLjMzIi8+PHN0b3Agb2Zmc2V0PSIxIiBzdG9wLWNvbG9yPSIjZmZmIiBzdG9wLW9wYWNpdHk9Ii42NiIvPjwvbGluZWFyR3JhZGllbnQ+PGxpbmVhckdyYWRpZW50IGlkPSJmIiB4Mj0iMSIgeTI9IjEiPjxzdG9wIG9mZnNldD0iMCIgc3RvcC1jb2xvcj0iI2ZmZiIgc3RvcC1vcGFjaXR5PSIuNjYiLz48c3RvcCBvZmZzZXQ9IjEiIHN0b3AtY29sb3I9IiNmZmYiLz48L2xpbmVhckdyYWRpZW50PjxtYXNrIGlkPSJnIj48ZyBmaWxsPSJub25lIiBzdHJva2Utd2lkdGg9IjQwIj48cGF0aCBzdHJva2U9InVybCgjYSkiIGQ9Ik04Ni42LTUwYTEwMCAxMDAgMCAwIDEgMCAxMDAiIHRyYW5zZm9ybT0idHJhbnNsYXRlKDEwMCAxMDApIi8+PHBhdGggc3Ryb2tlPSJ1cmwoI2IpIiBkPSJNODYuNiA1MEExMDAgMTAwIDAgMCAxIDAgMTAwIiB0cmFuc2Zvcm09InRyYW5zbGF0ZSgxMDAgMTAwKSIvPjxwYXRoIHN0cm9rZT0idXJsKCNjKSIgZD0iTTAgMTAwYTEwMCAxMDAgMCAwIDEtODYuNi01MCIgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoMTAwIDEwMCkiLz48cGF0aCBzdHJva2U9InVybCgjZCkiIGQ9Ik0tODYuNiA1MGExMDAgMTAwIDAgMCAxIDAtMTAwIiB0cmFuc2Zvcm09InRyYW5zbGF0ZSgxMDAgMTAwKSIvPjxwYXRoIHN0cm9rZT0idXJsKCNlKSIgZD0iTS04Ni42LTUwQTEwMCAxMDAgMCAwIDEgMC0xMDAiIHRyYW5zZm9ybT0idHJhbnNsYXRlKDEwMCAxMDApIi8+PHBhdGggc3Ryb2tlPSJ1cmwoI2YpIiBkPSJNMC0xMDBhMTAwIDEwMCAwIDAgMSA4Ni42IDUwIiB0cmFuc2Zvcm09InRyYW5zbGF0ZSgxMDAgMTAwKSIvPjwvZz48L21hc2s+PC9kZWZzPjxyZWN0IHdpZHRoPSIxMDAlIiBoZWlnaHQ9IjEwMCUiIHg9Ii0yMCIgeT0iLTIwIiBtYXNrPSJ1cmwoI2cpIiBmaWxsPSIjZmZmIi8+PC9zdmc+)
no-repeat;
position: absolute;
width: 32px;
height: 32px;
left: 50%;
top: 50%;
margin-left: -16px;
margin-top: -16px;
-webkit-animation: OT_spin 2s linear infinite;
animation: OT_spin 2s linear infinite;
}
@-webkit-keyframes OT_spin {
100% {
-webkit-transform: rotate(360deg);
}
}
@keyframes OT_spin {
100% {
-webkit-transform: rotate(360deg);
transform: rotate(360deg);
}
}
.OT_publisher.OT_loading .OT_video-loading,
.OT_subscriber.OT_loading .OT_video-loading {
display: block;
}
.OT_video-centering {
display: table;
width: 100%;
height: 100%;
}
.OT_video-container {
display: table-cell;
vertical-align: middle;
}
.OT_video-poster {
position: absolute;
z-index: 1;
width: 100%;
height: 100%;
display: none;
opacity: 0.25;
background-repeat: no-repeat;
background-image: url(data:image/svg+xml;base64,PHN2ZyB2aWV3Qm94PSIwIDAgNDcxIDQ2NCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48bGluZWFyR3JhZGllbnQgaWQ9ImEiIHgyPSIwIiB5Mj0iMSI+PHN0b3Agb2Zmc2V0PSI2Ni42NiUiIHN0b3AtY29sb3I9IiNmZmYiLz48c3RvcCBvZmZzZXQ9IjEwMCUiIHN0b3AtY29sb3I9IiNmZmYiIHN0b3Atb3BhY2l0eT0iMCIvPjwvbGluZWFyR3JhZGllbnQ+PHBhdGggZmlsbD0idXJsKCNhKSIgZD0iTTc5IDMwOGMxNC4yNS02LjUgNTQuMjUtMTkuNzUgNzEtMjkgOS0zLjI1IDI1LTIxIDI1LTIxczMuNzUtMTMgMy0yMmMtMS43NS02Ljc1LTE1LTQzLTE1LTQzLTIuNSAzLTQuNzQxIDMuMjU5LTcgMS0zLjI1LTcuNS0yMC41LTQ0LjUtMTYtNTcgMS4yNS03LjUgMTAtNiAxMC02LTExLjI1LTMzLjc1LTgtNjctOC02N3MuMDczLTcuMzQ2IDYtMTVjLTMuNDguNjM3LTkgNC05IDQgMi41NjMtMTEuNzI3IDE1LTIxIDE1LTIxIC4xNDgtLjMxMi0xLjMyMS0xLjQ1NC0xMCAxIDEuNS0yLjc4IDE2LjY3NS04LjY1NCAzMC0xMSAzLjc4Ny05LjM2MSAxMi43ODItMTcuMzk4IDIyLTIyLTIuMzY1IDMuMTMzLTMgNi0zIDZzMTUuNjQ3LTguMDg4IDQxLTZjLTE5Ljc1IDItMjQgNi0yNCA2czc0LjUtMTAuNzUgMTA0IDM3YzcuNSA5LjUgMjQuNzUgNTUuNzUgMTAgODkgMy43NS0xLjUgNC41LTQuNSA5IDEgLjI1IDE0Ljc1LTExLjUgNjMtMTkgNjItMi43NSAxLTQtMy00LTMtMTAuNzUgMjkuNS0xNCAzOC0xNCAzOC0yIDQuMjUtMy43NSAxOC41LTEgMjIgMS4yNSA0LjUgMjMgMjMgMjMgMjNsMTI3IDUzYzM3IDM1IDIzIDEzNSAyMyAxMzVMMCA0NjRzLTMtOTYuNzUgMTQtMTIwYzUuMjUtNi4yNSAyMS43NS0xOS43NSA2NS0zNnoiLz48L3N2Zz4=);
background-size: auto 76%;
}
.OT_fit-mode-cover .OT_video-element {
-o-object-fit: cover;
object-fit: cover;
}
/* Workaround for iOS freezing issue when cropping videos */
/* https://bugs.webkit.org/show_bug.cgi?id=176439 */
@media only screen and (orientation: portrait) {
.OT_subscriber.OT_ForceContain.OT_fit-mode-cover .OT_video-element {
-o-object-fit: contain !important;
object-fit: contain !important;
}
}
.OT_fit-mode-contain .OT_video-element {
-o-object-fit: contain;
object-fit: contain;
}
.OT_fit-mode-cover .OT_video-poster {
background-position: center bottom;
}
.OT_fit-mode-contain .OT_video-poster {
background-position: center;
}
.OT_audio-level-meter {
position: absolute;
width: 25%;
max-width: 224px;
min-width: 21px;
top: 0;
right: 0;
overflow: hidden;
}
.OT_audio-level-meter:before {
/* makes the height of the container equals its width */
content: '';
display: block;
padding-top: 100%;
}
.OT_audio-level-meter__bar {
position: absolute;
width: 192%; /* meter value can overflow of 8% */
height: 192%;
top: -96% /* half of the size */;
right: -96%;
border-radius: 50%;
background-color: rgba(0, 0, 0, 0.8);
}
.OT_audio-level-meter__audio-only-img {
position: absolute;
top: 22%;
right: 15%;
width: 40%;
opacity: 0.7;
background: url(data:image/svg+xml;base64,PHN2ZyB2aWV3Qm94PSIwIDAgNzkgODYiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PGcgZmlsbD0iI2ZmZiI+PHBhdGggZD0iTTkuNzU3IDQwLjkyNGMzLjczOC01LjE5MSAxMi43MTEtNC4zMDggMTIuNzExLTQuMzA4IDIuMjIzIDMuMDE0IDUuMTI2IDI0LjU4NiAzLjYyNCAyOC43MTgtMS40MDEgMS4zMDEtMTEuNjExIDEuNjI5LTEzLjM4LTEuNDM2LTEuMjI2LTguODA0LTIuOTU1LTIyLjk3NS0yLjk1NS0yMi45NzV6bTU4Ljc4NSAwYy0zLjczNy01LjE5MS0xMi43MTEtNC4zMDgtMTIuNzExLTQuMzA4LTIuMjIzIDMuMDE0LTUuMTI2IDI0LjU4Ni0zLjYyNCAyOC43MTggMS40MDEgMS4zMDEgMTEuNjExIDEuNjI5IDEzLjM4LTEuNDM2IDEuMjI2LTguODA0IDIuOTU1LTIyLjk3NSAyLjk1NS0yMi45NzV6Ii8+PHBhdGggZD0iTTY4LjY0NyA1OC42Yy43MjktNC43NTMgMi4zOC05LjU2MSAyLjM4LTE0LjgwNCAwLTIxLjQxMi0xNC4xMTUtMzguNzctMzEuNTI4LTM4Ljc3LTE3LjQxMiAwLTMxLjUyNyAxNy4zNTgtMzEuNTI3IDM4Ljc3IDAgNC41NDEuNTE1IDguOTM2IDEuODAyIDEyLjk1IDEuNjk4IDUuMjk1LTUuNTQyIDYuOTkxLTYuNjE2IDIuMDczQzIuNDEgNTUuMzk0IDAgNTEuNzg3IDAgNDguMTAzIDAgMjEuNTM2IDE3LjY4NSAwIDM5LjUgMCA2MS4zMTYgMCA3OSAyMS41MzYgNzkgNDguMTAzYzAgLjcxOC0yLjg5OSA5LjY5My0zLjI5MiAxMS40MDgtLjc1NCAzLjI5My03Ljc1MSAzLjU4OS03LjA2MS0uOTEyeiIvPjxwYXRoIGQ9Ik01LjA4NCA1MS4zODVjLS44MDQtMy43ODIuNTY5LTcuMzM1IDMuMTM0LTcuOTIxIDIuNjM2LS42MDMgNS40ODUgMi4xNSA2LjI4OSA2LjEzMi43OTcgMy45NDgtLjc1MiA3LjQ1Ny0zLjM4OCA3Ljg1OS0yLjU2Ni4zOTEtNS4yMzctMi4zMTgtNi4wMzQtNi4wN3ptNjguODM0IDBjLjgwNC0zLjc4Mi0uNTY4LTcuMzM1LTMuMTMzLTcuOTIxLTIuNjM2LS42MDMtNS40ODUgMi4xNS02LjI4OSA2LjEzMi0uNzk3IDMuOTQ4Ljc1MiA3LjQ1NyAzLjM4OSA3Ljg1OSAyLjU2NS4zOTEgNS4yMzctMi4zMTggNi4wMzQtNi4wN3ptLTIuMDM4IDguMjg4Yy0uOTI2IDE5LjY1OS0xNS4xMTIgMjQuNzU5LTI1Ljg1OSAyMC40NzUtNS40MDUtLjYwNi0zLjAzNCAxLjI2Mi0zLjAzNCAxLjI2MiAxMy42NjEgMy41NjIgMjYuMTY4IDMuNDk3IDMxLjI3My0yMC41NDktLjU4NS00LjUxMS0yLjM3OS0xLjE4Ny0yLjM3OS0xLjE4N3oiLz48cGF0aCBkPSJNNDEuNjYyIDc4LjQyMmw3LjU1My41NWMxLjE5Mi4xMDcgMi4xMiAxLjE1MyAyLjA3MiAyLjMzNWwtLjEwOSAyLjczOGMtLjA0NyAxLjE4Mi0xLjA1MSAyLjA1NC0yLjI0MyAxLjk0NmwtNy41NTMtLjU1Yy0xLjE5MS0uMTA3LTIuMTE5LTEuMTUzLTIuMDcyLTIuMzM1bC4xMDktMi43MzdjLjA0Ny0xLjE4MiAxLjA1Mi0yLjA1NCAyLjI0My0xLjk0N3oiLz48L2c+PC9zdmc+)
no-repeat center;
}
.OT_audio-level-meter__audio-only-img:before {
/* makes the height of the container equals its width */
content: '';
display: block;
padding-top: 100%;
}
.OT_audio-level-meter__value {
position: absolute;
border-radius: 50%;
background-image: radial-gradient(circle, rgba(151, 206, 0, 1) 0%, rgba(151, 206, 0, 0) 100%);
}
.OT_audio-level-meter.OT_mode-off {
display: none;
}
.OT_audio-level-meter.OT_mode-on,
.OT_audio-only .OT_audio-level-meter.OT_mode-auto {
display: block;
}
.OT_audio-only.OT_publisher .OT_video-element,
.OT_audio-only.OT_subscriber .OT_video-element {
display: none;
}
.OT_video-disabled-indicator {
opacity: 1;
border: none;
display: none;
position: absolute;
background-color: transparent;
background-repeat: no-repeat;
background-position: bottom right;
pointer-events: none;
top: 0;
left: 0;
bottom: 3px;
right: 3px;
}
.OT_video-disabled {
background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAFIAAAAoCAYAAABtla08AAAINUlEQVR42u2aaUxUVxTHcRBmAAEBRVTK4sKwDIsg+wCK7CqIw1CN1YobbbS2qYlJ06Qx1UpdqMbYWq2pSzWmH6ytNbXWJY1Lq7VuqBERtW64V0XFLYae0/xvcp3MMAMzDz6IyT/ge2ce5/7ucpY3Ts3NzZ1ygF57AJ0gO0G2jyZPmdbFyclJSAV1EeoEaUUSLGdSV5KLLFxzFmA7QVqGqDqjixhWkxCVeyRVl38wM6bwj6yYItYK47BAuu9B0gCqs6Ng2r494KQtkj/Dz2jHraw6qw2fdSE4rNmcCPCvZONP8iF1I6kdBdMaQJWZLeJqRWa2kPJAxXY+GxE+zxLI03GRh8lGSwoi9WCY8FWlCEh+8JOnT7MfPGjMuXX7Tt61hoaCi/9cKmKdv3BxeEtim/UbNpnbQiqF4MmT7kqrbr4lkMcTo46TTSpJB5g+8NHuVWnWuaampvhmO/7duHmrGluoO4C6OsJZGRrkDIld43ZqUOTnlkDSmXmabAoBU0vqBf+6KgFSxQ9++uzZ8rZApM81TJ8xM5me0Z/UF7PuBmdVdkGEb5gYDeQmyZNW3SJLIP9Kj64lGyMpmxRN6sOfIbkoAhKOdnv2/PmB1kB88eLFo+olyyrps3rSINIAzLonnqlqK8R9w+L86vtrt5L2nhug3Vc3ULu/Liz8AOuXESlZZONH6kmr7gtLIA9lRNeRzVukAvj3BslLnJNKgfScO69K+/Lly0ZbQW7e8tNK+pwBjqaSIjDrXgJkW1ciAZvbQjQ+RDahpBBKd5ZZsqN758hmImk4KQHnpDd8UwSkCyJarx07d4+3BeKJmlMHyX4qaRxpBCmNFE4KENvHDpAutVERn1kCVBMfeRRgYvZnx62wZPdnZkw92VQA5GClQXYRBze2S+iJmpPVVoJLA9l9QKokjcWKTCT1R5rhLg70NuSsziT16diIKkuAjibrTpJNDkn/e17CahtAjlAWJAYkb29Sb1LE9Rs391kILk8mVkyuIpuZcLKUlEmKkra1WuSTNuesEPzwoEploSVAh9Oiz+BIyd9dOHhtx4OEpFpVg6gbNK3yXX1j48N6U5Dz5i/gc/FDrMY3sTLiSMEkXxGxzUEUAGnbxlPaksMlHUXWAlHS8URCPseSohZbCSLjSSU7ixLXdzhIWVKq4Y7t2a/2bN0qGeKly1fYsVmk6RgIDz4J0bonyUOcjeYqm/8hRoYbWkigV2NH9CHAS60EkUkkw47hSRs6FqT1LR5AVcsrueXlK1d5AO+RpmBrZZEiefByytPCanRGNLZY0uF52gNDYr9sCRB8MHY0SJu2OJWKS2WQV65e4y31DmkCImEi0hBfufRime0RIhpbKen0/Ny9OYNW2ghyYytABjNIaxNuKttAWk6HPLn0k0FevdZwFinPWFIuKZbUV16NVko6jbWSDoPO3pOf8K0jQWLSQ0S9bdpkYck+m7vfWpAiHfKgBsZiGSSt0FqcTeU8WETqAHE2CgcAVd3Gkm4MD3xXYeI6B4NMItvKbcUpQ9gP+KMWnSsW+TaYJtoo+avBWLoKoK0CCSDud+7eXWQGZAXqV3YoQjQCfixJ8+fzj9ta3JHhlUeJ8wJOY2ws6eRKpPS3oqTvHAESEz9ya0naXL5WH6pt3FqSOhTHkTcKEXc6k1POh4Q9YJu/03TT4a8PoGMFI4i2EqSbOZAYaBkpCyD92RkG6KCSbjI/H0HEISBnlOZPFdcEzI2GTO4KBZICGKyAKLTEmJOB2txf5MbgohBINCl4FTqmpJMB2W+HiRn1Q2l6lXyPmiEP6VVE2TfGoaMYrHyPdtAnyI0jEOn9RLWmNEhvBBE7SjpFQZaShtLK+1S+T12lRwxUvrZlVPp8jE1PikeO7C/nyEqBDCB1t7+kUx4kKUWclea0yZC5BIGpiJSNSD9QgFR0RQKkL6KxHSWdsiARHJNYewoGrzG1/bk4dTPSunL2EyDjcbb7MQ+lQfZmkKiN7SjpFAM5CWAyGcwyY84YsZ1lUcbRNNtQMAdtQWGvQ0DyVjzYAKQfQFodeAeC1C8vzymXIZqD+ZEh/2OyLSalS/3VbnJZ+VqDXGjMrTCFuK4s66vVZUNfqaDolcbjOcb899sLpEE+I20GifywXe2QR3KElu99PzqjGufhREqB1pjCnG3IL3fY1v733r2FMsiGhutn0LAoJWWIGbPxjKwgjUbF0m52mPhigrpdXOecEq9pR6MkHbu2LOtrcZ9y3d0ODTb15y9MePz48aF79+8fvXnr9sljx2u2I7KNxDuaMPGVECoRs7mC4eT7SIruFNfNHK15MKuM2evwNq+4qjxvGnd5CHwNNynawW4cOlUZdG8b55IIJHmkItwrZHH6QxB3OSL9kTtAGpIvZiQB3Z4SKBfXQtEE9sashWAW87Bt3sYZNR6zn4uzJwWDKUKXfaKCdqUoBpLxSjYe9nqGiwWRBGipuGZ3Qm76itYLbbJI/PEhUApfw73uOIy9xfse3M9F9BuFJHcYrseSouGkHtCVtkuGTTikI8XgZzhg9SeF4VqcvSWiaSvNHQ8JwkNjIfEHemCmNLD1RaEfLs18mlgNuN6PFALHo7CyU5W2g00gFAQF4ozvibH04muwDbWraSFAyt/AAMzewgGR8uCeWn77xzBxPxgzPRCDDMZ14bQ/3jqGKGoHf2Hjgx3kw5LbaJDYWb52t9FMgw4AuWNWukNeuOYqOsmQi2jgws4PA/DD/z0B2x0/veCs4naw0cgybezid7X9jV3rX2RSs0wfLkll4pBGcgifg+NYxe1kJ2ycTaRq66uG/wBOl0vjcw70xwAAAABJRU5ErkJggg==);
background-size: 33px auto;
}
.OT_video-disabled-warning {
background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAFIAAAAoCAYAAABtla08AAAGMElEQVR4Ae2aA7D0yBaAc7oH12vbRmlLaxYWb23btm3btm2899a2bWuYtPZ01cmtU9lJrib315yqr9I3Oem/5/s7acwEnehEJzoxCcX2O+wEeIgRBDDaGjAZOgQ6ihRpLklHZDJIXK1WWymMIhGGkVBKCWMM+Iv/f/b5t7faYtM/sGgIS7j8RNLjceUVl41GvGN1BFiHy9sgtRWaYbhvuVQ6o1VOvV5/tLe3dyssKoZuh8xClkDEi2MMS6ZjR0cScxdK/+HgnJsmLccYOx0e/PUGUqfTJDEHkV5go9lcMQoj4R8RpSIRRUr4a9baTJFCCNfqESKJ7RYJibK0xoi05EhFRTxMi1Rit6xHAuLaKRLwEVi6q1x+EhlVpd3d3Wfh4VQkQhRhxthYLg7SRGqdLlIp7UVOHf+JhEhEMscUolVje3p63saeeOFoKsT7fjj++BNuw2I/0ouUENmGaQcQEilQvUU6xuWC0kqmVWCt8df6kG7WLoFA20VSCOyNh0RKPT+SyrTWtQsvuvTYCy84z3+oAdbgAiLGIvHjTz6bFuu/B3lKKfVkFKknwih6EnnipZdfXQZzepAupXSGSCfwUGZtkrx3t/0dSQGnnXbmdocdetArQoj+4VR23wMP3bj/vnv9Sv/rBmkish09ca655thHSrlWq4TFF1vkNDxsgjiUnPqZnHPABIq47jx7pPMcecShfz7x1DO7D6eit99576X1113nVd8rqLGAuDaNitJonTGIqHgQGQjDsJglMrUH5iDSEQbRa6y2yrNvv/PuWVmV/PTzLz8steTit1B9FtGJeZrJksmWdBzBMcami4xUkaY1A1Qe94WIaPGBApJhaERrLrXkElf8+NPPz6YMLs1DDjn0Wn9PnI/UiQadM4jNEkhzVsEGE8nIHESM1j5/KqRX+/IEiOQ/yifNBlEkpnb00cccesbpp13T3983H88/48xzrrvm6it/8U5JXgX5G6nSvSq1R5LATR7aYGkwMG1RSwkWABH+4jUb3vT/uJ1Z0xpjraTBRltrxUQhksIRmgTJyy69+Pv99tv3qYX6FxgU+fU33352xGEHf5wisU7nNWJpZRMkAjZ6aIN1mwV7h29Jo2wCHlveu/GV169z65E+T6koexCh6c+EEiky3lnxQKFjUeVyOeI5AOBzIiayRhJryd7YYnkIHgvB0qk9Tdql6N3XH4bRUIOIIIKJSiRb0hkSEpZKRd1CpEq8GxtIyCVmDSgFl94GacTgaJw1rUlYhYng0c4ewaUsmKRIJjpiqMSOCh9QeI+UYECmtQIsxEu6OorEcv6Rl0gu0woh8MhFkmSCTXVI4pC704WCFRJvSRNJSzrMMEZO2iKZTCHAZYnmvXCny7ed5vfZK3viHSBdIFCKEFj2+nt+73nw8m2uedcLJlktA++VNMEPaR45aYukcKnnCfY3/DFbZS8t7eHxNgsPM0N1hXhJJwwM1QbpoQFlog2R13a/zBxEYHAQEUYUM6qiVwEyBYoM6JFNF2kFLelI5KQf+fVI4dJFCguDS7oAyx2R6SFQJKRedSDj/cMg/RXQ6ZE05GSIDAaXdCi1I3L021SQWNJ1RLY5OiIdL4/yvuw8ADfWPFrSciaMyH8tEQPwf1uGG54g5+KlJGTmsrxsQdl5PKidnPFe2QS///7Hu+VS6WX/HYnf0sevGL7lXydwod2/9DykZq0s5yff0sgSWCigNOH7TPHL7ufj+/TH8P/+qYpL4HkBDiRYpEXeM8/89/9zzjn7EtY64dfd1nqccM7Bs8+9MKy8555/8TnKS+5MufH6EZVASkgPzf+mJXroet17JirU0ALST3nT0y5ONyLpeo1y64ih+vuQfsoTOeRFSJXa+SvyB90TUmdw49EjLaKpMQ0mzEeTzkWsd/oI6fzfiKM8gWg6X6OjpXstu5ZHnmIb0GFiu29MIUfUewkmVrEN3RqVQ/bY8FzNcquMBv/pCNUZ5pHHem01KdN/I/DG66/lLhKSvTO5M84kav5C5z2ZfyAivi9i9VGd45RH7UWJbjwGG/7NYsRECt7jiOToHedKAui8SW4CsxyRc54mKH/8f7ELhCCACyNcIl/wI+FaAJyc8yzRtinQPzWzuFZrFHq/AAAAAElFTkSuQmCC);
background-size: 33px auto;
}
.OT_video-disabled-indicator.OT_active {
display: block;
}
.OT_audio-blocked-indicator {
opacity: 1;
border: none;
display: none;
position: absolute;
background-color: transparent;
background-repeat: no-repeat;
background-position: center;
pointer-events: none;
top: 0;
left: 0;
bottom: 0;
right: 0;
}
.OT_audio-blocked {
background-image: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiB3aWR0aD0iMTUwIiBoZWlnaHQ9IjkwIj48ZGVmcz48cGF0aCBkPSJNNjcgMTJMNi40NDggNzIuNTUyIDAgMzFWMThMMjYgMGw0MSAxMnptMyA3bDYgNDctMjkgMTgtMzUuNTAyLTYuNDk4TDcwIDE5eiIgaWQ9ImEiLz48L2RlZnM+PHJlY3Qgd2lkdGg9IjE1MCIgaGVpZ2h0PSI5MCIgcng9IjM1IiByeT0iNDUiIG9wYWNpdHk9Ii41Ii8+PGcgZmlsbD0ibm9uZSIgZmlsbC1ydWxlPSJldmVub2RkIj48ZyB0cmFuc2Zvcm09InRyYW5zbGF0ZSgzNikiPjxtYXNrIGlkPSJiIiBmaWxsPSIjZmZmIj48dXNlIHhsaW5rOmhyZWY9IiNhIi8+PC9tYXNrPjxwYXRoIGQ9Ik0zOS4yNDkgNTEuMzEyYy42OTcgMTAuMzcgMi43ODUgMTcuODk3IDUuMjUxIDE3Ljg5NyAzLjAzOCAwIDUuNS0xMS40MTcgNS41LTI1LjVzLTIuNDYyLTI1LjUtNS41LTI1LjVjLTIuNTEgMC00LjYyOCA3Ljc5Ny01LjI4NyAxOC40NTNBOC45ODkgOC45ODkgMCAwIDEgNDMgNDRhOC45ODggOC45ODggMCAwIDEtMy43NTEgNy4zMTJ6TTIwLjk4NSAzMi4yMjRsMTUuNzQ2LTE2Ljg3N2E3LjM4NSA3LjM4NSAwIDAgMSAxMC4zNzQtLjQyQzUxLjcwMiAxOS4xMTQgNTQgMjkuMjA4IDU0IDQ1LjIwOGMwIDE0LjUyNy0yLjM0MyAyMy44OC03LjAzIDI4LjA1OGE3LjI4IDcuMjggMCAwIDEtMTAuMTY4LS40NjhMMjAuNDA1IDU1LjIyNEgxMmE1IDUgMCAwIDEtNS01di0xM2E1IDUgMCAwIDEgNS01aDguOTg1eiIgZmlsbD0iI0ZGRiIgbWFzaz0idXJsKCNiKSIvPjwvZz48cGF0aCBkPSJNMTA2LjUgMTMuNUw0NC45OTggNzUuMDAyIiBzdHJva2U9IiNGRkYiIHN0cm9rZS13aWR0aD0iMiIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIi8+PC9nPjwvc3ZnPg==);
background-size: 90px auto;
}
.OT_container-audio-blocked {
cursor: pointer;
}
.OT_container-audio-blocked.OT_mini .OT_edge-bar-item {
display: none;
}
.OT_container-audio-blocked .OT_mute {
display: none;
}
.OT_audio-blocked-indicator.OT_active {
display: block;
}
.OT_video-unsupported {
opacity: 1;
border: none;
display: none;
position: absolute;
background-color: transparent;
background-repeat: no-repeat;
background-position: center;
background-image: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iOTciIGhlaWdodD0iOTAiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiPjxkZWZzPjxwYXRoIGQ9Ik03MCAxMkw5LjQ0OCA3Mi41NTIgMCA2MmwzLTQ0TDI5IDBsNDEgMTJ6bTggMmwxIDUyLTI5IDE4LTM1LjUwMi02LjQ5OEw3OCAxNHoiIGlkPSJhIi8+PC9kZWZzPjxnIGZpbGw9Im5vbmUiIGZpbGwtcnVsZT0iZXZlbm9kZCI+PGcgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoOCAzKSI+PG1hc2sgaWQ9ImIiIGZpbGw9IiNmZmYiPjx1c2UgeGxpbms6aHJlZj0iI2EiLz48L21hc2s+PHBhdGggZD0iTTkuMTEgMjAuOTY4SDQ4LjFhNSA1IDAgMCAxIDUgNVY1OC4xOGE1IDUgMCAwIDEtNSA1SDkuMTFhNSA1IDAgMCAxLTUtNVYyNS45N2E1IDUgMCAwIDEgNS01em00Ny4wOCAxMy4zOTRjMC0uMzQ1IDUuNDcyLTMuMTU5IDE2LjQxNS04LjQ0M2EzIDMgMCAwIDEgNC4zMDQgMi43MDJ2MjYuODM1YTMgMyAwIDAgMS00LjMwNSAyLjcwMWMtMTAuOTQyLTUuMjg2LTE2LjQxMy04LjEtMTYuNDEzLTguNDQ2VjM0LjM2MnoiIGZpbGw9IiNGRkYiIG1hc2s9InVybCgjYikiLz48L2c+PHBhdGggZD0iTTgxLjUgMTYuNUwxOS45OTggNzguMDAyIiBzdHJva2U9IiNGRkYiIHN0cm9rZS13aWR0aD0iMiIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIi8+PC9nPjwvc3ZnPg==);
background-size: 58px auto;
pointer-events: none;
top: 0;
left: 0;
bottom: 0;
right: 0;
margin-top: -30px;
}
.OT_video-unsupported-bar {
display: none;
position: absolute;
width: 192%; /* copy the size of the audio meter bar for symmetry */
height: 192%;
top: -96% /* half of the size */;
left: -96%;
border-radius: 50%;
background-color: rgba(0, 0, 0, 0.8);
}
.OT_video-unsupported-img {
display: none;
position: absolute;
top: 11%;
left: 15%;
width: 70%;
opacity: 0.7;
background-image: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iOTciIGhlaWdodD0iOTAiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiPjxkZWZzPjxwYXRoIGQ9Ik03MCAxMkw5LjQ0OCA3Mi41NTIgMCA2MmwzLTQ0TDI5IDBsNDEgMTJ6bTggMmwxIDUyLTI5IDE4LTM1LjUwMi02LjQ5OEw3OCAxNHoiIGlkPSJhIi8+PC9kZWZzPjxnIGZpbGw9Im5vbmUiIGZpbGwtcnVsZT0iZXZlbm9kZCI+PGcgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoOCAzKSI+PG1hc2sgaWQ9ImIiIGZpbGw9IiNmZmYiPjx1c2UgeGxpbms6aHJlZj0iI2EiLz48L21hc2s+PHBhdGggZD0iTTkuMTEgMjAuOTY4SDQ4LjFhNSA1IDAgMCAxIDUgNVY1OC4xOGE1IDUgMCAwIDEtNSA1SDkuMTFhNSA1IDAgMCAxLTUtNVYyNS45N2E1IDUgMCAwIDEgNS01em00Ny4wOCAxMy4zOTRjMC0uMzQ1IDUuNDcyLTMuMTU5IDE2LjQxNS04LjQ0M2EzIDMgMCAwIDEgNC4zMDQgMi43MDJ2MjYuODM1YTMgMyAwIDAgMS00LjMwNSAyLjcwMWMtMTAuOTQyLTUuMjg2LTE2LjQxMy04LjEtMTYuNDEzLTguNDQ2VjM0LjM2MnoiIGZpbGw9IiNGRkYiIG1hc2s9InVybCgjYikiLz48L2c+PHBhdGggZD0iTTgxLjUgMTYuNUwxOS45OTggNzguMDAyIiBzdHJva2U9IiNGRkYiIHN0cm9rZS13aWR0aD0iMiIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIi8+PC9nPjwvc3ZnPg==);
background-repeat: no-repeat;
background-position: center;
background-size: 100% auto;
}
.OT_video-unsupported-img:before {
/* makes the height of the container 93% of its width (90/97 px) */
content: '';
display: block;
padding-top: 93%;
}
.OT_video-unsupported-text {
display: -webkit-box;
display: -ms-flexbox;
display: flex;
-webkit-box-pack: center;
-ms-flex-pack: center;
justify-content: center;
-webkit-box-align: center;
-ms-flex-align: center;
align-items: center;
text-align: center;
height: 100%;
margin-top: 40px;
}

View File

@ -167,10 +167,10 @@ export class RecordingActivityComponent implements OnInit {
*/ */
deleteRecording(id: string) { deleteRecording(id: string) {
const succsessCallback = () => { const succsessCallback = async () => {
this.onDeleteRecordingClicked.emit(id); this.onDeleteRecordingClicked.emit(id);
// Sending signal to all participants with the aim of updating their recordings list // Sending signal to all participants with the aim of updating their recordings list
this.openviduService.sendSignal(Signal.RECORDING_DELETED, this.openviduService.getRemoteConnections()); await this.openviduService.sendSignal(Signal.RECORDING_DELETED, this.openviduService.getRemoteConnections());
}; };
this.actionService.openDeleteRecordingDialog(succsessCallback.bind(this)); this.actionService.openDeleteRecordingDialog(succsessCallback.bind(this));
} }

View File

@ -194,17 +194,19 @@ export class SessionComponent implements OnInit, OnDestroy {
this.cd.markForCheck(); this.cd.markForCheck();
} }
ngOnDestroy() { async ngOnDestroy() {
// Reconnecting session is received in Firefox // Reconnecting session is received in Firefox
// To avoid 'Connection lost' message uses session.off() // To avoid 'Connection lost' message uses session.off()
if (!this.usedInPrejoinPage) {
this.session?.off('reconnecting'); this.session?.off('reconnecting');
this.participantService.clear(); await this.participantService.clear();
this.session = null; this.session = null;
this.sessionScreen = null; this.sessionScreen = null;
if (this.menuSubscription) this.menuSubscription.unsubscribe(); if (this.menuSubscription) this.menuSubscription.unsubscribe();
if (this.layoutWidthSubscription) this.layoutWidthSubscription.unsubscribe(); if (this.layoutWidthSubscription) this.layoutWidthSubscription.unsubscribe();
if (this.captionLanguageSubscription) this.captionLanguageSubscription.unsubscribe(); if (this.captionLanguageSubscription) this.captionLanguageSubscription.unsubscribe();
} }
}
leaveSession() { leaveSession() {
this.log.d('Leaving session...'); this.log.d('Leaving session...');
@ -251,20 +253,29 @@ export class SessionComponent implements OnInit, OnDestroy {
private async connectToSession(): Promise<void> { private async connectToSession(): Promise<void> {
try { try {
const webcamToken = this.openviduService.getWebcamToken(); const participant = this.participantService.getLocalParticipant();
const screenToken = this.openviduService.getScreenToken(); const nickname = participant.getNickname();
const participantId = participant.id;
const screenPublisher = this.participantService.getMyScreenPublisher();
const cameraPublisher = this.participantService.getMyCameraPublisher();
if (this.participantService.haveICameraAndScreenActive()) {
await this.openviduService.connectSession(this.openviduService.getWebcamSession(), webcamToken); if (participant.hasCameraAndScreenActives()) {
await this.openviduService.connectSession(this.openviduService.getScreenSession(), screenToken);
await this.openviduService.publish(this.participantService.getMyCameraPublisher()); const webcamSessionId = await this.openviduService.connectWebcamSession(participantId, nickname);
await this.openviduService.publish(this.participantService.getMyScreenPublisher()); if (webcamSessionId) this.participantService.setMyCameraConnectionId(webcamSessionId);
} else if (this.participantService.isOnlyMyScreenActive()) {
await this.openviduService.connectSession(this.openviduService.getScreenSession(), screenToken); const screenSessionId = await this.openviduService.connectScreenSession(participantId, nickname);
await this.openviduService.publish(this.participantService.getMyScreenPublisher()); if (screenSessionId) this.participantService.setMyScreenConnectionId(screenSessionId);
await this.openviduService.publishCamera(cameraPublisher);
await this.openviduService.publishScreen(screenPublisher);
} else if (participant.hasOnlyScreenActive()) {
await this.openviduService.connectScreenSession(participantId, nickname);
await this.openviduService.publishScreen(screenPublisher);
} else { } else {
await this.openviduService.connectSession(this.openviduService.getWebcamSession(), webcamToken); await this.openviduService.connectWebcamSession(participantId, nickname);
await this.openviduService.publish(this.participantService.getMyCameraPublisher()); await this.openviduService.publishCamera(cameraPublisher);
} }
} catch (error) { } catch (error) {
// this._error.emit({ error: error.error, messgae: error.message, code: error.code, status: error.status }); // this._error.emit({ error: error.error, messgae: error.message, code: error.code, status: error.status });
@ -287,21 +298,21 @@ export class SessionComponent implements OnInit, OnDestroy {
} }
private subscribeToConnectionCreatedAndDestroyed() { private subscribeToConnectionCreatedAndDestroyed() {
this.session.on('connectionCreated', (event: ConnectionEvent) => { this.session.on('connectionCreated', async (event: ConnectionEvent) => {
const connectionId = event.connection?.connectionId; const connectionId = event.connection?.connectionId;
const nickname: string = this.participantService.getNicknameFromConnectionData(event.connection.data); const connectionNickname: string = this.participantService.getNicknameFromConnectionData(event.connection.data);
const isRemoteConnection: boolean = !this.openviduService.isMyOwnConnection(connectionId); const isRemoteConnection: boolean = !this.openviduService.isMyOwnConnection(connectionId);
const isCameraConnection: boolean = !nickname?.includes(`_${VideoType.SCREEN}`); const isCameraConnection: boolean = !connectionNickname?.includes(`_${VideoType.SCREEN}`);
const nickname = this.participantService.getMyNickname();
const data = event.connection?.data; const data = event.connection?.data;
if (isRemoteConnection && isCameraConnection) { if (isRemoteConnection && isCameraConnection) {
// Adding participant when connection is created and it's not screen // Adding participant when connection is created and it's not screen
this.participantService.addRemoteConnection(connectionId, data, null); this.participantService.addRemoteConnection(connectionId, data, null);
//Sending nicnkanme signal to new participants //Sending nicnkanme signal to new connection
if (this.openviduService.needSendNicknameSignal()) { if (this.openviduService.myNicknameHasBeenChanged()) {
const data = { clientData: this.participantService.getMyNickname() }; await this.openviduService.sendSignal(Signal.NICKNAME_CHANGED, [event.connection], { clientData: nickname });
this.openviduService.sendSignal(Signal.NICKNAME_CHANGED, [event.connection], data);
} }
} }
}); });

View File

@ -3,7 +3,6 @@ import { PublisherProperties } from 'openvidu-browser';
import { Subscription } from 'rxjs'; import { Subscription } from 'rxjs';
import { CustomDevice } from '../../../models/device.model'; import { CustomDevice } from '../../../models/device.model';
import { ParticipantAbstractModel } from '../../../models/participant.model'; import { ParticipantAbstractModel } from '../../../models/participant.model';
import { VideoType } from '../../../models/video-type.model';
import { DeviceService } from '../../../services/device/device.service'; import { DeviceService } from '../../../services/device/device.service';
import { OpenViduService } from '../../../services/openvidu/openvidu.service'; import { OpenViduService } from '../../../services/openvidu/openvidu.service';
import { ParticipantService } from '../../../services/participant/participant.service'; import { ParticipantService } from '../../../services/participant/participant.service';
@ -58,7 +57,7 @@ export class AudioDevicesComponent implements OnInit, OnDestroy {
toggleMic() { toggleMic() {
const publish = this.isAudioMuted; const publish = this.isAudioMuted;
this.openviduService.publishAudio(publish); this.participantService.publishAudio(publish);
this.onAudioMutedClicked.emit(publish); this.onAudioMutedClicked.emit(publish);
} }
@ -66,7 +65,8 @@ export class AudioDevicesComponent implements OnInit, OnDestroy {
const audioSource = event?.value; const audioSource = event?.value;
if (this.deviceSrv.needUpdateAudioTrack(audioSource)) { if (this.deviceSrv.needUpdateAudioTrack(audioSource)) {
const pp: PublisherProperties = { audioSource, videoSource: false }; const pp: PublisherProperties = { audioSource, videoSource: false };
await this.openviduService.replaceTrack(VideoType.CAMERA, pp); const publisher = this.participantService.getMyCameraPublisher();
await this.openviduService.replaceCameraTrack(publisher, pp);
this.deviceSrv.setMicSelected(audioSource); this.deviceSrv.setMicSelected(audioSource);
this.microphoneSelected = this.deviceSrv.getMicrophoneSelected(); this.microphoneSelected = this.deviceSrv.getMicrophoneSelected();
} }

View File

@ -23,16 +23,17 @@
videocam_off videocam_off
</mat-icon> </mat-icon>
</button> </button>
<mat-form-field> <mat-form-field id="video-devices-form">
<mat-label *ngIf="hasVideoDevices">{{ 'PREJOIN.VIDEO_DEVICE' | translate }}</mat-label> <mat-label *ngIf="hasVideoDevices">{{ 'PREJOIN.VIDEO_DEVICE' | translate }}</mat-label>
<mat-label *ngIf="!hasVideoDevices">{{ 'PREJOIN.NO_VIDEO_DEVICE' | translate }}</mat-label> <mat-label *ngIf="!hasVideoDevices">{{ 'PREJOIN.NO_VIDEO_DEVICE' | translate }}</mat-label>
<mat-select <mat-select
[disabled]="isVideoMuted || !hasVideoDevices" [disabled]="isVideoMuted || !hasVideoDevices"
[value]="cameraSelected?.device" [compareWith]="compareObjectDevices"
[value]="cameraSelected"
(click)="onDeviceSelectorClicked.emit()" (click)="onDeviceSelectorClicked.emit()"
(selectionChange)="onCameraSelected($event)" (selectionChange)="onCameraSelected($event)"
> >
<mat-option *ngFor="let camera of cameras" [value]="camera.device"> <mat-option *ngFor="let camera of cameras" [value]="camera" id="option-{{camera.label}}">
{{ camera.label }} {{ camera.label }}
</mat-option> </mat-option>
</mat-select> </mat-select>

View File

@ -4,7 +4,6 @@ import { Subscription } from 'rxjs';
import { CustomDevice } from '../../../models/device.model'; import { CustomDevice } from '../../../models/device.model';
import { PanelType } from '../../../models/panel.model'; import { PanelType } from '../../../models/panel.model';
import { ParticipantAbstractModel } from '../../../models/participant.model'; import { ParticipantAbstractModel } from '../../../models/participant.model';
import { VideoType } from '../../../models/video-type.model';
import { DeviceService } from '../../../services/device/device.service'; import { DeviceService } from '../../../services/device/device.service';
import { OpenViduService } from '../../../services/openvidu/openvidu.service'; import { OpenViduService } from '../../../services/openvidu/openvidu.service';
import { PanelService } from '../../../services/panel/panel.service'; import { PanelService } from '../../../services/panel/panel.service';
@ -47,9 +46,8 @@ export class VideoDevicesComponent implements OnInit, OnDestroy {
await this.deviceSrv.refreshDevices(); await this.deviceSrv.refreshDevices();
} }
this.hasVideoDevices = this.deviceSrv.hasVideoDeviceAvailable(); this.hasVideoDevices = this.deviceSrv.hasVideoDeviceAvailable();
if(this.hasVideoDevices){ if (this.hasVideoDevices) {
this.cameras = this.deviceSrv.getCameras(); this.cameras = this.deviceSrv.getCameras();
this.cameraSelected = this.deviceSrv.getCameraSelected(); this.cameraSelected = this.deviceSrv.getCameraSelected();
} }
@ -67,7 +65,7 @@ export class VideoDevicesComponent implements OnInit, OnDestroy {
async toggleCam() { async toggleCam() {
this.videoMuteChanging = true; this.videoMuteChanging = true;
const publish = this.isVideoMuted; const publish = this.isVideoMuted;
await this.openviduService.publishVideo(publish); await this.participantService.publishVideo(publish);
if (this.isVideoMuted && this.panelService.isExternalPanelOpened()) { if (this.isVideoMuted && this.panelService.isExternalPanelOpened()) {
this.panelService.togglePanel(PanelType.BACKGROUND_EFFECTS); this.panelService.togglePanel(PanelType.BACKGROUND_EFFECTS);
} }
@ -76,19 +74,21 @@ export class VideoDevicesComponent implements OnInit, OnDestroy {
} }
async onCameraSelected(event: any) { async onCameraSelected(event: any) {
const videoSource = event?.value; const device: CustomDevice = event?.value;
// Is New deviceId different from the old one? // Is New deviceId different from the old one?
if (this.deviceSrv.needUpdateVideoTrack(videoSource)) { if (this.deviceSrv.needUpdateVideoTrack(device)) {
const mirror = this.deviceSrv.cameraNeedsMirror(videoSource); const mirror = this.deviceSrv.cameraNeedsMirror(device.device);
// Reapply Virtual Background to new Publisher if necessary // Reapply Virtual Background to new Publisher if necessary
const backgroundSelected = this.backgroundService.backgroundSelected.getValue(); const backgroundSelected = this.backgroundService.backgroundSelected.getValue();
const isBackgroundApplied = this.backgroundService.isBackgroundApplied() const isBackgroundApplied = this.backgroundService.isBackgroundApplied();
if (isBackgroundApplied) { if (isBackgroundApplied) {
await this.backgroundService.removeBackground(); await this.backgroundService.removeBackground();
} }
const pp: PublisherProperties = { videoSource, audioSource: false, mirror }; const pp: PublisherProperties = { videoSource: device.device, audioSource: false, mirror };
await this.openviduService.replaceTrack(VideoType.CAMERA, pp); const publisher = this.participantService.getMyCameraPublisher();
await this.openviduService.replaceCameraTrack(publisher, pp);
if (isBackgroundApplied) { if (isBackgroundApplied) {
const bgSelected = this.backgroundService.backgrounds.find((b) => b.id === backgroundSelected); const bgSelected = this.backgroundService.backgrounds.find((b) => b.id === backgroundSelected);
@ -97,11 +97,19 @@ export class VideoDevicesComponent implements OnInit, OnDestroy {
} }
} }
this.deviceSrv.setCameraSelected(videoSource); this.deviceSrv.setCameraSelected(device.device);
this.cameraSelected = this.deviceSrv.getCameraSelected(); this.cameraSelected = this.deviceSrv.getCameraSelected();
} }
} }
/**
* @internal
* Compare two devices to check if they are the same. Used by the mat-select
*/
compareObjectDevices(o1: CustomDevice, o2: CustomDevice): boolean {
return o1.label === o2.label;
}
protected subscribeToParticipantMediaProperties() { protected subscribeToParticipantMediaProperties() {
this.localParticipantSubscription = this.participantService.localParticipantObs.subscribe((p: ParticipantAbstractModel) => { this.localParticipantSubscription = this.participantService.localParticipantObs.subscribe((p: ParticipantAbstractModel) => {
if (p) { if (p) {

View File

@ -1,9 +1,8 @@
/* Fixes layout bug. The OT_root is created with the entire layout width and it has a weird UX behaviour */ /* Fixes layout bug. The OT_root is created with the entire layout width and it has a weird UX behaviour */
.no-size { .no-size {
height: 0px !important; height: 0px !important;
width: 0px !important; width: 0px !important;
} }
.nickname { .nickname {
padding: 0px; padding: 0px;
@ -11,46 +10,44 @@
z-index: 999; z-index: 999;
border-radius: var(--ov-video-radius); border-radius: var(--ov-video-radius);
color: var(--ov-text-color); color: var(--ov-text-color);
font-family: 'Roboto','RobotoDraft',Helvetica,Arial,sans-serif; font-family: 'Roboto', 'RobotoDraft', Helvetica, Arial, sans-serif;
} }
.nicknameContainer { .nicknameContainer {
background-color: var(--ov-secondary-color); background-color: var(--ov-secondary-color);
padding: 5px; padding: 5px;
color: var(--ov-text-color); color: var(--ov-text-color);
font-weight: bold; font-weight: bold;
border-radius: var(--ov-video-radius); border-radius: var(--ov-video-radius);
} }
#nickname-input-container { #nickname-input-container {
background-color: var(--ov-secondary-color); background-color: var(--ov-secondary-color);
border-radius: var(--ov-video-radius); border-radius: var(--ov-video-radius);
}
} #closeButton {
#closeButton {
position: absolute; position: absolute;
top: -3px; top: -3px;
right: 0; right: 0;
z-index: 999; z-index: 999;
} }
#nicknameForm { #nicknameForm {
padding: 10px; padding: 10px;
} }
#audio-wave-container { #audio-wave-container {
position: absolute; position: absolute;
right: 0; right: 0;
z-index: 999; z-index: 2;
padding: 5px; padding: 5px;
} }
.fullscreen { .fullscreen {
top: 40px; top: 40px;
} }
video {
video {
-o-object-fit: cover; -o-object-fit: cover;
object-fit: cover; object-fit: cover;
width: 100%; width: 100%;
@ -60,57 +57,61 @@
padding: 0; padding: 0;
border: 0; border: 0;
font-size: 100%; font-size: 100%;
} }
.status-icons, #settings-container { .status-icons,
#settings-container {
position: absolute; position: absolute;
bottom: 0; bottom: 0;
z-index: 9999; z-index: 9999;
text-align: center; text-align: center;
} }
.status-icons { .status-icons {
left: 0; left: 0;
} }
.status-icons button, #settings-container button { .status-icons button,
#settings-container button {
color: var(--ov-text-color); color: var(--ov-text-color);
width: 26px; width: 26px;
height: 26px; height: 26px;
margin: 5px; margin: 5px;
border-radius: var(--ov-buttons-radius); border-radius: var(--ov-buttons-radius);
} }
.status-icons button { .status-icons button {
background-color: var(--ov-warn-color); background-color: var(--ov-warn-color);
} }
.status-icons .mat-icon-button, #settings-container .mat-icon-button{ .status-icons .mat-icon-button,
#settings-container .mat-icon-button {
line-height: 0px; line-height: 0px;
} }
.status-icons mat-icon, #settings-container mat-icon { .status-icons mat-icon,
#settings-container mat-icon {
font-size: 18px; font-size: 18px;
} }
#settings-container{ #settings-container {
right: 0; right: 0;
} }
#settings-container button { #settings-container button {
background-color: var(--ov-secondary-color); background-color: var(--ov-secondary-color);
} }
/* Contains the video element, used to fix video letter-boxing */ /* Contains the video element, used to fix video letter-boxing */
.OV_stream { .OV_stream {
width: 100%; width: 100%;
height: 100%; height: 100%;
position: relative; position: relative;
overflow: hidden; overflow: hidden;
background-color: transparent; background-color: transparent;
border-radius: var(--ov-video-radius); border-radius: var(--ov-video-radius);
} }
input { input {
caret-color: #ffffff !important; caret-color: #ffffff !important;
} }

View File

@ -1,7 +1,7 @@
<div <div
*ngIf="this._stream" *ngIf="this._stream"
class="OV_stream" class="OV_stream"
[ngClass]="{'no-size': !showVideo}" [ngClass]="{ 'no-size': !showVideo }"
[id]="'container-' + this._stream.streamManager?.stream?.streamId" [id]="'container-' + this._stream.streamManager?.stream?.streamId"
#streamContainer #streamContainer
> >
@ -50,11 +50,18 @@
</div> </div>
<div *ngIf="!isMinimal && showSettingsButton" id="settings-container" class="videoButtons"> <div *ngIf="!isMinimal && showSettingsButton" id="settings-container" class="videoButtons">
<button mat-icon-button (click)="toggleVideoMenu($event)" matTooltip="{{ 'STREAM.SETTINGS' | translate }}" matTooltipPosition="above" aria-label="Video settings menu" id="stream-menu-btn"> <button
mat-icon-button
(click)="toggleVideoMenu($event)"
matTooltip="{{ 'STREAM.SETTINGS' | translate }}"
matTooltipPosition="above"
aria-label="Video settings menu"
id="video-settings-btn-{{this._stream.streamManager?.stream?.typeOfVideo}}"
>
<mat-icon>more_vert</mat-icon> <mat-icon>more_vert</mat-icon>
</button> </button>
<span [matMenuTriggerFor]="menu"></span> <span [matMenuTriggerFor]="menu"></span>
<mat-menu #menu="matMenu" yPosition="above" xPosition="before"> <mat-menu #menu="matMenu" yPosition="above" xPosition="before" class="video-settings-menu">
<button mat-menu-item id="videoZoomButton" (click)="toggleVideoEnlarged()"> <button mat-menu-item id="videoZoomButton" (click)="toggleVideoEnlarged()">
<mat-icon>{{ this.videoSizeIcon }}</mat-icon> <mat-icon>{{ this.videoSizeIcon }}</mat-icon>
<span *ngIf="videoSizeIcon === videoSizeIconEnum.NORMAL">{{ 'STREAM.ZOOM_OUT' | translate }}</span> <span *ngIf="videoSizeIcon === videoSizeIconEnum.NORMAL">{{ 'STREAM.ZOOM_OUT' | translate }}</span>
@ -70,7 +77,7 @@
<button <button
mat-menu-item mat-menu-item
(click)="replaceScreenTrack()" (click)="replaceScreenTrack()"
id="changeScreenButton" id="replace-screen-button"
*ngIf="!this._stream.streamManager?.remote && this._stream.streamManager?.stream?.typeOfVideo === videoTypeEnum.SCREEN" *ngIf="!this._stream.streamManager?.remote && this._stream.streamManager?.stream?.typeOfVideo === videoTypeEnum.SCREEN"
> >
<mat-icon>picture_in_picture</mat-icon> <mat-icon>picture_in_picture</mat-icon>

View File

@ -231,12 +231,12 @@ export class StreamComponent implements OnInit {
/** /**
* @ignore * @ignore
*/ */
updateNickname(event) { async updateNickname(event) {
if (event?.keyCode === 13 || event?.type === 'focusout') { if (event?.keyCode === 13 || event?.type === 'focusout') {
if (!!this.nickname) { if (!!this.nickname) {
this.participantService.setMyNickname(this.nickname); this.participantService.setMyNickname(this.nickname);
this.storageService.setNickname(this.nickname); this.storageService.setNickname(this.nickname);
this.openviduService.sendSignal(Signal.NICKNAME_CHANGED, undefined, { clientData: this.nickname }); await this.openviduService.sendSignal(Signal.NICKNAME_CHANGED, undefined, { clientData: this.nickname });
} }
this.toggleNicknameForm(); this.toggleNicknameForm();
} }
@ -252,7 +252,8 @@ export class StreamComponent implements OnInit {
publishAudio: !this.participantService.isMyCameraActive(), publishAudio: !this.participantService.isMyCameraActive(),
mirror: false mirror: false
}; };
await this.openviduService.replaceTrack(VideoType.SCREEN, properties); const publisher = this.participantService.getMyScreenPublisher();
await this.openviduService.replaceScreenTrack(publisher, properties);
} }
private checkVideoEnlarged() { private checkVideoEnlarged() {

View File

@ -500,7 +500,7 @@ export class ToolbarComponent implements OnInit, OnDestroy, AfterViewInit {
async toggleMicrophone() { async toggleMicrophone() {
this.onMicrophoneButtonClicked.emit(); this.onMicrophoneButtonClicked.emit();
try { try {
await this.openviduService.publishAudio(!this.isAudioActive); this.participantService.publishAudio(!this.isAudioActive);
} catch (error) { } catch (error) {
this.log.e('There was an error toggling microphone:', error.code, error.message); this.log.e('There was an error toggling microphone:', error.code, error.message);
this.actionService.openDialog( this.actionService.openDialog(
@ -521,7 +521,7 @@ export class ToolbarComponent implements OnInit, OnDestroy, AfterViewInit {
if (this.panelService.isExternalPanelOpened() && !publishVideo) { if (this.panelService.isExternalPanelOpened() && !publishVideo) {
this.panelService.togglePanel(PanelType.BACKGROUND_EFFECTS); this.panelService.togglePanel(PanelType.BACKGROUND_EFFECTS);
} }
await this.openviduService.publishVideo(publishVideo); await this.participantService.publishVideo(publishVideo);
} catch (error) { } catch (error) {
this.log.e('There was an error toggling camera:', error.code, error.message); this.log.e('There was an error toggling camera:', error.code, error.message);
this.actionService.openDialog(this.translateService.translate('ERRORS.TOGGLE_CAMERA'), error?.error || error?.message || error); this.actionService.openDialog(this.translateService.translate('ERRORS.TOGGLE_CAMERA'), error?.error || error?.message || error);

View File

@ -601,7 +601,14 @@ export class VideoconferenceComponent implements OnInit, OnDestroy, AfterViewIni
await this.handlePublisherError(e); await this.handlePublisherError(e);
resolve(); resolve();
}); });
publisher.once('accessAllowed', () => resolve()); publisher.once('accessAllowed', () => {
this.participantService.setMyCameraPublisher(publisher);
this.participantService.updateLocalParticipant();
resolve();
});
} else {
this.participantService.setMyCameraPublisher(undefined);
this.participantService.updateLocalParticipant();
} }
} catch (error) { } catch (error) {
this.actionService.openDialog(error.name.replace(/_/g, ' '), error.message, true); this.actionService.openDialog(error.name.replace(/_/g, ' '), error.message, true);

View File

@ -25,7 +25,7 @@ import { Directive, TemplateRef, ViewContainerRef } from '@angular/core';
* publishVideo = true; * publishVideo = true;
* publishAudio = true; * publishAudio = true;
* *
* constructor(private httpClient: HttpClient, private openviduService: OpenViduService) { } * constructor(private httpClient: HttpClient, private participantService: ParticipantService) { }
* *
* async ngOnInit() { * async ngOnInit() {
* this.tokens = { * this.tokens = {
@ -36,12 +36,12 @@ import { Directive, TemplateRef, ViewContainerRef } from '@angular/core';
* *
* toggleVideo() { * toggleVideo() {
* this.publishVideo = !this.publishVideo; * this.publishVideo = !this.publishVideo;
* this.openviduService.publishVideo(this.publishVideo); * this.participantService.publishVideo(this.publishVideo);
* } * }
* *
* toggleAudio() { * toggleAudio() {
* this.publishAudio = !this.publishAudio; * this.publishAudio = !this.publishAudio;
* this.openviduService.publishAudio(this.publishAudio); * this.participantService.publishAudio(this.publishAudio);
* } * }
* *
* async getToken(): Promise<string> { * async getToken(): Promise<string> {
@ -94,7 +94,6 @@ export class ToolbarDirective {
* *
* constructor( * constructor(
* private httpClient: HttpClient, * private httpClient: HttpClient,
* private openviduService: OpenViduService,
* private participantService: ParticipantService * private participantService: ParticipantService
* ) { } * ) { }
* *
@ -107,12 +106,12 @@ export class ToolbarDirective {
* *
* toggleVideo() { * toggleVideo() {
* const publishVideo = !this.participantService.isMyVideoActive(); * const publishVideo = !this.participantService.isMyVideoActive();
* this.openviduService.publishVideo(publishVideo); * this.participantService.publishVideo(publishVideo);
* } * }
* *
* toggleAudio() { * toggleAudio() {
* const publishAudio = !this.participantService.isMyAudioActive(); * const publishAudio = !this.participantService.isMyAudioActive();
* this.openviduService.publishAudio(publishAudio); * this.participantService.publishAudio(publishAudio);
* } * }
* *
* async getToken(): Promise<string> { * async getToken(): Promise<string> {

View File

@ -21,7 +21,7 @@ export interface StreamModel {
/** /**
* The streamManager object from openvidu-browser library.{@link https://docs.openvidu.io/en/stable/api/openvidu-browser/classes/StreamManager.html} * The streamManager object from openvidu-browser library.{@link https://docs.openvidu.io/en/stable/api/openvidu-browser/classes/StreamManager.html}
*/ */
streamManager: StreamManager; streamManager: StreamManager | undefined;
/** /**
* Whether the stream is enlarged or not * Whether the stream is enlarged or not
*/ */
@ -29,7 +29,7 @@ export interface StreamModel {
/** /**
* Unique identifier of the stream * Unique identifier of the stream
*/ */
connectionId: string; connectionId: string | undefined;
/** /**
* The participant object * The participant object
*/ */
@ -68,7 +68,7 @@ export abstract class ParticipantAbstractModel {
isMutedForcibly: boolean; isMutedForcibly: boolean;
constructor(props: ParticipantProperties, model?: StreamModel) { constructor(props: ParticipantProperties, model?: StreamModel) {
this.id = props.id ? props.id : Math.random().toString(32).replace('.','_'); this.id = props.id || Math.random().toString(32).replace('.','_');
this.local = props.local; this.local = props.local;
this.nickname = props.nickname; this.nickname = props.nickname;
this.colorProfile = !!props.colorProfile ? props.colorProfile : `hsl(${Math.random() * 360}, 100%, 80%)`; this.colorProfile = !!props.colorProfile ? props.colorProfile : `hsl(${Math.random() * 360}, 100%, 80%)`;
@ -76,9 +76,9 @@ export abstract class ParticipantAbstractModel {
let streamModel: StreamModel = { let streamModel: StreamModel = {
connected: model ? model.connected : true, connected: model ? model.connected : true,
type: model ? model.type : VideoType.CAMERA, type: model ? model.type : VideoType.CAMERA,
streamManager: model ? model.streamManager : null, streamManager: model?.streamManager,
videoEnlarged: model ? model.videoEnlarged : false, videoEnlarged: model ? model.videoEnlarged : false,
connectionId: model ? model.connectionId : null, connectionId: model?.connectionId,
participant: this participant: this
}; };
this.streams.set(streamModel.type, streamModel); this.streams.set(streamModel.type, streamModel);
@ -113,7 +113,7 @@ export abstract class ParticipantAbstractModel {
private isCameraAudioActive(): boolean { private isCameraAudioActive(): boolean {
const cameraConnection = this.getCameraConnection(); const cameraConnection = this.getCameraConnection();
if (cameraConnection?.connected) { if (cameraConnection?.connected) {
return cameraConnection.streamManager?.stream?.audioActive; return cameraConnection.streamManager?.stream?.audioActive || false;
} }
return false; return false;
} }
@ -132,7 +132,7 @@ export abstract class ParticipantAbstractModel {
isScreenAudioActive(): boolean { isScreenAudioActive(): boolean {
const screenConnection = this.getScreenConnection(); const screenConnection = this.getScreenConnection();
if (screenConnection?.connected) { if (screenConnection?.connected) {
return screenConnection?.streamManager?.stream?.audioActive; return screenConnection?.streamManager?.stream?.audioActive || false;
} }
return false; return false;
} }
@ -160,13 +160,14 @@ export abstract class ParticipantAbstractModel {
/** /**
* @internal * @internal
* @returns The participant active connection types
*/ */
getConnectionTypesActive(): VideoType[] { getActiveConnectionTypes(): VideoType[] {
let connType = []; const activeTypes: VideoType[] = [];
if (this.isCameraActive()) connType.push(VideoType.CAMERA); if (this.isCameraActive()) activeTypes.push(VideoType.CAMERA);
if (this.isScreenActive()) connType.push(VideoType.SCREEN); if (this.isScreenActive()) activeTypes.push(VideoType.SCREEN);
return connType; return activeTypes;
} }
/** /**
@ -218,7 +219,6 @@ export abstract class ParticipantAbstractModel {
*/ */
isLocal(): boolean { isLocal(): boolean {
return this.local; return this.local;
// return Array.from(this.streams.values()).every((conn) => conn.local);
} }
/** /**
@ -238,7 +238,7 @@ export abstract class ParticipantAbstractModel {
/** /**
* @internal * @internal
*/ */
setCameraPublisher(publisher: Publisher) { setCameraPublisher(publisher: Publisher | undefined) {
const cameraConnection = this.getCameraConnection(); const cameraConnection = this.getCameraConnection();
if (cameraConnection) cameraConnection.streamManager = publisher; if (cameraConnection) cameraConnection.streamManager = publisher;
} }
@ -307,6 +307,30 @@ export abstract class ParticipantAbstractModel {
if (screenConnection) screenConnection.connected = false; if (screenConnection) screenConnection.connected = false;
} }
/**
* @internal
* @returns true if both camera and screen are active
*/
hasCameraAndScreenActives(): boolean {
return this.isCameraActive() && this.isScreenActive();
}
/**
* @internal
* @returns true if only screen is active
*/
hasOnlyScreenActive(): boolean {
return this.isScreenActive() && !this.isCameraActive();
}
/**
* @internal
* @returns true if only camera is active
*/
hasOnlyCameraActive(): boolean {
return this.isCameraActive() && !this.isScreenActive();
}
/** /**
* @internal * @internal
*/ */

View File

@ -1,5 +1,5 @@
import { Pipe, PipeTransform } from '@angular/core'; import { Pipe, PipeTransform } from '@angular/core';
import { StreamModel, ParticipantAbstractModel } from '../models/participant.model'; import { ParticipantAbstractModel, StreamModel } from '../models/participant.model';
import { TranslateService } from '../services/translate/translate.service'; import { TranslateService } from '../services/translate/translate.service';
@Pipe({ name: 'streams' }) @Pipe({ name: 'streams' })
@ -10,11 +10,8 @@ export class ParticipantStreamsPipe implements PipeTransform {
let streams: StreamModel[] = []; let streams: StreamModel[] = [];
if(participants && Object.keys(participants).length > 0){ if(participants && Object.keys(participants).length > 0){
if (Array.isArray(participants)) { if (Array.isArray(participants)) {
participants.forEach((p) => { streams = participants.map(p => p.getAvailableConnections()).flat();
streams = streams.concat(p.getAvailableConnections());
});
} else { } else {
streams = participants.getAvailableConnections(); streams = participants.getAvailableConnections();
} }
} }
@ -30,15 +27,11 @@ export class StreamTypesEnabledPipe implements PipeTransform {
constructor(private translateService: TranslateService) {} constructor(private translateService: TranslateService) {}
transform(participant: ParticipantAbstractModel): string { transform(participant: ParticipantAbstractModel): string {
let result = '';
let activeStreams = participant?.getConnectionTypesActive().toString(); const activeStreams = participant?.getActiveConnectionTypes() ?? [];
const activeStreamsArr: string[] = activeStreams.split(','); const streamNames = activeStreams.map(streamType => this.translateService.translate(`PANEL.PARTICIPANTS.${streamType}`));
activeStreamsArr.forEach((type, index) => { const streamsString = streamNames.join(', ');
result += this.translateService.translate(`PANEL.PARTICIPANTS.${type}`)
if(activeStreamsArr.length > 0 && index < activeStreamsArr.length - 1){ return `(${streamsString})`;
result += ', ';
}
});
return `(${result})`;
} }
} }

View File

@ -64,7 +64,7 @@ export class ChatService {
}); });
} }
sendMessage(message: string) { async sendMessage(message: string) {
message = message.replace(/ +(?= )/g, ''); message = message.replace(/ +(?= )/g, '');
if (message !== '' && message !== ' ') { if (message !== '' && message !== ' ') {
const data = { const data = {
@ -72,7 +72,7 @@ export class ChatService {
nickname: this.participantService.getMyNickname() nickname: this.participantService.getMyNickname()
}; };
this.openviduService.sendSignal(Signal.CHAT, undefined, data); await this.openviduService.sendSignal(Signal.CHAT, undefined, data);
} }
} }

View File

@ -177,8 +177,8 @@ export class DeviceService {
return this.microphoneSelected; return this.microphoneSelected;
} }
setCameraSelected(deviceField: any) { setCameraSelected(deviceId: any) {
this.cameraSelected = this.getCameraByDeviceField(deviceField); this.cameraSelected = this.getCameraByDeviceField(deviceId);
this.saveCameraToStorage(this.cameraSelected); this.saveCameraToStorage(this.cameraSelected);
} }
@ -187,8 +187,8 @@ export class DeviceService {
this.saveMicrophoneToStorage(this.microphoneSelected); this.saveMicrophoneToStorage(this.microphoneSelected);
} }
needUpdateVideoTrack(newVideoSource: string): boolean { needUpdateVideoTrack(newDevice: CustomDevice): boolean {
return this.cameraSelected?.device !== newVideoSource; return this.cameraSelected?.device !== newDevice.device || this.cameraSelected?.label !== newDevice.label;
} }
needUpdateAudioTrack(newAudioSource: string): boolean { needUpdateAudioTrack(newAudioSource: string): boolean {

View File

@ -1,4 +1,4 @@
import { Injectable } from '@angular/core'; import { Injectable, Injector } from '@angular/core';
import { import {
Connection, Connection,
OpenVidu, OpenVidu,
@ -11,8 +11,6 @@ import {
Stream Stream
} from 'openvidu-browser'; } from 'openvidu-browser';
import { LoggerService } from '../logger/logger.service';
import { BehaviorSubject, Observable } from 'rxjs'; import { BehaviorSubject, Observable } from 'rxjs';
import { CameraType } from '../../models/device.model'; import { CameraType } from '../../models/device.model';
import { ILogger } from '../../models/logger.model'; import { ILogger } from '../../models/logger.model';
@ -21,6 +19,7 @@ import { Signal } from '../../models/signal.model';
import { ScreenType, VideoType } from '../../models/video-type.model'; import { ScreenType, VideoType } from '../../models/video-type.model';
import { OpenViduAngularConfigService } from '../config/openvidu-angular.config.service'; import { OpenViduAngularConfigService } from '../config/openvidu-angular.config.service';
import { DeviceService } from '../device/device.service'; import { DeviceService } from '../device/device.service';
import { LoggerService } from '../logger/logger.service';
import { ParticipantService } from '../participant/participant.service'; import { ParticipantService } from '../participant/participant.service';
import { PlatformService } from '../platform/platform.service'; import { PlatformService } from '../platform/platform.service';
@ -53,7 +52,7 @@ export class OpenViduService {
protected openviduAngularConfigSrv: OpenViduAngularConfigService, protected openviduAngularConfigSrv: OpenViduAngularConfigService,
protected platformService: PlatformService, protected platformService: PlatformService,
protected loggerSrv: LoggerService, protected loggerSrv: LoggerService,
private participantService: ParticipantService, private injector: Injector,
protected deviceService: DeviceService protected deviceService: DeviceService
) { ) {
this.log = this.loggerSrv.get('OpenViduService'); this.log = this.loggerSrv.get('OpenViduService');
@ -140,8 +139,6 @@ export class OpenViduService {
async clear() { async clear() {
this.videoSource = undefined; this.videoSource = undefined;
this.audioSource = undefined; this.audioSource = undefined;
await this.participantService.getMyCameraPublisher()?.stream?.disposeMediaStream();
await this.participantService.getMyScreenPublisher()?.stream?.disposeMediaStream();
} }
/** /**
@ -201,30 +198,42 @@ export class OpenViduService {
/** /**
* @internal * @internal
* Connects to webcam session using webcam token.
*/ */
async connectSession(session: Session, token: string): Promise<void> { async connectWebcamSession(participantId: string, nickname: string): Promise<string | undefined> {
if (!!token && session) { if (this.isWebcamSessionConnected()) {
const nickname = this.participantService.getMyNickname(); this.log.d('Webcam session is already connected');
const participantId = this.participantService.getLocalParticipant().id; return undefined;
if (session === this.webcamSession) { }
this.log.d('Connecting webcam session'); this.log.d('Connecting webcam session');
await this.webcamSession.connect(token, { await this.webcamSession.connect(this.getWebcamToken(), {
clientData: nickname, clientData: nickname,
participantId, participantId,
type: VideoType.CAMERA type: VideoType.CAMERA
}); });
this.participantService.setMyCameraConnectionId(this.webcamSession.connection.connectionId);
} else if (session === this.screenSession) { return this.webcamSession.connection.connectionId;
}
/**
* @internal
* Connects to screen session using screen token.
*/
async connectScreenSession(participantId: string, nickname: string): Promise<string | undefined> {
if (this.isScreenSessionConnected()) {
this.log.d('Screen session is already connected');
return undefined;
}
this.log.d('Connecting screen session'); this.log.d('Connecting screen session');
await this.screenSession.connect(token, { await this.screenSession.connect(this.getScreenToken(), {
clientData: `${nickname}_${VideoType.SCREEN}`, clientData: `${nickname}_${VideoType.SCREEN}`,
participantId, participantId,
type: VideoType.SCREEN type: VideoType.SCREEN
}); });
this.participantService.setMyScreenConnectionId(this.screenSession.connection.connectionId); return this.screenSession.connection.connectionId;
}
}
} }
/** /**
@ -273,198 +282,116 @@ export class OpenViduService {
mirror mirror
}; };
if (hasVideoDevices || hasAudioDevices) { if (hasVideoDevices || hasAudioDevices) {
const publisher = await this.initPublisher(properties); return this.initPublisher(properties);
this.participantService.setMyCameraPublisher(publisher);
this.participantService.updateLocalParticipant();
return publisher;
} else {
this.participantService.setMyCameraPublisher(null);
} }
} }
/** /**
* @internal * @internal
*/ */
private async initPublisher(properties: PublisherProperties, targetElement?: string | HTMLElement): Promise<Publisher> { private initPublisher(properties: PublisherProperties, targetElement?: string | HTMLElement): Promise<Publisher> {
this.log.d('Initializing publisher with properties: ', properties); this.log.d('Initializing publisher with properties: ', properties);
return this.OV.initPublisherAsync(targetElement, properties); return this.OV.initPublisherAsync(targetElement, properties);
} }
/** /**
* @internal * @internal
* @param hasAudio
* @returns
*/ */
async publish(publisher: Publisher): Promise<void> { initScreenPublisher(hasAudio: boolean): Promise<Publisher> {
if (!!publisher) { const hasAudioDevicesAvailable = this.deviceService.hasAudioDeviceAvailable();
if (publisher === this.participantService.getMyCameraPublisher()) {
if (this.webcamSession?.capabilities?.publish) { const properties: PublisherProperties = {
return await this.webcamSession.publish(publisher); videoSource: ScreenType.SCREEN,
} audioSource: hasAudioDevicesAvailable ? this.deviceService.getMicrophoneSelected().device : false,
this.log.e('Webcam publisher cannot be published'); publishVideo: true,
} else if (publisher === this.participantService.getMyScreenPublisher()) { publishAudio: hasAudio && hasAudioDevicesAvailable,
if (this.screenSession?.capabilities?.publish) { mirror: false
return await this.screenSession.publish(publisher); };
} return this.initPublisher(properties);
this.log.e('Screen publisher cannot be published');
}
}
} }
/** /**
* @internal * Publishes the publisher to the webcam Session
* @param publisher
*/ */
private async unpublish(publisher: Publisher): Promise<void> { async publishCamera(publisher: Publisher): Promise<void> {
if (!!publisher) { if (!publisher) return;
if (publisher === this.participantService.getMyCameraPublisher()) { if (this.webcamSession?.capabilities?.publish) {
this.publishAudioAux(this.participantService.getMyScreenPublisher(), this.participantService.isMyAudioActive()); return this.webcamSession.publish(publisher);
await this.webcamSession.unpublish(publisher);
} else if (publisher === this.participantService.getMyScreenPublisher()) {
await this.screenSession.unpublish(publisher);
} }
this.log.e('Webcam publisher cannot be published');
} }
/**
* Publishes the publisher to the screen Session
* @param publisher
*/
async publishScreen(publisher: Publisher): Promise<void> {
if (!publisher) return;
if (this.screenSession?.capabilities?.publish) {
return this.screenSession.publish(publisher);
}
this.log.e('Screen publisher cannot be published');
}
/**
* Unpublishes the publisher of the webcam Session
* @param publisher
*/
async unpublishCamera(publisher: Publisher): Promise<void> {
if (!publisher) return;
return this.webcamSession.unpublish(publisher);
}
/**
* Unpublishes the publisher of the screen Session
* @param publisher
*/
async unpublishScreen(publisher: Publisher): Promise<void> {
if (!publisher) return;
return this.screenSession.unpublish(publisher);
} }
/** /**
* Publish or unpublish the video stream (if available). * Publish or unpublish the video stream (if available).
* It hides the camera muted stream if screen is sharing. * It hides the camera muted stream if screen is sharing.
* See openvidu-browser {@link https://docs.openvidu.io/en/stable/api/openvidu-browser/classes/Publisher.html#publishVideo publishVideo} * See openvidu-browser {@link https://docs.openvidu.io/en/stable/api/openvidu-browser/classes/Publisher.html#publishVideo publishVideo}
*
* @deprecated This method has been moved to ParticipantService
*
* TODO: Remove this method in release 2.29.0
*/ */
async publishVideo(publish: boolean): Promise<void> { async publishVideo(publish: boolean): Promise<void> {
const publishAudio = this.participantService.isMyAudioActive(); const participantService = this.injector.get(ParticipantService);
return participantService.publishVideo(publish);
// Disabling webcam
if (this.participantService.haveICameraAndScreenActive()) {
await this.publishVideoAux(this.participantService.getMyCameraPublisher(), publish);
await this.unpublish(this.participantService.getMyCameraPublisher());
this.publishAudioAux(this.participantService.getMyScreenPublisher(), publishAudio);
this.participantService.disableWebcamStream();
} else if (this.participantService.isOnlyMyScreenActive()) {
// Enabling webcam
const hasAudio = this.participantService.hasScreenAudioActive();
if (!this.isWebcamSessionConnected()) {
await this.connectSession(this.getWebcamSession(), this.getWebcamToken());
}
await this.publish(this.participantService.getMyCameraPublisher());
await this.publishVideoAux(this.participantService.getMyCameraPublisher(), true);
this.publishAudioAux(this.participantService.getMyScreenPublisher(), false);
this.publishAudioAux(this.participantService.getMyCameraPublisher(), hasAudio);
this.participantService.enableWebcamStream();
} else {
// Muting/unmuting webcam
await this.publishVideoAux(this.participantService.getMyCameraPublisher(), publish);
}
}
/**
* @internal
*/
private async publishVideoAux(publisher: Publisher, publish: boolean): Promise<void> {
if (!!publisher) {
let resource: boolean | MediaStreamTrack = true;
if (publish) {
// Forcing restoration with a custom media stream (the older one instead the default)
const currentDeviceId = this.deviceService.getCameraSelected()?.device;
const mediaStream = await this.createMediaStream({ videoSource: currentDeviceId, audioSource: false });
resource = mediaStream.getVideoTracks()[0];
}
await publisher.publishVideo(publish, resource);
this.participantService.updateLocalParticipant();
}
}
/**
* Publish or unpublish the audio stream (if available).
* See openvidu-browser {@link https://docs.openvidu.io/en/stable/api/openvidu-browser/classes/Publisher.html#publishAudio publishAudio}.
*/
async publishAudio(publish: boolean): Promise<void> {
if (this.participantService.isMyCameraActive()) {
if (this.participantService.isMyScreenActive() && this.participantService.hasScreenAudioActive()) {
this.publishAudioAux(this.participantService.getMyScreenPublisher(), false);
}
this.publishAudioAux(this.participantService.getMyCameraPublisher(), publish);
} else {
this.publishAudioAux(this.participantService.getMyScreenPublisher(), publish);
}
} }
/** /**
* Share or unshare the screen. * Share or unshare the screen.
* Hide the camera muted stream when screen is sharing. * Hide the camera muted stream when screen is sharing.
* @deprecated This method has been moved to ParticipantService
*
* TODO: Remove this method in release 2.29.0
*/ */
async toggleScreenshare() { async toggleScreenshare() {
if (this.participantService.haveICameraAndScreenActive()) { const participantService = this.injector.get(ParticipantService);
// Disabling screenShare return participantService.toggleScreenshare();
this.participantService.disableScreenStream();
await this.unpublish(this.participantService.getMyScreenPublisher());
} else if (this.participantService.isOnlyMyCameraActive()) {
// I only have the camera published
const hasAudioDevicesAvailable = this.deviceService.hasAudioDeviceAvailable();
const willWebcamBePresent = this.participantService.isMyCameraActive() && this.participantService.isMyVideoActive();
const hasAudio = willWebcamBePresent ? false : hasAudioDevicesAvailable && this.participantService.isMyAudioActive();
const properties: PublisherProperties = {
videoSource: ScreenType.SCREEN,
audioSource: hasAudioDevicesAvailable ? this.deviceService.getMicrophoneSelected().device : false,
publishVideo: true,
publishAudio: hasAudio,
mirror: false
};
const screenPublisher = await this.initPublisher(properties);
screenPublisher.once('accessAllowed', async () => {
// Listen to event fired when native stop button is clicked
screenPublisher.stream
.getMediaStream()
.getVideoTracks()[0]
.addEventListener('ended', async () => {
this.log.d('Clicked native stop button. Stopping screen sharing');
await this.toggleScreenshare();
});
// Enabling screenShare
this.participantService.activeMyScreenShare(screenPublisher);
if (!this.isScreenSessionConnected()) {
await this.connectSession(this.getScreenSession(), this.getScreenToken());
}
await this.publish(this.participantService.getMyScreenPublisher());
if (!this.participantService.isMyVideoActive()) {
// Disabling webcam
this.participantService.disableWebcamStream();
await this.unpublish(this.participantService.getMyCameraPublisher());
}
});
screenPublisher.once('accessDenied', (error: any) => {
return Promise.reject(error);
});
} else {
// I only have my screenshare active and I have no camera or it is muted
const hasAudio = this.participantService.hasScreenAudioActive();
// Enable webcam
if (!this.isWebcamSessionConnected()) {
await this.connectSession(this.getWebcamSession(), this.getWebcamToken());
}
await this.publish(this.participantService.getMyCameraPublisher());
this.publishAudioAux(this.participantService.getMyCameraPublisher(), hasAudio);
this.participantService.enableWebcamStream();
// Disabling screenshare
this.participantService.disableScreenStream();
await this.unpublish(this.participantService.getMyScreenPublisher());
}
} }
/** /**
* @internal *
* Publish or unpublish the audio stream (if available).
* See openvidu-browser {@link https://docs.openvidu.io/en/stable/api/openvidu-browser/classes/Publisher.html#publishAudio publishAudio}.
* @deprecated This method has been moved to ParticipantService
*
* TODO: Remove this method in release 2.29.0
*/ */
private publishAudioAux(publisher: Publisher, value: boolean): void { publishAudio(publish: boolean): void {
if (!!publisher) { const participantService = this.injector.get(ParticipantService);
publisher.publishAudio(value); participantService.publishAudio(publish);
this.participantService.updateLocalParticipant();
}
} }
/** /**
@ -473,52 +400,60 @@ export class OpenViduService {
* @param type: type of signal * @param type: type of signal
* @param connections: if undefined, the signal will be sent to all participants * @param connections: if undefined, the signal will be sent to all participants
*/ */
sendSignal(type: Signal, connections?: Connection[], data?: any): void { sendSignal(type: Signal, connections?: Connection[], data?: any): Promise<void> {
const signalOptions: SignalOptions = { const signalOptions: SignalOptions = {
data: JSON.stringify(data), data: JSON.stringify(data),
type, type,
to: connections && connections.length > 0 ? connections : undefined to: connections && connections.length > 0 ? connections : undefined
}; };
this.webcamSession.signal(signalOptions); return this.webcamSession.signal(signalOptions);
} }
/** /**
* @internal * @internal
* @param cameraPublisher
* @param props
*/ */
async replaceTrack(videoType: VideoType, props: PublisherProperties) { async replaceCameraTrack(cameraPublisher: Publisher, props: PublisherProperties) {
try {
this.log.d(`Replacing ${videoType} track`, props);
if (videoType === VideoType.CAMERA) {
let mediaStream: MediaStream;
const isReplacingAudio = !!props.audioSource; const isReplacingAudio = !!props.audioSource;
const isReplacingVideo = !!props.videoSource; const isReplacingVideo = !!props.videoSource;
let mediaStream: MediaStream | undefined;
let track: MediaStreamTrack | undefined;
try {
if (isReplacingVideo || isReplacingAudio) {
mediaStream = await this.createMediaStream(props);
}
if (isReplacingVideo) { if (isReplacingVideo) {
mediaStream = await this.createMediaStream(props); track = mediaStream?.getVideoTracks()[0];
// Replace video track
const videoTrack: MediaStreamTrack = mediaStream.getVideoTracks()[0];
await this.participantService.getMyCameraPublisher().replaceTrack(videoTrack);
} else if (isReplacingAudio) { } else if (isReplacingAudio) {
mediaStream = await this.createMediaStream(props); track = mediaStream?.getAudioTracks()[0];
// Replace audio track
const audioTrack: MediaStreamTrack = mediaStream.getAudioTracks()[0];
await this.participantService.getMyCameraPublisher().replaceTrack(audioTrack);
}
} else if (videoType === VideoType.SCREEN) {
try {
let newScreenMediaStream = await this.OVScreen.getUserMedia(props);
this.participantService.getMyScreenPublisher().stream.getMediaStream().getVideoTracks()[0].stop();
await this.participantService.getMyScreenPublisher().replaceTrack(newScreenMediaStream.getVideoTracks()[0]);
} catch (error) {
this.log.w('Cannot create the new MediaStream', error);
} }
if (track) {
await cameraPublisher.replaceTrack(track);
} }
} catch (error) { } catch (error) {
this.log.e('Error replacing track ', error); this.log.e('Error replacing track ', error);
} }
} }
/**
* @internal
* @param screenPublisher
* @param props
*/
async replaceScreenTrack(screenPublisher: Publisher, props: PublisherProperties) {
try {
let newScreenMediaStream = await this.OVScreen.getUserMedia(props);
screenPublisher.stream.getMediaStream().getVideoTracks()[0].stop();
await screenPublisher.replaceTrack(newScreenMediaStream.getVideoTracks()[0]);
} catch (error) {
this.log.w('Cannot create the new MediaStream', error);
}
}
/** /**
* @internal * @internal
* Subscribe all `CAMERA` stream types to speech-to-text * Subscribe all `CAMERA` stream types to speech-to-text
@ -527,7 +462,9 @@ export class OpenViduService {
* @param lang The language of the Stream's audio track. * @param lang The language of the Stream's audio track.
*/ */
async subscribeRemotesToSTT(lang: string): Promise<void> { async subscribeRemotesToSTT(lang: string): Promise<void> {
const remoteParticipants = this.participantService.getRemoteParticipants(); const participantService = this.injector.get(ParticipantService);
const remoteParticipants = participantService.getRemoteParticipants();
let successNumber = 0; let successNumber = 0;
for (const p of remoteParticipants) { for (const p of remoteParticipants) {
@ -566,9 +503,11 @@ export class OpenViduService {
* Unsubscribe to all `CAMERA` stream types to speech-to-text if STT is up(ready) * Unsubscribe to all `CAMERA` stream types to speech-to-text if STT is up(ready)
*/ */
async unsubscribeRemotesFromSTT(): Promise<void> { async unsubscribeRemotesFromSTT(): Promise<void> {
const participantService = this.injector.get(ParticipantService);
clearTimeout(this.sttReconnectionTimeout); clearTimeout(this.sttReconnectionTimeout);
if (this.isSttReady()) { if (this.isSttReady()) {
for (const p of this.participantService.getRemoteParticipants()) { for (const p of participantService.getRemoteParticipants()) {
const stream = p.getCameraConnection().streamManager.stream; const stream = p.getCameraConnection().streamManager.stream;
if (stream) { if (stream) {
try { try {
@ -581,7 +520,14 @@ export class OpenViduService {
} }
} }
private async createMediaStream(pp: PublisherProperties): Promise<MediaStream> {
/**
* @internal
* @param pp {@link PublisherProperties}
* @returns Promise<MediaStream>
*/
async createMediaStream(pp: PublisherProperties): Promise<MediaStream> {
const participantService = this.injector.get(ParticipantService);
const currentCameraSelected = this.deviceService.getCameraSelected(); const currentCameraSelected = this.deviceService.getCameraSelected();
const currentMicSelected = this.deviceService.getMicrophoneSelected(); const currentMicSelected = this.deviceService.getMicrophoneSelected();
const isReplacingAudio = Boolean(pp.audioSource); const isReplacingAudio = Boolean(pp.audioSource);
@ -589,7 +535,7 @@ export class OpenViduService {
try { try {
const trackType = isReplacingAudio ? 'audio' : 'video'; const trackType = isReplacingAudio ? 'audio' : 'video';
this.forceStopMediaTracks(this.participantService.getMyCameraPublisher().stream.getMediaStream(), trackType); this.forceStopMediaTracks(participantService.getMyCameraPublisher().stream.getMediaStream(), trackType);
return this.OV.getUserMedia(pp); return this.OV.getUserMedia(pp);
} catch (error) { } catch (error) {
console.warn('Error creating MediaStream', error); console.warn('Error creating MediaStream', error);
@ -611,7 +557,8 @@ export class OpenViduService {
/** /**
* @internal * @internal
*/ */
needSendNicknameSignal(): boolean { myNicknameHasBeenChanged(): boolean {
const participantService = this.injector.get(ParticipantService);
let oldNickname: string = ""; let oldNickname: string = "";
try { try {
const connData = JSON.parse(this.cleanConnectionData(this.webcamSession.connection.data)); const connData = JSON.parse(this.cleanConnectionData(this.webcamSession.connection.data));
@ -619,7 +566,7 @@ export class OpenViduService {
} catch (error) { } catch (error) {
this.log.e(error); this.log.e(error);
} finally { } finally {
return oldNickname !== this.participantService.getMyNickname(); return oldNickname !== participantService.getMyNickname();
} }
} }

View File

@ -16,7 +16,7 @@ export class PanelService {
private isExternalOpened: boolean = false; private isExternalOpened: boolean = false;
private externalType: string; private externalType: string;
protected _panelOpened = <BehaviorSubject<PanelEvent>>new BehaviorSubject({ opened: false }); protected _panelOpened = <BehaviorSubject<PanelEvent>>new BehaviorSubject({ opened: false });
private panelMap: Map<string, boolean> = new Map(); private panelTypes: string[] = Object.values(PanelType);
/** /**
* @internal * @internal
@ -24,7 +24,6 @@ export class PanelService {
constructor(protected loggerSrv: LoggerService) { constructor(protected loggerSrv: LoggerService) {
this.log = this.loggerSrv.get('PanelService'); this.log = this.loggerSrv.get('PanelService');
this.panelOpenedObs = this._panelOpened.asObservable(); this.panelOpenedObs = this._panelOpened.asObservable();
Object.values(PanelType).forEach((panel) => this.panelMap.set(panel, false));
} }
/** /**
@ -33,31 +32,22 @@ export class PanelService {
*/ */
togglePanel(type: PanelType | string, expand?: PanelSettingsOptions | string) { togglePanel(type: PanelType | string, expand?: PanelSettingsOptions | string) {
let nextOpenedValue: boolean = false; let nextOpenedValue: boolean = false;
if (this.panelMap.has(type)) { const oldType = this._panelOpened.getValue().type;
const oldOpened = this._panelOpened.getValue().opened;
if (this.panelTypes.includes(type)) {
this.log.d(`Toggling ${type} menu`); this.log.d(`Toggling ${type} menu`);
this.panelMap.forEach((opened: boolean, panel: string) => { nextOpenedValue = oldType !== type ? true : !oldOpened;
if (panel === type) {
// Toggle panel
this.panelMap.set(panel, !opened);
nextOpenedValue = !opened;
} else {
// Close others
this.panelMap.set(panel, false);
}
});
} else { } else {
// Panel is external // Panel is external
this.log.d('Toggling external panel'); this.log.d('Toggling external panel');
// Close all panels
this.panelMap.forEach((_, panel: string) => this.panelMap.set(panel, false));
// Opening when external panel is closed or is opened with another type // Opening when external panel is closed or is opened with another type
this.isExternalOpened = !this.isExternalOpened || this.externalType !== type; this.isExternalOpened = !this.isExternalOpened || this.externalType !== type;
this.externalType = !this.isExternalOpened ? '' : type; this.externalType = !this.isExternalOpened ? '' : type;
nextOpenedValue = this.isExternalOpened; nextOpenedValue = this.isExternalOpened;
} }
const oldType = this._panelOpened.getValue().type;
this._panelOpened.next({ opened: nextOpenedValue, type, expand, oldType }); this._panelOpened.next({ opened: nextOpenedValue, type, expand, oldType });
} }
@ -65,51 +55,54 @@ export class PanelService {
* @internal * @internal
*/ */
isPanelOpened(): boolean { isPanelOpened(): boolean {
const anyOpened = Array.from(this.panelMap.values()).some((opened) => opened); return this._panelOpened.getValue().opened;
return anyOpened || this.isExternalPanelOpened();
} }
/** /**
* Closes the panel (if opened) * Closes the panel (if opened)
*/ */
closePanel(): void { closePanel(): void {
this.panelMap.forEach((_, panel: string) => this.panelMap.set(panel, false)); this._panelOpened.next({ opened: false, type: undefined, expand: undefined, oldType: undefined });
this._panelOpened.next({ opened: false });
} }
/** /**
* Whether the chat panel is opened or not. * Whether the chat panel is opened or not.
*/ */
isChatPanelOpened(): boolean { isChatPanelOpened(): boolean {
return !!this.panelMap.get(PanelType.CHAT); const panelState = this._panelOpened.getValue();
return panelState.opened && panelState.type === PanelType.CHAT;
} }
/** /**
* Whether the participants panel is opened or not. * Whether the participants panel is opened or not.
*/ */
isParticipantsPanelOpened(): boolean { isParticipantsPanelOpened(): boolean {
return !!this.panelMap.get(PanelType.PARTICIPANTS); const panelState = this._panelOpened.getValue();
return panelState.opened && panelState.type === PanelType.PARTICIPANTS;
} }
/** /**
* Whether the activities panel is opened or not. * Whether the activities panel is opened or not.
*/ */
isActivitiesPanelOpened(): boolean { isActivitiesPanelOpened(): boolean {
return !!this.panelMap.get(PanelType.ACTIVITIES); const panelState = this._panelOpened.getValue();
return panelState.opened && panelState.type === PanelType.ACTIVITIES;
} }
/** /**
* Whether the settings panel is opened or not. * Whether the settings panel is opened or not.
*/ */
isSettingsPanelOpened(): boolean { isSettingsPanelOpened(): boolean {
return !!this.panelMap.get(PanelType.SETTINGS); const panelState = this._panelOpened.getValue();
return panelState.opened && panelState.type === PanelType.SETTINGS;
} }
/** /**
* Whether the background effects panel is opened or not. * Whether the background effects panel is opened or not.
*/ */
isBackgroundEffectsPanelOpened(): boolean { isBackgroundEffectsPanelOpened(): boolean {
return !!this.panelMap.get(PanelType.BACKGROUND_EFFECTS); const panelState = this._panelOpened.getValue();
return panelState.opened && panelState.type === PanelType.BACKGROUND_EFFECTS;
} }
isExternalPanelOpened(): boolean { isExternalPanelOpened(): boolean {

View File

@ -2,10 +2,18 @@ import { Injectable } from '@angular/core';
import { Publisher, Subscriber } from 'openvidu-browser'; import { Publisher, Subscriber } from 'openvidu-browser';
import { BehaviorSubject, Observable } from 'rxjs'; import { BehaviorSubject, Observable } from 'rxjs';
import { ILogger } from '../../models/logger.model'; import { ILogger } from '../../models/logger.model';
import { OpenViduRole, ParticipantAbstractModel, ParticipantModel, ParticipantProperties, StreamModel } from '../../models/participant.model'; import {
OpenViduRole,
ParticipantAbstractModel,
ParticipantModel,
ParticipantProperties,
StreamModel
} from '../../models/participant.model';
import { VideoType } from '../../models/video-type.model'; import { VideoType } from '../../models/video-type.model';
import { OpenViduAngularConfigService } from '../config/openvidu-angular.config.service'; import { OpenViduAngularConfigService } from '../config/openvidu-angular.config.service';
import { DeviceService } from '../device/device.service';
import { LoggerService } from '../logger/logger.service'; import { LoggerService } from '../logger/logger.service';
import { OpenViduService } from '../openvidu/openvidu.service';
@Injectable({ @Injectable({
providedIn: 'root' providedIn: 'root'
@ -33,9 +41,13 @@ export class ParticipantService {
/** /**
* @internal * @internal
*/ */
constructor(protected openviduAngularConfigSrv: OpenViduAngularConfigService, protected loggerSrv: LoggerService) { constructor(
protected openviduAngularConfigSrv: OpenViduAngularConfigService,
private openviduService: OpenViduService,
private deviceService: DeviceService,
protected loggerSrv: LoggerService
) {
this.log = this.loggerSrv.get('ParticipantService'); this.log = this.loggerSrv.get('ParticipantService');
this.localParticipantObs = this._localParticipant.asObservable(); this.localParticipantObs = this._localParticipant.asObservable();
this.remoteParticipantsObs = this._remoteParticipants.asObservable(); this.remoteParticipantsObs = this._remoteParticipants.asObservable();
} }
@ -52,6 +64,129 @@ export class ParticipantService {
return this.localParticipant; return this.localParticipant;
} }
/**
* Publish or unpublish the local participant video stream (if available).
* It hides the camera stream (while muted) if screen is sharing.
* See openvidu-browser {@link https://docs.openvidu.io/en/stable/api/openvidu-browser/classes/Publisher.html#publishVideo publishVideo}
*
*/
async publishVideo(publish: boolean): Promise<void> {
const publishAudio = this.isMyAudioActive();
const cameraPublisher = this.getMyCameraPublisher();
const screenPublisher = this.getMyScreenPublisher();
// Disabling webcam
if (this.localParticipant.hasCameraAndScreenActives()) {
await this.publishVideoAux(cameraPublisher, publish);
this.disableWebcamStream();
this.openviduService.unpublishCamera(cameraPublisher);
this.publishAudioAux(screenPublisher, publishAudio);
} else if (this.localParticipant.hasOnlyScreenActive()) {
// Enabling webcam
const hasAudio = this.hasScreenAudioActive();
const sessionId = await this.openviduService.connectWebcamSession(this.getMyNickname(), this.getLocalParticipant().id);
if (sessionId) this.setMyCameraConnectionId(sessionId);
await this.openviduService.publishCamera(cameraPublisher);
await this.publishVideoAux(cameraPublisher, true);
this.publishAudioAux(screenPublisher, false);
this.publishAudioAux(cameraPublisher, hasAudio);
this.enableWebcamStream();
} else {
// Muting/unmuting webcam
await this.publishVideoAux(cameraPublisher, publish);
}
this.updateLocalParticipant();
}
/**
* Publish or unpublish the local participant audio stream (if available).
* See openvidu-browser {@link https://docs.openvidu.io/en/stable/api/openvidu-browser/classes/Publisher.html#publishAudio publishAudio}.
*
*/
publishAudio(publish: boolean): void {
if (this.isMyCameraActive()) {
if (this.localParticipant.isScreenActive() && this.hasScreenAudioActive()) {
this.publishAudioAux(this.getMyScreenPublisher(), false);
}
this.publishAudioAux(this.getMyCameraPublisher(), publish);
} else {
this.publishAudioAux(this.getMyScreenPublisher(), publish);
}
this.updateLocalParticipant();
}
/**
* Share or unshare the local participant screen.
* Hide the camera stream (while muted) when screen is sharing.
*
*/
async toggleScreenshare() {
const screenPublisher = this.getMyScreenPublisher();
const cameraPublisher = this.getMyCameraPublisher();
const participantNickname = this.getMyNickname();
const participantId = this.getLocalParticipant().id;
if (this.localParticipant.hasCameraAndScreenActives()) {
// Disabling screenShare
this.disableScreenStream();
this.updateLocalParticipant();
this.openviduService.unpublishScreen(screenPublisher);
} else if (this.localParticipant.hasOnlyCameraActive()) {
// I only have the camera published
const willWebcamBePresent = this.isMyCameraActive() && this.isMyVideoActive();
const hasAudio = willWebcamBePresent ? false : this.isMyAudioActive();
const screenPublisher = await this.openviduService.initScreenPublisher(hasAudio);
screenPublisher.once('accessAllowed', async () => {
// Listen to event fired when native stop button is clicked
screenPublisher.stream
.getMediaStream()
.getVideoTracks()[0]
.addEventListener('ended', async () => {
this.log.d('Clicked native stop button. Stopping screen sharing');
await this.toggleScreenshare();
});
// Enabling screenShare
this.activeMyScreenShare(screenPublisher);
if (!this.openviduService.isScreenSessionConnected()) {
await this.openviduService.connectScreenSession(participantId, participantNickname);
}
await this.openviduService.publishScreen(screenPublisher);
if (!this.isMyVideoActive()) {
// Disabling webcam
this.disableWebcamStream();
this.updateLocalParticipant();
this.openviduService.unpublishCamera(cameraPublisher);
}
});
screenPublisher.once('accessDenied', (error: any) => {
return Promise.reject(error);
});
} else {
// I only have my screenshare active and I have no camera or it is muted
const hasAudio = this.hasScreenAudioActive();
// Enable webcam
if (!this.openviduService.isWebcamSessionConnected()) {
await this.openviduService.connectWebcamSession(participantId, participantNickname);
}
await this.openviduService.publishCamera(cameraPublisher);
this.publishAudioAux(cameraPublisher, hasAudio);
this.enableWebcamStream();
// Disabling screenshare
this.disableScreenStream();
this.updateLocalParticipant();
this.openviduService.unpublishScreen(screenPublisher);
}
}
/** /**
* @internal * @internal
*/ */
@ -62,7 +197,7 @@ export class ParticipantService {
/** /**
* @internal * @internal
*/ */
setMyCameraPublisher(publisher: Publisher) { setMyCameraPublisher(publisher: Publisher | undefined) {
this.localParticipant.setCameraPublisher(publisher); this.localParticipant.setCameraPublisher(publisher);
} }
/** /**
@ -98,7 +233,6 @@ export class ParticipantService {
*/ */
enableWebcamStream() { enableWebcamStream() {
this.localParticipant.enableCamera(); this.localParticipant.enableCamera();
this.updateLocalParticipant();
} }
/** /**
@ -106,7 +240,6 @@ export class ParticipantService {
*/ */
disableWebcamStream() { disableWebcamStream() {
this.localParticipant.disableCamera(); this.localParticipant.disableCamera();
this.updateLocalParticipant();
} }
/** /**
@ -134,7 +267,6 @@ export class ParticipantService {
*/ */
disableScreenStream() { disableScreenStream() {
this.localParticipant.disableScreen(); this.localParticipant.disableScreen();
this.updateLocalParticipant();
} }
/** /**
@ -180,7 +312,9 @@ export class ParticipantService {
/** /**
* @internal * @internal
*/ */
clear() { async clear() {
await this.getMyCameraPublisher()?.stream?.disposeMediaStream();
await this.getMyScreenPublisher()?.stream?.disposeMediaStream();
this.disableScreenStream(); this.disableScreenStream();
this.remoteParticipants = []; this.remoteParticipants = [];
this.updateRemoteParticipants(); this.updateRemoteParticipants();
@ -202,34 +336,6 @@ export class ParticipantService {
return this.localParticipant?.hasAudioActive(); return this.localParticipant?.hasAudioActive();
} }
/**
* @internal
*/
isMyScreenActive(): boolean {
return this.localParticipant.isScreenActive();
}
/**
* @internal
*/
isOnlyMyCameraActive(): boolean {
return this.isMyCameraActive() && !this.isMyScreenActive();
}
/**
* @internal
*/
isOnlyMyScreenActive(): boolean {
return this.isMyScreenActive() && !this.isMyCameraActive();
}
/**
* @internal
*/
haveICameraAndScreenActive(): boolean {
return this.isMyCameraActive() && this.isMyScreenActive();
}
/** /**
* @internal * @internal
*/ */
@ -241,7 +347,32 @@ export class ParticipantService {
* Force to update the local participant object and fire a new {@link localParticipantObs} Observable event. * Force to update the local participant object and fire a new {@link localParticipantObs} Observable event.
*/ */
updateLocalParticipant() { updateLocalParticipant() {
this._localParticipant.next(Object.assign(Object.create(this.localParticipant), this.localParticipant)); this._localParticipant.next(
Object.assign(Object.create(Object.getPrototypeOf(this.localParticipant)), { ...this.localParticipant })
);
}
private publishAudioAux(publisher: Publisher, value: boolean): void {
if (!!publisher) {
publisher.publishAudio(value);
}
}
/**
* @internal
*/
private async publishVideoAux(publisher: Publisher, publish: boolean): Promise<void> {
if (!!publisher) {
let resource: boolean | MediaStreamTrack = true;
if (publish) {
// Forcing restoration with a custom media stream (the older one instead the default)
const currentDeviceId = this.deviceService.getCameraSelected()?.device;
const mediaStream = await this.openviduService.createMediaStream({ videoSource: currentDeviceId, audioSource: false });
resource = mediaStream.getVideoTracks()[0];
}
await publisher.publishVideo(publish, resource);
}
} }
/** /**