Merge branch 'master' into fix-generateOffer

pull/577/head
Juan Navarro 2021-05-19 18:54:51 +02:00
commit 84a5683b56
210 changed files with 41331 additions and 28063 deletions

View File

@ -5,7 +5,7 @@
[![Documentation Status](https://readthedocs.org/projects/openviduio-docs/badge/?version=stable)](https://docs.openvidu.io/en/stable/?badge=stable)
[![Docker badge](https://img.shields.io/docker/pulls/openvidu/openvidu-server-kms.svg)](https://hub.docker.com/r/openvidu/openvidu-server-kms)
[![Support badge](https://img.shields.io/badge/support-sof-yellowgreen.svg)](https://groups.google.com/forum/#!forum/openvidu)
[![Support badge](https://img.shields.io/badge/support-sof-yellowgreen.svg)](https://openvidu.discourse.group/)
[![Twitter Follow](https://img.shields.io/twitter/follow/openvidu.svg?style=social)](https://twitter.com/openvidu)
[![][OpenViduLogo]](https://openvidu.io)
@ -15,6 +15,10 @@ openvidu
Visit [openvidu.io](https://openvidu.io)
## Community Forum
Visit [OpenVidu Community Forum](https://openvidu.discourse.group/)
[OpenViduLogo]: https://secure.gravatar.com/avatar/5daba1d43042f2e4e85849733c8e5702?s=120
## Contributors

View File

@ -15,7 +15,8 @@ module.exports = {
"**/OpenViduInternal/WebRtcStats/WebRtcStats.ts",
"**/OpenViduInternal/WebRtcPeer/WebRtcPeer.ts",
"**/OpenViduInternal/ScreenSharing/**",
"**/OpenViduInternal/KurentoUtils/**"
"**/OpenViduInternal/KurentoUtils/**",
"**/OpenViduInternal/Logger/**",
],
excludeExternals: true,
excludePrivate: true,

View File

@ -3,13 +3,14 @@
"dependencies": {
"freeice": "2.2.2",
"hark": "1.2.3",
"jsnlog": "2.30.0",
"platform": "1.3.6",
"uuid": "8.3.1",
"uuid": "8.3.2",
"wolfy87-eventemitter": "5.2.9"
},
"description": "OpenVidu Browser",
"devDependencies": {
"@types/node": "14.14.7",
"@types/node": "14.14.32",
"@types/platform": "1.3.3",
"browserify": "17.0.0",
"grunt": "1.3.0",
@ -21,11 +22,11 @@
"grunt-postcss": "0.9.0",
"grunt-string-replace": "1.3.1",
"grunt-ts": "6.0.0-beta.22",
"terser": "5.3.8",
"terser": "5.6.0",
"tsify": "5.0.2",
"tslint": "6.1.3",
"typedoc": "0.19.2",
"typescript": "4.0.5"
"typescript": "4.0.7"
},
"license": "Apache-2.0",
"main": "lib/index.js",
@ -41,5 +42,5 @@
"docs": "./generate-docs.sh"
},
"types": "lib/index.d.ts",
"version": "2.16.0"
"version": "2.17.0"
}

View File

@ -22,6 +22,7 @@ import { RemoteConnectionOptions } from '../OpenViduInternal/Interfaces/Private/
import { InboundStreamOptions } from '../OpenViduInternal/Interfaces/Private/InboundStreamOptions';
import { StreamOptionsServer } from '../OpenViduInternal/Interfaces/Private/StreamOptionsServer';
import { OpenViduLogger } from '../OpenViduInternal/Logger/OpenViduLogger';
import { ExceptionEvent, ExceptionEventName } from '../OpenViduInternal/Events/ExceptionEvent';
/**
* @hidden
@ -142,8 +143,8 @@ export class Connection {
sdpMLineIndex: candidate.sdpMLineIndex
}, (error, response) => {
if (error) {
logger.error('Error sending ICE candidate: '
+ JSON.stringify(error));
logger.error('Error sending ICE candidate: ' + JSON.stringify(error));
this.session.emitEvent('exception', [new ExceptionEvent(this.session, ExceptionEventName.ICE_CANDIDATE_ERROR, this.session, "There was an unexpected error on the server-side processing an ICE candidate generated and sent by the client-side", error)]);
}
});
}
@ -173,7 +174,7 @@ export class Connection {
this.addStream(stream);
});
logger.info("Remote 'Connection' with 'connectionId' [" + this.connectionId + '] is now configured for receiving Streams with options: ', this.stream!.inboundStreamOpts);
}

View File

@ -19,7 +19,6 @@ import { Stream } from './Stream';
import { FilterEvent } from '../OpenViduInternal/Events/FilterEvent';
import { StreamPropertyChangedEvent } from '../OpenViduInternal/Events/StreamPropertyChangedEvent';
import { OpenViduError, OpenViduErrorName } from '../OpenViduInternal/Enums/OpenViduError';
import { ObjMap } from '../OpenViduInternal/Interfaces/Private/ObjMap';
import { OpenViduLogger } from '../OpenViduInternal/Logger/OpenViduLogger';
/**
@ -66,7 +65,7 @@ export class Filter {
/**
* @hidden
*/
handlers: ObjMap<(event: FilterEvent) => void> = {};
handlers: Map<string, (event: FilterEvent) => void> = new Map();
/**
* @hidden
@ -90,7 +89,7 @@ export class Filter {
* @param method Name of the method
* @param params Parameters of the method
*/
execMethod(method: string, params: Object): Promise<any> {
execMethod(method: string, params: Object): Promise<void> {
return new Promise((resolve, reject) => {
logger.info('Executing filter method to stream ' + this.stream.streamId);
let stringParams;
@ -138,7 +137,7 @@ export class Filter {
*
* @returns A Promise (to which you can optionally subscribe to) that is resolved if the event listener was successfully attached to the filter and rejected with an Error object if not
*/
addEventListener(eventType: string, handler: (event: FilterEvent) => void): Promise<any> {
addEventListener(eventType: string, handler: (event: FilterEvent) => void): Promise<void> {
return new Promise((resolve, reject) => {
logger.info('Adding filter event listener to event ' + eventType + ' to stream ' + this.stream.streamId);
this.stream.session.openvidu.sendRequest(
@ -153,7 +152,7 @@ export class Filter {
reject(error);
}
} else {
this.handlers[eventType] = handler;
this.handlers.set(eventType, handler);
logger.info('Filter event listener to event ' + eventType + ' successfully applied on Stream ' + this.stream.streamId);
resolve();
}
@ -170,7 +169,7 @@ export class Filter {
*
* @returns A Promise (to which you can optionally subscribe to) that is resolved if the event listener was successfully removed from the filter and rejected with an Error object in other case
*/
removeEventListener(eventType: string): Promise<any> {
removeEventListener(eventType: string): Promise<void> {
return new Promise((resolve, reject) => {
logger.info('Removing filter event listener to event ' + eventType + ' to stream ' + this.stream.streamId);
this.stream.session.openvidu.sendRequest(
@ -185,7 +184,7 @@ export class Filter {
reject(error);
}
} else {
delete this.handlers[eventType];
this.handlers.delete(eventType);
logger.info('Filter event listener to event ' + eventType + ' successfully removed on Stream ' + this.stream.streamId);
resolve();
}

View File

@ -74,7 +74,7 @@ export class LocalRecorder {
*
* @returns A Promise (to which you can optionally subscribe to) that is resolved if the recording successfully started and rejected with an Error object if not
*/
record(mimeType?: string): Promise<any> {
record(mimeType?: string): Promise<void> {
return new Promise((resolve, reject) => {
try {
if (typeof MediaRecorder === 'undefined') {
@ -146,7 +146,7 @@ export class LocalRecorder {
* Ends the recording of the Stream. [[state]] property must be `RECORDING` or `PAUSED`. After method succeeds is set to `FINISHED`
* @returns A Promise (to which you can optionally subscribe to) that is resolved if the recording successfully stopped and rejected with an Error object if not
*/
stop(): Promise<any> {
stop(): Promise<void> {
return new Promise((resolve, reject) => {
try {
if (this.state === LocalRecorderState.READY || this.state === LocalRecorderState.FINISHED) {
@ -168,7 +168,7 @@ export class LocalRecorder {
* Pauses the recording of the Stream. [[state]] property must be `RECORDING`. After method succeeds is set to `PAUSED`
* @returns A Promise (to which you can optionally subscribe to) that is resolved if the recording was successfully paused and rejected with an Error object if not
*/
pause(): Promise<any> {
pause(): Promise<void> {
return new Promise((resolve, reject) => {
try {
if (this.state !== LocalRecorderState.RECORDING) {
@ -187,7 +187,7 @@ export class LocalRecorder {
* Resumes the recording of the Stream. [[state]] property must be `PAUSED`. After method succeeds is set to `RECORDING`
* @returns A Promise (to which you can optionally subscribe to) that is resolved if the recording was successfully resumed and rejected with an Error object if not
*/
resume(): Promise<any> {
resume(): Promise<void> {
return new Promise((resolve, reject) => {
try {
if (this.state !== LocalRecorderState.PAUSED) {

View File

@ -32,6 +32,7 @@ import { PlatformUtils } from '../OpenViduInternal/Utils/Platform';
import * as screenSharingAuto from '../OpenViduInternal/ScreenSharing/Screen-Capturing-Auto';
import * as screenSharing from '../OpenViduInternal/ScreenSharing/Screen-Capturing';
import { OpenViduLoggerConfiguration } from "../OpenViduInternal/Logger/OpenViduLoggerConfiguration";
/**
* @hidden
*/
@ -99,6 +100,10 @@ export class OpenVidu {
* @hidden
*/
role: string;
/**
* @hidden
*/
finalUserId: string;
/**
* @hidden
*/
@ -106,7 +111,13 @@ export class OpenVidu {
/**
* @hidden
*/
webrtcStatsInterval: number = 0;
webrtcStatsInterval: number = -1;
/**
* @hidden
*/
sendBrowserLogs: OpenViduLoggerConfiguration = OpenViduLoggerConfiguration.disabled;
/**
* @hidden
*/
@ -116,85 +127,71 @@ export class OpenVidu {
*/
ee = new EventEmitter()
onOrientationChanged(handler): void {
(<any>window).addEventListener('orientationchange', handler);
}
constructor() {
platform = PlatformUtils.getInstance();
this.libraryVersion = packageJson.version;
logger.info("'OpenVidu' initialized");
logger.info("openvidu-browser version: " + this.libraryVersion);
logger.info("OpenVidu initialized");
logger.info('Platform detected: ' + platform.getDescription());
logger.info('openvidu-browser version: ' + this.libraryVersion);
if (platform.isMobileDevice()) {
if (platform.isMobileDevice() || platform.isReactNative()) {
// Listen to orientationchange only on mobile devices
(<any>window).addEventListener('orientationchange', () => {
this.onOrientationChanged(() => {
this.publishers.forEach(publisher => {
if (publisher.stream.isLocalStreamPublished && !!publisher.stream && !!publisher.stream.hasVideo && !!publisher.stream.streamManager.videos[0]) {
let attempts = 0;
const oldWidth = publisher.stream.videoDimensions.width;
const oldHeight = publisher.stream.videoDimensions.height;
const getNewVideoDimensions = (): Promise<{ newWidth: number, newHeight: number }> => {
return new Promise((resolve, reject) => {
if (platform.isIonicIos()) {
// iOS Ionic. Limitation: must get new dimensions from an existing video element already inserted into DOM
resolve({
newWidth: publisher.stream.streamManager.videos[0].video.videoWidth,
newHeight: publisher.stream.streamManager.videos[0].video.videoHeight
});
} else {
// Rest of platforms
// New resolution got from different places for Chrome and Firefox. Chrome needs a videoWidth and videoHeight of a videoElement.
// Firefox needs getSettings from the videoTrack
const firefoxSettings = publisher.stream.getMediaStream().getVideoTracks()[0].getSettings();
const newWidth = <number>((platform.isFirefoxBrowser() || platform.isFirefoxMobileBrowser()) ? firefoxSettings.width : publisher.videoReference.videoWidth);
const newHeight = <number>((platform.isFirefoxBrowser() || platform.isFirefoxMobileBrowser()) ? firefoxSettings.height : publisher.videoReference.videoHeight);
resolve({ newWidth, newHeight });
}
});
};
const repeatUntilChange = setInterval(() => {
getNewVideoDimensions().then(newDimensions => {
sendStreamPropertyChangedEvent(oldWidth, oldHeight, newDimensions.newWidth, newDimensions.newHeight);
});
}, 75);
const sendStreamPropertyChangedEvent = (oldWidth, oldHeight, newWidth, newHeight) => {
attempts++;
if (attempts > 10) {
clearTimeout(repeatUntilChange);
}
if (newWidth !== oldWidth || newHeight !== oldHeight) {
publisher.stream.videoDimensions = {
width: newWidth || 0,
height: newHeight || 0
};
this.sendRequest(
'streamPropertyChanged',
{
streamId: publisher.stream.streamId,
property: 'videoDimensions',
newValue: JSON.stringify(publisher.stream.videoDimensions),
reason: 'deviceRotated'
},
(error, response) => {
if (error) {
logger.error("Error sending 'streamPropertyChanged' event", error);
} else {
this.session.emitEvent('streamPropertyChanged', [new StreamPropertyChangedEvent(this.session, publisher.stream, 'videoDimensions', publisher.stream.videoDimensions, { width: oldWidth, height: oldHeight }, 'deviceRotated')]);
publisher.emitEvent('streamPropertyChanged', [new StreamPropertyChangedEvent(publisher, publisher.stream, 'videoDimensions', publisher.stream.videoDimensions, { width: oldWidth, height: oldHeight }, 'deviceRotated')]);
this.session.sendVideoData(publisher);
}
});
clearTimeout(repeatUntilChange);
}
};
if (publisher.stream.isLocalStreamPublished && !!publisher.stream && !!publisher.stream.hasVideo) {
this.sendNewVideoDimensionsIfRequired(publisher, 'deviceRotated', 75, 10);
}
});
});
}
}
sendNewVideoDimensionsIfRequired(publisher: Publisher, reason: string, WAIT_INTERVAL: number, MAX_ATTEMPTS: number) {
let attempts = 0;
const oldWidth = publisher.stream.videoDimensions.width;
const oldHeight = publisher.stream.videoDimensions.height;
const repeatUntilChangeOrMaxAttempts: NodeJS.Timeout = setInterval(() => {
attempts++;
if (attempts > MAX_ATTEMPTS) {
clearTimeout(repeatUntilChangeOrMaxAttempts);
}
publisher.getVideoDimensions(publisher.stream.getMediaStream()).then(newDimensions => {
if (newDimensions.width !== oldWidth || newDimensions.height !== oldHeight) {
clearTimeout(repeatUntilChangeOrMaxAttempts);
this.sendVideoDimensionsChangedEvent(publisher, reason, oldWidth, oldHeight, newDimensions.width, newDimensions.height);
}
});
}, WAIT_INTERVAL);
}
sendVideoDimensionsChangedEvent(publisher: Publisher, reason: string, oldWidth: number, oldHeight: number, newWidth: number, newHeight: number) {
publisher.stream.videoDimensions = {
width: newWidth || 0,
height: newHeight || 0
};
this.sendRequest(
'streamPropertyChanged',
{
streamId: publisher.stream.streamId,
property: 'videoDimensions',
newValue: JSON.stringify(publisher.stream.videoDimensions),
reason
},
(error, response) => {
if (error) {
logger.error("Error sending 'streamPropertyChanged' event", error);
} else {
this.session.emitEvent('streamPropertyChanged', [new StreamPropertyChangedEvent(this.session, publisher.stream, 'videoDimensions', publisher.stream.videoDimensions, { width: oldWidth, height: oldHeight }, reason)]);
publisher.emitEvent('streamPropertyChanged', [new StreamPropertyChangedEvent(publisher, publisher.stream, 'videoDimensions', publisher.stream.videoDimensions, { width: oldWidth, height: oldHeight }, reason)]);
this.session.sendVideoData(publisher);
}
});
};
/**
* Returns new session

View File

@ -51,7 +51,7 @@ let platform: PlatformUtils;
* - accessDialogClosed
* - streamCreated ([[StreamEvent]])
* - streamDestroyed ([[StreamEvent]])
* - streamPropertyChanged ([[StreamPropertyChangedEvent]])
* - _All events inherited from [[StreamManager]] class_
*/
export class Publisher extends StreamManager {
@ -295,59 +295,86 @@ export class Publisher extends StreamManager {
*
* You can get this new MediaStreamTrack by using the native Web API or simply with [[OpenVidu.getUserMedia]] method.
*
* **WARNING: this method has been proven to work, but there may be some combinations of published/replaced tracks that may be incompatible between them and break the connection in OpenVidu Server. A complete renegotiation may be the only solution in this case**
* **WARNING: this method has been proven to work in the majority of cases, but there may be some combinations of published/replaced tracks that may be incompatible
* between them and break the connection in OpenVidu Server. A complete renegotiation may be the only solution in this case.
* Visit [RTCRtpSender.replaceTrack](https://developer.mozilla.org/en-US/docs/Web/API/RTCRtpSender/replaceTrack) documentation for further details.**
*
* @param track The [MediaStreamTrack](https://developer.mozilla.org/en-US/docs/Web/API/MediaStreamTrack) object to replace the current one. If it is an audio track, the current audio track will be the replaced one. If it
* is a video track, the current video track will be the replaced one.
* @param track The [MediaStreamTrack](https://developer.mozilla.org/en-US/docs/Web/API/MediaStreamTrack) object to replace the current one.
* If it is an audio track, the current audio track will be the replaced one. If it is a video track, the current video track will be the replaced one.
*
* @returns A Promise (to which you can optionally subscribe to) that is resolved if the track was successfully replaced and rejected with an Error object in other case
*/
replaceTrack(track: MediaStreamTrack): Promise<any> {
async replaceTrack(track: MediaStreamTrack): Promise<void> {
const replaceMediaStreamTrack = () => {
const mediaStream: MediaStream = this.stream.displayMyRemote() ? this.stream.localMediaStreamWhenSubscribedToRemote! : this.stream.getMediaStream();
let removedTrack: MediaStreamTrack;
if (track.kind === 'video') {
removedTrack = mediaStream.getVideoTracks()[0];
} else {
removedTrack = mediaStream.getAudioTracks()[0];
}
mediaStream.removeTrack(removedTrack);
removedTrack.stop();
mediaStream.addTrack(track);
this.session.sendVideoData(this.stream.streamManager, 5, true, 5);
const replaceTrackInMediaStream = (): Promise<void> => {
return new Promise((resolve, reject) => {
const mediaStream: MediaStream = this.stream.displayMyRemote() ? this.stream.localMediaStreamWhenSubscribedToRemote! : this.stream.getMediaStream();
let removedTrack: MediaStreamTrack;
if (track.kind === 'video') {
removedTrack = mediaStream.getVideoTracks()[0];
} else {
removedTrack = mediaStream.getAudioTracks()[0];
}
mediaStream.removeTrack(removedTrack);
removedTrack.stop();
mediaStream.addTrack(track);
if (track.kind === 'video' && this.stream.isLocalStreamPublished) {
this.openvidu.sendNewVideoDimensionsIfRequired(this, 'trackReplaced', 50, 30);
this.session.sendVideoData(this.stream.streamManager, 5, true, 5);
}
resolve();
});
}
return new Promise((resolve, reject) => {
if (this.stream.isLocalStreamPublished) {
// Only if the Publisher has been published is necessary to call native Web API RTCRtpSender.replaceTrack
const replaceTrackInRtcRtpSender = (): Promise<void> => {
return new Promise((resolve, reject) => {
const senders: RTCRtpSender[] = this.stream.getRTCPeerConnection().getSenders();
let sender: RTCRtpSender | undefined;
if (track.kind === 'video') {
sender = senders.find(s => !!s.track && s.track.kind === 'video');
if (!sender) {
reject(new Error('There\'s no replaceable track for that kind of MediaStreamTrack in this Publisher object'))
reject(new Error('There\'s no replaceable track for that kind of MediaStreamTrack in this Publisher object'));
return;
}
} else if (track.kind === 'audio') {
sender = senders.find(s => !!s.track && s.track.kind === 'audio');
if (!sender) {
reject(new Error('There\'s no replaceable track for that kind of MediaStreamTrack in this Publisher object'))
reject(new Error('There\'s no replaceable track for that kind of MediaStreamTrack in this Publisher object'));
return;
}
} else {
reject(new Error('Unknown track kind ' + track.kind));
return;
}
(<any>sender).replaceTrack(track).then(() => {
replaceMediaStreamTrack();
(sender as RTCRtpSender).replaceTrack(track).then(() => {
resolve();
}).catch(error => {
reject(error);
});
});
}
// Set field "enabled" of the new track to the previous value
const trackOriginalEnabledValue: boolean = track.enabled;
if (track.kind === 'video') {
track.enabled = this.stream.videoActive;
} else if (track.kind === 'audio') {
track.enabled = this.stream.audioActive;
}
try {
if (this.stream.isLocalStreamPublished) {
// Only if the Publisher has been published is necessary to call native Web API RTCRtpSender.replaceTrack
// If it has not been published yet, replacing it on the MediaStream object is enough
await replaceTrackInRtcRtpSender();
return await replaceTrackInMediaStream();
} else {
// Publisher not published. Simply modify local MediaStream tracks
replaceMediaStreamTrack();
resolve();
// Publisher not published. Simply replace the track on the local MediaStream
return await replaceTrackInMediaStream();
}
});
} catch (error) {
track.enabled = trackOriginalEnabledValue;
throw error;
}
}
/* Hidden methods */
@ -355,12 +382,12 @@ export class Publisher extends StreamManager {
/**
* @hidden
*/
initialize(): Promise<any> {
initialize(): Promise<void> {
return new Promise((resolve, reject) => {
let constraints: MediaStreamConstraints = {};
let constraintsAux: MediaStreamConstraints = {};
const timeForDialogEvent = 1250;
const timeForDialogEvent = 1500;
let startTime;
const errorCallback = (openViduError: OpenViduError) => {
@ -403,102 +430,38 @@ export class Publisher extends StreamManager {
delete this.firstVideoElement;
if (this.stream.isSendVideo()) {
if (!this.stream.isSendScreen()) {
// Has video track
this.getVideoDimensions(mediaStream).then(dimensions => {
this.stream.videoDimensions = {
width: dimensions.width,
height: dimensions.height
};
if (platform.isIonicIos() || platform.isSafariBrowser()) {
// iOS Ionic or Safari. Limitation: cannot set videoDimensions directly, as the videoReference is not loaded
// if not added to DOM. Must add it to DOM and wait for videoWidth and videoHeight properties to be defined
this.videoReference.style.display = 'none';
document.body.appendChild(this.videoReference);
const videoDimensionsSet = () => {
this.stream.videoDimensions = {
width: this.videoReference.videoWidth,
height: this.videoReference.videoHeight
};
this.stream.isLocalStreamReadyToPublish = true;
this.stream.ee.emitEvent('stream-ready-to-publish', []);
document.body.removeChild(this.videoReference);
};
let interval;
this.videoReference.addEventListener('loadedmetadata', () => {
if (this.videoReference.videoWidth === 0) {
interval = setInterval(() => {
if (this.videoReference.videoWidth !== 0) {
clearInterval(interval);
videoDimensionsSet();
}
}, 40);
} else {
videoDimensionsSet();
}
});
} else {
// Rest of platforms
// With no screen share, video dimension can be set directly from MediaStream (getSettings)
// Orientation must be checked for mobile devices (width and height are reversed)
const { width, height } = this.getVideoDimensions(mediaStream);
if (platform.isMobileDevice() && (window.innerHeight > window.innerWidth)) {
// Mobile portrait mode
this.stream.videoDimensions = {
width: height || 0,
height: width || 0
};
} else {
this.stream.videoDimensions = {
width: width || 0,
height: height || 0
};
}
this.stream.isLocalStreamReadyToPublish = true;
this.stream.ee.emitEvent('stream-ready-to-publish', []);
}
} else {
// With screen share, video dimension must be got from a video element (onloadedmetadata event)
this.videoReference.addEventListener('loadedmetadata', () => {
this.stream.videoDimensions = {
width: this.videoReference.videoWidth,
height: this.videoReference.videoHeight
};
if (this.stream.isSendScreen()) {
// Set interval to listen for screen resize events
this.screenShareResizeInterval = setInterval(() => {
const firefoxSettings = mediaStream.getVideoTracks()[0].getSettings();
const newWidth = (platform.isChromeBrowser() || platform.isOperaBrowser()) ? this.videoReference.videoWidth : firefoxSettings.width;
const newHeight = (platform.isChromeBrowser() || platform.isOperaBrowser()) ? this.videoReference.videoHeight : firefoxSettings.height;
const settings: MediaTrackSettings = mediaStream.getVideoTracks()[0].getSettings();
const newWidth = settings.width;
const newHeight = settings.height;
if (this.stream.isLocalStreamPublished &&
(newWidth !== this.stream.videoDimensions.width ||
newHeight !== this.stream.videoDimensions.height)) {
const oldValue = { width: this.stream.videoDimensions.width, height: this.stream.videoDimensions.height };
this.stream.videoDimensions = {
width: newWidth || 0,
height: newHeight || 0
};
this.session.openvidu.sendRequest(
'streamPropertyChanged',
{
streamId: this.stream.streamId,
property: 'videoDimensions',
newValue: JSON.stringify(this.stream.videoDimensions),
reason: 'screenResized'
},
(error, response) => {
if (error) {
logger.error("Error sending 'streamPropertyChanged' event", error);
} else {
this.session.emitEvent('streamPropertyChanged', [new StreamPropertyChangedEvent(this.session, this.stream, 'videoDimensions', this.stream.videoDimensions, oldValue, 'screenResized')]);
this.emitEvent('streamPropertyChanged', [new StreamPropertyChangedEvent(this, this.stream, 'videoDimensions', this.stream.videoDimensions, oldValue, 'screenResized')]);
this.session.sendVideoData(this.stream.streamManager);
}
});
(newWidth !== this.stream.videoDimensions.width || newHeight !== this.stream.videoDimensions.height)) {
this.openvidu.sendVideoDimensionsChangedEvent(
this,
'screenResized',
this.stream.videoDimensions.width,
this.stream.videoDimensions.height,
newWidth || 0,
newHeight || 0
);
}
}, 500);
this.stream.isLocalStreamReadyToPublish = true;
this.stream.ee.emitEvent('stream-ready-to-publish', []);
});
}
}, 650);
}
this.stream.isLocalStreamReadyToPublish = true;
this.stream.ee.emitEvent('stream-ready-to-publish', []);
});
} else {
// Only audio track (no videoDimensions)
this.stream.isLocalStreamReadyToPublish = true;
this.stream.ee.emitEvent('stream-ready-to-publish', []);
}
@ -665,9 +628,66 @@ export class Publisher extends StreamManager {
/**
* @hidden
*
* To obtain the videoDimensions we wait for the video reference to have enough metadata
* and then try to use MediaStreamTrack.getSettingsMethod(). If not available, then we
* use the HTMLVideoElement properties videoWidth and videoHeight
*/
getVideoDimensions(mediaStream: MediaStream): MediaTrackSettings {
return mediaStream.getVideoTracks()[0].getSettings();
getVideoDimensions(mediaStream: MediaStream): Promise<{ width: number, height: number }> {
return new Promise((resolve, reject) => {
// Ionic iOS and Safari iOS supposedly require the video element to actually exist inside the DOM
const requiresDomInsertion: boolean = platform.isIonicIos() || platform.isIOSWithSafari();
let loadedmetadataListener;
const resolveDimensions = () => {
let width: number;
let height: number;
if (typeof this.stream.getMediaStream().getVideoTracks()[0].getSettings === 'function') {
const settings = this.stream.getMediaStream().getVideoTracks()[0].getSettings();
width = settings.width || this.videoReference.videoWidth;
height = settings.height || this.videoReference.videoHeight;
} else {
logger.warn('MediaStreamTrack does not have getSettings method on ' + platform.getDescription());
width = this.videoReference.videoWidth;
height = this.videoReference.videoHeight;
}
if (loadedmetadataListener != null) {
this.videoReference.removeEventListener('loadedmetadata', loadedmetadataListener);
}
if (requiresDomInsertion) {
document.body.removeChild(this.videoReference);
}
resolve({ width, height });
}
if (this.videoReference.readyState >= 1) {
// The video already has metadata available
// No need of loadedmetadata event
resolveDimensions();
} else {
// The video does not have metadata available yet
// Must listen to loadedmetadata event
loadedmetadataListener = () => {
if (!this.videoReference.videoWidth) {
let interval = setInterval(() => {
if (!!this.videoReference.videoWidth) {
clearInterval(interval);
resolveDimensions();
}
}, 40);
} else {
resolveDimensions();
}
};
this.videoReference.addEventListener('loadedmetadata', loadedmetadataListener);
if (requiresDomInsertion) {
document.body.appendChild(this.videoReference);
}
}
});
}
/**
@ -684,17 +704,15 @@ export class Publisher extends StreamManager {
*/
initializeVideoReference(mediaStream: MediaStream) {
this.videoReference = document.createElement('video');
this.videoReference.setAttribute('muted', 'true');
this.videoReference.style.display = 'none';
if (platform.isSafariBrowser()) {
this.videoReference.setAttribute('playsinline', 'true');
}
this.stream.setMediaStream(mediaStream);
if (!!this.firstVideoElement) {
this.createVideoElement(this.firstVideoElement.targetElement, <VideoInsertMode>this.properties.insertMode);
}
this.videoReference.srcObject = mediaStream;
}

View File

@ -28,9 +28,9 @@ import { SignalOptions } from '../OpenViduInternal/Interfaces/Public/SignalOptio
import { SubscriberProperties } from '../OpenViduInternal/Interfaces/Public/SubscriberProperties';
import { RemoteConnectionOptions } from '../OpenViduInternal/Interfaces/Private/RemoteConnectionOptions';
import { LocalConnectionOptions } from '../OpenViduInternal/Interfaces/Private/LocalConnectionOptions';
import { ObjMap } from '../OpenViduInternal/Interfaces/Private/ObjMap';
import { SessionOptions } from '../OpenViduInternal/Interfaces/Private/SessionOptions';
import { ConnectionEvent } from '../OpenViduInternal/Events/ConnectionEvent';
import { ExceptionEvent } from '../OpenViduInternal/Events/ExceptionEvent';
import { FilterEvent } from '../OpenViduInternal/Events/FilterEvent';
import { PublisherSpeakingEvent } from '../OpenViduInternal/Events/PublisherSpeakingEvent';
import { RecordingEvent } from '../OpenViduInternal/Events/RecordingEvent';
@ -77,6 +77,7 @@ let platform: PlatformUtils;
* - networkQualityLevelChanged ([[NetworkQualityLevelChangedEvent]])
* - reconnecting
* - reconnected
* - exception ([[ExceptionEvent]])
*/
export class Session extends EventDispatcher {
@ -105,20 +106,12 @@ export class Session extends EventDispatcher {
/**
* @hidden
*/
remoteStreamsCreated: ObjMap<boolean> = {};
remoteStreamsCreated: Map<string, boolean> = new Map();
/**
* @hidden
*/
isFirstIonicIosSubscriber = true;
/**
* @hidden
*/
countDownForIonicIosSubscribersActive = true;
/**
* @hidden
*/
remoteConnections: ObjMap<Connection> = {};
remoteConnections: Map<string, Connection> = new Map();
/**
* @hidden
*/
@ -127,22 +120,6 @@ export class Session extends EventDispatcher {
* @hidden
*/
options: SessionOptions;
/**
* @hidden
*/
startSpeakingEventsEnabled = false;
/**
* @hidden
*/
startSpeakingEventsEnabledOnce = false;
/**
* @hidden
*/
stopSpeakingEventsEnabled = false;
/**
* @hidden
*/
stopSpeakingEventsEnabledOnce = false;
/**
* @hidden
*/
@ -191,7 +168,7 @@ export class Session extends EventDispatcher {
* @returns A Promise to which you must subscribe that is resolved if the the connection to the Session was successful and rejected with an Error object if not
*
*/
connect(token: string, metadata?: any): Promise<any> {
connect(token: string, metadata?: any): Promise<void> {
return new Promise((resolve, reject) => {
this.processToken(token);
@ -281,13 +258,20 @@ export class Session extends EventDispatcher {
};
}
let completionHandler: (error: Error | undefined) => void;
let completionHandler: ((error: Error | undefined) => void) | undefined = undefined;
if (!!param3 && (typeof param3 === 'function')) {
completionHandler = param3;
} else if (!!param4) {
completionHandler = param4;
}
if (!this.sessionConnected()) {
if (completionHandler !== undefined) {
completionHandler(this.notConnectedError());
}
throw this.notConnectedError();
}
logger.info('Subscribing to ' + stream.connection.connectionId);
stream.subscribe()
@ -319,6 +303,10 @@ export class Session extends EventDispatcher {
subscribeAsync(stream: Stream, targetElement: string | HTMLElement, properties?: SubscriberProperties): Promise<Subscriber> {
return new Promise<Subscriber>((resolve, reject) => {
if (!this.sessionConnected()) {
reject(this.notConnectedError());
}
let subscriber: Subscriber;
const callback = (error: Error) => {
@ -349,25 +337,35 @@ export class Session extends EventDispatcher {
*
* See [[VideoElementEvent]] to learn more
*/
unsubscribe(subscriber: Subscriber): void {
const connectionId = subscriber.stream.connection.connectionId;
unsubscribe(subscriber: Subscriber): Promise<void> {
logger.info('Unsubscribing from ' + connectionId);
return new Promise((resolve, reject) => {
this.openvidu.sendRequest(
'unsubscribeFromVideo',
{ sender: subscriber.stream.connection.connectionId },
(error, response) => {
if (error) {
logger.error('Error unsubscribing from ' + connectionId, error);
} else {
logger.info('Unsubscribed correctly from ' + connectionId);
}
subscriber.stream.disposeWebRtcPeer();
subscriber.stream.disposeMediaStream();
if (!this.sessionConnected()) {
reject(this.notConnectedError());
} else {
const connectionId = subscriber.stream.connection.connectionId;
logger.info('Unsubscribing from ' + connectionId);
this.openvidu.sendRequest(
'unsubscribeFromVideo',
{ sender: subscriber.stream.connection.connectionId },
(error, response) => {
if (error) {
logger.error('Error unsubscribing from ' + connectionId);
reject(error);
} else {
logger.info('Unsubscribed correctly from ' + connectionId);
subscriber.stream.streamManager.removeAllVideos();
subscriber.stream.disposeWebRtcPeer();
subscriber.stream.disposeMediaStream();
resolve();
}
}
);
}
);
subscriber.stream.streamManager.removeAllVideos();
});
}
@ -384,8 +382,13 @@ export class Session extends EventDispatcher {
*
* @returns A Promise (to which you can optionally subscribe to) that is resolved only after the publisher was successfully published and rejected with an Error object if not
*/
publish(publisher: Publisher): Promise<any> {
publish(publisher: Publisher): Promise<void> {
return new Promise((resolve, reject) => {
if (!this.sessionConnected()) {
reject(this.notConnectedError());
}
publisher.session = this;
publisher.stream.session = this;
@ -441,36 +444,47 @@ export class Session extends EventDispatcher {
*
* See [[StreamEvent]] and [[VideoElementEvent]] to learn more.
*/
unpublish(publisher: Publisher): void {
unpublish(publisher: Publisher): Promise<void> {
const stream = publisher.stream;
return new Promise((resolve, reject) => {
if (!stream.connection) {
logger.error('The associated Connection object of this Publisher is null', stream);
return;
} else if (stream.connection !== this.connection) {
logger.error('The associated Connection object of this Publisher is not your local Connection.' +
"Only moderators can force unpublish on remote Streams via 'forceUnpublish' method", stream);
return;
} else {
if (!this.sessionConnected()) {
throw this.notConnectedError()
}
logger.info('Unpublishing local media (' + stream.connection.connectionId + ')');
const stream = publisher.stream;
this.openvidu.sendRequest('unpublishVideo', (error, response) => {
if (error) {
logger.error(error);
} else {
logger.info('Media unpublished correctly');
}
});
if (!stream.connection) {
reject(new Error('The associated Connection object of this Publisher is null'));
} else if (stream.connection !== this.connection) {
reject(new Error('The associated Connection object of this Publisher is not your local Connection.' +
"Only moderators can force unpublish on remote Streams via 'forceUnpublish' method"));
} else {
stream.disposeWebRtcPeer();
delete stream.connection.stream;
logger.info('Unpublishing local media (' + stream.connection.connectionId + ')');
const streamEvent = new StreamEvent(true, publisher, 'streamDestroyed', publisher.stream, 'unpublish');
publisher.emitEvent('streamDestroyed', [streamEvent]);
streamEvent.callDefaultBehavior();
}
this.openvidu.sendRequest('unpublishVideo', (error, response) => {
if (error) {
reject(error);
} else {
logger.info('Media unpublished correctly');
stream.disposeWebRtcPeer();
if (stream.connection.stream == stream) {
// The Connection.stream may have changed if Session.publish was called with other Publisher
delete stream.connection.stream;
}
const streamEvent = new StreamEvent(true, publisher, 'streamDestroyed', publisher.stream, 'unpublish');
publisher.emitEvent('streamDestroyed', [streamEvent]);
streamEvent.callDefaultBehavior();
resolve();
}
});
}
});
}
@ -491,8 +505,13 @@ export class Session extends EventDispatcher {
*
* @returns A Promise (to which you can optionally subscribe to) that is resolved only after the participant has been successfully evicted from the session and rejected with an Error object if not
*/
forceDisconnect(connection: Connection): Promise<any> {
forceDisconnect(connection: Connection): Promise<void> {
return new Promise((resolve, reject) => {
if (!this.sessionConnected()) {
reject(this.notConnectedError());
}
logger.info('Forcing disconnect for connection ' + connection.connectionId);
this.openvidu.sendRequest(
'forceDisconnect',
@ -530,8 +549,13 @@ export class Session extends EventDispatcher {
*
* @returns A Promise (to which you can optionally subscribe to) that is resolved only after the remote Stream has been successfully unpublished from the session and rejected with an Error object if not
*/
forceUnpublish(stream: Stream): Promise<any> {
forceUnpublish(stream: Stream): Promise<void> {
return new Promise((resolve, reject) => {
if (!this.sessionConnected()) {
reject(this.notConnectedError());
}
logger.info('Forcing unpublish for stream ' + stream.streamId);
this.openvidu.sendRequest(
'forceUnpublish',
@ -566,9 +590,13 @@ export class Session extends EventDispatcher {
* mean that openvidu-server could resend the message to all the listed receivers._
*/
/* tslint:disable:no-string-literal */
signal(signal: SignalOptions): Promise<any> {
signal(signal: SignalOptions): Promise<void> {
return new Promise((resolve, reject) => {
if (!this.sessionConnected()) {
reject(this.notConnectedError());
}
const signalMessage = {};
if (signal.to && signal.to.length > 0) {
@ -610,28 +638,32 @@ export class Session extends EventDispatcher {
/**
* See [[EventDispatcher.on]]
*/
on(type: string, handler: (event: SessionDisconnectedEvent | SignalEvent | StreamEvent | ConnectionEvent | PublisherSpeakingEvent | RecordingEvent | NetworkQualityLevelChangedEvent) => void): EventDispatcher {
on(type: string, handler: (event: SessionDisconnectedEvent | SignalEvent | StreamEvent | ConnectionEvent | PublisherSpeakingEvent | RecordingEvent | NetworkQualityLevelChangedEvent | ExceptionEvent) => void): EventDispatcher {
super.onAux(type, "Event '" + type + "' triggered by 'Session'", handler);
if (type === 'publisherStartSpeaking') {
this.startSpeakingEventsEnabled = true;
// If there are already available remote streams, enable hark 'speaking' event in all of them
for (const connectionId in this.remoteConnections) {
const str = this.remoteConnections[connectionId].stream;
if (!!str && str.hasAudio) {
str.enableStartSpeakingEvent();
// If there are already available remote streams with audio, enable hark 'speaking' event in all of them
this.remoteConnections.forEach(remoteConnection => {
if (!!remoteConnection.stream?.hasAudio) {
remoteConnection.stream.enableHarkSpeakingEvent();
}
});
if (!!this.connection?.stream?.hasAudio) {
// If connected to the Session and publishing with audio, also enable hark 'speaking' event for the Publisher
this.connection.stream.enableHarkSpeakingEvent();
}
}
if (type === 'publisherStopSpeaking') {
this.stopSpeakingEventsEnabled = true;
// If there are already available remote streams, enable hark 'stopped_speaking' event in all of them
for (const connectionId in this.remoteConnections) {
const str = this.remoteConnections[connectionId].stream;
if (!!str && str.hasAudio) {
str.enableStopSpeakingEvent();
// If there are already available remote streams with audio, enable hark 'stopped_speaking' event in all of them
this.remoteConnections.forEach(remoteConnection => {
if (!!remoteConnection.stream?.hasAudio) {
remoteConnection.stream.enableHarkStoppedSpeakingEvent();
}
});
if (!!this.connection?.stream?.hasAudio) {
// If connected to the Session and publishing with audio, also enable hark 'stopped_speaking' event for the Publisher
this.connection.stream.enableHarkStoppedSpeakingEvent();
}
}
@ -642,28 +674,32 @@ export class Session extends EventDispatcher {
/**
* See [[EventDispatcher.once]]
*/
once(type: string, handler: (event: SessionDisconnectedEvent | SignalEvent | StreamEvent | ConnectionEvent | PublisherSpeakingEvent | RecordingEvent | NetworkQualityLevelChangedEvent) => void): Session {
once(type: string, handler: (event: SessionDisconnectedEvent | SignalEvent | StreamEvent | ConnectionEvent | PublisherSpeakingEvent | RecordingEvent | NetworkQualityLevelChangedEvent | ExceptionEvent) => void): Session {
super.onceAux(type, "Event '" + type + "' triggered once by 'Session'", handler);
if (type === 'publisherStartSpeaking') {
this.startSpeakingEventsEnabledOnce = true;
// If there are already available remote streams, enable hark 'speaking' event in all of them once
for (const connectionId in this.remoteConnections) {
const str = this.remoteConnections[connectionId].stream;
if (!!str && str.hasAudio) {
str.enableOnceStartSpeakingEvent();
// If there are already available remote streams with audio, enable hark 'speaking' event (once) in all of them once
this.remoteConnections.forEach(remoteConnection => {
if (!!remoteConnection.stream?.hasAudio) {
remoteConnection.stream.enableOnceHarkSpeakingEvent();
}
});
if (!!this.connection?.stream?.hasAudio) {
// If connected to the Session and publishing with audio, also enable hark 'speaking' event (once) for the Publisher
this.connection.stream.enableOnceHarkSpeakingEvent();
}
}
if (type === 'publisherStopSpeaking') {
this.stopSpeakingEventsEnabledOnce = true;
// If there are already available remote streams, enable hark 'stopped_speaking' event in all of them once
for (const connectionId in this.remoteConnections) {
const str = this.remoteConnections[connectionId].stream;
if (!!str && str.hasAudio) {
str.enableOnceStopSpeakingEvent();
// If there are already available remote streams with audio, enable hark 'stopped_speaking' event (once) in all of them once
this.remoteConnections.forEach(remoteConnection => {
if (!!remoteConnection.stream?.hasAudio) {
remoteConnection.stream.enableOnceHarkStoppedSpeakingEvent();
}
});
if (!!this.connection?.stream?.hasAudio) {
// If connected to the Session and publishing with audio, also enable hark 'stopped_speaking' event (once) for the Publisher
this.connection.stream.enableOnceHarkStoppedSpeakingEvent();
}
}
@ -674,32 +710,44 @@ export class Session extends EventDispatcher {
/**
* See [[EventDispatcher.off]]
*/
off(type: string, handler?: (event: SessionDisconnectedEvent | SignalEvent | StreamEvent | ConnectionEvent | PublisherSpeakingEvent | RecordingEvent | NetworkQualityLevelChangedEvent) => void): Session {
off(type: string, handler?: (event: SessionDisconnectedEvent | SignalEvent | StreamEvent | ConnectionEvent | PublisherSpeakingEvent | RecordingEvent | NetworkQualityLevelChangedEvent | ExceptionEvent) => void): Session {
super.off(type, handler);
if (type === 'publisherStartSpeaking') {
let remainingStartSpeakingListeners = this.ee.getListeners(type).length;
if (remainingStartSpeakingListeners === 0) {
this.startSpeakingEventsEnabled = false;
// If there are already available remote streams, disable hark in all of them
for (const connectionId in this.remoteConnections) {
const str = this.remoteConnections[connectionId].stream;
if (!!str) {
str.disableStartSpeakingEvent(false);
// Check if Session object still has some listener for the event
if (!this.anySpeechEventListenerEnabled('publisherStartSpeaking', false)) {
this.remoteConnections.forEach(remoteConnection => {
if (!!remoteConnection.stream?.streamManager) {
// Check if Subscriber object still has some listener for the event
if (!this.anySpeechEventListenerEnabled('publisherStartSpeaking', false, remoteConnection.stream.streamManager)) {
remoteConnection.stream.disableHarkSpeakingEvent(false);
}
}
});
if (!!this.connection?.stream?.streamManager) {
// Check if Publisher object still has some listener for the event
if (!this.anySpeechEventListenerEnabled('publisherStartSpeaking', false, this.connection.stream.streamManager)) {
this.connection.stream.disableHarkSpeakingEvent(false);
}
}
}
}
if (type === 'publisherStopSpeaking') {
let remainingStopSpeakingListeners = this.ee.getListeners(type).length;
if (remainingStopSpeakingListeners === 0) {
this.stopSpeakingEventsEnabled = false;
// If there are already available remote streams, disable hark in all of them
for (const connectionId in this.remoteConnections) {
const str = this.remoteConnections[connectionId].stream;
if (!!str) {
str.disableStopSpeakingEvent(false);
// Check if Session object still has some listener for the event
if (!this.anySpeechEventListenerEnabled('publisherStopSpeaking', false)) {
this.remoteConnections.forEach(remoteConnection => {
if (!!remoteConnection.stream?.streamManager) {
// Check if Subscriber object still has some listener for the event
if (!this.anySpeechEventListenerEnabled('publisherStopSpeaking', false, remoteConnection.stream.streamManager)) {
remoteConnection.stream.disableHarkStoppedSpeakingEvent(false);
}
}
});
if (!!this.connection?.stream?.streamManager) {
// Check if Publisher object still has some listener for the event
if (!this.anySpeechEventListenerEnabled('publisherStopSpeaking', false, this.connection.stream.streamManager)) {
this.connection.stream.disableHarkStoppedSpeakingEvent(false);
}
}
}
@ -722,7 +770,7 @@ export class Session extends EventDispatcher {
})
.catch(openViduError => {
const connection = new Connection(this, response);
this.remoteConnections[response.id] = connection;
this.remoteConnections.set(response.id, connection);
this.ee.emitEvent('connectionCreated', [new ConnectionEvent(false, this, 'connectionCreated', connection, '')]);
});
}
@ -731,10 +779,9 @@ export class Session extends EventDispatcher {
* @hidden
*/
onParticipantLeft(msg): void {
this.getRemoteConnection(msg.connectionId, 'Remote connection ' + msg.connectionId + " unknown when 'onParticipantLeft'. " +
'Existing remote connections: ' + JSON.stringify(Object.keys(this.remoteConnections)))
.then(connection => {
if (this.remoteConnections.size > 0) {
this.getRemoteConnection(msg.connectionId).then(connection => {
if (!!connection.stream) {
const stream = connection.stream;
@ -742,19 +789,15 @@ export class Session extends EventDispatcher {
this.ee.emitEvent('streamDestroyed', [streamEvent]);
streamEvent.callDefaultBehavior();
delete this.remoteStreamsCreated[stream.streamId];
if (Object.keys(this.remoteStreamsCreated).length === 0) {
this.isFirstIonicIosSubscriber = true;
this.countDownForIonicIosSubscribersActive = true;
}
this.remoteStreamsCreated.delete(stream.streamId);
}
delete this.remoteConnections[connection.connectionId];
this.remoteConnections.delete(connection.connectionId);
this.ee.emitEvent('connectionDestroyed', [new ConnectionEvent(false, this, 'connectionDestroyed', connection, msg.reason)]);
})
.catch(openViduError => {
logger.error(openViduError);
});
.catch(openViduError => {
logger.error(openViduError);
});
}
}
/**
@ -764,9 +807,9 @@ export class Session extends EventDispatcher {
const afterConnectionFound = (connection) => {
this.remoteConnections[connection.connectionId] = connection;
this.remoteConnections.set(connection.connectionId, connection);
if (!this.remoteStreamsCreated[connection.stream.streamId]) {
if (!this.remoteStreamsCreated.get(connection.stream.streamId)) {
// Avoid race condition between stream.subscribe() in "onParticipantPublished" and in "joinRoom" rpc callback
// This condition is false if openvidu-server sends "participantPublished" event to a subscriber participant that has
// already subscribed to certain stream in the callback of "joinRoom" method
@ -774,14 +817,13 @@ export class Session extends EventDispatcher {
this.ee.emitEvent('streamCreated', [new StreamEvent(false, this, 'streamCreated', connection.stream, '')]);
}
this.remoteStreamsCreated[connection.stream.streamId] = true;
this.remoteStreamsCreated.set(connection.stream.streamId, true);
};
// Get the existing Connection created on 'onParticipantJoined' for
// existing participants or create a new one for new participants
let connection: Connection;
this.getRemoteConnection(response.id, "Remote connection '" + response.id + "' unknown when 'onParticipantPublished'. " +
'Existing remote connections: ' + JSON.stringify(Object.keys(this.remoteConnections)))
this.getRemoteConnection(response.id)
.then(con => {
// Update existing Connection
@ -806,8 +848,7 @@ export class Session extends EventDispatcher {
// Your stream has been forcedly unpublished from the session
this.stopPublisherStream(msg.reason);
} else {
this.getRemoteConnection(msg.connectionId, "Remote connection '" + msg.connectionId + "' unknown when 'onParticipantUnpublished'. " +
'Existing remote connections: ' + JSON.stringify(Object.keys(this.remoteConnections)))
this.getRemoteConnection(msg.connectionId)
.then(connection => {
@ -817,12 +858,7 @@ export class Session extends EventDispatcher {
// Deleting the remote stream
const streamId: string = connection.stream!.streamId;
delete this.remoteStreamsCreated[streamId];
if (Object.keys(this.remoteStreamsCreated).length === 0) {
this.isFirstIonicIosSubscriber = true;
this.countDownForIonicIosSubscribersActive = true;
}
this.remoteStreamsCreated.delete(streamId);
connection.removeStream(streamId);
})
@ -855,8 +891,8 @@ export class Session extends EventDispatcher {
if (!!msg.from) {
// Signal sent by other client
this.getConnection(msg.from, "Connection '" + msg.from + "' unknow when 'onNewMessage'. Existing remote connections: "
+ JSON.stringify(Object.keys(this.remoteConnections)) + '. Existing local connection: ' + this.connection.connectionId)
this.getConnection(msg.from, "Connection '" + msg.from + "' unknown when 'onNewMessage'. Existing remote connections: "
+ JSON.stringify(this.remoteConnections.keys()) + '. Existing local connection: ' + this.connection.connectionId)
.then(connection => {
this.ee.emitEvent('signal', [new SignalEvent(this, strippedType, msg.data, connection)]);
@ -929,8 +965,7 @@ export class Session extends EventDispatcher {
// Your stream has been forcedly changed (filter feature)
callback(this.connection);
} else {
this.getRemoteConnection(msg.connectionId, 'Remote connection ' + msg.connectionId + " unknown when 'onStreamPropertyChanged'. " +
'Existing remote connections: ' + JSON.stringify(Object.keys(this.remoteConnections)))
this.getRemoteConnection(msg.connectionId)
.then(connection => {
callback(connection);
})
@ -1087,7 +1122,7 @@ export class Session extends EventDispatcher {
.then(connection => {
logger.info('Filter event dispatched');
const stream: Stream = connection.stream!;
stream.filter!.handlers[response.eventType](new FilterEvent(stream.filter!, response.eventType, response.data));
stream.filter!.handlers.get(response.eventType)?.call(this, new FilterEvent(stream.filter!, response.eventType, response.data));
});
}
@ -1104,13 +1139,13 @@ export class Session extends EventDispatcher {
someReconnection = true;
}
// Re-establish Subscriber streams
for (let remoteConnection of Object.values(this.remoteConnections)) {
this.remoteConnections.forEach(remoteConnection => {
if (!!remoteConnection.stream && remoteConnection.stream.streamIceConnectionStateBroken()) {
logger.warn('Re-establishing Subscriber ' + remoteConnection.stream.streamId);
remoteConnection.stream.initWebRtcPeerReceive(true);
someReconnection = true;
}
}
});
if (!someReconnection) {
logger.info('There were no media streams in need of a reconnection');
}
@ -1155,6 +1190,7 @@ export class Session extends EventDispatcher {
} else {
logger.warn('You were not connected to the session ' + this.sessionId);
}
logger.flush();
}
/**
@ -1172,32 +1208,38 @@ export class Session extends EventDispatcher {
return joinParams;
}
/**
* @hidden
*/
sendVideoData(streamManager: StreamManager, intervalSeconds: number = 1, doInterval: boolean = false, maxLoops: number = 1) {
if (
platform.isChromeBrowser() || platform.isChromeMobileBrowser() || platform.isOperaBrowser() ||
platform.isOperaMobileBrowser() || platform.isEdgeBrowser() || platform.isElectron() ||
platform.isOperaMobileBrowser() || platform.isEdgeBrowser() || platform.isEdgeMobileBrowser() || platform.isElectron() ||
(platform.isSafariBrowser() && !platform.isIonicIos()) || platform.isAndroidBrowser() ||
platform.isSamsungBrowser() || platform.isIonicAndroid() || (platform.isIPhoneOrIPad() && platform.isIOSWithSafari())
platform.isSamsungBrowser() || platform.isIonicAndroid() || platform.isIOSWithSafari()
) {
const obtainAndSendVideo = async () => {
const statsMap = await streamManager.stream.getRTCPeerConnection().getStats();
const arr: any[] = [];
statsMap.forEach(stats => {
if (("frameWidth" in stats) && ("frameHeight" in stats) && (arr.length === 0)) {
arr.push(stats);
}
});
if (arr.length > 0) {
this.openvidu.sendRequest('videoData', {
height: arr[0].frameHeight,
width: arr[0].frameWidth,
videoActive: streamManager.stream.videoActive != null ? streamManager.stream.videoActive : false,
audioActive: streamManager.stream.audioActive != null ? streamManager.stream.audioActive : false
}, (error, response) => {
if (error) {
logger.error("Error sending 'videoData' event", error);
const pc = streamManager.stream.getRTCPeerConnection();
if (pc.connectionState === 'connected') {
const statsMap = await pc.getStats();
const arr: any[] = [];
statsMap.forEach(stats => {
if (("frameWidth" in stats) && ("frameHeight" in stats) && (arr.length === 0)) {
arr.push(stats);
}
});
if (arr.length > 0) {
this.openvidu.sendRequest('videoData', {
height: arr[0].frameHeight,
width: arr[0].frameWidth,
videoActive: streamManager.stream.videoActive != null ? streamManager.stream.videoActive : false,
audioActive: streamManager.stream.audioActive != null ? streamManager.stream.audioActive : false
}, (error, response) => {
if (error) {
logger.error("Error sending 'videoData' event", error);
}
});
}
}
}
if (doInterval) {
@ -1206,7 +1248,7 @@ export class Session extends EventDispatcher {
if (loops < maxLoops) {
loops++;
obtainAndSendVideo();
}else {
} else {
clearInterval(this.videoDataInterval);
}
}, intervalSeconds * 1000);
@ -1230,9 +1272,44 @@ export class Session extends EventDispatcher {
}
}
/**
* @hidden
*/
sessionConnected() {
return this.connection != null;
}
/**
* @hidden
*/
notConnectedError(): OpenViduError {
return new OpenViduError(OpenViduErrorName.OPENVIDU_NOT_CONNECTED, "There is no connection to the session. Method 'Session.connect' must be successfully completed first");
}
/**
* @hidden
*/
anySpeechEventListenerEnabled(event: string, onlyOnce: boolean, streamManager?: StreamManager): boolean {
let handlersInSession = this.ee.getListeners(event);
if (onlyOnce) {
handlersInSession = handlersInSession.filter(h => (h as any).once);
}
let listenersInSession = handlersInSession.length;
if (listenersInSession > 0) return true;
let listenersInStreamManager = 0;
if (!!streamManager) {
let handlersInStreamManager = streamManager.ee.getListeners(event);
if (onlyOnce) {
handlersInStreamManager = handlersInStreamManager.filter(h => (h as any).once);
}
listenersInStreamManager = handlersInStreamManager.length;
}
return listenersInStreamManager > 0;
}
/* Private methods */
private connectAux(token: string): Promise<any> {
private connectAux(token: string): Promise<void> {
return new Promise((resolve, reject) => {
this.openvidu.startWs((error) => {
if (!!error) {
@ -1246,8 +1323,12 @@ export class Session extends EventDispatcher {
reject(error);
} else {
// Process join room response
this.processJoinRoomResponse(response);
// Configure JSNLogs
OpenViduLogger.configureJSNLog(this.openvidu, token);
// Initialize local Connection object with values returned by openvidu-server
this.connection = new Connection(this, response);
@ -1259,10 +1340,10 @@ export class Session extends EventDispatcher {
const existingParticipants: RemoteConnectionOptions[] = response.value;
existingParticipants.forEach((remoteConnectionOptions: RemoteConnectionOptions) => {
const connection = new Connection(this, remoteConnectionOptions);
this.remoteConnections[connection.connectionId] = connection;
this.remoteConnections.set(connection.connectionId, connection);
events.connections.push(connection);
if (!!connection.stream) {
this.remoteStreamsCreated[connection.stream.streamId] = true;
this.remoteStreamsCreated.set(connection.stream.streamId, true);
events.streams.push(connection.stream);
}
});
@ -1314,7 +1395,7 @@ export class Session extends EventDispatcher {
protected getConnection(connectionId: string, errorMessage: string): Promise<Connection> {
return new Promise<Connection>((resolve, reject) => {
const connection = this.remoteConnections[connectionId];
const connection = this.remoteConnections.get(connectionId);
if (!!connection) {
// Resolve remote connection
resolve(connection);
@ -1330,14 +1411,17 @@ export class Session extends EventDispatcher {
});
}
private getRemoteConnection(connectionId: string, errorMessage: string): Promise<Connection> {
private getRemoteConnection(connectionId: string): Promise<Connection> {
return new Promise<Connection>((resolve, reject) => {
const connection = this.remoteConnections[connectionId];
const connection = this.remoteConnections.get(connectionId);
if (!!connection) {
// Resolve remote connection
resolve(connection);
} else {
// Remote connection not found. Reject with OpenViduError
const errorMessage = 'Remote connection ' + connectionId + " unknown when 'onParticipantLeft'. " +
'Existing remote connections: ' + JSON.stringify(this.remoteConnections.keys());
reject(new OpenViduError(OpenViduErrorName.GENERIC_ERROR, errorMessage));
}
});
@ -1369,6 +1453,7 @@ export class Session extends EventDispatcher {
const secret = queryParams['secret'];
const recorder = queryParams['recorder'];
const webrtcStatsInterval = queryParams['webrtcStatsInterval'];
const sendBrowserLogs = queryParams['sendBrowserLogs'];
if (!!secret) {
this.openvidu.secret = secret;
@ -1379,10 +1464,12 @@ export class Session extends EventDispatcher {
if (!!webrtcStatsInterval) {
this.openvidu.webrtcStatsInterval = +webrtcStatsInterval;
}
if (!!sendBrowserLogs) {
this.openvidu.sendBrowserLogs = sendBrowserLogs;
}
this.openvidu.wsUri = 'wss://' + url.host + '/openvidu';
this.openvidu.httpUri = 'https://' + url.host;
} else {
logger.error('Token "' + token + '" is not valid')
}
@ -1391,17 +1478,15 @@ export class Session extends EventDispatcher {
private processJoinRoomResponse(opts: LocalConnectionOptions) {
this.sessionId = opts.session;
if (opts.coturnIp != null && opts.turnUsername != null && opts.turnCredential != null) {
const stunUrl = 'stun:' + opts.coturnIp + ':3478';
const turnUrl1 = 'turn:' + opts.coturnIp + ':3478';
const turnUrl2 = turnUrl1 + '?transport=tcp';
this.openvidu.iceServers = [
{ urls: [stunUrl] },
{ urls: [turnUrl1, turnUrl2], username: opts.turnUsername, credential: opts.turnCredential }
{ urls: [turnUrl1], username: opts.turnUsername, credential: opts.turnCredential }
];
logger.log("STUN/TURN server IP: " + opts.coturnIp);
logger.log('TURN temp credentials [' + opts.turnUsername + ':' + opts.turnCredential + ']');
}
this.openvidu.role = opts.role;
this.openvidu.finalUserId = opts.finalUserId;
this.capabilities = {
subscribe: true,
publish: this.openvidu.role !== 'SUBSCRIBER',

View File

@ -16,16 +16,15 @@
*/
import { Connection } from './Connection';
import { Event } from '../OpenViduInternal/Events/Event';
import { Filter } from './Filter';
import { Session } from './Session';
import { StreamManager } from './StreamManager';
import { Subscriber } from './Subscriber';
import { EventDispatcher } from './EventDispatcher';
import { InboundStreamOptions } from '../OpenViduInternal/Interfaces/Private/InboundStreamOptions';
import { OutboundStreamOptions } from '../OpenViduInternal/Interfaces/Private/OutboundStreamOptions';
import { WebRtcPeer, WebRtcPeerSendonly, WebRtcPeerRecvonly, WebRtcPeerSendrecv } from '../OpenViduInternal/WebRtcPeer/WebRtcPeer';
import { WebRtcPeer, WebRtcPeerSendonly, WebRtcPeerRecvonly, WebRtcPeerSendrecv, WebRtcPeerConfiguration } from '../OpenViduInternal/WebRtcPeer/WebRtcPeer';
import { WebRtcStats } from '../OpenViduInternal/WebRtcStats/WebRtcStats';
import { ExceptionEvent, ExceptionEventName } from '../OpenViduInternal/Events/ExceptionEvent';
import { PublisherSpeakingEvent } from '../OpenViduInternal/Events/PublisherSpeakingEvent';
import { StreamManagerEvent } from '../OpenViduInternal/Events/StreamManagerEvent';
import { StreamPropertyChangedEvent } from '../OpenViduInternal/Events/StreamPropertyChangedEvent';
@ -37,6 +36,10 @@ import { PlatformUtils } from '../OpenViduInternal/Utils/Platform';
* @hidden
*/
import hark = require('hark');
/**
* @hidden
*/
import EventEmitter = require('wolfy87-eventemitter');
/**
* @hidden
*/
@ -52,7 +55,7 @@ let platform: PlatformUtils;
* Each [[Publisher]] and [[Subscriber]] has an attribute of type Stream, as they give access
* to one of them (sending and receiving it, respectively)
*/
export class Stream extends EventDispatcher {
export class Stream {
/**
* The Connection object that is publishing the stream
@ -178,27 +181,27 @@ export class Stream extends EventDispatcher {
/**
* @hidden
*/
publisherStartSpeakingEventEnabled = false;
harkSpeakingEnabled = false;
/**
* @hidden
*/
publisherStartSpeakingEventEnabledOnce = false;
harkSpeakingEnabledOnce = false;
/**
* @hidden
*/
publisherStopSpeakingEventEnabled = false;
harkStoppedSpeakingEnabled = false;
/**
* @hidden
*/
publisherStopSpeakingEventEnabledOnce = false;
harkStoppedSpeakingEnabledOnce = false;
/**
* @hidden
*/
volumeChangeEventEnabled = false;
harkVolumeChangeEnabled = false;
/**
* @hidden
*/
volumeChangeEventEnabledOnce = false;
harkVolumeChangeEnabledOnce = false;
/**
* @hidden
*/
@ -207,6 +210,10 @@ export class Stream extends EventDispatcher {
* @hidden
*/
localMediaStreamWhenSubscribedToRemote?: MediaStream;
/**
* @hidden
*/
ee = new EventEmitter();
/**
@ -214,7 +221,6 @@ export class Stream extends EventDispatcher {
*/
constructor(session: Session, options: InboundStreamOptions | OutboundStreamOptions | {}) {
super();
platform = PlatformUtils.getInstance();
this.session = session;
@ -271,33 +277,6 @@ export class Stream extends EventDispatcher {
}
/**
* See [[EventDispatcher.on]]
*/
on(type: string, handler: (event: Event) => void): EventDispatcher {
super.onAux(type, "Event '" + type + "' triggered by stream '" + this.streamId + "'", handler);
return this;
}
/**
* See [[EventDispatcher.once]]
*/
once(type: string, handler: (event: Event) => void): EventDispatcher {
super.onceAux(type, "Event '" + type + "' triggered once by stream '" + this.streamId + "'", handler);
return this;
}
/**
* See [[EventDispatcher.off]]
*/
off(type: string, handler?: (event: Event) => void): EventDispatcher {
super.off(type, handler);
return this;
}
/**
* Applies an audio/video filter to the stream.
*
@ -308,14 +287,20 @@ export class Stream extends EventDispatcher {
*/
applyFilter(type: string, options: Object): Promise<Filter> {
return new Promise((resolve, reject) => {
if (!this.session.sessionConnected()) {
reject(this.session.notConnectedError());
}
logger.info('Applying filter to stream ' + this.streamId);
options = !!options ? options : {};
if (typeof options !== 'string') {
options = JSON.stringify(options);
options = options != null ? options : {};
let optionsString = options;
if (typeof optionsString !== 'string') {
optionsString = JSON.stringify(optionsString);
}
this.session.openvidu.sendRequest(
'applyFilter',
{ streamId: this.streamId, type, options },
{ streamId: this.streamId, type, options: optionsString },
(error, response) => {
if (error) {
logger.error('Error applying filter for Stream ' + this.streamId, error);
@ -343,8 +328,13 @@ export class Stream extends EventDispatcher {
*
* @returns A Promise (to which you can optionally subscribe to) that is resolved if the previously applied filter has been successfully removed and rejected with an Error object in other case
*/
removeFilter(): Promise<any> {
removeFilter(): Promise<void> {
return new Promise((resolve, reject) => {
if (!this.session.sessionConnected()) {
reject(this.session.notConnectedError());
}
logger.info('Removing filter of stream ' + this.streamId);
this.session.openvidu.sendRequest(
'removeFilter',
@ -428,7 +418,7 @@ export class Stream extends EventDispatcher {
/**
* @hidden
*/
subscribe(): Promise<any> {
subscribe(): Promise<void> {
return new Promise((resolve, reject) => {
this.initWebRtcPeerReceive(false)
.then(() => {
@ -443,7 +433,7 @@ export class Stream extends EventDispatcher {
/**
* @hidden
*/
publish(): Promise<any> {
publish(): Promise<void> {
return new Promise((resolve, reject) => {
if (this.isLocalStreamReadyToPublish) {
this.initWebRtcPeerSend(false)
@ -550,13 +540,14 @@ export class Stream extends EventDispatcher {
/**
* @hidden
*/
enableStartSpeakingEvent(): void {
this.setSpeechEventIfNotExists();
if (!this.publisherStartSpeakingEventEnabled) {
this.publisherStartSpeakingEventEnabled = true;
enableHarkSpeakingEvent(): void {
this.setHarkListenerIfNotExists();
if (!this.harkSpeakingEnabled) {
this.harkSpeakingEnabled = true;
this.speechEvent.on('speaking', () => {
this.session.emitEvent('publisherStartSpeaking', [new PublisherSpeakingEvent(this.session, 'publisherStartSpeaking', this.connection, this.streamId)]);
this.publisherStartSpeakingEventEnabledOnce = false; // Disable 'once' version if 'on' version was triggered
this.streamManager.emitEvent('publisherStartSpeaking', [new PublisherSpeakingEvent(this.streamManager, 'publisherStartSpeaking', this.connection, this.streamId)]);
this.harkSpeakingEnabledOnce = false; // Disable 'once' version if 'on' version was triggered
});
}
}
@ -564,16 +555,17 @@ export class Stream extends EventDispatcher {
/**
* @hidden
*/
enableOnceStartSpeakingEvent(): void {
this.setSpeechEventIfNotExists();
if (!this.publisherStartSpeakingEventEnabledOnce) {
this.publisherStartSpeakingEventEnabledOnce = true;
enableOnceHarkSpeakingEvent(): void {
this.setHarkListenerIfNotExists();
if (!this.harkSpeakingEnabledOnce) {
this.harkSpeakingEnabledOnce = true;
this.speechEvent.once('speaking', () => {
if (this.publisherStartSpeakingEventEnabledOnce) {
if (this.harkSpeakingEnabledOnce) {
// If the listener has been disabled in the meantime (for example by the 'on' version) do not trigger the event
this.session.emitEvent('publisherStartSpeaking', [new PublisherSpeakingEvent(this.session, 'publisherStartSpeaking', this.connection, this.streamId)]);
this.streamManager.emitEvent('publisherStartSpeaking', [new PublisherSpeakingEvent(this.streamManager, 'publisherStartSpeaking', this.connection, this.streamId)]);
}
this.disableStartSpeakingEvent(true);
this.disableHarkSpeakingEvent(true);
});
}
}
@ -581,22 +573,22 @@ export class Stream extends EventDispatcher {
/**
* @hidden
*/
disableStartSpeakingEvent(disabledByOnce: boolean): void {
disableHarkSpeakingEvent(disabledByOnce: boolean): void {
if (!!this.speechEvent) {
this.publisherStartSpeakingEventEnabledOnce = false;
this.harkSpeakingEnabledOnce = false;
if (disabledByOnce) {
if (this.publisherStartSpeakingEventEnabled) {
if (this.harkSpeakingEnabled) {
// The 'on' version of this same event is enabled too. Do not remove the hark listener
return;
}
} else {
this.publisherStartSpeakingEventEnabled = false;
this.harkSpeakingEnabled = false;
}
// Shutting down the hark event
if (this.volumeChangeEventEnabled ||
this.volumeChangeEventEnabledOnce ||
this.publisherStopSpeakingEventEnabled ||
this.publisherStopSpeakingEventEnabledOnce) {
if (this.harkVolumeChangeEnabled ||
this.harkVolumeChangeEnabledOnce ||
this.harkStoppedSpeakingEnabled ||
this.harkStoppedSpeakingEnabledOnce) {
// Some other hark event is enabled. Cannot stop the hark process, just remove the specific listener
this.speechEvent.off('speaking');
} else {
@ -610,13 +602,14 @@ export class Stream extends EventDispatcher {
/**
* @hidden
*/
enableStopSpeakingEvent(): void {
this.setSpeechEventIfNotExists();
if (!this.publisherStopSpeakingEventEnabled) {
this.publisherStopSpeakingEventEnabled = true;
enableHarkStoppedSpeakingEvent(): void {
this.setHarkListenerIfNotExists();
if (!this.harkStoppedSpeakingEnabled) {
this.harkStoppedSpeakingEnabled = true;
this.speechEvent.on('stopped_speaking', () => {
this.session.emitEvent('publisherStopSpeaking', [new PublisherSpeakingEvent(this.session, 'publisherStopSpeaking', this.connection, this.streamId)]);
this.publisherStopSpeakingEventEnabledOnce = false; // Disable 'once' version if 'on' version was triggered
this.streamManager.emitEvent('publisherStopSpeaking', [new PublisherSpeakingEvent(this.streamManager, 'publisherStopSpeaking', this.connection, this.streamId)]);
this.harkStoppedSpeakingEnabledOnce = false; // Disable 'once' version if 'on' version was triggered
});
}
}
@ -624,16 +617,17 @@ export class Stream extends EventDispatcher {
/**
* @hidden
*/
enableOnceStopSpeakingEvent(): void {
this.setSpeechEventIfNotExists();
if (!this.publisherStopSpeakingEventEnabledOnce) {
this.publisherStopSpeakingEventEnabledOnce = true;
enableOnceHarkStoppedSpeakingEvent(): void {
this.setHarkListenerIfNotExists();
if (!this.harkStoppedSpeakingEnabledOnce) {
this.harkStoppedSpeakingEnabledOnce = true;
this.speechEvent.once('stopped_speaking', () => {
if (this.publisherStopSpeakingEventEnabledOnce) {
if (this.harkStoppedSpeakingEnabledOnce) {
// If the listener has been disabled in the meantime (for example by the 'on' version) do not trigger the event
this.session.emitEvent('publisherStopSpeaking', [new PublisherSpeakingEvent(this.session, 'publisherStopSpeaking', this.connection, this.streamId)]);
this.streamManager.emitEvent('publisherStopSpeaking', [new PublisherSpeakingEvent(this.streamManager, 'publisherStopSpeaking', this.connection, this.streamId)]);
}
this.disableStopSpeakingEvent(true);
this.disableHarkStoppedSpeakingEvent(true);
});
}
}
@ -641,23 +635,23 @@ export class Stream extends EventDispatcher {
/**
* @hidden
*/
disableStopSpeakingEvent(disabledByOnce: boolean): void {
disableHarkStoppedSpeakingEvent(disabledByOnce: boolean): void {
if (!!this.speechEvent) {
this.publisherStopSpeakingEventEnabledOnce = false;
this.harkStoppedSpeakingEnabledOnce = false;
if (disabledByOnce) {
if (this.publisherStopSpeakingEventEnabled) {
if (this.harkStoppedSpeakingEnabled) {
// We are cancelling the 'once' listener for this event, but the 'on' version
// of this same event is enabled too. Do not remove the hark listener
return;
}
} else {
this.publisherStopSpeakingEventEnabled = false;
this.harkStoppedSpeakingEnabled = false;
}
// Shutting down the hark event
if (this.volumeChangeEventEnabled ||
this.volumeChangeEventEnabledOnce ||
this.publisherStartSpeakingEventEnabled ||
this.publisherStartSpeakingEventEnabledOnce) {
if (this.harkVolumeChangeEnabled ||
this.harkVolumeChangeEnabledOnce ||
this.harkSpeakingEnabled ||
this.harkSpeakingEnabledOnce) {
// Some other hark event is enabled. Cannot stop the hark process, just remove the specific listener
this.speechEvent.off('stopped_speaking');
} else {
@ -671,10 +665,10 @@ export class Stream extends EventDispatcher {
/**
* @hidden
*/
enableVolumeChangeEvent(force: boolean): void {
if (this.setSpeechEventIfNotExists()) {
if (!this.volumeChangeEventEnabled || force) {
this.volumeChangeEventEnabled = true;
enableHarkVolumeChangeEvent(force: boolean): void {
if (this.setHarkListenerIfNotExists()) {
if (!this.harkVolumeChangeEnabled || force) {
this.harkVolumeChangeEnabled = true;
this.speechEvent.on('volume_change', harkEvent => {
const oldValue = this.speechEvent.oldVolumeValue;
const value = { newValue: harkEvent, oldValue };
@ -684,51 +678,51 @@ export class Stream extends EventDispatcher {
}
} else {
// This way whenever the MediaStream object is available, the event listener will be automatically added
this.volumeChangeEventEnabled = true;
this.harkVolumeChangeEnabled = true;
}
}
/**
* @hidden
*/
enableOnceVolumeChangeEvent(force: boolean): void {
if (this.setSpeechEventIfNotExists()) {
if (!this.volumeChangeEventEnabledOnce || force) {
this.volumeChangeEventEnabledOnce = true;
enableOnceHarkVolumeChangeEvent(force: boolean): void {
if (this.setHarkListenerIfNotExists()) {
if (!this.harkVolumeChangeEnabledOnce || force) {
this.harkVolumeChangeEnabledOnce = true;
this.speechEvent.once('volume_change', harkEvent => {
const oldValue = this.speechEvent.oldVolumeValue;
const value = { newValue: harkEvent, oldValue };
this.speechEvent.oldVolumeValue = harkEvent;
this.disableVolumeChangeEvent(true);
this.disableHarkVolumeChangeEvent(true);
this.streamManager.emitEvent('streamAudioVolumeChange', [new StreamManagerEvent(this.streamManager, 'streamAudioVolumeChange', value)]);
});
}
} else {
// This way whenever the MediaStream object is available, the event listener will be automatically added
this.volumeChangeEventEnabledOnce = true;
this.harkVolumeChangeEnabledOnce = true;
}
}
/**
* @hidden
*/
disableVolumeChangeEvent(disabledByOnce: boolean): void {
disableHarkVolumeChangeEvent(disabledByOnce: boolean): void {
if (!!this.speechEvent) {
this.volumeChangeEventEnabledOnce = false;
this.harkVolumeChangeEnabledOnce = false;
if (disabledByOnce) {
if (this.volumeChangeEventEnabled) {
if (this.harkVolumeChangeEnabled) {
// We are cancelling the 'once' listener for this event, but the 'on' version
// of this same event is enabled too. Do not remove the hark listener
return;
}
} else {
this.volumeChangeEventEnabled = false;
this.harkVolumeChangeEnabled = false;
}
// Shutting down the hark event
if (this.publisherStartSpeakingEventEnabled ||
this.publisherStartSpeakingEventEnabledOnce ||
this.publisherStopSpeakingEventEnabled ||
this.publisherStopSpeakingEventEnabledOnce) {
if (this.harkSpeakingEnabled ||
this.harkSpeakingEnabledOnce ||
this.harkStoppedSpeakingEnabled ||
this.harkStoppedSpeakingEnabledOnce) {
// Some other hark event is enabled. Cannot stop the hark process, just remove the specific listener
this.speechEvent.off('volume_change');
} else {
@ -780,16 +774,16 @@ export class Stream extends EventDispatcher {
return false;
}
if (this.isLocal() && !!this.session.openvidu.advancedConfiguration.forceMediaReconnectionAfterNetworkDrop) {
logger.warn('OpenVidu Browser advanced configuration option "forceMediaReconnectionAfterNetworkDrop" is enabled. Publisher stream ' + this.streamId + 'will force a reconnection');
logger.warn('OpenVidu Browser advanced configuration option "forceMediaReconnectionAfterNetworkDrop" is enabled. Stream ' + this.streamId + ' will force a reconnection');
return true;
}
const iceConnectionState: RTCIceConnectionState = this.getRTCPeerConnection().iceConnectionState;
return iceConnectionState === 'disconnected' || iceConnectionState === 'failed';
return iceConnectionState !== 'connected' && iceConnectionState !== 'completed';
}
/* Private methods */
private setSpeechEventIfNotExists(): boolean {
private setHarkListenerIfNotExists(): boolean {
if (!!this.mediaStream) {
if (!this.speechEvent) {
const harkOptions = !!this.harkOptions ? this.harkOptions : (this.session.openvidu.advancedConfiguration.publisherSpeakingEventsOptions || {});
@ -805,7 +799,7 @@ export class Stream extends EventDispatcher {
/**
* @hidden
*/
initWebRtcPeerSend(reconnect: boolean): Promise<any> {
initWebRtcPeerSend(reconnect: boolean): Promise<void> {
return new Promise((resolve, reject) => {
if (!reconnect) {
@ -817,15 +811,16 @@ export class Stream extends EventDispatcher {
video: this.isSendVideo()
};
const options = {
const options: WebRtcPeerConfiguration = {
mediaStream: this.mediaStream,
mediaConstraints: userMediaConstraints,
onicecandidate: this.connection.sendIceCandidate.bind(this.connection),
onexception: (exceptionName: ExceptionEventName, message: string, data?: any) => { this.session.emitEvent('exception', [new ExceptionEvent(this.session, exceptionName, this, message, data)]) },
iceServers: this.getIceServersConf(),
simulcast: false
};
const successCallback = (sdpOfferParam) => {
const successOfferCallback = (sdpOfferParam) => {
logger.debug('Sending SDP offer to publish as '
+ this.streamId, sdpOfferParam);
@ -833,7 +828,8 @@ export class Stream extends EventDispatcher {
let params;
if (reconnect) {
params = {
stream: this.streamId
stream: this.streamId,
sdpString: sdpOfferParam
}
} else {
let typeOfVideo = '';
@ -849,10 +845,10 @@ export class Stream extends EventDispatcher {
typeOfVideo,
frameRate: !!this.frameRate ? this.frameRate : -1,
videoDimensions: JSON.stringify(this.videoDimensions),
filter: this.outboundStreamOpts.publisherProperties.filter
filter: this.outboundStreamOpts.publisherProperties.filter,
sdpOffer: sdpOfferParam
}
}
params['sdpOffer'] = sdpOfferParam;
this.session.openvidu.sendRequest(method, params, (error, response) => {
if (error) {
@ -862,7 +858,7 @@ export class Stream extends EventDispatcher {
reject('Error on publishVideo: ' + JSON.stringify(error));
}
} else {
this.webRtcPeer.processAnswer(response.sdpAnswer, false)
this.webRtcPeer.processRemoteAnswer(response.sdpAnswer)
.then(() => {
this.streamId = response.id;
this.creationTime = response.createdAt;
@ -870,7 +866,7 @@ export class Stream extends EventDispatcher {
this.publishedOnce = true;
if (this.displayMyRemote()) {
this.localMediaStreamWhenSubscribedToRemote = this.mediaStream;
this.remotePeerSuccessfullyEstablished();
this.remotePeerSuccessfullyEstablished(reconnect);
}
if (reconnect) {
this.ee.emitEvent('stream-reconnected-by-publisher', []);
@ -897,10 +893,15 @@ export class Stream extends EventDispatcher {
this.webRtcPeer = new WebRtcPeerSendonly(options);
}
this.webRtcPeer.addIceConnectionStateChangeListener('publisher of ' + this.connection.connectionId);
this.webRtcPeer.generateOffer().then(sdpOffer => {
successCallback(sdpOffer);
this.webRtcPeer.createOffer().then(sdpOffer => {
this.webRtcPeer.processLocalOffer(sdpOffer)
.then(() => {
successOfferCallback(sdpOffer.sdp);
}).catch(error => {
reject(new Error('(publish) SDP process local offer error: ' + JSON.stringify(error)));
});
}).catch(error => {
reject(new Error('(publish) SDP offer error: ' + JSON.stringify(error)));
reject(new Error('(publish) SDP create offer error: ' + JSON.stringify(error)));
});
});
}
@ -908,7 +909,31 @@ export class Stream extends EventDispatcher {
/**
* @hidden
*/
initWebRtcPeerReceive(reconnect: boolean): Promise<any> {
initWebRtcPeerReceive(reconnect: boolean): Promise<void> {
return new Promise((resolve, reject) => {
this.session.openvidu.sendRequest('prepareReceiveVideoFrom', { sender: this.streamId, reconnect }, (error, response) => {
if (error) {
reject(new Error('Error on prepareReceiveVideoFrom: ' + JSON.stringify(error)));
} else {
this.completeWebRtcPeerReceive(response.sdpOffer, reconnect)
.then(() => {
logger.info("'Subscriber' (" + this.streamId + ") successfully " + (reconnect ? "reconnected" : "subscribed"));
this.remotePeerSuccessfullyEstablished(reconnect);
this.initWebRtcStats();
resolve();
})
.catch(error => {
reject(error);
});
}
});
});
}
/**
* @hidden
*/
completeWebRtcPeerReceive(sdpOffer: string, reconnect: boolean): Promise<void> {
return new Promise((resolve, reject) => {
const offerConstraints = {
@ -919,55 +944,47 @@ export class Stream extends EventDispatcher {
offerConstraints);
const options = {
onicecandidate: this.connection.sendIceCandidate.bind(this.connection),
onexception: (exceptionName: ExceptionEventName, message: string, data?: any) => { this.session.emitEvent('exception', [new ExceptionEvent(this.session, exceptionName, this, message, data)]) },
mediaConstraints: offerConstraints,
iceServers: this.getIceServersConf(),
simulcast: false
};
const successCallback = (sdpOfferParam) => {
logger.debug('Sending SDP offer to subscribe to '
+ this.streamId, sdpOfferParam);
const successAnswerCallback = (sdpAnswer) => {
logger.debug('Sending SDP answer to subscribe to '
+ this.streamId, sdpAnswer);
const method = reconnect ? 'reconnectStream' : 'receiveVideoFrom';
const params = { sdpOffer: sdpOfferParam };
const params = {};
params[reconnect ? 'stream' : 'sender'] = this.streamId;
params[reconnect ? 'sdpString' : 'sdpAnswer'] = sdpAnswer;
this.session.openvidu.sendRequest(method, params, (error, response) => {
if (error) {
reject(new Error('Error on recvVideoFrom: ' + JSON.stringify(error)));
reject(new Error('Error on ' + method + ' : ' + JSON.stringify(error)));
} else {
// Ios Ionic. Limitation: some bug in iosrtc cordova plugin makes it necessary
// to add a timeout before calling PeerConnection#setRemoteDescription during
// some time (400 ms) from the moment first subscriber stream is received
if (this.session.isFirstIonicIosSubscriber) {
this.session.isFirstIonicIosSubscriber = false;
setTimeout(() => {
// After 400 ms Ionic iOS subscribers won't need to run
// PeerConnection#setRemoteDescription after 250 ms timeout anymore
this.session.countDownForIonicIosSubscribersActive = false;
}, 400);
}
const needsTimeoutOnProcessAnswer = this.session.countDownForIonicIosSubscribersActive;
this.webRtcPeer.processAnswer(response.sdpAnswer, needsTimeoutOnProcessAnswer).then(() => {
logger.info("'Subscriber' (" + this.streamId + ") successfully " + (reconnect ? "reconnected" : "subscribed"));
this.remotePeerSuccessfullyEstablished();
this.initWebRtcStats();
resolve();
}).catch(error => {
reject(error);
});
resolve();
}
});
};
this.webRtcPeer = new WebRtcPeerRecvonly(options);
this.webRtcPeer.addIceConnectionStateChangeListener(this.streamId);
this.webRtcPeer.generateOffer()
.then(sdpOffer => {
successCallback(sdpOffer);
this.webRtcPeer.processRemoteOffer(sdpOffer)
.then(() => {
this.webRtcPeer.createAnswer().then(sdpAnswer => {
this.webRtcPeer.processLocalAnswer(sdpAnswer)
.then(() => {
successAnswerCallback(sdpAnswer.sdp);
}).catch(error => {
reject(new Error('(subscribe) SDP process local answer error: ' + JSON.stringify(error)));
});
}).catch(error => {
reject(new Error('(subscribe) SDP create answer error: ' + JSON.stringify(error)));
});
})
.catch(error => {
reject(new Error('(subscribe) SDP offer error: ' + JSON.stringify(error)));
reject(new Error('(subscribe) SDP process remote offer error: ' + JSON.stringify(error)));
});
});
}
@ -975,7 +992,13 @@ export class Stream extends EventDispatcher {
/**
* @hidden
*/
remotePeerSuccessfullyEstablished(): void {
remotePeerSuccessfullyEstablished(reconnect: boolean): void {
if (reconnect && this.mediaStream != null) {
// Now we can destroy the existing MediaStream
this.disposeMediaStream();
}
this.mediaStream = new MediaStream();
let receiver: RTCRtpReceiver;
for (receiver of this.webRtcPeer.pc.getReceivers()) {
@ -990,11 +1013,11 @@ export class Stream extends EventDispatcher {
if (this.streamManager instanceof Subscriber) {
// Apply SubscriberProperties.subscribeToAudio and SubscriberProperties.subscribeToVideo
if (!!this.mediaStream.getAudioTracks()[0]) {
const enabled = !!((<Subscriber>this.streamManager).properties.subscribeToAudio);
const enabled = reconnect ? this.audioActive : !!((this.streamManager as Subscriber).properties.subscribeToAudio);
this.mediaStream.getAudioTracks()[0].enabled = enabled;
}
if (!!this.mediaStream.getVideoTracks()[0]) {
const enabled = !!((<Subscriber>this.streamManager).properties.subscribeToVideo);
const enabled = reconnect ? this.videoActive : !!((this.streamManager as Subscriber).properties.subscribeToVideo);
this.mediaStream.getVideoTracks()[0].enabled = enabled;
}
}
@ -1007,27 +1030,23 @@ export class Stream extends EventDispatcher {
private initHarkEvents(): void {
if (!!this.mediaStream!.getAudioTracks()[0]) {
// Hark events can only be set if audio track is available
if (this.streamManager.remote) {
// publisherStartSpeaking/publisherStopSpeaking is only defined for remote streams
if (this.session.startSpeakingEventsEnabled) {
this.enableStartSpeakingEvent();
}
if (this.session.startSpeakingEventsEnabledOnce) {
this.enableOnceStartSpeakingEvent();
}
if (this.session.stopSpeakingEventsEnabled) {
this.enableStopSpeakingEvent();
}
if (this.session.stopSpeakingEventsEnabledOnce) {
this.enableOnceStopSpeakingEvent();
}
if (this.session.anySpeechEventListenerEnabled('publisherStartSpeaking', true, this.streamManager)) {
this.enableOnceHarkSpeakingEvent();
}
// streamAudioVolumeChange event is defined for both Publishers and Subscribers
if (this.volumeChangeEventEnabled) {
this.enableVolumeChangeEvent(true);
if (this.session.anySpeechEventListenerEnabled('publisherStartSpeaking', false, this.streamManager)) {
this.enableHarkSpeakingEvent();
}
if (this.volumeChangeEventEnabledOnce) {
this.enableOnceVolumeChangeEvent(true);
if (this.session.anySpeechEventListenerEnabled('publisherStopSpeaking', true, this.streamManager)) {
this.enableOnceHarkStoppedSpeakingEvent();
}
if (this.session.anySpeechEventListenerEnabled('publisherStopSpeaking', false, this.streamManager)) {
this.enableHarkStoppedSpeakingEvent();
}
if (this.harkVolumeChangeEnabledOnce) {
this.enableOnceHarkVolumeChangeEvent(true);
}
if (this.harkVolumeChangeEnabled) {
this.enableHarkVolumeChangeEvent(true);
}
}
}

View File

@ -48,6 +48,9 @@ let platform: PlatformUtils;
* - videoElementCreated ([[VideoElementEvent]])
* - videoElementDestroyed ([[VideoElementEvent]])
* - streamPlaying ([[StreamManagerEvent]])
* - streamPropertyChanged ([[StreamPropertyChangedEvent]])
* - publisherStartSpeaking ([[PublisherSpeakingEvent]])
* - publisherStopSpeaking ([[PublisherSpeakingEvent]])
* - streamAudioVolumeChange ([[StreamManagerEvent]])
*
*/
@ -174,8 +177,16 @@ export class StreamManager extends EventDispatcher {
this.ee.emitEvent('videoPlaying', [new VideoElementEvent(this.videos[0].video, this, 'videoPlaying')]);
}
}
if (type === 'streamAudioVolumeChange' && this.stream.hasAudio) {
this.stream.enableVolumeChangeEvent(false);
if (this.stream.hasAudio) {
if (type === 'publisherStartSpeaking') {
this.stream.enableHarkSpeakingEvent();
}
if (type === 'publisherStopSpeaking') {
this.stream.enableHarkStoppedSpeakingEvent();
}
if (type === 'streamAudioVolumeChange') {
this.stream.enableHarkVolumeChangeEvent(false);
}
}
return this;
}
@ -202,8 +213,16 @@ export class StreamManager extends EventDispatcher {
this.ee.emitEvent('videoPlaying', [new VideoElementEvent(this.videos[0].video, this, 'videoPlaying')]);
}
}
if (type === 'streamAudioVolumeChange' && this.stream.hasAudio) {
this.stream.enableOnceVolumeChangeEvent(false);
if (this.stream.hasAudio) {
if (type === 'publisherStartSpeaking') {
this.stream.enableOnceHarkSpeakingEvent();
}
if (type === 'publisherStopSpeaking') {
this.stream.enableOnceHarkStoppedSpeakingEvent();
}
if (type === 'streamAudioVolumeChange') {
this.stream.enableOnceHarkVolumeChangeEvent(false);
}
}
return this;
}
@ -215,10 +234,25 @@ export class StreamManager extends EventDispatcher {
super.off(type, handler);
if (type === 'publisherStartSpeaking') {
// Both StreamManager and Session can have "publisherStartSpeaking" event listeners
const remainingStartSpeakingEventListeners = this.ee.getListeners(type).length + this.stream.session.ee.getListeners(type).length;
if (remainingStartSpeakingEventListeners === 0) {
this.stream.disableHarkSpeakingEvent(false);
}
}
if (type === 'publisherStopSpeaking') {
// Both StreamManager and Session can have "publisherStopSpeaking" event listeners
const remainingStopSpeakingEventListeners = this.ee.getListeners(type).length + this.stream.session.ee.getListeners(type).length;
if (remainingStopSpeakingEventListeners === 0) {
this.stream.disableHarkStoppedSpeakingEvent(false);
}
}
if (type === 'streamAudioVolumeChange') {
let remainingVolumeEventListeners = this.ee.getListeners(type).length;
// Only StreamManager can have "streamAudioVolumeChange" event listeners
const remainingVolumeEventListeners = this.ee.getListeners(type).length;
if (remainingVolumeEventListeners === 0) {
this.stream.disableVolumeChangeEvent(false);
this.stream.disableHarkVolumeChangeEvent(false);
}
}
@ -271,7 +305,7 @@ export class StreamManager extends EventDispatcher {
id: video.id,
canplayListenerAdded: false
});
logger.info('New video element associated to ', this);
return returnNumber;
@ -419,9 +453,10 @@ export class StreamManager extends EventDispatcher {
this.videos.forEach(streamManagerVideo => {
// Remove oncanplay event listener (only OpenVidu browser listener, not the user ones)
if(!!streamManagerVideo.video && !!streamManagerVideo.video.removeEventListener){
if (!!streamManagerVideo.video && !!streamManagerVideo.video.removeEventListener) {
streamManagerVideo.video.removeEventListener('canplay', this.canPlayListener);
} streamManagerVideo.canplayListenerAdded = false;
}
streamManagerVideo.canplayListenerAdded = false;
if (!!streamManagerVideo.targetElement) {
// Only remove from DOM videos created by OpenVidu Browser (those generated by passing a valid targetElement in OpenVidu.initPublisher
// and Session.subscribe or those created by StreamManager.createVideoElement). All this videos triggered a videoElementCreated event
@ -486,9 +521,9 @@ export class StreamManager extends EventDispatcher {
this.ee.emitEvent(type, eventArray);
}
/**
* @hidden
*/
/**
* @hidden
*/
createVideo(): HTMLVideoElement {
return document.createElement('video');
}

View File

@ -27,6 +27,10 @@ const logger: OpenViduLogger = OpenViduLogger.getInstance();
/**
* Packs remote media streams. Participants automatically receive them when others publish their streams. Initialized with [[Session.subscribe]] method
*
* ### Available event listeners (and events dispatched)
*
* - _All events inherited from [[StreamManager]] class_
*/
export class Subscriber extends StreamManager {

View File

@ -102,12 +102,13 @@ export enum OpenViduErrorName {
OPENVIDU_PERMISSION_DENIED = 'OPENVIDU_PERMISSION_DENIED',
/**
* _Not in use yet_
* There is no connection to the Session. This error will be thrown when any method requiring a connection to
* openvidu-server is called before successfully calling method [[Session.connect]]
*/
OPENVIDU_NOT_CONNECTED = 'OPENVIDU_NOT_CONNECTED',
/**
* _Not in use yet_
* Generic error
*/
GENERIC_ERROR = 'GENERIC_ERROR'
}
@ -117,7 +118,14 @@ export enum OpenViduErrorName {
*/
export class OpenViduError {
/**
* Uniquely identifying name of the error
*/
name: OpenViduErrorName;
/**
* Full description of the error
*/
message: string;
/**

View File

@ -0,0 +1,103 @@
/*
* (C) Copyright 2017-2020 OpenVidu (https://openvidu.io)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
import { Session } from '../../OpenVidu/Session';
import { Stream } from '../../OpenVidu/Stream';
import { Event } from './Event';
/**
* Defines property [[ExceptionEvent.name]]
*/
export enum ExceptionEventName {
/**
* There was an unexpected error on the server-side processing an ICE candidate generated and sent by the client-side.
*
* [[ExceptionEvent]] objects with this [[ExceptionEvent.name]] will have as [[ExceptionEvent.origin]] property a [[Session]] object.
*/
ICE_CANDIDATE_ERROR = 'ICE_CANDIDATE_ERROR',
/**
* The [ICE connection state](https://developer.mozilla.org/en-US/docs/Web/API/RTCPeerConnection/iceConnectionState)
* of an [RTCPeerConnection](https://developer.mozilla.org/en-US/docs/Web/API/RTCPeerConnection) reached `failed` status.
*
* This is a terminal error that won't have any kind of possible recovery.
*
* [[ExceptionEvent]] objects with this [[ExceptionEvent.name]] will have as [[ExceptionEvent.origin]] property a [[Stream]] object.
*/
ICE_CONNECTION_FAILED = 'ICE_CONNECTION_FAILED',
/**
* The [ICE connection state](https://developer.mozilla.org/en-US/docs/Web/API/RTCPeerConnection/iceConnectionState)
* of an [RTCPeerConnection](https://developer.mozilla.org/en-US/docs/Web/API/RTCPeerConnection) reached `disconnected` status.
*
* This is not a terminal error, and it is possible for the ICE connection to be reconnected.
*
* [[ExceptionEvent]] objects with this [[ExceptionEvent.name]] will have as [[ExceptionEvent.origin]] property a [[Stream]] object.
*/
ICE_CONNECTION_DISCONNECTED = 'ICE_CONNECTION_DISCONNECTED'
}
/**
* Defines event `exception` dispatched by [[Session]] object.
*
* This event acts as a global handler for asynchronous errors that may be triggered for multiple reasons and from multiple origins.
*/
export class ExceptionEvent extends Event {
/**
* Name of the exception
*/
name: ExceptionEventName;
/**
* Object affected by the exception. Depending on the [[ExceptionEvent.name]] property:
* - [[Session]]: `ICE_CANDIDATE_ERROR`
* - [[Stream]]: `ICE_CONNECTION_FAILED`, `ICE_CONNECTION_DISCONNECTED`
*/
origin: Session | Stream;
/**
* Informative description of the exception
*/
message: string;
/**
* Any extra information associated to the exception
*/
data?: any;
/**
* @hidden
*/
constructor(session: Session, name: ExceptionEventName, origin: Session | Stream, message: string, data?: any) {
super(false, session, 'exception');
this.name = name;
this.origin = origin;
this.message = message;
this.data = data;
}
/**
* @hidden
*/
// tslint:disable-next-line:no-empty
callDefaultBehavior() { }
}

View File

@ -17,16 +17,17 @@
import { Event } from './Event';
import { Connection } from '../../OpenVidu/Connection';
import { Session } from '../..';
import { Session } from '../../OpenVidu/Session';
import { StreamManager } from '../../OpenVidu/StreamManager';
/**
* Defines the following events:
* - `publisherStartSpeaking`: dispatched by [[Session]] when a remote user has started speaking
* - `publisherStopSpeaking`: dispatched by [[Session]] when a remote user has stopped speaking
* - `publisherStartSpeaking`: dispatched by [[Session]] and [[StreamManager]] when a user has started speaking
* - `publisherStopSpeaking`: dispatched by [[Session]] and [[StreamManager]] when a user has stopped speaking
*
* More information:
* - This events will only be triggered for **remote streams that have audio tracks** ([[Stream.hasAudio]] must be true)
* - This events will only be triggered for **streams that have audio tracks** ([[Stream.hasAudio]] must be true)
* - You can further configure how the events are dispatched by setting property `publisherSpeakingEventsOptions` in the call of [[OpenVidu.setAdvancedConfiguration]]
*/
export class PublisherSpeakingEvent extends Event {
@ -44,7 +45,7 @@ export class PublisherSpeakingEvent extends Event {
/**
* @hidden
*/
constructor(target: Session, type: string, connection: Connection, streamId: string) {
constructor(target: Session | StreamManager, type: string, connection: Connection, streamId: string) {
super(false, target, type);
this.type = type;
this.connection = connection;

View File

@ -60,18 +60,22 @@ export class SessionDisconnectedEvent extends Event {
const session = <Session>this.target;
// Dispose and delete all remote Connections
for (const connectionId in session.remoteConnections) {
if (!!session.remoteConnections[connectionId].stream) {
session.remoteConnections[connectionId].stream!.disposeWebRtcPeer();
session.remoteConnections[connectionId].stream!.disposeMediaStream();
if (session.remoteConnections[connectionId].stream!.streamManager) {
session.remoteConnections[connectionId].stream!.streamManager.removeAllVideos();
session.remoteConnections.forEach(remoteConnection => {
const connectionId = remoteConnection.connectionId;
if (!!session.remoteConnections.get(connectionId)?.stream) {
session.remoteConnections.get(connectionId)?.stream!.disposeWebRtcPeer();
session.remoteConnections.get(connectionId)?.stream!.disposeMediaStream();
if (session.remoteConnections.get(connectionId)?.stream!.streamManager) {
session.remoteConnections.get(connectionId)?.stream!.streamManager.removeAllVideos();
}
delete session.remoteStreamsCreated[session.remoteConnections[connectionId].stream!.streamId];
session.remoteConnections[connectionId].dispose();
const streamId = session.remoteConnections.get(connectionId)?.stream?.streamId;
if (!!streamId) {
session.remoteStreamsCreated.delete(streamId);
}
session.remoteConnections.get(connectionId)?.dispose();
}
delete session.remoteConnections[connectionId];
}
session.remoteConnections.delete(connectionId);
});
}
}

View File

@ -97,10 +97,10 @@ export class StreamEvent extends Event {
if (this.stream.streamManager) this.stream.streamManager.removeAllVideos();
// Delete stream from Session.remoteStreamsCreated map
delete this.stream.session.remoteStreamsCreated[this.stream.streamId];
this.stream.session.remoteStreamsCreated.delete(this.stream.streamId);
// Delete StreamOptionsServer from remote Connection
const remoteConnection = this.stream.session.remoteConnections[this.stream.connection.connectionId];
const remoteConnection = this.stream.session.remoteConnections.get(this.stream.connection.connectionId);
if (!!remoteConnection && !!remoteConnection.remoteOptions) {
const streamOptionsServer = remoteConnection.remoteOptions.streams;
for (let i = streamOptionsServer.length - 1; i >= 0; --i) {

View File

@ -41,7 +41,7 @@ export class StreamPropertyChangedEvent extends Event {
* Cause of the change on the stream's property:
* - For `videoActive`: `"publishVideo"`
* - For `audioActive`: `"publishAudio"`
* - For `videoDimensions`: `"deviceRotated"` or `"screenResized"`
* - For `videoDimensions`: `"deviceRotated"`, `"screenResized"` or `"trackReplaced"`
* - For `filter`: `"applyFilter"`, `"execFilterMethod"` or `"removeFilter"`
*/
reason: string;

View File

@ -19,6 +19,7 @@ import { RemoteConnectionOptions } from './RemoteConnectionOptions';
export interface LocalConnectionOptions {
id: string;
finalUserId: string;
createdAt: number;
metadata: string;
value: RemoteConnectionOptions[];

View File

@ -1,18 +0,0 @@
/*
* (C) Copyright 2017-2020 OpenVidu (https://openvidu.io)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
export interface ObjMap<T> { [s: string]: T; }

View File

@ -1,49 +1,45 @@
function Mapper()
{
function Mapper() {
var sources = {};
this.forEach = function(callback)
{
for(var key in sources)
{
this.forEach = function (callback) {
for (var key in sources) {
var source = sources[key];
for(var key2 in source)
for (var key2 in source)
callback(source[key2]);
};
};
this.get = function(id, source)
{
this.get = function (id, source) {
var ids = sources[source];
if(ids == undefined)
if (ids == undefined)
return undefined;
return ids[id];
};
this.remove = function(id, source)
{
this.remove = function (id, source) {
var ids = sources[source];
if(ids == undefined)
if (ids == undefined)
return;
delete ids[id];
// Check it's empty
for(var i in ids){return false}
for (var i in ids) {
return false
}
delete sources[source];
};
this.set = function(value, id, source)
{
if(value == undefined)
this.set = function (value, id, source) {
if (value == undefined)
return this.remove(id, source);
var ids = sources[source];
if(ids == undefined)
if (ids == undefined)
sources[source] = ids = {};
ids[id] = value;
@ -51,10 +47,9 @@ function Mapper()
};
Mapper.prototype.pop = function(id, source)
{
Mapper.prototype.pop = function (id, source) {
var value = this.get(id, source);
if(value == undefined)
if (value == undefined)
return undefined;
this.remove(id, source);
@ -63,4 +58,4 @@ Mapper.prototype.pop = function(id, source)
};
module.exports = Mapper;
module.exports = Mapper;

View File

@ -15,7 +15,7 @@
*
*/
var JsonRpcClient = require('./jsonrpcclient');
var JsonRpcClient = require('./jsonrpcclient');
exports.JsonRpcClient = JsonRpcClient;
exports.JsonRpcClient = JsonRpcClient;

View File

@ -161,9 +161,6 @@ function JsonRpcClient(configuration) {
});
this.send = function (method, params, callback) {
if (method !== 'ping') {
Logger.debug('Request: method:' + method + " params:" + JSON.stringify(params));
}
var requestTime = Date.now();

View File

@ -15,7 +15,6 @@
*
*/
var WebSocketWithReconnection = require('./webSocketWithReconnection');
var WebSocketWithReconnection = require('./webSocketWithReconnection');
exports.WebSocketWithReconnection = WebSocketWithReconnection;
exports.WebSocketWithReconnection = WebSocketWithReconnection;

View File

@ -16,7 +16,8 @@
"use strict";
var Logger = console;
var OpenViduLogger = require('../../../../Logger/OpenViduLogger').OpenViduLogger;
var Logger = OpenViduLogger.getInstance();
var MAX_RETRIES = 2000; // Forever...
var RETRY_TIME_MS = 3000; // FIXME: Implement exponential wait times...

View File

@ -17,36 +17,32 @@
var defineProperty_IE8 = false
if(Object.defineProperty)
{
try
{
if (Object.defineProperty) {
try {
Object.defineProperty({}, "x", {});
}
catch(e)
{
} catch (e) {
defineProperty_IE8 = true
}
}
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Function/bind
if (!Function.prototype.bind) {
Function.prototype.bind = function(oThis) {
Function.prototype.bind = function (oThis) {
if (typeof this !== 'function') {
// closest thing possible to the ECMAScript 5
// internal IsCallable function
throw new TypeError('Function.prototype.bind - what is trying to be bound is not callable');
}
var aArgs = Array.prototype.slice.call(arguments, 1),
fToBind = this,
fNOP = function() {},
fBound = function() {
return fToBind.apply(this instanceof fNOP && oThis
? this
: oThis,
aArgs.concat(Array.prototype.slice.call(arguments)));
};
var aArgs = Array.prototype.slice.call(arguments, 1),
fToBind = this,
fNOP = function () {},
fBound = function () {
return fToBind.apply(this instanceof fNOP && oThis ?
this :
oThis,
aArgs.concat(Array.prototype.slice.call(arguments)));
};
fNOP.prototype = this.prototype;
fBound.prototype = new fNOP();
@ -67,17 +63,14 @@ var Mapper = require('./Mapper');
var BASE_TIMEOUT = 5000;
function unifyResponseMethods(responseMethods)
{
if(!responseMethods) return {};
function unifyResponseMethods(responseMethods) {
if (!responseMethods) return {};
for(var key in responseMethods)
{
for (var key in responseMethods) {
var value = responseMethods[key];
if(typeof value == 'string')
responseMethods[key] =
{
if (typeof value == 'string')
responseMethods[key] = {
response: value
}
};
@ -85,35 +78,34 @@ function unifyResponseMethods(responseMethods)
return responseMethods;
};
function unifyTransport(transport)
{
if(!transport) return;
function unifyTransport(transport) {
if (!transport) return;
// Transport as a function
if(transport instanceof Function)
return {send: transport};
if (transport instanceof Function)
return {
send: transport
};
// WebSocket & DataChannel
if(transport.send instanceof Function)
if (transport.send instanceof Function)
return transport;
// Message API (Inter-window & WebWorker)
if(transport.postMessage instanceof Function)
{
if (transport.postMessage instanceof Function) {
transport.send = transport.postMessage;
return transport;
}
// Stream API
if(transport.write instanceof Function)
{
if (transport.write instanceof Function) {
transport.send = transport.write;
return transport;
}
// Transports that only can receive messages, but not send
if(transport.onmessage !== undefined) return;
if(transport.pause instanceof Function) return;
if (transport.onmessage !== undefined) return;
if (transport.pause instanceof Function) return;
throw new SyntaxError("Transport is not a function nor a valid object");
};
@ -129,17 +121,19 @@ function unifyTransport(transport)
* @param {String} method -method of the notification
* @param params - parameters of the notification
*/
function RpcNotification(method, params)
{
if(defineProperty_IE8)
{
function RpcNotification(method, params) {
if (defineProperty_IE8) {
this.method = method
this.params = params
}
else
{
Object.defineProperty(this, 'method', {value: method, enumerable: true});
Object.defineProperty(this, 'params', {value: params, enumerable: true});
} else {
Object.defineProperty(this, 'method', {
value: method,
enumerable: true
});
Object.defineProperty(this, 'params', {
value: params,
enumerable: true
});
}
};
@ -157,50 +151,46 @@ function RpcNotification(method, params)
*
* @param {Function} [onRequest]
*/
function RpcBuilder(packer, options, transport, onRequest)
{
function RpcBuilder(packer, options, transport, onRequest) {
var self = this;
if(!packer)
if (!packer)
throw new SyntaxError('Packer is not defined');
if(!packer.pack || !packer.unpack)
if (!packer.pack || !packer.unpack)
throw new SyntaxError('Packer is invalid');
var responseMethods = unifyResponseMethods(packer.responseMethods);
if(options instanceof Function)
{
if(transport != undefined)
if (options instanceof Function) {
if (transport != undefined)
throw new SyntaxError("There can't be parameters after onRequest");
onRequest = options;
transport = undefined;
options = undefined;
options = undefined;
};
if(options && options.send instanceof Function)
{
if(transport && !(transport instanceof Function))
if (options && options.send instanceof Function) {
if (transport && !(transport instanceof Function))
throw new SyntaxError("Only a function can be after transport");
onRequest = transport;
transport = options;
options = undefined;
options = undefined;
};
if(transport instanceof Function)
{
if(onRequest != undefined)
if (transport instanceof Function) {
if (onRequest != undefined)
throw new SyntaxError("There can't be parameters after onRequest");
onRequest = transport;
transport = undefined;
};
if(transport && transport.send instanceof Function)
if(onRequest && !(onRequest instanceof Function))
if (transport && transport.send instanceof Function)
if (onRequest && !(onRequest instanceof Function))
throw new SyntaxError("Only a function can be after transport");
options = options || {};
@ -208,59 +198,55 @@ function RpcBuilder(packer, options, transport, onRequest)
EventEmitter.call(this);
if(onRequest)
if (onRequest)
this.on('request', onRequest);
if(defineProperty_IE8)
if (defineProperty_IE8)
this.peerID = options.peerID
else
Object.defineProperty(this, 'peerID', {value: options.peerID});
Object.defineProperty(this, 'peerID', {
value: options.peerID
});
var max_retries = options.max_retries || 0;
function transportMessage(event)
{
function transportMessage(event) {
self.decode(event.data || event);
};
this.getTransport = function()
{
this.getTransport = function () {
return transport;
}
this.setTransport = function(value)
{
this.setTransport = function (value) {
// Remove listener from old transport
if(transport)
{
if (transport) {
// W3C transports
if(transport.removeEventListener)
if (transport.removeEventListener)
transport.removeEventListener('message', transportMessage);
// Node.js Streams API
else if(transport.removeListener)
else if (transport.removeListener)
transport.removeListener('data', transportMessage);
};
// Set listener on new transport
if(value)
{
if (value) {
// W3C transports
if(value.addEventListener)
if (value.addEventListener)
value.addEventListener('message', transportMessage);
// Node.js Streams API
else if(value.addListener)
else if (value.addListener)
value.addListener('data', transportMessage);
};
transport = unifyTransport(value);
}
if(!defineProperty_IE8)
Object.defineProperty(this, 'transport',
{
if (!defineProperty_IE8)
Object.defineProperty(this, 'transport', {
get: this.getTransport.bind(this),
set: this.setTransport.bind(this)
})
@ -268,15 +254,15 @@ function RpcBuilder(packer, options, transport, onRequest)
this.setTransport(transport);
var request_timeout = options.request_timeout || BASE_TIMEOUT;
var request_timeout = options.request_timeout || BASE_TIMEOUT;
var ping_request_timeout = options.ping_request_timeout || request_timeout;
var response_timeout = options.response_timeout || BASE_TIMEOUT;
var duplicates_timeout = options.duplicates_timeout || BASE_TIMEOUT;
var response_timeout = options.response_timeout || BASE_TIMEOUT;
var duplicates_timeout = options.duplicates_timeout || BASE_TIMEOUT;
var requestID = 0;
var requests = new Mapper();
var requests = new Mapper();
var responses = new Mapper();
var processedResponses = new Mapper();
@ -286,17 +272,14 @@ function RpcBuilder(packer, options, transport, onRequest)
/**
* Store the response to prevent to process duplicate request later
*/
function storeResponse(message, id, dest)
{
var response =
{
function storeResponse(message, id, dest) {
var response = {
message: message,
/** Timeout to auto-clean old responses */
timeout: setTimeout(function()
{
responses.remove(id, dest);
},
response_timeout)
timeout: setTimeout(function () {
responses.remove(id, dest);
},
response_timeout)
};
responses.set(response, id, dest);
@ -305,13 +288,11 @@ function RpcBuilder(packer, options, transport, onRequest)
/**
* Store the response to ignore duplicated messages later
*/
function storeProcessedResponse(ack, from)
{
var timeout = setTimeout(function()
{
processedResponses.remove(ack, from);
},
duplicates_timeout);
function storeProcessedResponse(ack, from) {
var timeout = setTimeout(function () {
processedResponses.remove(ack, from);
},
duplicates_timeout);
processedResponses.set(timeout, ack, from);
};
@ -330,22 +311,18 @@ function RpcBuilder(packer, options, transport, onRequest)
* @param {Integer} id - identifier of the request
* @param [from] - source of the notification
*/
function RpcRequest(method, params, id, from, transport)
{
function RpcRequest(method, params, id, from, transport) {
RpcNotification.call(this, method, params);
this.getTransport = function()
{
this.getTransport = function () {
return transport;
}
this.setTransport = function(value)
{
this.setTransport = function (value) {
transport = unifyTransport(value);
}
if(!defineProperty_IE8)
Object.defineProperty(this, 'transport',
{
if (!defineProperty_IE8)
Object.defineProperty(this, 'transport', {
get: this.getTransport.bind(this),
set: this.setTransport.bind(this)
})
@ -355,13 +332,11 @@ function RpcBuilder(packer, options, transport, onRequest)
/**
* @constant {Boolean} duplicated
*/
if(!(transport || self.getTransport()))
{
if(defineProperty_IE8)
if (!(transport || self.getTransport())) {
if (defineProperty_IE8)
this.duplicated = Boolean(response)
else
Object.defineProperty(this, 'duplicated',
{
Object.defineProperty(this, 'duplicated', {
value: Boolean(response)
});
}
@ -378,23 +353,18 @@ function RpcBuilder(packer, options, transport, onRequest)
*
* @returns {string}
*/
this.reply = function(error, result, transport)
{
this.reply = function (error, result, transport) {
// Fix optional parameters
if(error instanceof Function || error && error.send instanceof Function)
{
if(result != undefined)
if (error instanceof Function || error && error.send instanceof Function) {
if (result != undefined)
throw new SyntaxError("There can't be parameters after callback");
transport = error;
result = null;
error = undefined;
}
else if(result instanceof Function
|| result && result.send instanceof Function)
{
if(transport != undefined)
} else if (result instanceof Function ||
result && result.send instanceof Function) {
if (transport != undefined)
throw new SyntaxError("There can't be parameters after callback");
transport = result;
@ -404,57 +374,48 @@ function RpcBuilder(packer, options, transport, onRequest)
transport = unifyTransport(transport);
// Duplicated request, remove old response timeout
if(response)
if (response)
clearTimeout(response.timeout);
if(from != undefined)
{
if(error)
if (from != undefined) {
if (error)
error.dest = from;
if(result)
if (result)
result.dest = from;
};
var message;
// New request or overriden one, create new response with provided data
if(error || result != undefined)
{
if(self.peerID != undefined)
{
if(error)
if (error || result != undefined) {
if (self.peerID != undefined) {
if (error)
error.from = self.peerID;
else
result.from = self.peerID;
}
// Protocol indicates that responses has own request methods
if(responseMethod)
{
if(responseMethod.error == undefined && error)
message =
{
if (responseMethod) {
if (responseMethod.error == undefined && error)
message = {
error: error
};
else
{
var method = error
? responseMethod.error
: responseMethod.response;
else {
var method = error ?
responseMethod.error :
responseMethod.response;
message =
{
message = {
method: method,
params: error || result
};
}
}
else
message =
{
error: error,
} else
message = {
error: error,
result: result
};
@ -462,12 +423,14 @@ function RpcBuilder(packer, options, transport, onRequest)
}
// Duplicate & not-overriden request, re-send old response
else if(response)
else if (response)
message = response.message;
// New empty reply, response null value
else
message = packer.pack({result: null}, id);
message = packer.pack({
result: null
}, id);
// Store the response to prevent to process a duplicated request later
storeResponse(message, id, from);
@ -475,7 +438,7 @@ function RpcBuilder(packer, options, transport, onRequest)
// Return the stored response so it can be directly send back
transport = transport || this.getTransport() || self.getTransport();
if(transport)
if (transport)
return transport.send(message);
return message;
@ -484,15 +447,14 @@ function RpcBuilder(packer, options, transport, onRequest)
inherits(RpcRequest, RpcNotification);
function cancel(message)
{
function cancel(message) {
var key = message2Key[message];
if(!key) return;
if (!key) return;
delete message2Key[message];
var request = requests.pop(key.id, key.dest);
if(!request) return;
if (!request) return;
clearTimeout(request.timeout);
@ -505,20 +467,18 @@ function RpcBuilder(packer, options, transport, onRequest)
*
* If `message` is not given, cancel all the request
*/
this.cancel = function(message)
{
if(message) return cancel(message);
this.cancel = function (message) {
if (message) return cancel(message);
for(var message in message2Key)
for (var message in message2Key)
cancel(message);
};
this.close = function()
{
this.close = function () {
// Prevent to receive new messages
var transport = this.getTransport();
if(transport && transport.close)
if (transport && transport.close)
transport.close(4003, "Cancel request");
// Request & processed responses
@ -527,8 +487,7 @@ function RpcBuilder(packer, options, transport, onRequest)
processedResponses.forEach(clearTimeout);
// Responses
responses.forEach(function(response)
{
responses.forEach(function (response) {
clearTimeout(response.timeout);
});
};
@ -546,102 +505,89 @@ function RpcBuilder(packer, options, transport, onRequest)
*
* @returns {string} A raw JsonRPC 2.0 request or notification string
*/
this.encode = function(method, params, dest, transport, callback)
{
this.encode = function (method, params, dest, transport, callback) {
// Fix optional parameters
if(params instanceof Function)
{
if(dest != undefined)
if (params instanceof Function) {
if (dest != undefined)
throw new SyntaxError("There can't be parameters after callback");
callback = params;
callback = params;
transport = undefined;
dest = undefined;
params = undefined;
}
else if(dest instanceof Function)
{
if(transport != undefined)
dest = undefined;
params = undefined;
} else if (dest instanceof Function) {
if (transport != undefined)
throw new SyntaxError("There can't be parameters after callback");
callback = dest;
callback = dest;
transport = undefined;
dest = undefined;
}
else if(transport instanceof Function)
{
if(callback != undefined)
dest = undefined;
} else if (transport instanceof Function) {
if (callback != undefined)
throw new SyntaxError("There can't be parameters after callback");
callback = transport;
callback = transport;
transport = undefined;
};
if(self.peerID != undefined)
{
if (self.peerID != undefined) {
params = params || {};
params.from = self.peerID;
};
if(dest != undefined)
{
if (dest != undefined) {
params = params || {};
params.dest = dest;
};
// Encode message
var message =
{
var message = {
method: method,
params: params
};
if(callback)
{
if (callback) {
var id = requestID++;
var retried = 0;
message = packer.pack(message, id);
function dispatchCallback(error, result)
{
function dispatchCallback(error, result) {
self.cancel(message);
callback(error, result);
};
var request =
{
message: message,
callback: dispatchCallback,
var request = {
message: message,
callback: dispatchCallback,
responseMethods: responseMethods[method] || {}
};
var encode_transport = unifyTransport(transport);
function sendRequest(transport)
{
function sendRequest(transport) {
var rt = (method === 'ping' ? ping_request_timeout : request_timeout);
request.timeout = setTimeout(timeout, rt*Math.pow(2, retried++));
message2Key[message] = {id: id, dest: dest};
request.timeout = setTimeout(timeout, rt * Math.pow(2, retried++));
message2Key[message] = {
id: id,
dest: dest
};
requests.set(request, id, dest);
transport = transport || encode_transport || self.getTransport();
if(transport)
if (transport)
return transport.send(message);
return message;
};
function retry(transport)
{
function retry(transport) {
transport = unifyTransport(transport);
console.warn(retried+' retry for request message:',message);
console.warn(retried + ' retry for request message:', message);
var timeout = processedResponses.pop(id, dest);
clearTimeout(timeout);
@ -649,13 +595,12 @@ function RpcBuilder(packer, options, transport, onRequest)
return sendRequest(transport);
};
function timeout()
{
if(retried < max_retries)
function timeout() {
if (retried < max_retries)
return retry(transport);
var error = new Error('Request has timed out');
error.request = message;
error.request = message;
error.retry = retry;
@ -669,7 +614,7 @@ function RpcBuilder(packer, options, transport, onRequest)
message = packer.pack(message);
transport = transport || this.getTransport();
if(transport)
if (transport)
return transport.send(message);
return message;
@ -686,23 +631,19 @@ function RpcBuilder(packer, options, transport, onRequest)
*
* @throws {TypeError} - Message is not defined
*/
this.decode = function(message, transport)
{
if(!message)
this.decode = function (message, transport) {
if (!message)
throw new TypeError("Message is not defined");
try
{
try {
message = packer.unpack(message);
}
catch(e)
{
} catch (e) {
// Ignore invalid messages
return console.debug(e, message);
};
var id = message.id;
var ack = message.ack;
var id = message.id;
var ack = message.ack;
var method = message.method;
var params = message.params || {};
@ -710,43 +651,38 @@ function RpcBuilder(packer, options, transport, onRequest)
var dest = params.dest;
// Ignore messages send by us
if(self.peerID != undefined && from == self.peerID) return;
if (self.peerID != undefined && from == self.peerID) return;
// Notification
if(id == undefined && ack == undefined)
{
if (id == undefined && ack == undefined) {
var notification = new RpcNotification(method, params);
if(self.emit('request', notification)) return;
if (self.emit('request', notification)) return;
return notification;
};
function processRequest()
{
function processRequest() {
// If we have a transport and it's a duplicated request, reply inmediatly
transport = unifyTransport(transport) || self.getTransport();
if(transport)
{
if (transport) {
var response = responses.get(id, from);
if(response)
if (response)
return transport.send(response.message);
};
var idAck = (id != undefined) ? id : ack;
var request = new RpcRequest(method, params, idAck, from, transport);
if(self.emit('request', request)) return;
if (self.emit('request', request)) return;
return request;
};
function processResponse(request, error, result)
{
function processResponse(request, error, result) {
request.callback(error, result);
};
function duplicatedResponse(timeout)
{
function duplicatedResponse(timeout) {
console.warn("Response already processed", message);
// Update duplicated responses timeout
@ -756,27 +692,24 @@ function RpcBuilder(packer, options, transport, onRequest)
// Request, or response with own method
if(method)
{
if (method) {
// Check if it's a response with own method
if(dest == undefined || dest == self.peerID)
{
if (dest == undefined || dest == self.peerID) {
var request = requests.get(ack, from);
if(request)
{
if (request) {
var responseMethods = request.responseMethods;
if(method == responseMethods.error)
if (method == responseMethods.error)
return processResponse(request, params);
if(method == responseMethods.response)
if (method == responseMethods.response)
return processResponse(request, null, params);
return processRequest();
}
var processed = processedResponses.get(ack, from);
if(processed)
if (processed)
return duplicatedResponse(processed);
}
@ -784,19 +717,18 @@ function RpcBuilder(packer, options, transport, onRequest)
return processRequest();
};
var error = message.error;
var error = message.error;
var result = message.result;
// Ignore responses not send to us
if(error && error.dest && error.dest != self.peerID) return;
if(result && result.dest && result.dest != self.peerID) return;
if (error && error.dest && error.dest != self.peerID) return;
if (result && result.dest && result.dest != self.peerID) return;
// Response
var request = requests.get(ack, from);
if(!request)
{
if (!request) {
var processed = processedResponses.get(ack, from);
if(processed)
if (processed)
return duplicatedResponse(processed);
return console.warn("No callback was defined for this message", message);
@ -819,4 +751,4 @@ var transports = require('./clients/transports');
RpcBuilder.clients = clients;
RpcBuilder.clients.transports = transports;
RpcBuilder.packers = packers;
RpcBuilder.packers = packers;

View File

@ -10,37 +10,31 @@
*
* @return {String} - the stringified JsonRPC 2.0 message
*/
function pack(message, id)
{
var result =
{
function pack(message, id) {
var result = {
jsonrpc: "2.0"
};
// Request
if(message.method)
{
if (message.method) {
result.method = message.method;
if(message.params)
if (message.params)
result.params = message.params;
// Request is a notification
if(id != undefined)
if (id != undefined)
result.id = id;
}
// Response
else if(id != undefined)
{
if(message.error)
{
if(message.result !== undefined)
else if (id != undefined) {
if (message.error) {
if (message.result !== undefined)
throw new TypeError("Both result and error are defined");
result.error = message.error;
}
else if(message.result !== undefined)
} else if (message.result !== undefined)
result.result = message.result;
else
throw new TypeError("No result or error is defined");
@ -60,35 +54,33 @@ function pack(message, id)
*
* @return {Object} - object filled with the JsonRPC 2.0 message content
*/
function unpack(message)
{
function unpack(message) {
var result = message;
if(typeof message === 'string' || message instanceof String) {
if (typeof message === 'string' || message instanceof String) {
result = JSON.parse(message);
}
// Check if it's a valid message
var version = result.jsonrpc;
if(version !== '2.0')
if (version !== '2.0')
throw new TypeError("Invalid JsonRPC version '" + version + "': " + message);
// Response
if(result.method == undefined)
{
if(result.id == undefined)
throw new TypeError("Invalid message: "+message);
if (result.method == undefined) {
if (result.id == undefined)
throw new TypeError("Invalid message: " + message);
var result_defined = result.result !== undefined;
var error_defined = result.error !== undefined;
var error_defined = result.error !== undefined;
// Check only result or error is defined, not both or none
if(result_defined && error_defined)
throw new TypeError("Both result and error are defined: "+message);
if (result_defined && error_defined)
throw new TypeError("Both result and error are defined: " + message);
if(!result_defined && !error_defined)
throw new TypeError("No result or error is defined: "+message);
if (!result_defined && !error_defined)
throw new TypeError("No result or error is defined: " + message);
result.ack = result.id;
delete result.id;
@ -99,5 +91,5 @@ function unpack(message)
};
exports.pack = pack;
exports.unpack = unpack;
exports.pack = pack;
exports.unpack = unpack;

View File

@ -1,13 +1,10 @@
function pack(message)
{
function pack(message) {
throw new TypeError("Not yet implemented");
};
function unpack(message)
{
function unpack(message) {
throw new TypeError("Not yet implemented");
};
exports.pack = pack;
exports.unpack = unpack;
exports.pack = pack;
exports.unpack = unpack;

View File

@ -1,6 +1,6 @@
var JsonRPC = require('./JsonRPC');
var XmlRPC = require('./XmlRPC');
var XmlRPC = require('./XmlRPC');
exports.JsonRPC = JsonRPC;
exports.XmlRPC = XmlRPC;
exports.XmlRPC = XmlRPC;

View File

@ -1,12 +1,124 @@
import {JL} from 'jsnlog'
import {OpenVidu} from "../../OpenVidu/OpenVidu";
import {OpenViduLoggerConfiguration} from "./OpenViduLoggerConfiguration";
import JSNLogAjaxAppender = JL.JSNLogAjaxAppender;
export class OpenViduLogger {
private static instance: OpenViduLogger;
private JSNLOG_URL: string = "/openvidu/elk/openvidu-browser-logs";
private MAX_JSNLOG_BATCH_LOG_MESSAGES: number = 100;
private MAX_MSECONDS_BATCH_MESSAGES: number = 5000;
private MAX_LENGTH_STRING_JSON: number = 1000;
private logger: Console = window.console;
private LOG_FNS = [this.logger.log, this.logger.debug, this.logger.info, this.logger.warn, this.logger.error];
private currentAppender: any;
private isProdMode = false;
private isJSNLogSetup = false;
// This two variables are used to restart JSNLog
// on different sessions and different userIds
private loggingSessionId: string | undefined;
private loggingFinalUserId: string | undefined;
private constructor() {}
static configureJSNLog(openVidu: OpenVidu, token: string) {
try {
// If instance is created and it is OpenVidu Pro
if (this.instance && openVidu.webrtcStatsInterval > -1
// If logs are enabled
&& openVidu.sendBrowserLogs === OpenViduLoggerConfiguration.debug
// Only reconfigure it if session or finalUserId has changed
&& this.instance.canConfigureJSNLog(openVidu, this.instance)) {
// isJSNLogSetup will not be true until completed setup
this.instance.isJSNLogSetup = false;
this.instance.info("Configuring JSNLogs.");
const finalUserId = openVidu.finalUserId;
const sessionId = openVidu.session.sessionId;
const beforeSendCallback = (xhr) => {
// If 401 or 403 or 404 modify ready and status so JSNLog don't retry to send logs
// https://github.com/mperdeck/jsnlog.js/blob/v2.30.0/jsnlog.ts#L805-L818
const parentReadyStateFunction = xhr.onreadystatechange;
xhr.onreadystatechange = () => {
if ((xhr.status == 401) || (xhr.status == 403) || (xhr.status == 404)) {
Object.defineProperty( xhr, "readyState", {value: 4});
Object.defineProperty( xhr, "status", {value: 200});
}
parentReadyStateFunction();
}
// Headers to identify and authenticate logs
xhr.setRequestHeader('Authorization', "Basic " + btoa(`${finalUserId}%/%${sessionId}` + ":" + token));
xhr.setRequestHeader('X-Requested-With', 'XMLHttpRequest')
// Additional headers for OpenVidu
xhr.setRequestHeader('OV-Final-User-Id', finalUserId);
xhr.setRequestHeader('OV-Session-Id', sessionId);
xhr.setRequestHeader('OV-Token', token);
}
// Creation of the appender.
this.instance.currentAppender = JL.createAjaxAppender(`appender-${finalUserId}-${sessionId}`);
this.instance.currentAppender.setOptions({
beforeSend: beforeSendCallback,
maxBatchSize: 1000,
batchSize: this.instance.MAX_JSNLOG_BATCH_LOG_MESSAGES,
batchTimeout: this.instance.MAX_MSECONDS_BATCH_MESSAGES
});
// Avoid circular dependencies
const logSerializer = (obj): string => {
const getCircularReplacer = () => {
const seen = new WeakSet();
return (key, value) => {
if (typeof value === "object" && value != null) {
if (seen.has(value) || value instanceof HTMLElement) {
return;
}
seen.add(value);
}
return value;
};
};
// Cut long messages
const stringifyJson = JSON.stringify(obj, getCircularReplacer());
if (stringifyJson.length > this.instance.MAX_LENGTH_STRING_JSON) {
return `${stringifyJson.substring(0, this.instance.MAX_LENGTH_STRING_JSON)}...`;
}
return stringifyJson;
};
// Initialize JL to send logs
JL.setOptions({
defaultAjaxUrl: openVidu.httpUri + this.instance.JSNLOG_URL,
serialize: logSerializer
});
JL().setOptions({
appenders: [this.instance.currentAppender]
});
this.instance.isJSNLogSetup = true;
this.instance.loggingSessionId = sessionId;
this.instance.loggingFinalUserId = finalUserId;
this.instance.info("JSNLog configured.");
}
} catch (e) {
console.error("Error configuring JSNLog: ");
console.error(e);
this.instance.isJSNLogSetup = false;
this.instance.loggingSessionId = undefined;
this.instance.loggingFinalUserId = undefined;
this.instance.currentAppender = undefined;
}
}
static getInstance(): OpenViduLogger {
if(!OpenViduLogger.instance){
OpenViduLogger.instance = new OpenViduLogger();
@ -14,10 +126,21 @@ export class OpenViduLogger {
return OpenViduLogger.instance;
}
private isDebugLogEnabled() {
return this.isJSNLogSetup;
}
private canConfigureJSNLog(openVidu: OpenVidu, logger: OpenViduLogger): boolean {
return openVidu.session.sessionId != logger.loggingSessionId
}
log(...args: any[]){
if (!this.isProdMode) {
this.LOG_FNS[0].apply(this.logger, arguments);
}
if (this.isDebugLogEnabled()) {
JL().info(arguments);
}
}
debug(...args: any[]) {
@ -30,19 +153,35 @@ export class OpenViduLogger {
if (!this.isProdMode) {
this.LOG_FNS[2].apply(this.logger, arguments);
}
if (this.isDebugLogEnabled()) {
JL().info(arguments);
}
}
warn(...args: any[]) {
if (!this.isProdMode) {
this.LOG_FNS[3].apply(this.logger, arguments);
}
if (this.isDebugLogEnabled()) {
JL().warn(arguments);
}
}
error(...args: any[]) {
this.LOG_FNS[4].apply(this.logger, arguments);
if (this.isDebugLogEnabled()) {
JL().error(arguments);
}
}
flush() {
if(this.isDebugLogEnabled() && this.currentAppender != null) {
this.currentAppender.sendBatch();
}
}
enableProdMode(){
this.isProdMode = true;
}
}
}

View File

@ -0,0 +1,4 @@
export enum OpenViduLoggerConfiguration {
disabled = 'disabled',
debug = 'debug'
}

View File

@ -2,7 +2,7 @@ import platform = require("platform");
export class PlatformUtils {
protected static instance: PlatformUtils;
constructor() {}
constructor() { }
static getInstance(): PlatformUtils {
if (!this.instance) {
@ -118,7 +118,7 @@ export class PlatformUtils {
*/
public isIOSWithSafari(): boolean {
const userAgent = !!platform.ua ? platform.ua : navigator.userAgent;
return (
return this.isIPhoneOrIPad() && (
/\b(\w*Apple\w*)\b/.test(navigator.vendor) &&
/\b(\w*Safari\w*)\b/.test(userAgent) &&
!/\b(\w*CriOS\w*)\b/.test(userAgent) &&
@ -156,6 +156,18 @@ export class PlatformUtils {
return false;
}
/**
* @hidden
*/
public isChromium(): boolean {
return this.isChromeBrowser() || this.isChromeMobileBrowser() ||
this.isOperaBrowser() || this.isOperaMobileBrowser() ||
this.isEdgeBrowser() || this.isEdgeMobileBrowser() ||
this.isSamsungBrowser() ||
this.isIonicAndroid() || this.isIonicIos() ||
this.isElectron();
}
/**
* @hidden
*/

View File

@ -16,7 +16,8 @@
*/
import freeice = require('freeice');
import uuid = require('uuid');
import { v4 as uuidv4 } from 'uuid';
import { ExceptionEventName } from '../Events/ExceptionEvent';
import { OpenViduLogger } from '../Logger/OpenViduLogger';
import { PlatformUtils } from '../Utils/Platform';
@ -36,8 +37,9 @@ export interface WebRtcPeerConfiguration {
video: boolean
};
simulcast: boolean;
onicecandidate: (event) => void;
iceServers?: RTCIceServer[];
onicecandidate: (event: RTCIceCandidate) => void;
onexception: (exceptionName: ExceptionEventName, message: string, data?: any) => void;
iceServers: RTCIceServer[] | undefined;
mediaStream?: MediaStream | null;
mode?: 'sendonly' | 'recvonly' | 'sendrecv';
id?: string;
@ -69,49 +71,23 @@ export class WebRtcPeer {
this.pc = new RTCPeerConnection({ iceServers: this.configuration.iceServers });
this.pc.onicecandidate = event => {
if (!!event.candidate) {
this.pc.addEventListener('icecandidate', (event: RTCPeerConnectionIceEvent) => {
if (event.candidate != null) {
const candidate: RTCIceCandidate = event.candidate;
if (candidate) {
this.configuration.onicecandidate(candidate);
if (candidate.candidate !== '') {
this.localCandidatesQueue.push(<RTCIceCandidate>{ candidate: candidate.candidate });
this.candidategatheringdone = false;
this.configuration.onicecandidate(event.candidate);
} else if (!this.candidategatheringdone) {
this.candidategatheringdone = true;
}
}
};
});
this.pc.onsignalingstatechange = () => {
this.pc.addEventListener('signalingstatechange', () => {
if (this.pc.signalingState === 'stable') {
while (this.iceCandidateList.length > 0) {
let candidate = this.iceCandidateList.shift();
this.pc.addIceCandidate(<RTCIceCandidate>candidate);
}
}
};
this.start();
}
/**
* This function creates the RTCPeerConnection object taking into account the
* properties received in the constructor. It starts the SDP negotiation
* process: generates the SDP offer and invokes the onsdpoffer callback. This
* callback is expected to send the SDP offer, in order to obtain an SDP
* answer from another peer.
*/
start(): Promise<any> {
return new Promise((resolve, reject) => {
if (this.pc.signalingState === 'closed') {
reject('The peer connection object is in "closed" state. This is most likely due to an invocation of the dispose method before accepting in the dialogue');
}
if (!!this.configuration.mediaStream) {
for (const track of this.configuration.mediaStream.getTracks()) {
this.pc.addTrack(track, this.configuration.mediaStream);
}
resolve();
}
});
}
@ -131,20 +107,20 @@ export class WebRtcPeer {
}
/**
* Function that creates an offer, sets it as local description and returns the offer param
* to send to OpenVidu Server (will be the remote description of other peer)
* Creates an SDP offer from the local RTCPeerConnection to send to the other peer
* Only if the negotiation was initiated by the this peer
*/
generateOffer(): Promise<string> {
createOffer(): Promise<RTCSessionDescriptionInit> {
return new Promise((resolve, reject) => {
const useAudio = this.configuration.mediaConstraints.audio;
const useVideo = this.configuration.mediaConstraints.video;
const hasAudio = this.configuration.mediaConstraints.audio;
const hasVideo = this.configuration.mediaConstraints.video;
let offerPromise: Promise<RTCSessionDescriptionInit>;
// TODO: Delete this conditional when all supported browsers are
// modern enough to implement the getTransceivers() method.
if ("getTransceivers" in this.pc) {
logger.debug("[generateOffer] Method pc.getTransceivers() is available; using it");
// modern enough to implement the Transceiver methods.
if ("addTransceiver" in this.pc) {
logger.debug("[createOffer] Method RTCPeerConnection.addTransceiver() is available; using it");
// At this point, all "send" audio/video tracks have been added
// with pc.addTrack(), which in modern versions of libwebrtc
@ -183,13 +159,13 @@ export class WebRtcPeer {
);
}
if (useAudio) {
if (hasAudio) {
this.pc.addTransceiver("audio", {
direction: this.configuration.mode,
});
}
if (useVideo) {
if (hasVideo) {
this.pc.addTransceiver("video", {
direction: this.configuration.mode,
});
@ -206,9 +182,9 @@ export class WebRtcPeer {
const constraints: RTCOfferOptions = {
offerToReceiveAudio:
this.configuration.mode !== "sendonly" && useAudio,
this.configuration.mode !== "sendonly" && hasAudio,
offerToReceiveVideo:
this.configuration.mode !== "sendonly" && useVideo,
this.configuration.mode !== "sendonly" && hasVideo,
};
logger.debug(
@ -244,10 +220,94 @@ export class WebRtcPeer {
}
/**
* Function invoked when a SDP answer is received. Final step in SDP negotiation, the peer
* just needs to set the answer as its remote description
* Creates an SDP answer from the local RTCPeerConnection to send to the other peer
* Only if the negotiation was initiated by the other peer
*/
processAnswer(sdpAnswer: string, needsTimeoutOnProcessAnswer: boolean): Promise<string> {
createAnswer(): Promise<RTCSessionDescriptionInit> {
return new Promise((resolve, reject) => {
let offerAudio, offerVideo = true;
if (!!this.configuration.mediaConstraints) {
offerAudio = (typeof this.configuration.mediaConstraints.audio === 'boolean') ?
this.configuration.mediaConstraints.audio : true;
offerVideo = (typeof this.configuration.mediaConstraints.video === 'boolean') ?
this.configuration.mediaConstraints.video : true;
}
const constraints: RTCOfferOptions = {
offerToReceiveAudio: offerAudio,
offerToReceiveVideo: offerVideo
};
this.pc.createAnswer(constraints).then(sdpAnswer => {
resolve(sdpAnswer);
}).catch(error => {
reject(error);
});
});
}
/**
* This peer initiated negotiation. Step 1/4 of SDP offer-answer protocol
*/
processLocalOffer(offer: RTCSessionDescriptionInit): Promise<void> {
return new Promise((resolve, reject) => {
this.pc.setLocalDescription(offer)
.then(() => {
const localDescription = this.pc.localDescription;
if (!!localDescription) {
logger.debug('Local description set', localDescription.sdp);
resolve();
} else {
reject('Local description is not defined');
}
})
.catch(error => {
reject(error);
});
});
}
/**
* Other peer initiated negotiation. Step 2/4 of SDP offer-answer protocol
*/
processRemoteOffer(sdpOffer: string): Promise<void> {
return new Promise((resolve, reject) => {
const offer: RTCSessionDescriptionInit = {
type: 'offer',
sdp: sdpOffer
};
logger.debug('SDP offer received, setting remote description', offer);
if (this.pc.signalingState === 'closed') {
reject('RTCPeerConnection is closed when trying to set remote description');
}
this.setRemoteDescription(offer)
.then(() => {
resolve();
})
.catch(error => {
reject(error);
});
});
}
/**
* Other peer initiated negotiation. Step 3/4 of SDP offer-answer protocol
*/
processLocalAnswer(answer: RTCSessionDescriptionInit): Promise<void> {
return new Promise((resolve, reject) => {
logger.debug('SDP answer created, setting local description');
if (this.pc.signalingState === 'closed') {
reject('RTCPeerConnection is closed when trying to set local description');
}
this.pc.setLocalDescription(answer)
.then(() => resolve())
.catch(error => reject(error));
});
}
/**
* This peer initiated negotiation. Step 4/4 of SDP offer-answer protocol
*/
processRemoteAnswer(sdpAnswer: string): Promise<void> {
return new Promise((resolve, reject) => {
const answer: RTCSessionDescriptionInit = {
type: 'answer',
@ -256,34 +316,19 @@ export class WebRtcPeer {
logger.debug('SDP answer received, setting remote description');
if (this.pc.signalingState === 'closed') {
reject('RTCPeerConnection is closed');
reject('RTCPeerConnection is closed when trying to set remote description');
}
this.setRemoteDescription(answer, needsTimeoutOnProcessAnswer, resolve, reject);
this.setRemoteDescription(answer)
.then(() => resolve())
.catch(error => reject(error));
});
}
/**
* @hidden
*/
setRemoteDescription(answer: RTCSessionDescriptionInit, needsTimeoutOnProcessAnswer: boolean, resolve: (value?: string | PromiseLike<string> | undefined) => void, reject: (reason?: any) => void) {
if (platform.isIonicIos()) {
// Ionic iOS platform
if (needsTimeoutOnProcessAnswer) {
// 400 ms have not elapsed yet since first remote stream triggered Stream#initWebRtcPeerReceive
setTimeout(() => {
logger.info('setRemoteDescription run after timeout for Ionic iOS device');
this.pc.setRemoteDescription(new RTCSessionDescription(answer)).then(() => resolve()).catch(error => reject(error));
}, 250);
} else {
// 400 ms have elapsed
this.pc.setRemoteDescription(new RTCSessionDescription(answer)).then(() => resolve()).catch(error => reject(error));
}
} else {
// Rest of platforms
this.pc.setRemoteDescription(answer).then(() => resolve()).catch(error => reject(error));
}
async setRemoteDescription(sdp: RTCSessionDescriptionInit): Promise<void> {
return this.pc.setRemoteDescription(sdp);
}
/**
@ -313,15 +358,19 @@ export class WebRtcPeer {
}
addIceConnectionStateChangeListener(otherId: string) {
this.pc.oniceconnectionstatechange = () => {
this.pc.addEventListener('iceconnectionstatechange', () => {
const iceConnectionState: RTCIceConnectionState = this.pc.iceConnectionState;
switch (iceConnectionState) {
case 'disconnected':
// Possible network disconnection
logger.warn('IceConnectionState of RTCPeerConnection ' + this.configuration.id + ' (' + otherId + ') change to "disconnected". Possible network disconnection');
const msg1 = 'IceConnectionState of RTCPeerConnection ' + this.id + ' (' + otherId + ') change to "disconnected". Possible network disconnection';
logger.warn(msg1);
this.configuration.onexception(ExceptionEventName.ICE_CONNECTION_DISCONNECTED, msg1);
break;
case 'failed':
logger.error('IceConnectionState of RTCPeerConnection ' + this.configuration.id + ' (' + otherId + ') to "failed"');
const msg2 = 'IceConnectionState of RTCPeerConnection ' + this.id + ' (' + otherId + ') to "failed"';
logger.error(msg2);
this.configuration.onexception(ExceptionEventName.ICE_CONNECTION_FAILED, msg2);
break;
case 'closed':
logger.log('IceConnectionState of RTCPeerConnection ' + this.configuration.id + ' (' + otherId + ') change to "closed"');
@ -339,14 +388,14 @@ export class WebRtcPeer {
logger.log('IceConnectionState of RTCPeerConnection ' + this.configuration.id + ' (' + otherId + ') change to "completed"');
break;
}
}
});
}
/**
* @hidden
*/
generateUniqueId(): string {
return uuid.v4();
return uuidv4();
}
}

View File

@ -29,39 +29,76 @@ const logger: OpenViduLogger = OpenViduLogger.getInstance();
*/
let platform: PlatformUtils;
interface WebrtcStatsConfig {
interval: number,
httpEndpoint: string
}
interface JSONStatsResponse {
'@timestamp': string,
participant_id: string,
session_id: string,
platform: string,
platform_description: string,
stream: string,
webrtc_stats: IWebrtcStats
}
/**
* Common WebRtcSTats for latest Chromium and Firefox versions
*/
interface IWebrtcStats {
inbound?: {
audio: {
bytesReceived: number,
packetsReceived: number,
packetsLost: number,
jitter: number
} | {},
video: {
bytesReceived: number,
packetsReceived: number,
packetsLost: number,
jitter?: number, // Firefox
jitterBufferDelay?: number, // Chrome
framesDecoded: number,
firCount: number,
nackCount: number,
pliCount: number,
frameHeight?: number, // Chrome
frameWidth?: number, // Chrome
framesDropped?: number, // Chrome
framesReceived?: number // Chrome
} | {}
},
outbound?: {
audio: {
bytesSent: number,
packetsSent: number,
} | {},
video: {
bytesSent: number,
packetsSent: number,
firCount: number,
framesEncoded: number,
nackCount: number,
pliCount: number,
qpSum: number,
frameHeight?: number, // Chrome
frameWidth?: number, // Chrome
framesSent?: number // Chrome
} | {}
}
};
export class WebRtcStats {
private readonly STATS_ITEM_NAME = 'webrtc-stats-config';
private webRtcStatsEnabled = false;
private webRtcStatsIntervalId: NodeJS.Timer;
private statsInterval = 1;
private stats: any = {
inbound: {
audio: {
bytesReceived: 0,
packetsReceived: 0,
packetsLost: 0
},
video: {
bytesReceived: 0,
packetsReceived: 0,
packetsLost: 0,
framesDecoded: 0,
nackCount: 0
}
},
outbound: {
audio: {
bytesSent: 0,
packetsSent: 0,
},
video: {
bytesSent: 0,
packetsSent: 0,
framesEncoded: 0,
nackCount: 0
}
}
};
private POST_URL: string;
constructor(private stream: Stream) {
platform = PlatformUtils.getInstance();
@ -73,28 +110,174 @@ export class WebRtcStats {
public initWebRtcStats(): void {
const elastestInstrumentation = localStorage.getItem('elastest-instrumentation');
if (!!elastestInstrumentation) {
// ElasTest instrumentation object found in local storage
logger.warn('WebRtc stats enabled for stream ' + this.stream.streamId + ' of connection ' + this.stream.connection.connectionId);
const webrtcObj = localStorage.getItem(this.STATS_ITEM_NAME);
if (!!webrtcObj) {
this.webRtcStatsEnabled = true;
const webrtcStatsConfig: WebrtcStatsConfig = JSON.parse(webrtcObj);
// webrtc object found in local storage
logger.warn('WebRtc stats enabled for stream ' + this.stream.streamId + ' of connection ' + this.stream.connection.connectionId);
logger.warn('localStorage item: ' + JSON.stringify(webrtcStatsConfig));
const instrumentation = JSON.parse(elastestInstrumentation);
this.statsInterval = instrumentation.webrtc.interval; // Interval in seconds
this.POST_URL = webrtcStatsConfig.httpEndpoint;
this.statsInterval = webrtcStatsConfig.interval; // Interval in seconds
logger.warn('localStorage item: ' + JSON.stringify(instrumentation));
this.webRtcStatsIntervalId = setInterval(() => {
this.sendStatsToHttpEndpoint(instrumentation);
this.webRtcStatsIntervalId = setInterval(async () => {
await this.sendStatsToHttpEndpoint();
}, this.statsInterval * 1000);
return;
} else {
logger.debug('WebRtc stats not enabled');
}
}
logger.debug('WebRtc stats not enabled');
// {
// "localCandidate": {
// "id": "RTCIceCandidate_/r4P1y2Q",
// "timestamp": 1616080155617,
// "type": "local-candidate",
// "transportId": "RTCTransport_0_1",
// "isRemote": false,
// "networkType": "wifi",
// "ip": "123.45.67.89",
// "port": 63340,
// "protocol": "udp",
// "candidateType": "srflx",
// "priority": 1686052607,
// "deleted": false,
// "raw": [
// "candidate:3345412921 1 udp 1686052607 123.45.67.89 63340 typ srflx raddr 192.168.1.31 rport 63340 generation 0 ufrag 0ZtT network-id 1 network-cost 10",
// "candidate:58094482 1 udp 41885695 98.76.54.32 44431 typ relay raddr 123.45.67.89 rport 63340 generation 0 ufrag 0ZtT network-id 1 network-cost 10"
// ]
// },
// "remoteCandidate": {
// "id": "RTCIceCandidate_1YO18gph",
// "timestamp": 1616080155617,
// "type": "remote-candidate",
// "transportId": "RTCTransport_0_1",
// "isRemote": true,
// "ip": "12.34.56.78",
// "port": 64989,
// "protocol": "udp",
// "candidateType": "srflx",
// "priority": 1679819263,
// "deleted": false,
// "raw": [
// "candidate:16 1 UDP 1679819263 12.34.56.78 64989 typ srflx raddr 172.19.0.1 rport 64989",
// "candidate:16 1 UDP 1679819263 12.34.56.78 64989 typ srflx raddr 172.19.0.1 rport 64989"
// ]
// }
// }
// Have been tested in:
// - Linux Desktop:
// - Chrome 89.0.4389.90
// - Opera 74.0.3911.218
// - Firefox 86
// - Microsoft Edge 91.0.825.0
// - Electron 11.3.0 (Chromium 87.0.4280.141)
// - Windows Desktop:
// - Chrome 89.0.4389.90
// - Opera 74.0.3911.232
// - Firefox 86.0.1
// - Microsoft Edge 89.0.774.54
// - Electron 11.3.0 (Chromium 87.0.4280.141)
// - MacOS Desktop:
// - Chrome 89.0.4389.90
// - Firefox 87.0
// - Opera 75.0.3969.93
// - Microsoft Edge 89.0.774.57
// - Safari 14.0 (14610.1.28.1.9)
// - Electron 11.3.0 (Chromium 87.0.4280.141)
// - Android:
// - Chrome Mobile 89.0.4389.90
// - Opera 62.3.3146.57763
// - Firefox Mobile 86.6.1
// - Microsoft Edge Mobile 46.02.4.5147
// - Ionic 5
// - React Native 0.64
// - iOS:
// - Safari Mobile
// - ¿Ionic?
// - ¿React Native?
public getSelectedIceCandidateInfo(): Promise<any> {
return new Promise(async (resolve, reject) => {
const statsReport: any = await this.stream.getRTCPeerConnection().getStats();
let transportStat;
const candidatePairs: Map<string, any> = new Map();
const localCandidates: Map<string, any> = new Map();
const remoteCandidates: Map<string, any> = new Map();
statsReport.forEach((stat: any) => {
if (stat.type === 'transport' && (platform.isChromium() || platform.isSafariBrowser() || platform.isReactNative())) {
transportStat = stat;
}
switch (stat.type) {
case 'candidate-pair':
candidatePairs.set(stat.id, stat);
break;
case 'local-candidate':
localCandidates.set(stat.id, stat);
break;
case 'remote-candidate':
remoteCandidates.set(stat.id, stat);
break;
}
});
let selectedCandidatePair;
if (transportStat != null) {
const selectedCandidatePairId = transportStat.selectedCandidatePairId
selectedCandidatePair = candidatePairs.get(selectedCandidatePairId);
} else {
// This is basically Firefox
const length = candidatePairs.size;
const iterator = candidatePairs.values();
for (let i = 0; i < length; i++) {
const candidatePair = iterator.next().value;
if (candidatePair['selected']) {
selectedCandidatePair = candidatePair;
break;
}
}
}
const localCandidateId = selectedCandidatePair.localCandidateId;
const remoteCandidateId = selectedCandidatePair.remoteCandidateId;
let finalLocalCandidate = localCandidates.get(localCandidateId);
if (!!finalLocalCandidate) {
const candList = this.stream.getLocalIceCandidateList();
const cand = candList.filter((c: RTCIceCandidate) => {
return (!!c.candidate &&
(c.candidate.indexOf(finalLocalCandidate.ip) >= 0 || c.candidate.indexOf(finalLocalCandidate.address) >= 0) &&
c.candidate.indexOf(finalLocalCandidate.port) >= 0);
});
finalLocalCandidate.raw = [];
for (let c of cand) {
finalLocalCandidate.raw.push(c.candidate);
}
} else {
finalLocalCandidate = 'ERROR: No active local ICE candidate. Probably ICE-TCP is being used';
}
let finalRemoteCandidate = remoteCandidates.get(remoteCandidateId);
if (!!finalRemoteCandidate) {
const candList = this.stream.getRemoteIceCandidateList();
const cand = candList.filter((c: RTCIceCandidate) => {
return (!!c.candidate &&
(c.candidate.indexOf(finalRemoteCandidate.ip) >= 0 || c.candidate.indexOf(finalRemoteCandidate.address) >= 0) &&
c.candidate.indexOf(finalRemoteCandidate.port) >= 0);
});
finalRemoteCandidate.raw = [];
for (let c of cand) {
finalRemoteCandidate.raw.push(c.candidate);
}
} else {
finalRemoteCandidate = 'ERROR: No active remote ICE candidate. Probably ICE-TCP is being used';
}
resolve({
localCandidate: finalLocalCandidate,
remoteCandidate: finalRemoteCandidate
});
});
}
public stopWebRtcStats() {
@ -104,319 +287,146 @@ export class WebRtcStats {
}
}
public getSelectedIceCandidateInfo(): Promise<any> {
return new Promise((resolve, reject) => {
this.getStatsAgnostic(this.stream.getRTCPeerConnection(),
(stats) => {
if (platform.isChromeBrowser() || platform.isChromeMobileBrowser() || platform.isOperaBrowser() || platform.isOperaMobileBrowser()) {
let localCandidateId, remoteCandidateId, googCandidatePair;
const localCandidates = {};
const remoteCandidates = {};
for (const key in stats) {
const stat = stats[key];
if (stat.type === 'localcandidate') {
localCandidates[stat.id] = stat;
} else if (stat.type === 'remotecandidate') {
remoteCandidates[stat.id] = stat;
} else if (stat.type === 'googCandidatePair' && (stat.googActiveConnection === 'true')) {
googCandidatePair = stat;
localCandidateId = stat.localCandidateId;
remoteCandidateId = stat.remoteCandidateId;
}
}
let finalLocalCandidate = localCandidates[localCandidateId];
if (!!finalLocalCandidate) {
const candList = this.stream.getLocalIceCandidateList();
const cand = candList.filter((c: RTCIceCandidate) => {
return (!!c.candidate &&
c.candidate.indexOf(finalLocalCandidate.ipAddress) >= 0 &&
c.candidate.indexOf(finalLocalCandidate.portNumber) >= 0 &&
c.candidate.indexOf(finalLocalCandidate.priority) >= 0);
});
finalLocalCandidate.raw = !!cand[0] ? cand[0].candidate : 'ERROR: Cannot find local candidate in list of sent ICE candidates';
} else {
finalLocalCandidate = 'ERROR: No active local ICE candidate. Probably ICE-TCP is being used';
}
let finalRemoteCandidate = remoteCandidates[remoteCandidateId];
if (!!finalRemoteCandidate) {
const candList = this.stream.getRemoteIceCandidateList();
const cand = candList.filter((c: RTCIceCandidate) => {
return (!!c.candidate &&
c.candidate.indexOf(finalRemoteCandidate.ipAddress) >= 0 &&
c.candidate.indexOf(finalRemoteCandidate.portNumber) >= 0 &&
c.candidate.indexOf(finalRemoteCandidate.priority) >= 0);
});
finalRemoteCandidate.raw = !!cand[0] ? cand[0].candidate : 'ERROR: Cannot find remote candidate in list of received ICE candidates';
} else {
finalRemoteCandidate = 'ERROR: No active remote ICE candidate. Probably ICE-TCP is being used';
}
resolve({
googCandidatePair,
localCandidate: finalLocalCandidate,
remoteCandidate: finalRemoteCandidate
});
} else {
reject('Selected ICE candidate info only available for Chrome');
}
private async sendStats(url: string, response: JSONStatsResponse): Promise<void> {
try {
const configuration: RequestInit = {
headers: {
'Content-type': 'application/json'
},
(error) => {
reject(error);
});
});
}
private sendStatsToHttpEndpoint(instrumentation): void {
const sendPost = (json) => {
const http: XMLHttpRequest = new XMLHttpRequest();
const url: string = instrumentation.webrtc.httpEndpoint;
http.open('POST', url, true);
http.setRequestHeader('Content-type', 'application/json');
http.onreadystatechange = () => { // Call a function when the state changes.
if (http.readyState === 4 && http.status === 200) {
logger.log('WebRtc stats successfully sent to ' + url + ' for stream ' + this.stream.streamId + ' of connection ' + this.stream.connection.connectionId);
}
body: JSON.stringify(response),
method: 'POST',
};
http.send(json);
};
await fetch(url, configuration);
const f = (stats) => {
if (platform.isFirefoxBrowser() || platform.isFirefoxMobileBrowser()) {
stats.forEach((stat) => {
let json = {};
if ((stat.type === 'inbound-rtp') &&
(
// Avoid firefox empty outbound-rtp statistics
stat.nackCount !== null &&
stat.isRemote === false &&
stat.id.startsWith('inbound') &&
stat.remoteId.startsWith('inbound')
)) {
const metricId = 'webrtc_inbound_' + stat.mediaType + '_' + stat.ssrc;
const jit = stat.jitter * 1000;
const metrics = {
bytesReceived: (stat.bytesReceived - this.stats.inbound[stat.mediaType].bytesReceived) / this.statsInterval,
jitter: jit,
packetsReceived: (stat.packetsReceived - this.stats.inbound[stat.mediaType].packetsReceived) / this.statsInterval,
packetsLost: (stat.packetsLost - this.stats.inbound[stat.mediaType].packetsLost) / this.statsInterval
};
const units = {
bytesReceived: 'bytes',
jitter: 'ms',
packetsReceived: 'packets',
packetsLost: 'packets'
};
if (stat.mediaType === 'video') {
metrics['framesDecoded'] = (stat.framesDecoded - this.stats.inbound.video.framesDecoded) / this.statsInterval;
metrics['nackCount'] = (stat.nackCount - this.stats.inbound.video.nackCount) / this.statsInterval;
units['framesDecoded'] = 'frames';
units['nackCount'] = 'packets';
this.stats.inbound.video.framesDecoded = stat.framesDecoded;
this.stats.inbound.video.nackCount = stat.nackCount;
}
this.stats.inbound[stat.mediaType].bytesReceived = stat.bytesReceived;
this.stats.inbound[stat.mediaType].packetsReceived = stat.packetsReceived;
this.stats.inbound[stat.mediaType].packetsLost = stat.packetsLost;
json = {
'@timestamp': new Date(stat.timestamp).toISOString(),
'exec': instrumentation.exec,
'component': instrumentation.component,
'stream': 'webRtc',
'et_type': metricId,
'stream_type': 'composed_metrics',
'units': units
};
json[metricId] = metrics;
sendPost(JSON.stringify(json));
} else if ((stat.type === 'outbound-rtp') &&
(
// Avoid firefox empty inbound-rtp statistics
stat.isRemote === false &&
stat.id.toLowerCase().includes('outbound')
)) {
const metricId = 'webrtc_outbound_' + stat.mediaType + '_' + stat.ssrc;
const metrics = {
bytesSent: (stat.bytesSent - this.stats.outbound[stat.mediaType].bytesSent) / this.statsInterval,
packetsSent: (stat.packetsSent - this.stats.outbound[stat.mediaType].packetsSent) / this.statsInterval
};
const units = {
bytesSent: 'bytes',
packetsSent: 'packets'
};
if (stat.mediaType === 'video') {
metrics['framesEncoded'] = (stat.framesEncoded - this.stats.outbound.video.framesEncoded) / this.statsInterval;
units['framesEncoded'] = 'frames';
this.stats.outbound.video.framesEncoded = stat.framesEncoded;
}
this.stats.outbound[stat.mediaType].bytesSent = stat.bytesSent;
this.stats.outbound[stat.mediaType].packetsSent = stat.packetsSent;
json = {
'@timestamp': new Date(stat.timestamp).toISOString(),
'exec': instrumentation.exec,
'component': instrumentation.component,
'stream': 'webRtc',
'et_type': metricId,
'stream_type': 'composed_metrics',
'units': units
};
json[metricId] = metrics;
sendPost(JSON.stringify(json));
}
});
} else if (platform.isChromeBrowser() || platform.isChromeMobileBrowser() || platform.isOperaBrowser() || platform.isOperaMobileBrowser()) {
for (const key of Object.keys(stats)) {
const stat = stats[key];
if (stat.type === 'ssrc') {
let json = {};
if ('bytesReceived' in stat && (
(stat.mediaType === 'audio' && 'audioOutputLevel' in stat) ||
(stat.mediaType === 'video' && 'qpSum' in stat)
)) {
// inbound-rtp
const metricId = 'webrtc_inbound_' + stat.mediaType + '_' + stat.ssrc;
const metrics = {
bytesReceived: (stat.bytesReceived - this.stats.inbound[stat.mediaType].bytesReceived) / this.statsInterval,
jitter: stat.googJitterBufferMs,
packetsReceived: (stat.packetsReceived - this.stats.inbound[stat.mediaType].packetsReceived) / this.statsInterval,
packetsLost: (stat.packetsLost - this.stats.inbound[stat.mediaType].packetsLost) / this.statsInterval
};
const units = {
bytesReceived: 'bytes',
jitter: 'ms',
packetsReceived: 'packets',
packetsLost: 'packets'
};
if (stat.mediaType === 'video') {
metrics['framesDecoded'] = (stat.framesDecoded - this.stats.inbound.video.framesDecoded) / this.statsInterval;
metrics['nackCount'] = (stat.googNacksSent - this.stats.inbound.video.nackCount) / this.statsInterval;
units['framesDecoded'] = 'frames';
units['nackCount'] = 'packets';
this.stats.inbound.video.framesDecoded = stat.framesDecoded;
this.stats.inbound.video.nackCount = stat.googNacksSent;
}
this.stats.inbound[stat.mediaType].bytesReceived = stat.bytesReceived;
this.stats.inbound[stat.mediaType].packetsReceived = stat.packetsReceived;
this.stats.inbound[stat.mediaType].packetsLost = stat.packetsLost;
json = {
'@timestamp': new Date(stat.timestamp).toISOString(),
'exec': instrumentation.exec,
'component': instrumentation.component,
'stream': 'webRtc',
'et_type': metricId,
'stream_type': 'composed_metrics',
'units': units
};
json[metricId] = metrics;
sendPost(JSON.stringify(json));
} else if ('bytesSent' in stat) {
// outbound-rtp
const metricId = 'webrtc_outbound_' + stat.mediaType + '_' + stat.ssrc;
const metrics = {
bytesSent: (stat.bytesSent - this.stats.outbound[stat.mediaType].bytesSent) / this.statsInterval,
packetsSent: (stat.packetsSent - this.stats.outbound[stat.mediaType].packetsSent) / this.statsInterval
};
const units = {
bytesSent: 'bytes',
packetsSent: 'packets'
};
if (stat.mediaType === 'video') {
metrics['framesEncoded'] = (stat.framesEncoded - this.stats.outbound.video.framesEncoded) / this.statsInterval;
units['framesEncoded'] = 'frames';
this.stats.outbound.video.framesEncoded = stat.framesEncoded;
}
this.stats.outbound[stat.mediaType].bytesSent = stat.bytesSent;
this.stats.outbound[stat.mediaType].packetsSent = stat.packetsSent;
json = {
'@timestamp': new Date(stat.timestamp).toISOString(),
'exec': instrumentation.exec,
'component': instrumentation.component,
'stream': 'webRtc',
'et_type': metricId,
'stream_type': 'composed_metrics',
'units': units
};
json[metricId] = metrics;
sendPost(JSON.stringify(json));
}
}
}
}
};
this.getStatsAgnostic(this.stream.getRTCPeerConnection(), f, (error) => { logger.log(error); });
}
private standardizeReport(response) {
logger.log(response);
const standardReport = {};
if (platform.isFirefoxBrowser() || platform.isFirefoxMobileBrowser()) {
Object.keys(response).forEach(key => {
logger.log(response[key]);
});
return response;
} catch (error) {
logger.error(error);
}
response.result().forEach(report => {
const standardStats = {
id: report.id,
timestamp: report.timestamp,
type: report.type
};
report.names().forEach((name) => {
standardStats[name] = report.stat(name);
});
standardReport[standardStats.id] = standardStats;
});
return standardReport;
}
private getStatsAgnostic(pc, successCb, failureCb) {
if (platform.isFirefoxBrowser() || platform.isFirefoxMobileBrowser()) {
// getStats takes args in different order in Chrome and Firefox
return pc.getStats(null).then(response => {
const report = this.standardizeReport(response);
successCb(report);
}).catch(failureCb);
} else if (platform.isChromeBrowser() || platform.isChromeMobileBrowser() || platform.isOperaBrowser() || platform.isOperaMobileBrowser()) {
// In Chrome, the first two arguments are reversed
return pc.getStats((response) => {
const report = this.standardizeReport(response);
successCb(report);
}, null, failureCb);
private async sendStatsToHttpEndpoint(): Promise<void> {
try {
const webrtcStats: IWebrtcStats = await this.getCommonStats();
const response = this.generateJSONStatsResponse(webrtcStats);
await this.sendStats(this.POST_URL, response);
} catch (error) {
logger.log(error);
}
}
// Have been tested in:
// - Linux Desktop:
// - Chrome 89.0.4389.90
// - Opera 74.0.3911.218
// - Firefox 86
// - Microsoft Edge 91.0.825.0
// - Electron 11.3.0 (Chromium 87.0.4280.141)
// - Windows Desktop:
// - Chrome 89.0.4389.90
// - Opera 74.0.3911.232
// - Firefox 86.0.1
// - Microsoft Edge 89.0.774.54
// - Electron 11.3.0 (Chromium 87.0.4280.141)
// - MacOS Desktop:
// - Chrome 89.0.4389.90
// - Opera 75.0.3969.93
// - Firefox 87.0
// - Microsoft Edge 89.0.774.57
// - Safari 14.0 (14610.1.28.1.9)
// - Electron 11.3.0 (Chromium 87.0.4280.141)
// - Android:
// - Chrome Mobile 89.0.4389.90
// - Opera 62.3.3146.57763
// - Firefox Mobile 86.6.1
// - Microsoft Edge Mobile 46.02.4.5147
// - Ionic 5
// - React Native 0.64
// - iOS:
// - Safari Mobile
// - ¿Ionic?
// - ¿React Native?
public async getCommonStats(): Promise<IWebrtcStats> {
return new Promise(async (resolve, reject) => {
const statsReport: any = await this.stream.getRTCPeerConnection().getStats();
const response = this.getWebRtcStatsResponseOutline();
const videoTrackStats = ['framesReceived', 'framesDropped', 'framesSent', 'frameHeight', 'frameWidth'];
statsReport.forEach((stat: any) => {
let mediaType = stat.mediaType != null ? stat.mediaType : stat.kind;
const addStat = (direction: string, key: string): void => {
if (stat[key] != null && response[direction] != null) {
if (!mediaType && (videoTrackStats.indexOf(key) > -1)) {
mediaType = 'video';
}
if (direction != null && mediaType != null && key != null && response[direction] != null && response[direction][mediaType] != null) {
response[direction][mediaType][key] = Number(stat[key]);
}
}
}
switch (stat.type) {
case "outbound-rtp":
addStat('outbound', 'bytesSent');
addStat('outbound', 'packetsSent');
addStat('outbound', 'framesEncoded');
addStat('outbound', 'nackCount');
addStat('outbound', 'firCount');
addStat('outbound', 'pliCount');
addStat('outbound', 'qpSum');
break;
case "inbound-rtp":
addStat('inbound', 'bytesReceived');
addStat('inbound', 'packetsReceived');
addStat('inbound', 'packetsLost');
addStat('inbound', 'jitter');
addStat('inbound', 'framesDecoded');
addStat('inbound', 'nackCount');
addStat('inbound', 'firCount');
addStat('inbound', 'pliCount');
break;
case 'track':
addStat('inbound', 'jitterBufferDelay');
addStat('inbound', 'framesReceived');
addStat('outbound', 'framesDropped');
addStat('outbound', 'framesSent');
addStat(this.stream.isLocal() ? 'outbound' : 'inbound', 'frameHeight');
addStat(this.stream.isLocal() ? 'outbound' : 'inbound', 'frameWidth');
break;
}
});
return resolve(response);
});
}
private generateJSONStatsResponse(stats: IWebrtcStats): JSONStatsResponse {
return {
'@timestamp': new Date().toISOString(),
participant_id: this.stream.connection.data,
session_id: this.stream.session.sessionId,
platform: platform.getName(),
platform_description: platform.getDescription(),
stream: 'webRTC',
webrtc_stats: stats
};
}
private getWebRtcStatsResponseOutline(): IWebrtcStats {
if (this.stream.isLocal()) {
return {
outbound: {
audio: {},
video: {}
}
};
} else {
return {
inbound: {
audio: {},
video: {}
}
};
}
}

View File

@ -25,6 +25,7 @@ export { StreamPropertyChangedEvent } from './OpenViduInternal/Events/StreamProp
export { ConnectionPropertyChangedEvent } from './OpenViduInternal/Events/ConnectionPropertyChangedEvent';
export { FilterEvent } from './OpenViduInternal/Events/FilterEvent';
export { NetworkQualityLevelChangedEvent } from './OpenViduInternal/Events/NetworkQualityLevelChangedEvent';
export { ExceptionEvent } from './OpenViduInternal/Events/ExceptionEvent';
export { Capabilities } from './OpenViduInternal/Interfaces/Public/Capabilities';
export { Device } from './OpenViduInternal/Interfaces/Public/Device';

View File

@ -1,7 +1,7 @@
[![License badge](https://img.shields.io/badge/license-Apache2-orange.svg)](http://www.apache.org/licenses/LICENSE-2.0)
[![Documentation Status](https://readthedocs.org/projects/openviduio-docs/badge/?version=stable)](https://docs.openvidu.io/en/stable/?badge=stable)
[![Docker badge](https://img.shields.io/docker/pulls/fiware/orion.svg)](https://hub.docker.com/r/openvidu/)
[![Support badge](https://img.shields.io/badge/support-sof-yellowgreen.svg)](https://groups.google.com/forum/#!forum/openvidu)
[![Support badge](https://img.shields.io/badge/support-sof-yellowgreen.svg)](https://openvidu.discourse.group/)
[![][OpenViduLogo]](https://openvidu.io)

View File

@ -36,7 +36,7 @@ public class OpenViduException extends JsonRpcErrorException {
ROOM_CANNOT_BE_CREATED_ERROR_CODE(204), ROOM_CLOSED_ERROR_CODE(203), ROOM_NOT_FOUND_ERROR_CODE(202),
ROOM_GENERIC_ERROR_CODE(201),
USER_NOT_STREAMING_ERROR_CODE(105), EXISTING_USER_IN_ROOM_ERROR_CODE(104), USER_CLOSED_ERROR_CODE(103),
USER_ALREADY_STREAMING_ERROR_CODE(106), USER_NOT_STREAMING_ERROR_CODE(105), EXISTING_USER_IN_ROOM_ERROR_CODE(104), USER_CLOSED_ERROR_CODE(103),
USER_NOT_FOUND_ERROR_CODE(102), USER_GENERIC_ERROR_CODE(10),
USER_UNAUTHORIZED_ERROR_CODE(401), ROLE_NOT_FOUND_ERROR_CODE(402), SESSIONID_CANNOT_BE_CREATED_ERROR_CODE(403),

View File

@ -70,6 +70,10 @@ public class ProtocolElements {
public static final String UNPUBLISHVIDEO_METHOD = "unpublishVideo";
public static final String PREPARERECEIVEVIDEO_METHOD = "prepareReceiveVideoFrom";
public static final String PREPARERECEIVEVIDEO_SDPOFFER_PARAM = "sdpOffer";
public static final String PREPARERECEIVEVIDEO_RECONNECT_PARAM = "reconnect";
public static final String RECEIVEVIDEO_METHOD = "receiveVideoFrom";
public static final String RECEIVEVIDEO_SDPOFFER_PARAM = "sdpOffer";
public static final String RECEIVEVIDEO_SENDER_PARAM = "sender";
@ -129,7 +133,10 @@ public class ProtocolElements {
public static final String RECONNECTSTREAM_METHOD = "reconnectStream";
public static final String RECONNECTSTREAM_STREAM_PARAM = "stream";
public static final String RECONNECTSTREAM_SDPSTRING_PARAM = "sdpString";
// TODO: REMOVE ON 2.18.0
public static final String RECONNECTSTREAM_SDPOFFER_PARAM = "sdpOffer";
// ENDTODO
public static final String VIDEODATA_METHOD = "videoData";
@ -137,6 +144,7 @@ public class ProtocolElements {
public static final String PARTICIPANTJOINED_METHOD = "participantJoined";
public static final String PARTICIPANTJOINED_USER_PARAM = "id";
public static final String PARTICIPANTJOINED_FINALUSERID_PARAM = "finalUserId";
public static final String PARTICIPANTJOINED_CREATEDAT_PARAM = "createdAt";
public static final String PARTICIPANTJOINED_METADATA_PARAM = "metadata";
public static final String PARTICIPANTJOINED_VALUE_PARAM = "value";

View File

@ -1,6 +1,6 @@
[![License badge](https://img.shields.io/badge/license-Apache2-orange.svg)](http://www.apache.org/licenses/LICENSE-2.0)
[![Documentation Status](https://readthedocs.org/projects/openviduio-docs/badge/?version=stable)](https://docs.openvidu.io/en/stable/?badge=stable)
[![Support badge](https://img.shields.io/badge/support-sof-yellowgreen.svg)](https://groups.google.com/forum/#!forum/openvidu)
[![Support badge](https://img.shields.io/badge/support-sof-yellowgreen.svg)](https://openvidu.discourse.group/)
[![][OpenViduLogo]](https://openvidu.io)

View File

@ -10,7 +10,7 @@
</parent>
<artifactId>openvidu-java-client</artifactId>
<version>2.16.0</version>
<version>2.17.0</version>
<packaging>jar</packaging>
<name>OpenVidu Java Client</name>
@ -138,6 +138,7 @@
<version>${version.javadoc.plugin}</version>
<configuration>
<show>public</show>
<javadocExecutable>${java.home}/bin/javadoc</javadocExecutable>
</configuration>
<executions>
<execution>
@ -159,6 +160,12 @@
<goals>
<goal>sign</goal>
</goals>
<configuration>
<gpgArguments>
<arg>--pinentry-mode</arg>
<arg>loopback</arg>
</gpgArguments>
</configuration>
</execution>
</executions>
</plugin>

View File

@ -18,7 +18,6 @@
package io.openvidu.java.client;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.security.KeyManagementException;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
@ -196,32 +195,10 @@ public class OpenVidu {
HttpPost request = new HttpPost(this.hostname + API_RECORDINGS_START);
JsonObject json = new JsonObject();
JsonObject json = properties.toJson();
json.addProperty("session", sessionId);
json.addProperty("name", properties.name());
json.addProperty("outputMode", properties.outputMode() != null ? properties.outputMode().name() : null);
json.addProperty("hasAudio", properties.hasAudio());
json.addProperty("hasVideo", properties.hasVideo());
json.addProperty("shmSize", properties.shmSize());
json.addProperty("mediaNode", properties.mediaNode());
if ((properties.outputMode() == null || Recording.OutputMode.COMPOSED.equals(properties.outputMode())
|| (Recording.OutputMode.COMPOSED_QUICK_START.equals(properties.outputMode())))
&& properties.hasVideo()) {
json.addProperty("resolution", properties.resolution());
json.addProperty("recordingLayout",
(properties.recordingLayout() != null) ? properties.recordingLayout().name() : "");
if (RecordingLayout.CUSTOM.equals(properties.recordingLayout())) {
json.addProperty("customLayout", (properties.customLayout() != null) ? properties.customLayout() : "");
}
}
StringEntity params = null;
try {
params = new StringEntity(json.toString());
} catch (UnsupportedEncodingException e1) {
throw new OpenViduJavaClientException(e1.getMessage(), e1.getCause());
}
StringEntity params = new StringEntity(json.toString(), "UTF-8");
request.setHeader(HttpHeaders.CONTENT_TYPE, "application/json");
request.setEntity(params);
@ -258,12 +235,10 @@ public class OpenVidu {
* Starts the recording of a {@link io.openvidu.java.client.Session}
*
* @param sessionId The sessionId of the session you want to start recording
* @param name The name you want to give to the video file. You can access
* this same value in your clients on recording events
* (recordingStarted, recordingStopped). <strong>WARNING: this
* parameter follows an overwriting policy.</strong> If you
* name two recordings the same, the newest MP4 file will
* overwrite the oldest one
* @param name The name you want to give to the video file.
* <strong>WARNING: this parameter follows an overwriting
* policy.</strong> If you name two recordings the same, the
* newest MP4 file will overwrite the oldest one
*
* @return The started recording. If this method successfully returns the
* Recording object it means that the recording can be stopped with
@ -613,7 +588,7 @@ public class OpenVidu {
private JsonObject httpResponseToJson(HttpResponse response) throws OpenViduJavaClientException {
try {
JsonObject json = new Gson().fromJson(EntityUtils.toString(response.getEntity()), JsonObject.class);
JsonObject json = new Gson().fromJson(EntityUtils.toString(response.getEntity(), "UTF-8"), JsonObject.class);
return json;
} catch (JsonSyntaxException | ParseException | IOException e) {
throw new OpenViduJavaClientException(e.getMessage(), e.getCause());

View File

@ -120,22 +120,7 @@ public class Recording {
}
this.status = Recording.Status.valueOf(json.get("status").getAsString());
boolean hasAudio = json.get("hasAudio").getAsBoolean();
boolean hasVideo = json.get("hasVideo").getAsBoolean();
OutputMode outputMode = OutputMode.valueOf(json.get("outputMode").getAsString());
RecordingProperties.Builder builder = new RecordingProperties.Builder().name(json.get("name").getAsString())
.outputMode(outputMode).hasAudio(hasAudio).hasVideo(hasVideo);
if ((OutputMode.COMPOSED.equals(outputMode) || OutputMode.COMPOSED_QUICK_START.equals(outputMode))
&& hasVideo) {
builder.resolution(json.get("resolution").getAsString());
builder.recordingLayout(RecordingLayout.valueOf(json.get("recordingLayout").getAsString()));
JsonElement customLayout = json.get("customLayout");
if (customLayout != null) {
builder.customLayout(customLayout.getAsString());
}
}
this.recordingProperties = builder.build();
this.recordingProperties = RecordingProperties.fromJson(json);
}
/**
@ -153,14 +138,28 @@ public class Recording {
}
/**
* Name of the recording. The video file will be named after this property. You
* can access this same value in your clients on recording events
* (<code>recordingStarted</code>, <code>recordingStopped</code>)
* Name of the recording. The video file will be named after this property
*/
public String getName() {
return this.recordingProperties.name();
}
/**
* <code>true</code> if the recording has an audio track, <code>false</code>
* otherwise (currently fixed to true)
*/
public boolean hasAudio() {
return this.recordingProperties.hasAudio();
}
/**
* <code>true</code> if the recording has a video track, <code>false</code>
* otherwise (currently fixed to true)
*/
public boolean hasVideo() {
return this.recordingProperties.hasVideo();
}
/**
* Mode of recording: COMPOSED for a single archive in a grid layout or
* INDIVIDUAL for one archive for each stream
@ -170,15 +169,22 @@ public class Recording {
}
/**
* The layout used in this recording. Only defined if OutputMode is COMPOSED
* The layout used in this recording. Only applicable if
* {@link io.openvidu.java.client.Recording.OutputMode} is
* {@link io.openvidu.java.client.Recording.OutputMode#COMPOSED} or
* {@link io.openvidu.java.client.Recording.OutputMode#COMPOSED_QUICK_START} and
* {@link Recording#hasVideo()} is true
*/
public RecordingLayout getRecordingLayout() {
return this.recordingProperties.recordingLayout();
}
/**
* The custom layout used in this recording. Only defined if if OutputMode is
* COMPOSED and
* The custom layout used in this recording. Only applicable if
* {@link io.openvidu.java.client.Recording.OutputMode} is
* {@link io.openvidu.java.client.Recording.OutputMode#COMPOSED} or
* {@link io.openvidu.java.client.Recording.OutputMode#COMPOSED_QUICK_START},
* {@link Recording#hasVideo()} is true and
* {@link io.openvidu.java.client.RecordingProperties.Builder#customLayout(String)}
* has been called
*/
@ -186,6 +192,15 @@ public class Recording {
return this.recordingProperties.customLayout();
}
/**
* Whether failed streams were ignored when the recording process started or
* not. Only applicable if {@link io.openvidu.java.client.Recording.OutputMode}
* is {@link io.openvidu.java.client.Recording.OutputMode#INDIVIDUAL}
*/
public boolean ignoreFailedStreams() {
return this.recordingProperties.ignoreFailedStreams();
}
/**
* Session associated to the recording
*/
@ -227,28 +242,25 @@ public class Recording {
}
/**
* Resolution of the video file. Only defined if OutputMode of the Recording is
* set to {@link io.openvidu.java.client.Recording.OutputMode#COMPOSED} or
* {@link io.openvidu.java.client.Recording.OutputMode#COMPOSED_QUICK_START}
* Resolution of the video file. Only applicable if
* {@link io.openvidu.java.client.Recording.OutputMode} is
* {@link io.openvidu.java.client.Recording.OutputMode#COMPOSED} or
* {@link io.openvidu.java.client.Recording.OutputMode#COMPOSED_QUICK_START} and
* {@link Recording#hasVideo()} is true
*/
public String getResolution() {
return this.recordingProperties.resolution();
}
/**
* <code>true</code> if the recording has an audio track, <code>false</code>
* otherwise (currently fixed to true)
* Frame rate of the video file. Only applicable if
* {@link io.openvidu.java.client.Recording.OutputMode} is
* {@link io.openvidu.java.client.Recording.OutputMode#COMPOSED} or
* {@link io.openvidu.java.client.Recording.OutputMode#COMPOSED_QUICK_START} and
* {@link Recording#hasVideo()} is true
*/
public boolean hasAudio() {
return this.recordingProperties.hasAudio();
}
/**
* <code>true</code> if the recording has a video track, <code>false</code>
* otherwise (currently fixed to true)
*/
public boolean hasVideo() {
return this.recordingProperties.hasVideo();
public Integer getFrameRate() {
return this.recordingProperties.frameRate();
}
}

View File

@ -19,9 +19,7 @@ package io.openvidu.java.client;
/**
* See
* {@link io.openvidu.java.client.SessionProperties.Builder#defaultRecordingLayout(RecordingLayout)}
* and
* {@link io.openvidu.java.client.RecordingProperties.Builder#recordingLayout(RecordingLayout)}
* {@link io.openvidu.java.client.RecordingProperties#recordingLayout(RecordingLayout)}
*/
public enum RecordingLayout {
@ -46,8 +44,8 @@ public enum RecordingLayout {
HORIZONTAL_PRESENTATION,
/**
* Use your own custom recording layout. See
* <a href="https://docs.openvidu.io/en/stable/advanced-features/recording#custom-recording-layouts"
* Use your own custom recording layout. See <a href=
* "https://docs.openvidu.io/en/stable/advanced-features/recording#custom-recording-layouts"
* target="_blank">Custom recording layouts</a> to learn more
*/
CUSTOM

View File

@ -17,20 +17,42 @@
package io.openvidu.java.client;
import com.google.gson.JsonObject;
import io.openvidu.java.client.Recording.OutputMode;
/**
* See
* {@link io.openvidu.java.client.OpenVidu#startRecording(String, RecordingProperties)}
*/
public class RecordingProperties {
private String name;
private Recording.OutputMode outputMode;
public static class DefaultValues {
public static final Boolean hasAudio = true;
public static final Boolean hasVideo = true;
public static final Recording.OutputMode outputMode = Recording.OutputMode.COMPOSED;
public static final RecordingLayout recordingLayout = RecordingLayout.BEST_FIT;
public static final String resolution = "1280x720";
public static final Integer frameRate = 25;
public static final Long shmSize = 536870912L;
public static final Boolean ignoreFailedStreams = false;
}
// For all
private String name = "";
private Boolean hasAudio = true;
private Boolean hasVideo = true;
private Recording.OutputMode outputMode = Recording.OutputMode.COMPOSED;
// For COMPOSED/COMPOSED_QUICK_START + hasVideo
private RecordingLayout recordingLayout;
private String customLayout;
private String resolution;
private boolean hasAudio;
private boolean hasVideo;
private long shmSize; // For COMPOSED recording
private Integer frameRate;
private Long shmSize;
// For COMPOSED/COMPOSED_QUICK_START + hasVideo + RecordingLayout.CUSTOM
private String customLayout;
// For INDIVIDUAL
private Boolean ignoreFailedStreams;
// For OpenVidu Pro
private String mediaNode;
/**
@ -39,97 +61,51 @@ public class RecordingProperties {
public static class Builder {
private String name = "";
private Recording.OutputMode outputMode;
private Boolean hasAudio = DefaultValues.hasAudio;
private Boolean hasVideo = DefaultValues.hasVideo;
private Recording.OutputMode outputMode = DefaultValues.outputMode;
private RecordingLayout recordingLayout;
private String customLayout;
private String resolution;
private boolean hasAudio = true;
private boolean hasVideo = true;
private long shmSize = 536870912L;
private Integer frameRate;
private Long shmSize;
private String customLayout;
private Boolean ignoreFailedStreams = DefaultValues.ignoreFailedStreams;
private String mediaNode;
public Builder() {
}
public Builder(RecordingProperties props) {
this.name = props.name();
this.hasAudio = props.hasAudio();
this.hasVideo = props.hasVideo();
this.outputMode = props.outputMode();
this.recordingLayout = props.recordingLayout();
this.resolution = props.resolution();
this.frameRate = props.frameRate();
this.shmSize = props.shmSize();
this.customLayout = props.customLayout();
this.ignoreFailedStreams = props.ignoreFailedStreams();
this.mediaNode = props.mediaNode();
}
/**
* Builder for {@link io.openvidu.java.client.RecordingProperties}
*/
public RecordingProperties build() {
return new RecordingProperties(this.name, this.outputMode, this.recordingLayout, this.customLayout,
this.resolution, this.hasAudio, this.hasVideo, this.shmSize, this.mediaNode);
return new RecordingProperties(this.name, this.hasAudio, this.hasVideo, this.outputMode,
this.recordingLayout, this.resolution, this.frameRate, this.shmSize, this.customLayout,
this.ignoreFailedStreams, this.mediaNode);
}
/**
* Call this method to set the name of the video file. You can access this same
* value in your clients on recording events (<code>recordingStarted</code>,
* <code>recordingStopped</code>)
* Call this method to set the name of the video file
*/
public RecordingProperties.Builder name(String name) {
this.name = name;
return this;
}
/**
* Call this method to set the mode of recording: COMPOSED for a single archive
* in a grid layout or INDIVIDUAL for one archive for each stream
*/
public RecordingProperties.Builder outputMode(Recording.OutputMode outputMode) {
this.outputMode = outputMode;
return this;
}
/**
* Call this method to set the layout to be used in the recording. Will only
* have effect if
* {@link io.openvidu.java.client.RecordingProperties.Builder#outputMode(Recording.OutputMode)}
* has been called with value
* {@link io.openvidu.java.client.Recording.OutputMode#COMPOSED} or
* {@link io.openvidu.java.client.Recording.OutputMode#COMPOSED_QUICK_START}
*/
public RecordingProperties.Builder recordingLayout(RecordingLayout layout) {
this.recordingLayout = layout;
return this;
}
/**
* If setting
* {@link io.openvidu.java.client.RecordingProperties.Builder#recordingLayout(RecordingLayout)}
* to {@link io.openvidu.java.client.RecordingLayout#CUSTOM} you can call this
* method to set the relative path to the specific custom layout you want to
* use.<br>
* Will only have effect if
* {@link io.openvidu.java.client.RecordingProperties.Builder#outputMode(Recording.OutputMode)
* Builder.outputMode()} has been called with value
* {@link io.openvidu.java.client.Recording.OutputMode#COMPOSED
* OutputMode.COMPOSED} or
* {@link io.openvidu.java.client.Recording.OutputMode#COMPOSED_QUICK_START
* OutputMode.COMPOSED_QUICK_START}.<br>
* See <a href=
* "https://docs.openvidu.io/en/stable/advanced-features/recording#custom-recording-layouts"
* target="_blank">Custom recording layouts</a> to learn more
*/
public RecordingProperties.Builder customLayout(String path) {
this.customLayout = path;
return this;
}
/**
* Call this method to specify the recording resolution. Must be a string with
* format "WIDTHxHEIGHT", being both WIDTH and HEIGHT the number of pixels
* between 100 and 1999.<br>
* Will only have effect if
* {@link io.openvidu.java.client.RecordingProperties.Builder#outputMode(Recording.OutputMode)
* Builder.outputMode()} has been called with value
* {@link io.openvidu.java.client.Recording.OutputMode#COMPOSED
* OutputMode.COMPOSED} or
* {@link io.openvidu.java.client.Recording.OutputMode#COMPOSED_QUICK_START
* OutputMode.COMPOSED_QUICK_START}. For
* {@link io.openvidu.java.client.Recording.OutputMode#INDIVIDUAL
* OutputMode.INDIVIDUAL} all individual video files will have the native
* resolution of the published stream
*/
public RecordingProperties.Builder resolution(String resolution) {
this.resolution = resolution;
return this;
}
/**
* Call this method to specify whether to record audio or not. Cannot be set to
* false at the same time as
@ -151,15 +127,112 @@ public class RecordingProperties {
}
/**
* If COMPOSED recording, call this method to specify the amount of shared
* memory reserved for the recording process in bytes. Minimum 134217728 (128
* MB). Property ignored if INDIVIDUAL recording
* Call this method to set the mode of recording:
* {@link Recording.OutputMode#COMPOSED} or
* {@link io.openvidu.java.client.Recording.OutputMode#COMPOSED_QUICK_START} for
* a single archive in a grid layout or {@link Recording.OutputMode#INDIVIDUAL}
* for one archive for each stream.
*/
public RecordingProperties.Builder outputMode(Recording.OutputMode outputMode) {
this.outputMode = outputMode;
return this;
}
/**
* Call this method to set the layout to be used in the recording. Will only
* have effect for {@link io.openvidu.java.client.Recording.OutputMode#COMPOSED}
* or {@link io.openvidu.java.client.Recording.OutputMode#COMPOSED_QUICK_START}
* recordings with {@link RecordingProperties#hasVideo()} to true.
*/
public RecordingProperties.Builder recordingLayout(RecordingLayout layout) {
this.recordingLayout = layout;
return this;
}
/**
* Call this method to specify the recording resolution. Must be a string with
* format "WIDTHxHEIGHT", being both WIDTH and HEIGHT the number of pixels
* between 100 and 1999.<br>
* Will only have effect for
* {@link io.openvidu.java.client.Recording.OutputMode#COMPOSED} or
* {@link io.openvidu.java.client.Recording.OutputMode#COMPOSED_QUICK_START}
* recordings with {@link RecordingProperties#hasVideo()} to true. Property
* ignored for INDIVIDUAL recordings and audio-only recordings. For
* {@link io.openvidu.java.client.Recording.OutputMode#INDIVIDUAL} all
* individual video files will have the native resolution of the published
* stream.
*/
public RecordingProperties.Builder resolution(String resolution) {
this.resolution = resolution;
return this;
}
/**
* Call this method to specify the recording frame rate. Will only have effect
* for {@link io.openvidu.java.client.Recording.OutputMode#COMPOSED} or
* {@link io.openvidu.java.client.Recording.OutputMode#COMPOSED_QUICK_START}
* recordings with {@link RecordingProperties#hasVideo()} to true. Property
* ignored for INDIVIDUAL recordings and audio-only recordings. For
* {@link io.openvidu.java.client.Recording.OutputMode#INDIVIDUAL} all
* individual video files will have the native frame rate of the published
* stream.
*/
public RecordingProperties.Builder frameRate(int frameRate) {
this.frameRate = frameRate;
return this;
}
/**
* Call this method to specify the amount of shared memory reserved for the
* recording process in bytes. Minimum 134217728 (128MB).<br>
* Will only have effect for
* {@link io.openvidu.java.client.Recording.OutputMode#COMPOSED} or
* {@link io.openvidu.java.client.Recording.OutputMode#COMPOSED_QUICK_START}
* recordings with {@link RecordingProperties#hasVideo()} to true. Property
* ignored for INDIVIDUAL recordings and audio-only recordings
*/
public RecordingProperties.Builder shmSize(long shmSize) {
this.shmSize = shmSize;
return this;
}
/**
* If setting
* {@link io.openvidu.java.client.RecordingProperties.Builder#recordingLayout(RecordingLayout)}
* to {@link io.openvidu.java.client.RecordingLayout#CUSTOM} you can call this
* method to set the relative path to the specific custom layout you want to
* use.<br>
* See <a href=
* "https://docs.openvidu.io/en/stable/advanced-features/recording#custom-recording-layouts"
* target="_blank">Custom recording layouts</a> to learn more
*/
public RecordingProperties.Builder customLayout(String path) {
this.customLayout = path;
return this;
}
/**
* Call this method to specify whether to ignore failed streams or not when
* starting the recording. This property only applies to
* {@link io.openvidu.java.client.Recording.OutputMode#INDIVIDUAL} recordings.
* For this type of recordings, when calling
* {@link io.openvidu.java.client.OpenVidu#startRecording(String, RecordingProperties)}
* by default all the streams available at the moment the recording process
* starts must be healthy and properly sending media. If some stream that should
* be sending media is broken, then the recording process fails after a 10s
* timeout. In this way your application is notified that some stream is not
* being recorded, so it can retry the process again. But you can disable this
* rollback behavior and simply ignore any failed stream, which will be
* susceptible to be recorded in the future if media starts flowing as expected
* at any point. The downside of this behavior is that you will have no
* guarantee that all streams present at the beginning of a recording are
* actually being recorded.
*/
public RecordingProperties.Builder ignoreFailedStreams(boolean ignoreFailedStreams) {
this.ignoreFailedStreams = ignoreFailedStreams;
return this;
}
/**
* <a href="https://docs.openvidu.io/en/stable/openvidu-pro/" target="_blank"
* style="display: inline-block; background-color: rgb(0, 136, 170); color:
@ -167,8 +240,12 @@ public class RecordingProperties {
* 3px; font-size: 13px; line-height:21px; font-family: Montserrat,
* sans-serif">PRO</a> Call this method to force the recording to be hosted in
* the Media Node with identifier <code>mediaNodeId</code>. This property only
* applies to COMPOSED recordings and is ignored for INDIVIDUAL recordings, that
* are always hosted in the same Media Node hosting its Session
* applies to {@link io.openvidu.java.client.Recording.OutputMode#COMPOSED} or
* {@link io.openvidu.java.client.Recording.OutputMode#COMPOSED_QUICK_START}
* recordings with {@link RecordingProperties#hasVideo()} to true and is ignored
* for {@link io.openvidu.java.client.Recording.OutputMode#INDIVIDUAL}
* recordings and audio-only recordings, that are always hosted in the same
* Media Node hosting its Session
*/
public RecordingProperties.Builder mediaNode(String mediaNodeId) {
this.mediaNode = mediaNodeId;
@ -177,29 +254,58 @@ public class RecordingProperties {
}
protected RecordingProperties(String name, Recording.OutputMode outputMode, RecordingLayout layout,
String customLayout, String resolution, boolean hasAudio, boolean hasVideo, long shmSize,
String mediaNode) {
this.name = name;
this.outputMode = outputMode;
this.recordingLayout = layout;
this.customLayout = customLayout;
this.resolution = resolution;
this.hasAudio = hasAudio;
this.hasVideo = hasVideo;
this.shmSize = shmSize;
protected RecordingProperties(String name, Boolean hasAudio, Boolean hasVideo, Recording.OutputMode outputMode,
RecordingLayout layout, String resolution, Integer frameRate, Long shmSize, String customLayout,
Boolean ignoreFailedStreams, String mediaNode) {
this.name = name != null ? name : "";
this.hasAudio = hasAudio != null ? hasAudio : DefaultValues.hasAudio;
this.hasVideo = hasVideo != null ? hasVideo : DefaultValues.hasVideo;
this.outputMode = outputMode != null ? outputMode : DefaultValues.outputMode;
if ((OutputMode.COMPOSED.equals(this.outputMode) || OutputMode.COMPOSED_QUICK_START.equals(this.outputMode))
&& this.hasVideo) {
this.recordingLayout = layout != null ? layout : DefaultValues.recordingLayout;
this.resolution = resolution != null ? resolution : DefaultValues.resolution;
this.frameRate = frameRate != null ? frameRate : DefaultValues.frameRate;
this.shmSize = shmSize != null ? shmSize : DefaultValues.shmSize;
if (RecordingLayout.CUSTOM.equals(this.recordingLayout)) {
this.customLayout = customLayout;
}
}
if (OutputMode.INDIVIDUAL.equals(this.outputMode)) {
this.ignoreFailedStreams = ignoreFailedStreams;
}
this.mediaNode = mediaNode;
}
/**
* Defines the name you want to give to the video file. You can access this same
* value in your clients on recording events (<code>recordingStarted</code>,
* <code>recordingStopped</code>)
* Defines the name you want to give to the video file
*/
public String name() {
return this.name;
}
/**
* Defines whether to record audio or not. Cannot be set to false at the same
* time as {@link RecordingProperties#hasVideo()}.<br>
* <br>
*
* Default to true
*/
public Boolean hasAudio() {
return this.hasAudio;
}
/**
* Defines whether to record video or not. Cannot be set to false at the same
* time as {@link RecordingProperties#hasAudio()}.<br>
* <br>
*
* Default to true
*/
public Boolean hasVideo() {
return this.hasVideo;
}
/**
* Defines the mode of recording: {@link Recording.OutputMode#COMPOSED} or
* {@link io.openvidu.java.client.Recording.OutputMode#COMPOSED_QUICK_START} for
@ -215,12 +321,11 @@ public class RecordingProperties {
/**
* Defines the layout to be used in the recording.<br>
* Will only have effect if
* {@link io.openvidu.java.client.RecordingProperties.Builder#outputMode(Recording.OutputMode)
* Builder.outputMode()} has been called with value
* {@link Recording.OutputMode#COMPOSED OutputMode.COMPOSED} or
* {@link io.openvidu.java.client.Recording.OutputMode#COMPOSED_QUICK_START
* OutputMode.COMPOSED_QUICK_START}.<br>
* Will only have effect for
* {@link io.openvidu.java.client.Recording.OutputMode#COMPOSED} or
* {@link io.openvidu.java.client.Recording.OutputMode#COMPOSED_QUICK_START}
* recordings with {@link RecordingProperties#hasVideo()} to true. Property
* ignored for INDIVIDUAL recordings and audio-only recordings<br>
* <br>
*
* Default to {@link RecordingLayout#BEST_FIT RecordingLayout.BEST_FIT}
@ -229,6 +334,58 @@ public class RecordingProperties {
return this.recordingLayout;
}
/**
* Defines the resolution of the recorded video.<br>
* Will only have effect for
* {@link io.openvidu.java.client.Recording.OutputMode#COMPOSED} or
* {@link io.openvidu.java.client.Recording.OutputMode#COMPOSED_QUICK_START}
* recordings with {@link RecordingProperties#hasVideo()} to true. Property
* ignored for INDIVIDUAL recordings and audio-only recordings. For
* {@link io.openvidu.java.client.Recording.OutputMode#INDIVIDUAL} all
* individual video files will have the native resolution of the published
* stream.<br>
* <br>
*
* Default to "1280x720"
*/
public String resolution() {
return this.resolution;
}
/**
* Defines the frame rate of the recorded video.<br>
* Will only have effect for
* {@link io.openvidu.java.client.Recording.OutputMode#COMPOSED} or
* {@link io.openvidu.java.client.Recording.OutputMode#COMPOSED_QUICK_START}
* recordings with {@link RecordingProperties#hasVideo()} to true. Property
* ignored for INDIVIDUAL recordings and audio-only recordings. For
* {@link io.openvidu.java.client.Recording.OutputMode#INDIVIDUAL} all
* individual video files will have the native frame rate of the published
* stream.<br>
* <br>
*
* Default to 25
*/
public Integer frameRate() {
return this.frameRate;
}
/**
* The amount of shared memory reserved for the recording process in bytes.
* Minimum 134217728 (128MB).<br>
* Will only have effect for
* {@link io.openvidu.java.client.Recording.OutputMode#COMPOSED} or
* {@link io.openvidu.java.client.Recording.OutputMode#COMPOSED_QUICK_START}
* recordings with {@link RecordingProperties#hasVideo()} to true. Property
* ignored for INDIVIDUAL recordings and audio-only recordings<br>
* <br>
*
* Default to 536870912 (512 MB)
*/
public Long shmSize() {
return this.shmSize;
}
/**
* If {@link io.openvidu.java.client.RecordingProperties#recordingLayout()} is
* set to {@link io.openvidu.java.client.RecordingLayout#CUSTOM}, this property
@ -242,55 +399,27 @@ public class RecordingProperties {
}
/**
* Defines the resolution of the recorded video.<br>
* Will only have effect if
* {@link io.openvidu.java.client.RecordingProperties.Builder#outputMode(Recording.OutputMode)}
* has been called with value
* {@link io.openvidu.java.client.Recording.OutputMode#COMPOSED} or
* {@link io.openvidu.java.client.Recording.OutputMode#COMPOSED_QUICK_START}.
* For {@link io.openvidu.java.client.Recording.OutputMode#INDIVIDUAL} all
* individual video files will have the native resolution of the published
* stream.<br>
* Defines whether to ignore failed streams or not when starting the recording.
* This property only applies to
* {@link io.openvidu.java.client.Recording.OutputMode#INDIVIDUAL} recordings.
* For this type of recordings, when calling
* {@link io.openvidu.java.client.OpenVidu#startRecording(String, RecordingProperties)}
* by default all the streams available at the moment the recording process
* starts must be healthy and properly sending media. If some stream that should
* be sending media is broken, then the recording process fails after a 10s
* timeout. In this way your application is notified that some stream is not
* being recorded, so it can retry the process again. But you can disable this
* rollback behavior and simply ignore any failed stream, which will be
* susceptible to be recorded in the future if media starts flowing as expected
* at any point. The downside of this behavior is that you will have no
* guarantee that all streams present at the beginning of a recording are
* actually being recorded.<br>
* <br>
*
* Default to "1920x1080"
* Default to false
*/
public String resolution() {
return this.resolution;
}
/**
* Defines whether to record audio or not. Cannot be set to false at the same
* time as {@link RecordingProperties#hasVideo()}.<br>
* <br>
*
* Default to true
*/
public boolean hasAudio() {
return this.hasAudio;
}
/**
* Defines whether to record video or not. Cannot be set to false at the same
* time as {@link RecordingProperties#hasAudio()}.<br>
* <br>
*
* Default to true
*/
public boolean hasVideo() {
return this.hasVideo;
}
/**
* If COMPOSED recording, the amount of shared memory reserved for the recording
* process in bytes. Minimum 134217728 (128MB). Property ignored if INDIVIDUAL
* recording<br>
* <br>
*
* Default to 536870912 (512 MB)
*/
public long shmSize() {
return this.shmSize;
public Boolean ignoreFailedStreams() {
return this.ignoreFailedStreams;
}
/**
@ -300,11 +429,105 @@ public class RecordingProperties {
* 3px; font-size: 13px; line-height:21px; font-family: Montserrat,
* sans-serif">PRO</a> The Media Node where to host the recording. The default
* option if this property is not defined is the same Media Node hosting the
* Session to record. This property only applies to COMPOSED recordings and is
* ignored for INDIVIDUAL recordings
* Session to record. This property only applies to COMPOSED or
* COMPOSED_QUICK_START recordings with {@link RecordingProperties#hasVideo()}
* to true and is ignored for INDIVIDUAL recordings and audio-only recordings
*/
public String mediaNode() {
return this.mediaNode;
}
/**
* @hidden
*/
public JsonObject toJson() {
JsonObject json = new JsonObject();
json.addProperty("name", name);
json.addProperty("hasAudio", hasAudio != null ? hasAudio : DefaultValues.hasAudio);
json.addProperty("hasVideo", hasVideo != null ? hasVideo : DefaultValues.hasVideo);
json.addProperty("outputMode", outputMode != null ? outputMode.name() : DefaultValues.outputMode.name());
if ((OutputMode.COMPOSED.equals(outputMode) || OutputMode.COMPOSED_QUICK_START.equals(outputMode))
&& hasVideo) {
json.addProperty("recordingLayout",
recordingLayout != null ? recordingLayout.name() : DefaultValues.recordingLayout.name());
json.addProperty("resolution", resolution != null ? resolution : DefaultValues.resolution);
json.addProperty("frameRate", frameRate != null ? frameRate : DefaultValues.frameRate);
json.addProperty("shmSize", shmSize != null ? shmSize : DefaultValues.shmSize);
if (RecordingLayout.CUSTOM.equals(recordingLayout)) {
json.addProperty("customLayout", customLayout != null ? customLayout : "");
}
}
if (OutputMode.INDIVIDUAL.equals(outputMode)) {
json.addProperty("ignoreFailedStreams",
ignoreFailedStreams != null ? ignoreFailedStreams : DefaultValues.ignoreFailedStreams);
}
if (this.mediaNode != null) {
json.addProperty("mediaNode", mediaNode);
}
return json;
}
/**
* @hidden
*/
public static RecordingProperties fromJson(JsonObject json) {
Boolean hasVideoAux = true;
Recording.OutputMode outputModeAux = null;
RecordingLayout recordingLayoutAux = null;
Builder builder = new RecordingProperties.Builder();
if (json.has("name")) {
builder.name(json.get("name").getAsString());
}
if (json.has("hasAudio")) {
builder.hasAudio(json.get("hasAudio").getAsBoolean());
}
if (json.has("hasVideo")) {
hasVideoAux = json.get("hasVideo").getAsBoolean();
builder.hasVideo(hasVideoAux);
}
if (json.has("outputMode")) {
outputModeAux = OutputMode.valueOf(json.get("outputMode").getAsString());
builder.outputMode(outputModeAux);
}
if ((OutputMode.COMPOSED.equals(outputModeAux) || OutputMode.COMPOSED_QUICK_START.equals(outputModeAux))
&& hasVideoAux) {
if (json.has("recordingLayout")) {
recordingLayoutAux = RecordingLayout.valueOf(json.get("recordingLayout").getAsString());
builder.recordingLayout(recordingLayoutAux);
}
if (json.has("resolution")) {
builder.resolution(json.get("resolution").getAsString());
}
if (json.has("frameRate")) {
builder.frameRate(json.get("frameRate").getAsInt());
}
if (json.has("shmSize")) {
builder.shmSize(json.get("shmSize").getAsLong());
}
if (RecordingLayout.CUSTOM.equals(recordingLayoutAux)) {
if (json.has("customLayout")) {
builder.customLayout(json.get("customLayout").getAsString());
}
}
}
if (json.has("ignoreFailedStreams") && OutputMode.INDIVIDUAL.equals(outputModeAux)) {
builder.ignoreFailedStreams(json.get("ignoreFailedStreams").getAsBoolean());
}
if (json.has("mediaNode")) {
String mediaNodeId = null;
if (json.get("mediaNode").isJsonObject()) {
mediaNodeId = json.get("mediaNode").getAsJsonObject().get("id").getAsString();
} else if (json.get("mediaNode").isJsonPrimitive()) {
mediaNodeId = json.get("mediaNode").getAsString();
}
if (mediaNodeId != null && !mediaNodeId.isEmpty()) {
builder.mediaNode(mediaNodeId);
}
}
return builder.build();
}
}

View File

@ -18,7 +18,6 @@
package io.openvidu.java.client;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
@ -121,13 +120,7 @@ public class Session {
}
HttpPost request = new HttpPost(this.openVidu.hostname + OpenVidu.API_TOKENS);
StringEntity params;
try {
params = new StringEntity(tokenOptions.toJsonObject(sessionId).toString());
} catch (UnsupportedEncodingException e1) {
throw new OpenViduJavaClientException(e1.getMessage(), e1.getCause());
}
StringEntity params = new StringEntity(tokenOptions.toJsonObject(sessionId).toString(), "UTF-8");
request.setHeader(HttpHeaders.CONTENT_TYPE, "application/json");
request.setEntity(params);
@ -190,13 +183,7 @@ public class Session {
HttpPost request = new HttpPost(
this.openVidu.hostname + OpenVidu.API_SESSIONS + "/" + this.sessionId + "/connection");
StringEntity params;
try {
params = new StringEntity(connectionProperties.toJson(sessionId).toString());
} catch (UnsupportedEncodingException e1) {
throw new OpenViduJavaClientException(e1.getMessage(), e1.getCause());
}
StringEntity params = new StringEntity(connectionProperties.toJson(sessionId).toString(), "UTF-8");
request.setHeader(HttpHeaders.CONTENT_TYPE, "application/json");
request.setEntity(params);
@ -506,13 +493,8 @@ public class Session {
HttpPatch request = new HttpPatch(
this.openVidu.hostname + OpenVidu.API_SESSIONS + "/" + this.sessionId + "/connection/" + connectionId);
StringEntity params = new StringEntity(connectionProperties.toJson(this.sessionId).toString(), "UTF-8");
StringEntity params;
try {
params = new StringEntity(connectionProperties.toJson(this.sessionId).toString());
} catch (UnsupportedEncodingException e1) {
throw new OpenViduJavaClientException(e1.getMessage(), e1.getCause());
}
request.setHeader(HttpHeaders.CONTENT_TYPE, "application/json");
request.setEntity(params);
@ -661,12 +643,7 @@ public class Session {
}
HttpPost request = new HttpPost(this.openVidu.hostname + OpenVidu.API_SESSIONS);
StringEntity params = null;
try {
params = new StringEntity(properties.toJson().toString());
} catch (UnsupportedEncodingException e1) {
throw new OpenViduJavaClientException(e1.getMessage(), e1.getCause());
}
StringEntity params = new StringEntity(properties.toJson().toString(), "UTF-8");
request.setHeader(HttpHeaders.CONTENT_TYPE, "application/json");
request.setEntity(params);
@ -684,22 +661,17 @@ public class Session {
this.sessionId = responseJson.get("id").getAsString();
this.createdAt = responseJson.get("createdAt").getAsLong();
// forcedVideoCodec and allowTranscoding values are configured in OpenVidu Server
// via configuration or session
// forcedVideoCodec and allowTranscoding values are configured in OpenVidu
// Server via configuration or session
VideoCodec forcedVideoCodec = VideoCodec.valueOf(responseJson.get("forcedVideoCodec").getAsString());
Boolean allowTranscoding = responseJson.get("allowTranscoding").getAsBoolean();
SessionProperties responseProperties = new SessionProperties.Builder()
.customSessionId(properties.customSessionId())
.mediaMode(properties.mediaMode())
.customSessionId(properties.customSessionId()).mediaMode(properties.mediaMode())
.recordingMode(properties.recordingMode())
.defaultOutputMode(properties.defaultOutputMode())
.defaultRecordingLayout(properties.defaultRecordingLayout())
.defaultCustomLayout(properties.defaultCustomLayout())
.mediaNode(properties.mediaNode())
.forcedVideoCodec(forcedVideoCodec)
.allowTranscoding(allowTranscoding)
.build();
.defaultRecordingProperties(properties.defaultRecordingProperties())
.mediaNode(properties.mediaNode()).forcedVideoCodec(forcedVideoCodec)
.allowTranscoding(allowTranscoding).build();
this.properties = responseProperties;
log.info("Session '{}' created", this.sessionId);
@ -717,7 +689,7 @@ public class Session {
private JsonObject httpResponseToJson(HttpResponse response) throws OpenViduJavaClientException {
JsonObject json;
try {
json = new Gson().fromJson(EntityUtils.toString(response.getEntity()), JsonObject.class);
json = new Gson().fromJson(EntityUtils.toString(response.getEntity(), "UTF-8"), JsonObject.class);
} catch (JsonSyntaxException | IOException e) {
throw new OpenViduJavaClientException(e.getMessage(), e.getCause());
}
@ -734,13 +706,10 @@ public class Session {
this.recording = json.get("recording").getAsBoolean();
SessionProperties.Builder builder = new SessionProperties.Builder()
.mediaMode(MediaMode.valueOf(json.get("mediaMode").getAsString()))
.recordingMode(RecordingMode.valueOf(json.get("recordingMode").getAsString()))
.defaultOutputMode(Recording.OutputMode.valueOf(json.get("defaultOutputMode").getAsString()));
if (json.has("defaultRecordingLayout")) {
builder.defaultRecordingLayout(RecordingLayout.valueOf(json.get("defaultRecordingLayout").getAsString()));
}
if (json.has("defaultCustomLayout")) {
builder.defaultCustomLayout(json.get("defaultCustomLayout").getAsString());
.recordingMode(RecordingMode.valueOf(json.get("recordingMode").getAsString()));
if (json.has("defaultRecordingProperties")) {
builder.defaultRecordingProperties(
RecordingProperties.fromJson(json.get("defaultRecordingProperties").getAsJsonObject()));
}
if (json.has("customSessionId")) {
builder.customSessionId(json.get("customSessionId").getAsString());
@ -791,10 +760,10 @@ public class Session {
json.addProperty("recording", this.recording);
json.addProperty("mediaMode", this.properties.mediaMode().name());
json.addProperty("recordingMode", this.properties.recordingMode().name());
json.addProperty("defaultOutputMode", this.properties.defaultOutputMode().name());
json.addProperty("defaultRecordingLayout", this.properties.defaultRecordingLayout().name());
json.addProperty("defaultCustomLayout", this.properties.defaultCustomLayout());
if(this.properties.forcedVideoCodec() != null) {
if (this.properties.defaultRecordingProperties() != null) {
json.add("defaultRecordingProperties", this.properties.defaultRecordingProperties().toJson());
}
if (this.properties.forcedVideoCodec() != null) {
json.addProperty("forcedVideoCodec", this.properties.forcedVideoCodec().name());
}
if (this.properties.isTranscodingAllowed() != null) {

View File

@ -19,8 +19,6 @@ package io.openvidu.java.client;
import com.google.gson.JsonObject;
import io.openvidu.java.client.Recording.OutputMode;
/**
* See {@link io.openvidu.java.client.OpenVidu#createSession(SessionProperties)}
*/
@ -28,9 +26,7 @@ public class SessionProperties {
private MediaMode mediaMode;
private RecordingMode recordingMode;
private OutputMode defaultOutputMode;
private RecordingLayout defaultRecordingLayout;
private String defaultCustomLayout;
private RecordingProperties defaultRecordingProperties;
private String customSessionId;
private String mediaNode;
private VideoCodec forcedVideoCodec;
@ -43,30 +39,26 @@ public class SessionProperties {
private MediaMode mediaMode = MediaMode.ROUTED;
private RecordingMode recordingMode = RecordingMode.MANUAL;
private OutputMode defaultOutputMode = OutputMode.COMPOSED;
private RecordingLayout defaultRecordingLayout = RecordingLayout.BEST_FIT;
private String defaultCustomLayout = "";
private RecordingProperties defaultRecordingProperties = new RecordingProperties.Builder().build();
private String customSessionId = "";
private String mediaNode;
private VideoCodec forcedVideoCodec;
private Boolean allowTranscoding;
private VideoCodec forcedVideoCodec = VideoCodec.VP8;
private Boolean allowTranscoding = false;
/**
* Returns the {@link io.openvidu.java.client.SessionProperties} object properly
* configured
*/
public SessionProperties build() {
return new SessionProperties(this.mediaMode, this.recordingMode, this.defaultOutputMode,
this.defaultRecordingLayout, this.defaultCustomLayout, this.customSessionId, this.mediaNode,
this.forcedVideoCodec, this.allowTranscoding);
return new SessionProperties(this.mediaMode, this.recordingMode, this.defaultRecordingProperties,
this.customSessionId, this.mediaNode, this.forcedVideoCodec, this.allowTranscoding);
}
/**
* Call this method to set how the media streams will be sent and received by
* your clients: routed through OpenVidu Media Node
* (<code>MediaMode.ROUTED</code>) or attempting direct p2p connections
* (<code>MediaMode.RELAYED</code>, <i>not available yet</i>)
*
* (<code>MediaMode.RELAYED</code>, <i>not available yet</i>)<br>
* Default value is <code>MediaMode.ROUTED</code>
*/
public SessionProperties.Builder mediaMode(MediaMode mediaMode) {
@ -85,53 +77,14 @@ public class SessionProperties {
}
/**
* Call this method to set the the default value used to initialize property
* {@link io.openvidu.java.client.RecordingProperties#outputMode()} of every
* recording of this session. You can easily override this value later when
* starting a {@link io.openvidu.java.client.Recording} by calling
* {@link io.openvidu.java.client.RecordingProperties.Builder#outputMode(Recording.OutputMode)}
* with any other value.<br>
* Default value is {@link Recording.OutputMode#COMPOSED}
* Call this method to set the default recording properties of this session. You
* can easily override this value later when starting a
* {@link io.openvidu.java.client.Recording} by providing new
* {@link RecordingProperties}<br>
* Default values defined in {@link RecordingProperties} class.
*/
public SessionProperties.Builder defaultOutputMode(OutputMode outputMode) {
this.defaultOutputMode = outputMode;
return this;
}
/**
* Call this method to set the the default value used to initialize property
* {@link io.openvidu.java.client.RecordingProperties#recordingLayout()} of
* every recording of this session. You can easily override this value later
* when starting a {@link io.openvidu.java.client.Recording} by calling
* {@link io.openvidu.java.client.RecordingProperties.Builder#recordingLayout(RecordingLayout)}
* with any other value.<br>
* Default value is {@link RecordingLayout#BEST_FIT}<br>
* <br>
* Recording layouts are only applicable to recordings with OutputMode
* {@link io.openvidu.java.client.Recording.OutputMode#COMPOSED} or
* {@link io.openvidu.java.client.Recording.OutputMode#COMPOSED_QUICK_START}
*/
public SessionProperties.Builder defaultRecordingLayout(RecordingLayout layout) {
this.defaultRecordingLayout = layout;
return this;
}
/**
* Call this method to set the default value used to initialize property
* {@link io.openvidu.java.client.RecordingProperties#customLayout()} of every
* recording of this session. You can easily override this value later when
* starting a {@link io.openvidu.java.client.Recording} by calling
* {@link io.openvidu.java.client.RecordingProperties.Builder#customLayout(String)}
* with any other value.<br>
* <br>
*
* Custom layouts are only applicable to recordings with OutputMode
* {@link io.openvidu.java.client.Recording.OutputMode#COMPOSED} (or
* {@link io.openvidu.java.client.Recording.OutputMode#COMPOSED_QUICK_START})
* and RecordingLayout {@link io.openvidu.java.client.RecordingLayout#CUSTOM}
*/
public SessionProperties.Builder defaultCustomLayout(String path) {
this.defaultCustomLayout = path;
public SessionProperties.Builder defaultRecordingProperties(RecordingProperties defaultRecordingProperties) {
this.defaultRecordingProperties = defaultRecordingProperties;
return this;
}
@ -161,12 +114,13 @@ public class SessionProperties {
}
/**
* Call this method to define which video codec do you want to be forcibly used for this session.
* This allows browsers/clients to use the same codec avoiding transcoding in the media server.
* If the browser/client is not compatible with the specified codec and {@link #allowTranscoding(Boolean)}
* is <code>false</code> and exception will occur.
*
* If forcedVideoCodec is set to NONE, no codec will be forced.
* Call this method to define which video codec do you want to be forcibly used
* for this session. This allows browsers/clients to use the same codec avoiding
* transcoding in the media server. If the browser/client is not compatible with
* the specified codec and {@link #allowTranscoding(Boolean)} is
* <code>false</code> and exception will occur. If forcedVideoCodec is set to
* NONE, no codec will be forced.<br>
* Default value is {@link VideoCodec#VP8}
*/
public SessionProperties.Builder forcedVideoCodec(VideoCodec forcedVideoCodec) {
this.forcedVideoCodec = forcedVideoCodec;
@ -174,8 +128,10 @@ public class SessionProperties {
}
/**
* Call this method to define if you want to allow transcoding in the media server or not
* when {@link #forcedVideoCodec(VideoCodec)} is not compatible with the browser/client.
* Call this method to define if you want to allow transcoding in the media
* server or not when {@link #forcedVideoCodec(VideoCodec)} is not compatible
* with the browser/client.<br>
* Default value is false
*/
public SessionProperties.Builder allowTranscoding(Boolean allowTranscoding) {
this.allowTranscoding = allowTranscoding;
@ -187,21 +143,17 @@ public class SessionProperties {
protected SessionProperties() {
this.mediaMode = MediaMode.ROUTED;
this.recordingMode = RecordingMode.MANUAL;
this.defaultOutputMode = OutputMode.COMPOSED;
this.defaultRecordingLayout = RecordingLayout.BEST_FIT;
this.defaultCustomLayout = "";
this.defaultRecordingProperties = new RecordingProperties.Builder().build();
this.customSessionId = "";
this.mediaNode = "";
}
private SessionProperties(MediaMode mediaMode, RecordingMode recordingMode, OutputMode outputMode,
RecordingLayout layout, String defaultCustomLayout, String customSessionId, String mediaNode,
private SessionProperties(MediaMode mediaMode, RecordingMode recordingMode,
RecordingProperties defaultRecordingProperties, String customSessionId, String mediaNode,
VideoCodec forcedVideoCodec, Boolean allowTranscoding) {
this.mediaMode = mediaMode;
this.recordingMode = recordingMode;
this.defaultOutputMode = outputMode;
this.defaultRecordingLayout = layout;
this.defaultCustomLayout = defaultCustomLayout;
this.defaultRecordingProperties = defaultRecordingProperties;
this.customSessionId = customSessionId;
this.mediaNode = mediaNode;
this.forcedVideoCodec = forcedVideoCodec;
@ -227,46 +179,13 @@ public class SessionProperties {
}
/**
* Defines the default value used to initialize property
* {@link io.openvidu.java.client.RecordingProperties#outputMode()} of every
* recording of this session. You can easily override this value later when
* starting a {@link io.openvidu.java.client.Recording} by calling
* {@link io.openvidu.java.client.RecordingProperties.Builder#outputMode(Recording.OutputMode)}
* with any other value
* Defines the default recording properties of this session. You can easily
* override this value later when starting a
* {@link io.openvidu.java.client.Recording} by providing new
* {@link RecordingProperties}
*/
public OutputMode defaultOutputMode() {
return this.defaultOutputMode;
}
/**
* Defines the default value used to initialize property
* {@link io.openvidu.java.client.RecordingProperties#recordingLayout()} of
* every recording of this session. You can easily override this value later
* when starting a {@link io.openvidu.java.client.Recording} by calling
* {@link io.openvidu.java.client.RecordingProperties.Builder#recordingLayout(RecordingLayout)}
* with any other value.<br>
* Recording layouts are only applicable to recordings with OutputMode
* {@link io.openvidu.java.client.Recording.OutputMode#COMPOSED} or
* {@link io.openvidu.java.client.Recording.OutputMode#COMPOSED_QUICK_START}
*/
public RecordingLayout defaultRecordingLayout() {
return this.defaultRecordingLayout;
}
/**
* Defines the default value used to initialize property
* {@link io.openvidu.java.client.RecordingProperties#customLayout()} of every
* recording of this session. You can easily override this value later when
* starting a {@link io.openvidu.java.client.Recording} by calling
* {@link io.openvidu.java.client.RecordingProperties.Builder#customLayout(String)}
* with any other value.<br>
* Custom layouts are only applicable to recordings with OutputMode
* {@link io.openvidu.java.client.Recording.OutputMode#COMPOSED} (or
* {@link io.openvidu.java.client.Recording.OutputMode#COMPOSED_QUICK_START})
* and RecordingLayout {@link io.openvidu.java.client.RecordingLayout#CUSTOM}
*/
public String defaultCustomLayout() {
return this.defaultCustomLayout;
public RecordingProperties defaultRecordingProperties() {
return this.defaultRecordingProperties;
}
/**
@ -301,8 +220,8 @@ public class SessionProperties {
}
/**
* Defines if transcoding is allowed or not when {@link #forcedVideoCodec}
* is not a compatible codec with the browser/client.
* Defines if transcoding is allowed or not when {@link #forcedVideoCodec} is
* not a compatible codec with the browser/client.
*/
public Boolean isTranscodingAllowed() {
return this.allowTranscoding;
@ -312,10 +231,8 @@ public class SessionProperties {
JsonObject json = new JsonObject();
json.addProperty("mediaMode", mediaMode().name());
json.addProperty("recordingMode", recordingMode().name());
json.addProperty("defaultOutputMode", defaultOutputMode().name());
json.addProperty("defaultRecordingLayout", defaultRecordingLayout().name());
json.addProperty("defaultCustomLayout", defaultCustomLayout());
json.addProperty("customSessionId", customSessionId());
json.add("defaultRecordingProperties", defaultRecordingProperties.toJson());
if (mediaNode() != null) {
JsonObject mediaNodeJson = new JsonObject();
mediaNodeJson.addProperty("id", mediaNode());

View File

@ -1,6 +1,6 @@
[![License badge](https://img.shields.io/badge/license-Apache2-orange.svg)](http://www.apache.org/licenses/LICENSE-2.0)
[![Documentation Status](https://readthedocs.org/projects/openviduio-docs/badge/?version=stable)](https://docs.openvidu.io/en/stable/?badge=stable)
[![Support badge](https://img.shields.io/badge/support-sof-yellowgreen.svg)](https://groups.google.com/forum/#!forum/openvidu)
[![Support badge](https://img.shields.io/badge/support-sof-yellowgreen.svg)](https://openvidu.discourse.group/)
[![][OpenViduLogo]](https://openvidu.io)

File diff suppressed because it is too large Load Diff

View File

@ -2,21 +2,21 @@
"author": "OpenVidu",
"dependencies": {
"axios": "0.21.1",
"buffer": "6.0.2"
"buffer": "6.0.3"
},
"description": "OpenVidu Node Client",
"devDependencies": {
"@types/node": "14.14.7",
"@types/node": "14.14.37",
"grunt": "1.3.0",
"grunt-cli": "1.3.2",
"grunt-cli": "1.4.2",
"grunt-contrib-copy": "1.0.0",
"grunt-contrib-sass": "2.0.0",
"grunt-contrib-uglify": "5.0.0",
"grunt-contrib-uglify": "5.0.1",
"grunt-contrib-watch": "1.1.0",
"grunt-postcss": "0.9.0",
"grunt-string-replace": "1.3.1",
"grunt-ts": "6.0.0-beta.22",
"ts-node": "9.0.0",
"ts-node": "9.1.1",
"tslint": "6.1.3",
"typedoc": "0.19.2",
"typescript": "3.8.3"
@ -33,5 +33,5 @@
"docs": "./generate-docs.sh"
},
"typings": "lib/index.d.ts",
"version": "2.16.0"
"version": "2.17.0"
}

View File

@ -126,6 +126,7 @@ export class OpenVidu {
*
* @param sessionId The `sessionId` of the [[Session]] you want to start recording
* @param name The name you want to give to the video file. You can access this same value in your clients on recording events (`recordingStarted`, `recordingStopped`)
* @param properties Custom RecordingProperties to apply to this Recording. This will override the global default values set to the Session with [[SessionProperties.defaultRecordingProperties]]
*
* @returns A Promise that is resolved to the [[Recording]] if it successfully started (the recording can be stopped with guarantees) and rejected with an Error
* object if not. This Error object has as `message` property with the following values:
@ -141,32 +142,29 @@ export class OpenVidu {
let data;
if (!!param2) {
if (!(typeof param2 === 'string')) {
const properties = <RecordingProperties>param2;
data = {
session: sessionId,
name: !!properties.name ? properties.name : '',
outputMode: properties.outputMode,
hasAudio: properties.hasAudio != null ? properties.hasAudio : null,
hasVideo: properties.hasVideo != null ? properties.hasVideo : null,
shmSize: properties.shmSize,
mediaNode: properties.mediaNode
};
if ((data.hasVideo == null || data.hasVideo) && (data.outputMode == null || data.outputMode.toString() === Recording.OutputMode[Recording.OutputMode.COMPOSED]
|| data.outputMode.toString() === Recording.OutputMode[Recording.OutputMode.COMPOSED_QUICK_START])) {
data.resolution = properties.resolution;
data.recordingLayout = !!properties.recordingLayout ? properties.recordingLayout : '';
if (data.recordingLayout.toString() === RecordingLayout[RecordingLayout.CUSTOM]) {
data.customLayout = !!properties.customLayout ? properties.customLayout : '';
}
}
data = JSON.stringify(data);
} else {
if (param2 != null) {
if (typeof param2 === 'string') {
data = JSON.stringify({
session: sessionId,
name: param2
});
} else {
const properties: RecordingProperties = param2 as RecordingProperties;
data = {
session: sessionId,
name: properties.name,
outputMode: properties.outputMode,
recordingLayout: properties.recordingLayout,
customLayout: properties.customLayout,
ignoreFailedStreams: properties.ignoreFailedStreams,
resolution: properties.resolution,
frameRate: properties.frameRate,
hasAudio: properties.hasAudio,
hasVideo: properties.hasVideo,
shmSize: properties.shmSize,
mediaNode: properties.mediaNode
};
data = JSON.stringify(data);
}
} else {
data = JSON.stringify({
@ -509,8 +507,8 @@ export class OpenVidu {
events: publisherExtended.events,
localCandidate: publisherExtended.localCandidate,
remoteCandidate: publisherExtended.remoteCandidate,
receivedCandidates: publisherExtended.receivedCandidates,
gatheredCandidates: publisherExtended.gatheredCandidates,
clientIceCandidates: publisherExtended.clientIceCandidates,
serverIceCandidates: publisherExtended.serverIceCandidates,
webrtcEndpointName: publisherExtended.webrtcEndpointName,
localSdp: publisherExtended.localSdp,
remoteSdp: publisherExtended.remoteSdp
@ -535,8 +533,8 @@ export class OpenVidu {
events: subscriberExtended.events,
localCandidate: subscriberExtended.localCandidate,
remoteCandidate: subscriberExtended.remoteCandidate,
receivedCandidates: subscriberExtended.receivedCandidates,
gatheredCandidates: subscriberExtended.gatheredCandidates,
clientIceCandidates: subscriberExtended.clientIceCandidates,
serverIceCandidates: subscriberExtended.serverIceCandidates,
webrtcEndpointName: subscriberExtended.webrtcEndpointName,
localSdp: subscriberExtended.localSdp,
remoteSdp: subscriberExtended.remoteSdp

View File

@ -77,19 +77,26 @@ export class Recording {
this.url = json['url'];
this.status = json['status'];
this.properties = {
name: !!(json['name']) ? json['name'] : this.id,
outputMode: !!(json['outputMode']) ? json['outputMode'] : Recording.OutputMode.COMPOSED,
hasAudio: !!(json['hasAudio']),
hasVideo: !!json['hasVideo']
name: (json['name'] != null) ? json['name'] : this.id,
hasAudio: (json['hasAudio'] != null) ? !!json['hasAudio'] : Recording.DefaultRecordingPropertiesValues.hasAudio,
hasVideo: (json['hasVideo'] != null) ? !!json['hasVideo'] : Recording.DefaultRecordingPropertiesValues.hasVideo,
outputMode: (json['outputMode'] != null) ? json['outputMode'] : Recording.DefaultRecordingPropertiesValues.outputMode,
mediaNode: json['mediaNode']
};
if (this.properties.outputMode.toString() === Recording.OutputMode[Recording.OutputMode.COMPOSED]
|| this.properties.outputMode.toString() === Recording.OutputMode[Recording.OutputMode.COMPOSED_QUICK_START]) {
this.properties.resolution = !!(json['resolution']) ? json['resolution'] : '1920x1080';
this.properties.recordingLayout = !!(json['recordingLayout']) ? json['recordingLayout'] : RecordingLayout.BEST_FIT;
if ((this.properties.outputMode.toString() === Recording.OutputMode[Recording.OutputMode.COMPOSED]
|| this.properties.outputMode.toString() === Recording.OutputMode[Recording.OutputMode.COMPOSED_QUICK_START])
&& this.properties.hasVideo) {
this.properties.recordingLayout = (json['recordingLayout'] != null) ? json['recordingLayout'] : Recording.DefaultRecordingPropertiesValues.recordingLayout;
this.properties.resolution = (json['resolution'] != null) ? json['resolution'] : Recording.DefaultRecordingPropertiesValues.resolution;
this.properties.frameRate = (json['frameRate'] != null) ? Number(json['frameRate']) : Recording.DefaultRecordingPropertiesValues.frameRate;
this.properties.shmSize = (json['shmSize'] != null) ? Number(json['shmSize']) : Recording.DefaultRecordingPropertiesValues.shmSize;
if (this.properties.recordingLayout.toString() === RecordingLayout[RecordingLayout.CUSTOM]) {
this.properties.customLayout = json['customLayout'];
this.properties.customLayout = (json['customLayout'] != null) ? json['customLayout'] : '';
}
}
if (this.properties.outputMode.toString() === Recording.OutputMode[Recording.OutputMode.INDIVIDUAL]) {
this.properties.ignoreFailedStreams = (json['ignoreFailedStreams'] != null) ? !!json['ignoreFailedStreams'] : Recording.DefaultRecordingPropertiesValues.ignoreFailedStreams;
}
}
/* tslint:enable:no-string-literal */
}
@ -165,4 +172,19 @@ export namespace Recording {
*/
INDIVIDUAL = 'INDIVIDUAL'
}
/**
* @hidden
*/
export class DefaultRecordingPropertiesValues {
static readonly hasAudio: boolean = true;
static readonly hasVideo: boolean = true;
static readonly outputMode: Recording.OutputMode = Recording.OutputMode.COMPOSED;
static readonly recordingLayout: RecordingLayout = RecordingLayout.BEST_FIT;
static readonly resolution: string = "1280x720";
static readonly frameRate: number = 25;
static readonly shmSize: number = 536870912;
static readonly ignoreFailedStreams: boolean = false;
}
}

View File

@ -16,7 +16,7 @@
*/
/**
* See [[SessionProperties.defaultRecordingLayout]] and [[RecordingProperties.recordingLayout]]
* See [[RecordingProperties.recordingLayout]]
*/
export enum RecordingLayout {

View File

@ -30,50 +30,85 @@ export interface RecordingProperties {
*/
name?: string;
/**
* The mode of recording: COMPOSED for a single archive in a grid layout or INDIVIDUAL for one archive for each stream
*/
outputMode?: Recording.OutputMode;
/**
* The layout to be used in the recording.<br>
* Will only have effect if [[RecordingProperties.outputMode]] is `COMPOSED` or `COMPOSED_QUICK_START`
*/
recordingLayout?: RecordingLayout;
/**
* The relative path to the specific custom layout you want to use.<br>
* Will only have effect if [[RecordingProperties.outputMode]] is `COMPOSED` (or `COMPOSED_QUICK_START`) and [[RecordingProperties.recordingLayout]] is `CUSTOM`<br>
* See [Custom recording layouts](/en/stable/advanced-features/recording#custom-recording-layouts) to learn more
*/
customLayout?: string;
/**
* Recording video file resolution. Must be a string with format "WIDTHxHEIGHT",
* being both WIDTH and HEIGHT the number of pixels between 100 and 1999.<br>
* Will only have effect if [[RecordingProperties.outputMode]]
* is set to [[Recording.OutputMode.COMPOSED]] or [[Recording.OutputMode.COMPOSED_QUICK_START]].
* For [[Recording.OutputMode.INDIVIDUAL]] all
* individual video files will have the native resolution of the published stream
*/
resolution?: string;
/**
* Whether or not to record audio. Cannot be set to false at the same time as [[RecordingProperties.hasVideo]]
*
* Default to true
*/
hasAudio?: boolean;
/**
* Whether or not to record video. Cannot be set to false at the same time as [[RecordingProperties.hasAudio]]
*
* Default to true
*/
hasVideo?: boolean;
/**
* If COMPOSED recording, the amount of shared memory reserved for the recording process in bytes.
* Minimum 134217728 (128MB). Property ignored if INDIVIDUAL recording. Default to 536870912 (512 MB)
* The mode of recording: COMPOSED for a single archive in a grid layout or INDIVIDUAL for one archive for each stream
*
* Default to [[Recording.OutputMode.COMPOSED]]
*/
outputMode?: Recording.OutputMode;
/**
* The layout to be used in the recording.<br>
* Will only have effect if [[RecordingProperties.outputMode]] is set to [[Recording.OutputMode.COMPOSED]] or [[Recording.OutputMode.COMPOSED_QUICK_START]]
*
* Default to [[RecordingLayout.BEST_FIT]]
*/
recordingLayout?: RecordingLayout;
/**
* Recording video file resolution. Must be a string with format "WIDTHxHEIGHT",
* being both WIDTH and HEIGHT the number of pixels between 100 and 1999.<br>
* Will only have effect if [[RecordingProperties.outputMode]] is set to [[Recording.OutputMode.COMPOSED]] or [[Recording.OutputMode.COMPOSED_QUICK_START]]
* and [[RecordingProperties.hasVideo]] is set to true. For [[Recording.OutputMode.INDIVIDUAL]] all individual video files will have the native resolution of the published stream.
*
* Default to "1280x720"
*/
resolution?: string;
/**
* Recording video file frame rate.<br>
* Will only have effect if [[RecordingProperties.outputMode]] is set to [[Recording.OutputMode.COMPOSED]] or [[Recording.OutputMode.COMPOSED_QUICK_START]]
* and [[RecordingProperties.hasVideo]] is set to true. For [[Recording.OutputMode.INDIVIDUAL]] all individual video files will have the native frame rate of the published stream.
*
* Default to 25
*/
frameRate?: number;
/**
* The amount of shared memory reserved for the recording process in bytes.
* Will only have effect if [[RecordingProperties.outputMode]] is set to [[Recording.OutputMode.COMPOSED]] or [[Recording.OutputMode.COMPOSED_QUICK_START]]
* and [[RecordingProperties.hasVideo]] is set to true. Property ignored for INDIVIDUAL recordings and audio-only recordings.
* Minimum 134217728 (128MB).
*
* Default to 536870912 (512 MB)
*/
shmSize?: number;
/**
* The relative path to the specific custom layout you want to use.<br>
* Will only have effect if [[RecordingProperties.outputMode]] is set to [[Recording.OutputMode.COMPOSED]] or [[Recording.OutputMode.COMPOSED_QUICK_START]]
* and [[RecordingProperties.recordingLayout]] is set to [[RecordingLayout.CUSTOM]]<br>
* See [Custom recording layouts](/en/stable/advanced-features/recording#custom-recording-layouts) to learn more.
*/
customLayout?: string;
/**
* Whether to ignore failed streams or not when starting the recording. This property only applies if [[RecordingProperties.outputMode]] is set to [[Recording.OutputMode.INDIVIDUAL]].
* For this type of recordings, when calling [[OpenVidu.startRecording]] by default all the streams available at the moment the recording process starts must be healthy
* and properly sending media. If some stream that should be sending media is broken, then the recording process fails after a 10s timeout. In this way your application is notified
* that some stream is not being recorded, so it can retry the process again.
*
* But you can disable this rollback behavior and simply ignore any failed stream, which will be susceptible to be recorded in the future if media starts flowing as expected at any point.
* The downside of this behavior is that you will have no guarantee that all streams present at the beginning of a recording are actually being recorded.
*
* Default to false
*/
ignoreFailedStreams?: boolean;
/**
* **This feature is part of OpenVidu Pro tier** <a href="https://docs.openvidu.io/en/stable/openvidu-pro/" target="_blank" style="display: inline-block; background-color: rgb(0, 136, 170); color: white; font-weight: bold; padding: 0px 5px; margin-right: 5px; border-radius: 3px; font-size: 13px; line-height:21px; font-family: Montserrat, sans-serif">PRO</a>
*

View File

@ -16,6 +16,7 @@
*/
import axios, { AxiosError } from 'axios';
import { VideoCodec } from './VideoCodec';
import { Connection } from './Connection';
import { ConnectionProperties } from './ConnectionProperties';
import { MediaMode } from './MediaMode';
@ -26,6 +27,7 @@ import { RecordingLayout } from './RecordingLayout';
import { RecordingMode } from './RecordingMode';
import { SessionProperties } from './SessionProperties';
import { TokenOptions } from './TokenOptions';
import { RecordingProperties } from 'RecordingProperties';
export class Session {
@ -90,12 +92,7 @@ export class Session {
// Empty parameter
this.properties = {};
}
this.properties.mediaMode = !!this.properties.mediaMode ? this.properties.mediaMode : MediaMode.ROUTED;
this.properties.recordingMode = !!this.properties.recordingMode ? this.properties.recordingMode : RecordingMode.MANUAL;
this.properties.defaultOutputMode = !!this.properties.defaultOutputMode ? this.properties.defaultOutputMode : Recording.OutputMode.COMPOSED;
this.properties.defaultRecordingLayout = !!this.properties.defaultRecordingLayout ? this.properties.defaultRecordingLayout : RecordingLayout.BEST_FIT;
this.properties.forcedVideoCodec = !!this.properties.forcedVideoCodec ? this.properties.forcedVideoCodec : undefined;
this.properties.allowTranscoding = this.properties.allowTranscoding != null ? this.properties.allowTranscoding : undefined;
this.sanitizeDefaultSessionProperties(this.properties);
}
/**
@ -145,10 +142,15 @@ export class Session {
public createConnection(connectionProperties?: ConnectionProperties): Promise<Connection> {
return new Promise<Connection>((resolve, reject) => {
const data = JSON.stringify({
role: (!!connectionProperties && !!connectionProperties.role) ? connectionProperties.role : null,
type: (!!connectionProperties && !!connectionProperties.type) ? connectionProperties.type : null,
data: (!!connectionProperties && !!connectionProperties.data) ? connectionProperties.data : null,
record: !!connectionProperties ? connectionProperties.record : null,
kurentoOptions: (!!connectionProperties && !!connectionProperties.kurentoOptions) ? connectionProperties.kurentoOptions : null
role: (!!connectionProperties && !!connectionProperties.role) ? connectionProperties.role : null,
kurentoOptions: (!!connectionProperties && !!connectionProperties.kurentoOptions) ? connectionProperties.kurentoOptions : null,
rtspUri: (!!connectionProperties && !!connectionProperties.rtspUri) ? connectionProperties.rtspUri : null,
adaptativeBitrate: !!connectionProperties ? connectionProperties.adaptativeBitrate : null,
onlyPlayWithSubscribers: !!connectionProperties ? connectionProperties.onlyPlayWithSubscribers : null,
networkCache: (!!connectionProperties && (connectionProperties.networkCache != null)) ? connectionProperties.networkCache : null
});
axios.post(
this.ov.host + OpenVidu.API_SESSIONS + '/' + this.sessionId + '/connection',
@ -184,8 +186,8 @@ export class Session {
*
* @returns A Promise that is resolved if the session has been closed successfully and rejected with an Error object if not
*/
public close(): Promise<any> {
return new Promise<any>((resolve, reject) => {
public close(): Promise<void> {
return new Promise<void>((resolve, reject) => {
axios.delete(
this.ov.host + OpenVidu.API_SESSIONS + '/' + this.sessionId,
{
@ -267,8 +269,8 @@ export class Session {
*
* @returns A Promise that is resolved if the Connection was successfully removed from the Session and rejected with an Error object if not
*/
public forceDisconnect(connection: string | Connection): Promise<any> {
return new Promise<any>((resolve, reject) => {
public forceDisconnect(connection: string | Connection): Promise<void> {
return new Promise<void>((resolve, reject) => {
const connectionId: string = typeof connection === 'string' ? connection : (<Connection>connection).connectionId;
axios.delete(
this.ov.host + OpenVidu.API_SESSIONS + '/' + this.sessionId + '/connection/' + connectionId,
@ -339,8 +341,8 @@ export class Session {
*
* @returns A Promise that is resolved if the stream was successfully unpublished and rejected with an Error object if not
*/
public forceUnpublish(publisher: string | Publisher): Promise<any> {
return new Promise<any>((resolve, reject) => {
public forceUnpublish(publisher: string | Publisher): Promise<void> {
return new Promise<void>((resolve, reject) => {
const streamId: string = typeof publisher === 'string' ? publisher : (<Publisher>publisher).streamId;
axios.delete(
this.ov.host + OpenVidu.API_SESSIONS + '/' + this.sessionId + '/stream/' + streamId,
@ -461,18 +463,11 @@ export class Session {
resolve(this.sessionId);
}
const mediaMode = !!this.properties.mediaMode ? this.properties.mediaMode : MediaMode.ROUTED;
const recordingMode = !!this.properties.recordingMode ? this.properties.recordingMode : RecordingMode.MANUAL;
const defaultOutputMode = !!this.properties.defaultOutputMode ? this.properties.defaultOutputMode : Recording.OutputMode.COMPOSED;
const defaultRecordingLayout = !!this.properties.defaultRecordingLayout ? this.properties.defaultRecordingLayout : RecordingLayout.BEST_FIT;
const defaultCustomLayout = !!this.properties.defaultCustomLayout ? this.properties.defaultCustomLayout : '';
const customSessionId = !!this.properties.customSessionId ? this.properties.customSessionId : '';
const mediaNode = !!this.properties.mediaNode ? this.properties.mediaNode : undefined;
const forcedVideoCodec = !!this.properties.forcedVideoCodec ? this.properties.forcedVideoCodec : undefined;
const allowTranscoding = this.properties.allowTranscoding != null ? this.properties.allowTranscoding : undefined;
this.sanitizeDefaultSessionProperties(this.properties);
const data = JSON.stringify({mediaMode, recordingMode, defaultOutputMode, defaultRecordingLayout, defaultCustomLayout,
customSessionId, mediaNode, forcedVideoCodec, allowTranscoding});
const data = JSON.stringify(
this.properties
);
axios.post(
this.ov.host + OpenVidu.API_SESSIONS,
@ -489,15 +484,14 @@ export class Session {
// SUCCESS response from openvidu-server. Resolve token
this.sessionId = res.data.id;
this.createdAt = res.data.createdAt;
this.properties.mediaMode = mediaMode;
this.properties.recordingMode = recordingMode;
this.properties.defaultOutputMode = defaultOutputMode;
this.properties.defaultRecordingLayout = defaultRecordingLayout;
this.properties.defaultCustomLayout = defaultCustomLayout;
this.properties.customSessionId = customSessionId;
this.properties.mediaNode = mediaNode;
this.properties.mediaMode = res.data.mediaMode;
this.properties.recordingMode = res.data.recordingMode;
this.properties.customSessionId = res.data.customSessionId;
this.properties.defaultRecordingProperties = res.data.defaultRecordingProperties;
this.properties.mediaNode = res.data.mediaNode;
this.properties.forcedVideoCodec = res.data.forcedVideoCodec;
this.properties.allowTranscoding = res.data.allowTranscoding;
this.sanitizeDefaultSessionProperties(this.properties);
resolve(this.sessionId);
} else {
// ERROR response from openvidu-server. Resolve HTTP status
@ -539,21 +533,17 @@ export class Session {
customSessionId: json.customSessionId,
mediaMode: json.mediaMode,
recordingMode: json.recordingMode,
defaultOutputMode: json.defaultOutputMode,
defaultRecordingLayout: json.defaultRecordingLayout,
defaultCustomLayout: json.defaultCustomLayout,
defaultRecordingProperties: json.defaultRecordingProperties,
forcedVideoCodec: json.forcedVideoCodec,
allowTranscoding: json.allowTranscoding
};
if (json.defaultRecordingLayout == null) {
delete this.properties.defaultRecordingLayout;
this.sanitizeDefaultSessionProperties(this.properties);
if (json.defaultRecordingProperties == null) {
delete this.properties.defaultRecordingProperties;
}
if (json.customSessionId == null) {
delete this.properties.customSessionId;
}
if (json.defaultCustomLayout == null) {
delete this.properties.defaultCustomLayout;
}
if (json.mediaNode == null) {
delete this.properties.mediaNode;
}
@ -661,4 +651,52 @@ export class Session {
}
}
/**
* @hidden
*/
private sanitizeDefaultSessionProperties(props: SessionProperties) {
props.mediaMode = (props.mediaMode != null) ? props.mediaMode : MediaMode.ROUTED;
props.recordingMode = (props.recordingMode != null) ? props.recordingMode : RecordingMode.MANUAL;
props.customSessionId = (props.customSessionId != null) ? props.customSessionId : '';
props.mediaNode = (props.mediaNode != null) ? props.mediaNode : undefined;
props.forcedVideoCodec = (props.forcedVideoCodec != null) ? props.forcedVideoCodec : VideoCodec.VP8;
props.allowTranscoding = (props.allowTranscoding != null) ? props.allowTranscoding : false;
if (!props.defaultRecordingProperties) {
props.defaultRecordingProperties = {};
}
props.defaultRecordingProperties.name = (props.defaultRecordingProperties?.name != null) ? props.defaultRecordingProperties.name : '';
props.defaultRecordingProperties.hasAudio = (props.defaultRecordingProperties?.hasAudio != null) ? props.defaultRecordingProperties.hasAudio : Recording.DefaultRecordingPropertiesValues.hasAudio;
props.defaultRecordingProperties.hasVideo = (props.defaultRecordingProperties?.hasVideo != null) ? props.defaultRecordingProperties.hasVideo : Recording.DefaultRecordingPropertiesValues.hasVideo;
props.defaultRecordingProperties.outputMode = (props.defaultRecordingProperties?.outputMode != null) ? props.defaultRecordingProperties.outputMode : Recording.DefaultRecordingPropertiesValues.outputMode;
props.defaultRecordingProperties.mediaNode = props.defaultRecordingProperties?.mediaNode;
if ((props.defaultRecordingProperties.outputMode === Recording.OutputMode.COMPOSED || props.defaultRecordingProperties.outputMode == Recording.OutputMode.COMPOSED_QUICK_START) && props.defaultRecordingProperties.hasVideo) {
props.defaultRecordingProperties.recordingLayout = (props.defaultRecordingProperties.recordingLayout != null) ? props.defaultRecordingProperties.recordingLayout : Recording.DefaultRecordingPropertiesValues.recordingLayout;
props.defaultRecordingProperties.resolution = (props.defaultRecordingProperties.resolution != null) ? props.defaultRecordingProperties.resolution : Recording.DefaultRecordingPropertiesValues.resolution;
props.defaultRecordingProperties.frameRate = (props.defaultRecordingProperties.frameRate != null) ? props.defaultRecordingProperties.frameRate : Recording.DefaultRecordingPropertiesValues.frameRate;
props.defaultRecordingProperties.shmSize = (props.defaultRecordingProperties.shmSize != null) ? props.defaultRecordingProperties.shmSize : Recording.DefaultRecordingPropertiesValues.shmSize;
if (props.defaultRecordingProperties.recordingLayout === RecordingLayout.CUSTOM) {
props.defaultRecordingProperties.customLayout = (props.defaultRecordingProperties.customLayout != null) ? props.defaultRecordingProperties.customLayout : '';
}
}
if (props.defaultRecordingProperties.outputMode === Recording.OutputMode.INDIVIDUAL) {
props.defaultRecordingProperties.ignoreFailedStreams = (props.defaultRecordingProperties?.ignoreFailedStreams != null) ? props.defaultRecordingProperties.ignoreFailedStreams : Recording.DefaultRecordingPropertiesValues.ignoreFailedStreams;
}
this.formatMediaNodeObjectIfNecessary(props.defaultRecordingProperties);
this.formatMediaNodeObjectIfNecessary(props);
}
/**
* @hidden
*/
private formatMediaNodeObjectIfNecessary(properties: RecordingProperties | SessionProperties) {
if (properties.mediaNode != null) {
if (typeof properties.mediaNode === 'string') {
properties.mediaNode = { id: properties.mediaNode };
}
}
}
}

View File

@ -16,9 +16,9 @@
*/
import { MediaMode } from './MediaMode';
import { Recording } from './Recording';
import { RecordingLayout } from './RecordingLayout';
import { RecordingProperties } from './RecordingProperties';
import { RecordingMode } from './RecordingMode';
import { VideoCodec } from './VideoCodec';
/**
* See [[OpenVidu.createSession]]
@ -28,35 +28,25 @@ export interface SessionProperties {
/**
* How the media streams will be sent and received by your clients: routed through OpenVidu Media Node
* (`MediaMode.ROUTED`) or attempting direct p2p connections (`MediaMode.RELAYED`, _not available yet_)
*
* Default to [[MediaMode.ROUTED]]
*/
mediaMode?: MediaMode;
/**
* Whether the Session will be automatically recorded (`RecordingMode.ALWAYS`) or not (`RecordingMode.MANUAL`)
*
* Default to [[RecordingMode.MANUAL]]
*/
recordingMode?: RecordingMode;
/**
* Default value used to initialize property [[RecordingProperties.outputMode]] of every recording of this session.
*
* You can easily override this value later by setting [[RecordingProperties.outputMode]] to any other value
* Default recording properties of this session. You can easily override this value later when starting a
* [[Recording]] by providing new [[RecordingProperties]]
*
* Default values defined in [[RecordingProperties]] class
*/
defaultOutputMode?: Recording.OutputMode;
/**
* Default value used to initialize property [[RecordingProperties.recordingLayout]] of every recording of this session.
*
* You can easily override this value later by setting [[RecordingProperties.recordingLayout]] to any other value
*/
defaultRecordingLayout?: RecordingLayout;
/**
* Default value used to initialize property [[RecordingProperties.customLayout]] of every recording of this session.
* This property can only be defined if [[SessionProperties.defaultRecordingLayout]] is set to [[RecordingLayout.CUSTOM]].
*
* You can easily override this value later by setting [[RecordingProperties.customLayout]] to any other value
*/
defaultCustomLayout?: string;
defaultRecordingProperties?: RecordingProperties;
/**
* Fix the sessionId that will be assigned to the session with this parameter. You can take advantage of this property
@ -79,16 +69,18 @@ export interface SessionProperties {
/**
* It defines which video codec do you want to be forcibly used for this session.
* This allows browsers/clients to use the same codec avoiding transcoding in the media server.
* If the browser/client is not compatible with the specified codec and [[allowTranscoding]]
* is <code>false</code> and exception will occur.
*
* If forcedVideoCodec is set to NONE, no codec will be forced.
* If the browser/client is not compatible with the specified codec and [[allowTranscoding]] is <code>false</code>
* and exception will occur. If forcedVideoCodec is set to [[VideoCodec.NONE]], no codec will be forced.
*
* Default to [[VideoCodec.VP8]]
*/
forcedVideoCodec?: string;
forcedVideoCodec?: VideoCodec;
/**
* It defines if you want to allow transcoding in the media server or not
* when [[forcedVideoCodec]] is not compatible with the browser/client.
*
* Default to false
*/
allowTranscoding?: boolean;

View File

@ -1,7 +1,7 @@
[![License badge](https://img.shields.io/badge/license-Apache2-orange.svg)](http://www.apache.org/licenses/LICENSE-2.0)
[![Documentation Status](https://readthedocs.org/projects/openviduio-docs/badge/?version=stable)](https://docs.openvidu.io/en/stable/?badge=stable)
[![Docker badge](https://img.shields.io/docker/pulls/fiware/orion.svg)](https://hub.docker.com/r/openvidu/)
[![Support badge](https://img.shields.io/badge/support-sof-yellowgreen.svg)](https://groups.google.com/forum/#!forum/openvidu)
[![Support badge](https://img.shields.io/badge/support-sof-yellowgreen.svg)](https://openvidu.discourse.group/)
[![][OpenViduLogo]](https://openvidu.io)

View File

@ -100,7 +100,7 @@ Parameters:
# Other configuration
WantToDeployDemos:
Description: "Choose if you want to deploy OpenVidu Call application alongside OpenVidu platform."
Description: "Choose if you want to deploy OpenVidu Call application alongside OpenVidu platform."
Type: String
AllowedValues:
- true
@ -180,7 +180,7 @@ Metadata:
Conditions:
WhichCertPresent: !Not [ !Equals [!Ref WhichCert, ""] ]
PublicElasticIPPresent: !Not [ !Equals [!Ref PublicElasticIP, ""] ]
Resources:
OpenviduServer:
@ -224,7 +224,7 @@ Resources:
content: !Sub |
#!/bin/bash -x
WORKINGDIR=/opt/openvidu
# Replace secret
sed -i "s/OPENVIDU_SECRET=/OPENVIDU_SECRET=${OpenViduSecret}/" $WORKINGDIR/.env
@ -243,7 +243,7 @@ Resources:
# Replace certificated type
sed -i "s/CERTIFICATE_TYPE=selfsigned/CERTIFICATE_TYPE=${WhichCert}/" $WORKINGDIR/.env
sed -i "s/LETSENCRYPT_EMAIL=user@example.com/LETSENCRYPT_EMAIL=${LetsEncryptEmail}/" $WORKINGDIR/.env
# Without Application
if [ "${WantToDeployDemos}" == "false" ]; then
sed -i "s/WITH_APP=true/WITH_APP=false/" $WORKINGDIR/docker-compose.yml
@ -265,7 +265,7 @@ Resources:
content: !Sub |
#!/bin/bash -x
WORKINGDIR=/opt/openvidu
# Get new amazon URL
OldPublicHostname=$(cat /usr/share/openvidu/old-host-name)
PublicHostname=$(curl http://169.254.169.254/latest/meta-data/public-hostname)
@ -302,14 +302,14 @@ Resources:
export HOME="/root"
# Replace .env variables
/usr/local/bin/feedGroupVars.sh || { echo "[Openvidu] Parameters incorrect/insufficient"; exit 1; }
/usr/local/bin/feedGroupVars.sh || { echo "[OpenVidu] Parameters incorrect/insufficient"; exit 1; }
# Launch on reboot
echo "@reboot /usr/local/bin/restartCE.sh" | crontab
# Download certs if "WichCert" mode
if [ "${WhichCert}" == "owncert" ]; then
/usr/local/bin/buildCerts.sh || { echo "[Openvidu] error with the certificate files"; exit 1; }
/usr/local/bin/buildCerts.sh || { echo "[OpenVidu] error with the certificate files"; exit 1; }
fi
# Start openvidu application
@ -323,15 +323,15 @@ Resources:
# Send info to openvidu
if [ "${WantToSendInfo}" == "true" ]; then
/usr/local/bin/ping.sh
/usr/local/bin/ping.sh || true
fi
rm /usr/local/bin/ping.sh
rm /usr/local/bin/ping.sh
# Wait for the app
/usr/local/bin/check_app_ready.sh
# Start up the cfn-hup daemon to listen for changes to the Web Server metadata
/usr/local/bin/cfn-hup -v || { echo "[Openvidu] Failed to start cfn-hup"; exit 1; }
/usr/local/bin/cfn-hup -v || { echo "[OpenVidu] Failed to start cfn-hup"; exit 1; }
# sending the finish call
/usr/local/bin/cfn-signal -e $? --stack ${AWS::StackId} --resource WaitCondition --region ${AWS::Region}
@ -366,30 +366,58 @@ Resources:
FromPort: 22
ToPort: 22
CidrIp: 0.0.0.0/0
- IpProtocol: tcp
FromPort: 22
ToPort: 22
CidrIpv6: ::/0
- IpProtocol: tcp
FromPort: 80
ToPort: 80
CidrIp: 0.0.0.0/0
- IpProtocol: tcp
FromPort: 80
ToPort: 80
CidrIpv6: ::/0
- IpProtocol: tcp
FromPort: 443
ToPort: 443
CidrIp: 0.0.0.0/0
- IpProtocol: tcp
FromPort: 443
ToPort: 443
CidrIpv6: ::/0
- IpProtocol: tcp
FromPort: 3478
ToPort: 3478
CidrIp: 0.0.0.0/0
- IpProtocol: tcp
FromPort: 3478
ToPort: 3478
CidrIpv6: ::/0
- IpProtocol: udp
FromPort: 3478
ToPort: 3478
CidrIp: 0.0.0.0/0
- IpProtocol: udp
FromPort: 3478
ToPort: 3478
CidrIpv6: ::/0
- IpProtocol: udp
FromPort: 40000
ToPort: 57000
CidrIp: 0.0.0.0/0
- IpProtocol: udp
FromPort: 40000
ToPort: 57000
CidrIpv6: ::/0
- IpProtocol: tcp
FromPort: 40000
ToPort: 57000
CidrIp: 0.0.0.0/0
- IpProtocol: tcp
FromPort: 40000
ToPort: 57000
CidrIp: 0.0.0.0/0
CidrIpv6: ::/0
Outputs:
OpenViduServerURL:

View File

@ -94,7 +94,7 @@ Resources:
# Openvidu recording
docker pull openvidu/openvidu-recording:OPENVIDU_RECORDING_DOCKER_TAG
# Openvidu CE images
# OpenVidu CE images
cd /opt/openvidu
docker-compose pull
mode: "000755"
@ -119,11 +119,11 @@ Resources:
cfn-init --region ${AWS::Region} --stack ${AWS::StackId} --resource OpenviduServerCE
/usr/local/bin/installDockerAndDockerCompose.sh || { echo "[Openvidu] error installing docker and compose"; exit 1; }
/usr/local/bin/installDockerAndDockerCompose.sh || { echo "[OpenVidu] error installing docker and compose"; exit 1; }
/usr/local/bin/installOpenviduCE.sh || { echo "[Openvidu] error installing Openvidu CE"; exit 1; }
/usr/local/bin/installOpenviduCE.sh || { echo "[OpenVidu] error installing OpenVidu CE"; exit 1; }
/usr/local/bin/getDockerImages.sh || { echo "[Openvidu] error getting docker images"; exit 1; }
/usr/local/bin/getDockerImages.sh || { echo "[OpenVidu] error getting docker images"; exit 1; }
# sending the finish call
/usr/local/bin/cfn-signal -e $? --stack ${AWS::StackId} --resource WaitCondition --region ${AWS::Region}

View File

@ -148,7 +148,7 @@ OPENVIDU_WEBHOOK=false
# List of events that will be sent by OpenVidu Webhook service
# Default value is all available events
OPENVIDU_WEBHOOK_EVENTS=[sessionCreated,sessionDestroyed,participantJoined,participantLeft,webrtcConnectionCreated,webrtcConnectionDestroyed,recordingStatusChanged,filterEventDispatched,mediaNodeStatusChanged]
OPENVIDU_WEBHOOK_EVENTS=[sessionCreated,sessionDestroyed,participantJoined,participantLeft,webrtcConnectionCreated,webrtcConnectionDestroyed,recordingStatusChanged,filterEventDispatched,mediaNodeStatusChanged,nodeCrashed]
# How often the garbage collector of non active sessions runs.
# This helps cleaning up sessions that have been initialized through
@ -174,7 +174,7 @@ OPENVIDU_CDR_PATH=/opt/openvidu/cdr
# --------------------------
# Docker hub kurento media server: https://hub.docker.com/r/kurento/kurento-media-server
# Uncomment the next line and define this variable with KMS image that you want use
# KMS_IMAGE=kurento/kurento-media-server:6.15.0
# KMS_IMAGE=kurento/kurento-media-server:6.16.0
# Kurento Media Server Level logs
# -------------------------------

View File

@ -9,11 +9,11 @@ services:
#
# Default Application
#
# Openvidu-Call Version: 2.16.0
# Openvidu-Call Version: 2.17.0
#
# --------------------------------------------------------------
app:
image: openvidu/openvidu-call:2.16.0
image: openvidu/openvidu-call:2.18.0-beta8
restart: on-failure
network_mode: host
environment:

View File

@ -11,7 +11,7 @@
#
# This file will be overridden when update OpenVidu Platform
#
# Openvidu Version: 2.16.0
# Openvidu Version: 2.17.0
#
# Installation Mode: On Premises
#
@ -22,7 +22,7 @@ version: '3.1'
services:
openvidu-server:
image: openvidu/openvidu-server:2.17.0-dev2
image: openvidu/openvidu-server:2.18.0-dev2
restart: on-failure
network_mode: host
entrypoint: ['/usr/local/bin/entrypoint.sh']
@ -45,7 +45,7 @@ services:
max-size: "${DOCKER_LOGS_MAX_SIZE:-100M}"
kms:
image: ${KMS_IMAGE:-kurento/kurento-media-server:6.15.0}
image: ${KMS_IMAGE:-kurento/kurento-media-server:6.16.0}
restart: always
network_mode: host
ulimits:
@ -65,7 +65,7 @@ services:
max-size: "${DOCKER_LOGS_MAX_SIZE:-100M}"
redis:
image: openvidu/openvidu-redis:2.0.0-dev2
image: openvidu/openvidu-redis:2.0.0
restart: always
network_mode: host
environment:
@ -75,23 +75,28 @@ services:
max-size: "${DOCKER_LOGS_MAX_SIZE:-100M}"
coturn:
image: openvidu/openvidu-coturn:3.0.0-dev2
image: openvidu/openvidu-coturn:4.0.0-dev2
restart: on-failure
network_mode: host
environment:
environment:
- REDIS_IP=127.0.0.1
- TURN_LISTEN_PORT=3478
- DB_NAME=0
- DB_PASSWORD=${OPENVIDU_SECRET}
- MIN_PORT=57001
- MAX_PORT=65535
- ENABLE_COTURN_LOGS=true
command:
- --log-file=stdout
- --listening-port=3478
- --fingerprint
- --lt-cred-mech
- --min-port=57001
- --max-port=65535
- --realm=openvidu
- --verbose
logging:
options:
max-size: "${DOCKER_LOGS_MAX_SIZE:-100M}"
nginx:
image: openvidu/openvidu-proxy:5.0.0-dev2
image: openvidu/openvidu-proxy:6.0.0-dev1
restart: on-failure
network_mode: host
volumes:

View File

@ -16,7 +16,7 @@ fatal_error() {
new_ov_installation() {
printf '\n'
printf '\n ======================================='
printf '\n Install Openvidu CE %s' "${OPENVIDU_VERSION}"
printf '\n Install OpenVidu CE %s' "${OPENVIDU_VERSION}"
printf '\n ======================================='
printf '\n'
@ -25,7 +25,7 @@ new_ov_installation() {
mkdir "${OPENVIDU_FOLDER}" || fatal_error "Error while creating the folder '${OPENVIDU_FOLDER}'"
# Download necessary files
printf '\n => Downloading Openvidu CE files:'
printf '\n => Downloading OpenVidu CE files:'
curl --silent ${DOWNLOAD_URL}/openvidu-server/deployments/ce/docker-compose/.env \
--output "${OPENVIDU_FOLDER}/.env" || fatal_error "Error when downloading the file '.env'"
@ -72,7 +72,7 @@ new_ov_installation() {
printf '\n $ ./openvidu start'
printf '\n'
printf '\n For more information, check:'
printf "\n https://docs.openvidu.io/en/${OPENVIDU_VERSION//v}/deployment/deploying-on-premises/"
printf '\n https://docs.openvidu.io/en/%s/deployment/deploying-on-premises/' "${OPENVIDU_VERSION//v}"
printf '\n'
printf '\n'
exit 0
@ -104,7 +104,7 @@ upgrade_ov() {
[ -z "${OPENVIDU_PREVIOUS_FOLDER}" ] && fatal_error "No previous Openvidu installation found"
# Uppgrade Openvidu
# Upgrade Openvidu
OPENVIDU_PREVIOUS_VERSION=$(grep 'Openvidu Version:' "${OPENVIDU_PREVIOUS_FOLDER}/docker-compose.yml" | awk '{ print $4 }')
[ -z "${OPENVIDU_PREVIOUS_VERSION}" ] && fatal_error "Can't find previous OpenVidu version"
@ -116,7 +116,7 @@ upgrade_ov() {
printf '\n'
printf '\n ======================================='
printf '\n Upgrade Openvidu CE %s to %s' "${OPENVIDU_PREVIOUS_VERSION}" "${OPENVIDU_VERSION}"
printf '\n Upgrade OpenVidu CE %s to %s' "${OPENVIDU_PREVIOUS_VERSION}" "${OPENVIDU_VERSION}"
printf '\n ======================================='
printf '\n'
@ -132,7 +132,7 @@ upgrade_ov() {
mkdir "${TMP_FOLDER}" || fatal_error "Error while creating the folder 'temporal'"
# Download necessary files
printf '\n => Downloading new Openvidu CE files:'
printf '\n => Downloading new OpenVidu CE files:'
curl --silent ${DOWNLOAD_URL}/openvidu-server/deployments/ce/docker-compose/docker-compose.yml \
--output "${TMP_FOLDER}/docker-compose.yml" || fatal_error "Error when downloading the file 'docker-compose.yml'"
@ -150,24 +150,24 @@ upgrade_ov() {
--output "${TMP_FOLDER}/openvidu" || fatal_error "Error when downloading the file 'openvidu'"
printf '\n - openvidu'
# Dowloading new images and stoped actual Openvidu
printf '\n => Dowloading new images...'
# Downloading new images and stopped actual Openvidu
printf '\n => Downloading new images...'
printf '\n'
sleep 1
printf "\n => Moving to 'tmp' folder..."
printf '\n'
cd "${TMP_FOLDER}" || fatal_error "Error when moving to 'tmp' folder"
docker-compose pull | true
docker-compose pull || true
printf '\n => Stoping Openvidu...'
printf '\n => Stopping Openvidu...'
printf '\n'
sleep 1
printf "\n => Moving to 'openvidu' folder..."
printf '\n'
cd "${OPENVIDU_PREVIOUS_FOLDER}" || fatal_error "Error when moving to 'openvidu' folder"
docker-compose down | true
docker-compose down || true
printf '\n'
printf '\n => Moving to working dir...'
@ -179,7 +179,7 @@ upgrade_ov() {
mv "${OPENVIDU_PREVIOUS_FOLDER}/docker-compose.yml" "${ROLL_BACK_FOLDER}" || fatal_error "Error while moving previous 'docker-compose.yml'"
printf '\n - docker-compose.yml'
if [ ! -z "${USE_OV_CALL}" ]; then
if [ -n "${USE_OV_CALL}" ]; then
mv "${OPENVIDU_PREVIOUS_FOLDER}/docker-compose.override.yml" "${ROLL_BACK_FOLDER}" || fatal_error "Error while moving previous 'docker-compose.override.yml'"
printf '\n - docker-compose.override.yml'
fi
@ -201,7 +201,7 @@ upgrade_ov() {
mv "${TMP_FOLDER}/docker-compose.yml" "${OPENVIDU_PREVIOUS_FOLDER}" || fatal_error "Error while updating 'docker-compose.yml'"
printf '\n - docker-compose.yml'
if [ ! -z "${USE_OV_CALL}" ]; then
if [ -n "${USE_OV_CALL}" ]; then
mv "${TMP_FOLDER}/docker-compose.override.yml" "${OPENVIDU_PREVIOUS_FOLDER}" || fatal_error "Error while updating 'docker-compose.override.yml'"
printf '\n - docker-compose.override.yml'
else
@ -224,7 +224,7 @@ upgrade_ov() {
# Define old mode: On Premise or Cloud Formation
OLD_MODE=$(grep -E "Installation Mode:.*$" "${ROLL_BACK_FOLDER}/docker-compose.yml" | awk '{ print $4,$5 }')
[ ! -z "${OLD_MODE}" ] && sed -i -r "s/Installation Mode:.+/Installation Mode: ${OLD_MODE}/" "${OPENVIDU_PREVIOUS_FOLDER}/docker-compose.yml"
[ -n "${OLD_MODE}" ] && sed -i -r "s/Installation Mode:.+/Installation Mode: ${OLD_MODE}/" "${OPENVIDU_PREVIOUS_FOLDER}/docker-compose.yml"
# Ready to use
printf '\n'
@ -249,8 +249,8 @@ upgrade_ov() {
printf "\n If you want to rollback, all the files from the previous installation have been copied to folder '.old-%s'" "${OPENVIDU_PREVIOUS_VERSION}"
printf '\n'
printf '\n For more information, check:'
printf "\n https://docs.openvidu.io/en/${OPENVIDU_VERSION//v}/deployment/deploying-on-premises/"
printf "\n https://docs.openvidu.io/en/${OPENVIDU_VERSION//v}/deployment/upgrading/"
printf '\n https://docs.openvidu.io/en/%s/deployment/deploying-on-premises/' "${OPENVIDU_VERSION//v}"
printf '\n https://docs.openvidu.io/en/%s/deployment/upgrading/' "${OPENVIDU_VERSION//v}"
printf '\n'
printf '\n'
}
@ -273,7 +273,7 @@ else
fi
# Check type of installation
if [[ ! -z "$1" && "$1" == "upgrade" ]]; then
if [[ -n "$1" && "$1" == "upgrade" ]]; then
upgrade_ov
else
new_ov_installation

View File

@ -4,7 +4,7 @@ upgrade_ov() {
UPGRADE_SCRIPT_URL="https://s3-eu-west-1.amazonaws.com/aws.openvidu.io/install_openvidu_OVVERSION.sh"
HTTP_STATUS=$(curl -s -o /dev/null -I -w "%{http_code}" ${UPGRADE_SCRIPT_URL//OVVERSION/$1})
printf " => Upgrading Openvidu CE to '%s' version" "$1"
printf " => Upgrading OpenVidu CE to '%s' version" "$1"
if [ "$HTTP_STATUS" == "200" ]; then
printf "\n => Downloading and upgrading new version"
@ -13,7 +13,7 @@ upgrade_ov() {
curl --silent ${UPGRADE_SCRIPT_URL//OVVERSION/$1} | bash -s upgrade
else
printf "\n =======¡ERROR!======="
printf "\n Openvidu CE Version '%s' not exist" "$1"
printf "\n OpenVidu CE Version '%s' not exist" "$1"
printf "\n"
exit 0
fi
@ -28,7 +28,7 @@ collect_basic_information() {
OV_VERSION=$(grep 'Openvidu Version:' "${OV_FOLDER}/docker-compose.yml" | awk '{ print $4 }')
CONTAINERS=$(docker ps | awk '{if(NR>1) print $NF}')
if [ ! -z "$(grep -E '^ image: openvidu/openvidu-call:.*$' "${OV_FOLDER}/docker-compose.override.yml" | tr -d '[:space:]')" ]; then
if [ -n "$(grep -E '^ image: openvidu/openvidu-call:.*$' "${OV_FOLDER}/docker-compose.override.yml" | tr -d '[:space:]')" ]; then
OV_CALL_VERSION=$(grep -E 'Openvidu-Call Version:' "${OV_FOLDER}/docker-compose.override.yml" | awk '{ print $4 }')
fi
[ -z "${OV_CALL_VERSION}" ] && OV_CALL_VERSION="No present"
@ -71,7 +71,7 @@ generate_report() {
REPORT_CREATION_DATE=$(date +"%d-%m-%Y")
REPORT_CREATION_TIME=$(date +"%H:%M:%S")
REPORT_NAME="openvidu-report-${REPORT_CREATION_DATE}-$(date +"%H-%M").txt"
REPORT_OUPUT="${OV_FOLDER}/${REPORT_NAME}"
REPORT_OUTPUT="${OV_FOLDER}/${REPORT_NAME}"
{
printf "\n ======================================="
@ -128,7 +128,7 @@ generate_report() {
printf '\n'
cat "${OV_FOLDER}/docker-compose.yml"
printf '\n'
printf '\n ==== docker-compose.override.yml ===='
printf '\n'
@ -150,10 +150,10 @@ generate_report() {
do
printf '\n'
printf "\n ---------------------------------------"
printf "\n %s" $CONTAINER
printf "\n %s" "$CONTAINER"
printf "\n ---------------------------------------"
printf '\n'
docker logs $CONTAINER
docker logs "$CONTAINER"
printf "\n ---------------------------------------"
printf '\n'
printf '\n'
@ -161,7 +161,7 @@ generate_report() {
printf '\n'
printf "\n ---------------------------------------"
printf "\n KMS"
printf "\n KMS"
printf "\n ---------------------------------------"
printf '\n'
kurento_logs
@ -177,19 +177,19 @@ generate_report() {
do
printf '\n'
printf "\n ======================================="
printf "\n %s" $CONTAINER
printf "\n %s" "$CONTAINER"
printf "\n ---------------------------------------"
printf '\n'
docker exec $CONTAINER env
docker exec "$CONTAINER" env
printf "\n ---------------------------------------"
printf '\n'
printf '\n'
done
} >> "${REPORT_OUPUT}" 2>&1
} >> "${REPORT_OUTPUT}" 2>&1
printf "\n Generation of the report completed with success"
printf "\n You can get your report at path '%s'" "${REPORT_OUPUT}"
printf "\n You can get your report at path '%s'" "${REPORT_OUTPUT}"
printf "\n"
}
@ -198,10 +198,10 @@ usage() {
printf "\n\nAvailable Commands:"
printf "\n\tstart\t\t\tStart all services"
printf "\n\tstop\t\t\tStop all services"
printf "\n\trestart\t\t\tRestart all stoped and running services"
printf "\n\trestart\t\t\tRestart all stopped and running services"
printf "\n\tlogs [-f]\t\tShow openvidu logs."
printf "\n\tkms-logs [-f]\t\tShow kms logs"
printf "\n\tupgrade\t\t\tUpgrade to the lastest Openvidu version"
printf "\n\tupgrade\t\t\tUpgrade to the latest Openvidu version"
printf "\n\tupgrade [version]\tUpgrade to the specific Openvidu version"
printf "\n\tversion\t\t\tShow version of Openvidu Server"
printf "\n\treport\t\t\tGenerate a report with the current status of Openvidu"
@ -212,7 +212,7 @@ usage() {
kurento_logs() {
if [[ "$1" == "-f" ]]; then
tail -f /opt/openvidu/kurento-logs/*.log
else
else
cat /opt/openvidu/kurento-logs/*.log
fi
}
@ -245,7 +245,7 @@ case $1 in
;;
kms-logs)
kurento_logs $2
kurento_logs "$2"
;;
upgrade)
@ -255,7 +255,7 @@ case $1 in
UPGRADE_VERSION="$2"
fi
read -r -p " You're about to update Openvidu CE to '${UPGRADE_VERSION}' version. Are you sure? [y/N]: " response
read -r -p " You're about to update OpenVidu CE to '${UPGRADE_VERSION}' version. Are you sure? [y/N]: " response
case "$response" in
[yY][eE][sS]|[yY])
upgrade_ov "${UPGRADE_VERSION}"
@ -273,7 +273,7 @@ case $1 in
report)
read -r -p " You are about to generate a report on the current status of Openvidu, this may take some time. Do you want to continue? [y/N]: " response
case "$response" in
[yY][eE][sS]|[yY])
[yY][eE][sS]|[yY])
generate_report
;;
*)
@ -285,4 +285,4 @@ case $1 in
*)
usage
;;
esac
esac

View File

@ -120,11 +120,11 @@ Resources:
cfn-init --region ${AWS::Region} --stack ${AWS::StackId} --resource KurentoMediaServer
/usr/local/bin/installDockerAndDockerCompose.sh || { echo "[Openvidu] error installing software"; exit 1; }
/usr/local/bin/installDockerAndDockerCompose.sh || { echo "[OpenVidu] error installing software"; exit 1; }
/usr/local/bin/installMediaNode.sh || { echo "[Openvidu] error installing Media Node"; exit 1; }
/usr/local/bin/installMediaNode.sh || { echo "[OpenVidu] error installing Media Node"; exit 1; }
/usr/local/bin/runMediaNode.sh || { echo "[Openvidu] error running Media Node"; exit 1; }
/usr/local/bin/runMediaNode.sh || { echo "[OpenVidu] error running Media Node"; exit 1; }
# sending the finish call
/usr/local/bin/cfn-signal -e $? --stack ${AWS::StackId} --resource WaitCondition --region ${AWS::Region}

View File

@ -91,7 +91,7 @@ Resources:
# Openvidu recording
docker pull openvidu/openvidu-recording:OPENVIDU_RECORDING_DOCKER_TAG
# Openvidu PRO images
# OpenVidu Pro images
cd /opt/openvidu
docker-compose pull
mode: "000755"
@ -122,11 +122,11 @@ Resources:
cfn-init --region ${AWS::Region} --stack ${AWS::StackId} --resource OpenviduServerPro
/usr/local/bin/installDockerAndDockerCompose.sh || { echo "[Openvidu] error installing docker and compose"; exit 1; }
/usr/local/bin/installDockerAndDockerCompose.sh || { echo "[OpenVidu] error installing docker and compose"; exit 1; }
/usr/local/bin/installOpenviduServerPRO.sh || { echo "[Openvidu] error installing Openvidu Server PRO"; exit 1; }
/usr/local/bin/installOpenviduServerPRO.sh || { echo "[OpenVidu] error installing Openvidu Server PRO"; exit 1; }
/usr/local/bin/getDockerImages.sh || { echo "[Openvidu] error getting docker images"; exit 1; }
/usr/local/bin/getDockerImages.sh || { echo "[OpenVidu] error getting docker images"; exit 1; }
# sending the finish call
/usr/local/bin/cfn-signal -e $? --stack ${AWS::StackId} --resource WaitCondition --region ${AWS::Region}

View File

@ -1,6 +1,6 @@
---
AWSTemplateFormatVersion: 2010-09-09
Description: Openvidu Pro CloudFormation template
Description: OpenVidu Pro CloudFormation template
Parameters:
@ -41,11 +41,11 @@ Parameters:
LetsEncryptEmail:
Description: "If certificate type is 'letsencrypt', this email will be used for Let's Encrypt notifications"
Type: String
Recording:
Description: |
If 'disabled', recordings will not be active.
If 'local' recordings will be saved in EC2 instance locally.
If 'local' recordings will be saved in EC2 instance locally.
If 's3', recordings will be stored in a S3 bucket"
Type: String
AllowedValues:
@ -328,15 +328,15 @@ Conditions:
CreateS3Bucket: !And
- !Equals [!Ref Recording, 's3' ]
- !Equals [!Ref S3RecordingsBucketName, '']
Rules:
RecordingValidation:
RuleCondition:
RuleCondition:
Fn::Or: [ !Equals [!Ref Recording, 'disabled' ], !Equals [!Ref Recording, 'local' ] ]
Assertions:
- AssertDescription: If recording Storage is 'disabled' or 'local', you don't need to specify a S3 bucket.
Assert:
Assert:
Fn::Equals: [ !Ref S3RecordingsBucketName, "" ]
Resources:
@ -380,7 +380,7 @@ Resources:
- 's3:DeleteObject'
- 's3:GetObject'
- 's3:PutObject'
Resource:
Resource:
- Fn::If:
# Get bucket name depending if the user defines a bucket name or not
- CreateS3Bucket
@ -392,10 +392,10 @@ Resources:
# Only apply this policy if S3 is configured
- RecordingStorageIsS3
- Effect: Allow
Action:
Action:
- 's3:ListBucket'
- 's3:GetBucketLocation'
Resource:
Resource:
- Fn::If:
# Get bucket name depending if the user defines a bucket name or not
- CreateS3Bucket
@ -429,7 +429,7 @@ Resources:
### Unique bucket name using Stack ID
BucketName: !Join ["-" , [ 'openvidu-recordings', !Select [0, !Split ["-", !Select [2, !Split [/, !Ref AWS::StackId ]]]]]]
AccessControl: Private
PublicAccessBlockConfiguration:
PublicAccessBlockConfiguration:
BlockPublicAcls: true
BlockPublicPolicy: true
IgnorePublicAcls : true
@ -483,13 +483,13 @@ Resources:
echo $PublicHostname > /usr/share/openvidu/old-host-name
fi
# Openvidu Pro mode
# OpenVidu Pro mode
sed -i "s/OPENVIDU_PRO_CLUSTER_MODE=manual/OPENVIDU_PRO_CLUSTER_MODE=auto/" $WORKINGDIR/.env
# Openvidu Pro Media Nodes
# OpenVidu Pro Media Nodes
sed -i "s/#OPENVIDU_PRO_CLUSTER_MEDIA_NODES=/OPENVIDU_PRO_CLUSTER_MEDIA_NODES=${MediaNodesStartNumber}/" $WORKINGDIR/.env
# Openvidu Pro enviroment
# OpenVidu Pro enviroment
sed -i "s/OPENVIDU_PRO_CLUSTER_ENVIRONMENT=on_premise/OPENVIDU_PRO_CLUSTER_ENVIRONMENT=aws/" $WORKINGDIR/.env
# Replace certificated type
@ -507,15 +507,18 @@ Resources:
sed -i "s/ELASTICSEARCH_PASSWORD=/ELASTICSEARCH_PASSWORD=${ElasticsearchPassword}/" $WORKINGDIR/.env
# Replace vars AWS
INSTANCE_ID=$(curl http://169.254.169.254/latest/meta-data/instance-id)
sed -i "s/#AWS_DEFAULT_REGION=/AWS_DEFAULT_REGION=${AWS::Region}/" $WORKINGDIR/.env
sed -i "s/#AWS_IMAGE_ID=/AWS_IMAGE_ID=${kmsAmi}/" $WORKINGDIR/.env
sed -i "s/#AWS_INSTANCE_TYPE=/AWS_INSTANCE_TYPE=${AwsInstanceTypeKMS}/" $WORKINGDIR/.env
sed -i "s/#AWS_INSTANCE_ID=/AWS_INSTANCE_ID=$INSTANCE_ID/" $WORKINGDIR/.env
sed -i "s/#AWS_KEY_NAME=/AWS_KEY_NAME=${KeyName}/" $WORKINGDIR/.env
sed -i "s/#AWS_SUBNET_ID=/AWS_SUBNET_ID=${OpenViduSubnet}/" $WORKINGDIR/.env
sed -i "s/#AWS_STACK_ID=/AWS_STACK_ID=$(echo ${AWS::StackId} | sed 's#/#\\/#g')/" $WORKINGDIR/.env
sed -i "s/#AWS_STACK_NAME=/AWS_STACK_NAME=${AWS::StackName}/" $WORKINGDIR/.env
sed -i "s/#AWS_CLI_DOCKER_TAG=/AWS_CLI_DOCKER_TAG=AWS_DOCKER_TAG/" $WORKINGDIR/.env
sed -i "s/#AWS_VOLUME_SIZE=/AWS_VOLUME_SIZE=50/" $WORKINGDIR/.env
sed -i "s/#OPENVIDU_PRO_AWS_REGION=/OPENVIDU_PRO_AWS_REGION=${AWS::Region}/" $WORKINGDIR/.env
# Get security group id of kms and use it as env variable
SECGRPIDKMS=$(/usr/local/bin/getSecurityGroupKms.sh)
@ -647,17 +650,17 @@ Resources:
export HOME="/root"
# Replace .env variables
/usr/local/bin/feedGroupVars.sh || { echo "[Openvidu] Parameters incorrect/insufficient"; exit 1; }
/usr/local/bin/feedGroupVars.sh || { echo "[OpenVidu] Parameters incorrect/insufficient"; exit 1; }
# Create security groups
/usr/local/bin/create_security_group_rules.sh || { echo "[Openvidu] Error creating security groups"; exit 1; }
/usr/local/bin/create_security_group_rules.sh || { echo "[OpenVidu] Error creating security groups"; exit 1; }
# Launch on reboot
echo "@reboot /usr/local/bin/restartPRO.sh" | crontab
# Download certs if "WichCert" mode
if [ "${WhichCert}" == "owncert" ]; then
/usr/local/bin/buildCerts.sh || { echo "[Openvidu] error with the certificate files"; exit 1; }
/usr/local/bin/buildCerts.sh || { echo "[OpenVidu] error with the certificate files"; exit 1; }
fi
# Start openvidu application
@ -690,23 +693,43 @@ Resources:
FromPort: 22
ToPort: 22
CidrIp: 0.0.0.0/0
- IpProtocol: tcp
FromPort: 22
ToPort: 22
CidrIpv6: ::/0
- IpProtocol: udp
FromPort: 40000
ToPort: 65535
CidrIp: 0.0.0.0/0
- IpProtocol: udp
FromPort: 40000
ToPort: 65535
CidrIpv6: ::/0
- IpProtocol: tcp
FromPort: 40000
ToPort: 65535
CidrIp: 0.0.0.0/0
- IpProtocol: tcp
FromPort: 40000
ToPort: 65535
CidrIp: 0.0.0.0/0
CidrIpv6: ::/0
SecurityGroupEgress:
- IpProtocol: tcp
FromPort: 1
ToPort: 65535
CidrIp: 0.0.0.0/0
- IpProtocol: tcp
FromPort: 1
ToPort: 65535
CidrIpv6: ::/0
- IpProtocol: udp
FromPort: 1
ToPort: 65535
CidrIp: 0.0.0.0/0
- IpProtocol: udp
FromPort: 1
ToPort: 65535
CidrIpv6: ::/0
OpenViduSecurityGroup:
Type: 'AWS::EC2::SecurityGroup'
@ -719,39 +742,75 @@ Resources:
FromPort: 22
ToPort: 22
CidrIp: 0.0.0.0/0
- IpProtocol: tcp
FromPort: 22
ToPort: 22
CidrIpv6: ::/0
- IpProtocol: tcp
FromPort: 80
ToPort: 80
CidrIp: 0.0.0.0/0
- IpProtocol: tcp
FromPort: 80
ToPort: 80
CidrIpv6: ::/0
- IpProtocol: tcp
FromPort: 443
ToPort: 443
CidrIp: 0.0.0.0/0
- IpProtocol: tcp
FromPort: 443
ToPort: 443
CidrIpv6: ::/0
- IpProtocol: tcp
FromPort: 3478
ToPort: 3478
CidrIp: 0.0.0.0/0
- IpProtocol: tcp
FromPort: 3478
ToPort: 3478
CidrIpv6: ::/0
- IpProtocol: udp
FromPort: 3478
ToPort: 3478
CidrIp: 0.0.0.0/0
- IpProtocol: udp
FromPort: 3478
ToPort: 3478
CidrIpv6: ::/0
- IpProtocol: udp
FromPort: 40000
ToPort: 65535
CidrIp: 0.0.0.0/0
- IpProtocol: udp
FromPort: 40000
ToPort: 65535
CidrIpv6: ::/0
- IpProtocol: tcp
FromPort: 40000
ToPort: 65535
CidrIp: 0.0.0.0/0
- IpProtocol: tcp
FromPort: 40000
ToPort: 65535
CidrIp: 0.0.0.0/0
CidrIpv6: ::/0
SecurityGroupEgress:
- IpProtocol: tcp
FromPort: 1
ToPort: 65535
CidrIp: 0.0.0.0/0
- IpProtocol: tcp
FromPort: 1
ToPort: 65535
CidrIpv6: ::/0
- IpProtocol: udp
FromPort: 1
ToPort: 65535
CidrIp: 0.0.0.0/0
- IpProtocol: udp
FromPort: 1
ToPort: 65535
CidrIpv6: ::/0
WaitCondition:
Type: AWS::CloudFormation::WaitCondition

View File

@ -9,7 +9,7 @@
# For example: 198.51.100.1, or openvidu.example.com
DOMAIN_OR_PUBLIC_IP=
# OpenVidu PRO License
# OpenVidu Pro License
OPENVIDU_PRO_LICENSE=
# OpenVidu SECRET used for apps to connect to OpenVidu server and users to access to OpenVidu Dashboard
@ -42,9 +42,9 @@ LETSENCRYPT_EMAIL=user@example.com
# SDKs, REST clients and browsers will have to connect to this port
# HTTPS_PORT=443
# Old paths are considered now deprecated, but still supported by default.
# OpenVidu Server will log a WARN message every time a deprecated path is called, indicating
# the new path that should be used instead. You can set property SUPPORT_DEPRECATED_API=false
# Old paths are considered now deprecated, but still supported by default.
# OpenVidu Server will log a WARN message every time a deprecated path is called, indicating
# the new path that should be used instead. You can set property SUPPORT_DEPRECATED_API=false
# to stop allowing the use of old paths.
# Default value is true
# SUPPORT_DEPRECATED_API=true
@ -53,7 +53,7 @@ LETSENCRYPT_EMAIL=user@example.com
# Default value is false
# REDIRECT_WWW=false
# How many workers to configure in nginx proxy.
# How many workers to configure in nginx proxy.
# The more workers, the more requests will be handled
# Default value is 10240
# WORKER_CONNECTIONS=10240
@ -88,7 +88,7 @@ OPENVIDU_PRO_CLUSTER_MODE=manual
OPENVIDU_PRO_CLUSTER_ENVIRONMENT=on_premise
# Unique identifier of your cluster. Each OpenVidu Server Pro instance corresponds to one cluster.
# You can launch as many clusters as you want with your license key.
# You can launch as many clusters as you want with your license key.
# Cluster ID will always be stored to disk so restarting OpenVidu Server Pro will keep the same previous cluster ID
# if this configuration parameter is not given a distinct value.
# OPENVIDU_PRO_CLUSTER_ID=
@ -110,7 +110,7 @@ OPENVIDU_PRO_CLUSTER_ENVIRONMENT=on_premise
# Whether to enable or disable autoscaling. With autoscaling the number of Media Nodes will
# be automatically adjusted according to existing load
# Values: true | false
# OPENVIDU_PRO_CLUSTER_AUTOSCALING=false
OPENVIDU_PRO_CLUSTER_AUTOSCALING=false
# How often the autoscaling algorithm runs, in seconds
# Type number >= 0
@ -185,10 +185,10 @@ OPENVIDU_PRO_CLUSTER_LOAD_STRATEGY=streams
# and write into the s3 bucket, you don't need this parameter
# OPENVIDU_PRO_AWS_SECRET_KEY=
# AWS region in which the S3 bucket is located (e.g. eu-west-1). If not provided,
# the region will try to be discovered automatically, although this is not always possible.
# AWS region in which the S3 bucket is located (e.g. eu-west-1). If not provided,
# the region will try to be discovered automatically, although this is not always possible.
# This property is only taken into account if OPENVIDU_PRO_RECORDING_STORAGE=s3
# OPENVIDU_PRO_AWS_REGION=
#OPENVIDU_PRO_AWS_REGION=
# Whether to enable recording module or not
OPENVIDU_RECORDING=false
@ -250,6 +250,10 @@ OPENVIDU_STREAMS_VIDEO_MIN_SEND_BANDWIDTH=300
# Default value is false
# OPENVIDU_STREAMS_ALLOW_TRANSCODING=false
# Send openvidu-browser logs of clients to Elasticsearch
# Default value is 'disabled'
# OPENVIDU_BROWSER_LOGS=disabled
# true to enable OpenVidu Webhook service. false' otherwise
# Values: true | false
OPENVIDU_WEBHOOK=false
@ -263,7 +267,7 @@ OPENVIDU_WEBHOOK=false
# List of events that will be sent by OpenVidu Webhook service
# Default value is all available events
OPENVIDU_WEBHOOK_EVENTS=[sessionCreated,sessionDestroyed,participantJoined,participantLeft,webrtcConnectionCreated,webrtcConnectionDestroyed,recordingStatusChanged,filterEventDispatched,mediaNodeStatusChanged]
OPENVIDU_WEBHOOK_EVENTS=[sessionCreated,sessionDestroyed,participantJoined,participantLeft,webrtcConnectionCreated,webrtcConnectionDestroyed,recordingStatusChanged,filterEventDispatched,mediaNodeStatusChanged,nodeCrashed]
# How often the garbage collector of non active sessions runs.
# This helps cleaning up sessions that have been initialized through
@ -324,7 +328,7 @@ ELASTICSEARCH_PASSWORD=
# Uncomment the next line and define this variable with KMS image that you want use
# By default, KMS_IMAGE is defined in media nodes and it does not need to be specified unless
# you want to use a specific version of KMS
# KMS_IMAGE=kurento/kurento-media-server:6.15.0
# KMS_IMAGE=kurento/kurento-media-server:6.16.0
# Uncomment the next line and define this variable to change
# the verbosity level of the logs of KMS
@ -337,6 +341,7 @@ ELASTICSEARCH_PASSWORD=
#AWS_DEFAULT_REGION=
#AWS_IMAGE_ID=
#AWS_INSTANCE_TYPE=
#AWS_INSTANCE_ID=
#AWS_KEY_NAME=
#AWS_SUBNET_ID=
#AWS_SECURITY_GROUP=
@ -350,3 +355,4 @@ ELASTICSEARCH_PASSWORD=
RM_REDIS_IP=
RM_REDIS_PORT=
RM_SQS_QUEUE=
RM_CLOUDFORMATION_ARN=

View File

@ -2,6 +2,11 @@ filebeat.inputs:
- type: container
paths:
- '/var/lib/docker/containers/*/*.log'
fields:
cluster_id: ${OPENVIDU_PRO_CLUSTER_ID:${DOMAIN_OR_PUBLIC_IP:undefined}}
node_id: master_${AWS_INSTANCE_ID:${OPENVIDU_PRO_CLUSTER_ID:${DOMAIN_OR_PUBLIC_IP:undefined}}}
node_role: masternode
fields_under_root: true
processors:
- add_docker_metadata:
@ -25,9 +30,8 @@ processors:
container.image.name: docker.elastic.co/beats/filebeat-oss
- contains:
container.image.name: docker.elastic.co/beats/metricbeat-oss
- add_fields:
fields:
cluster-id: ${OPENVIDU_PRO_CLUSTER_ID:undefined}
- contains:
container.image.name: openvidu/openvidu-server-pro
output:
elasticsearch:
@ -41,7 +45,10 @@ output:
when.or:
- contains:
container.image.name: openvidu/openvidu-proxy
- index: "filebeat-openvidu-recording-%{+yyyy.MM.dd}"
when.or:
- contains:
container.image.name: openvidu/openvidu-recording
logging.json: true
logging.metrics.enabled: false

View File

@ -1,10 +1,40 @@
metricbeat.modules:
- module: nginx
metricsets: ["stubstatus"]
enabled: true
period: 10s
hosts: ["http://127.0.0.1"]
server_status_path: "nginx_status"
- module: nginx
metricsets: ["stubstatus"]
enabled: true
period: ${OPENVIDU_PRO_STATS_MONITORING_INTERVAL}s
hosts: ["http://127.0.0.1"]
server_status_path: "nginx_status"
- module: system
metricsets:
- cpu
- diskio
- memory
- network
- filesystem
- fsstat
- process_summary
- uptime
filesystem.ignore_types: [nfs, smbfs, autofs, devtmpfs, devpts, hugetlbfs, tmpfs, sysfs, securityfs, cgroup2, cgroup, pstore, debugfs, configfs, fusectl, proc, fuse.lxcfs, squashfs]
processes: ['.*']
processors:
- drop_event:
when:
or:
- regexp:
system.network.name: '^(veth|lo|docker|br-)($|)'
- regexp:
system.filesystem.mount_point: '^/(sys|cgroup|proc|dev|etc|host)($|/)'
- regexp:
system.filesystem.mount_point: '^/hostfs/(sys|cgroup|proc|dev|etc|host)($|/)'
enabled: true
period: ${OPENVIDU_PRO_STATS_MONITORING_INTERVAL}s
cpu.metrics: [normalized_percentages]
fields:
cluster_id: ${OPENVIDU_PRO_CLUSTER_ID:${DOMAIN_OR_PUBLIC_IP:undefined}}
node_id: master_${AWS_INSTANCE_ID:${OPENVIDU_PRO_CLUSTER_ID:${DOMAIN_OR_PUBLIC_IP:undefined}}}
node_role: masternode
output:
elasticsearch:
hosts: ["${OPENVIDU_PRO_ELASTICSEARCH_HOST}"]
setup.ilm.enabled: false

View File

@ -35,6 +35,14 @@ exit_on_error () {
esac
}
# Check custom parameters
if [[ -n "${CUSTOM_INSTANCE_TYPE}" ]]; then
AWS_INSTANCE_TYPE="${CUSTOM_INSTANCE_TYPE}"
fi
if [[ -n "${CUSTOM_VOLUME_SIZE}" ]]; then
AWS_VOLUME_SIZE="${CUSTOM_VOLUME_SIZE}"
fi
docker run --rm amazon/aws-cli:${AWS_CLI_DOCKER_TAG} ec2 run-instances \
--image-id ${AWS_IMAGE_ID} --count 1 \
--instance-type ${AWS_INSTANCE_TYPE} \
@ -51,18 +59,6 @@ docker run --rm amazon/aws-cli:${AWS_CLI_DOCKER_TAG} ec2 wait instance-running -
KMS_IP=$(cat ${OUTPUT} | jq --raw-output ' .Instances[] | .NetworkInterfaces[0] | .PrivateIpAddress')
KMS_ID=$(cat ${OUTPUT} | jq --raw-output ' .Instances[] | .InstanceId')
# Wait media-node controller
attempt_counter=0
max_attempts=10
until $(curl --output /dev/null --silent --head --fail -u OPENVIDUAPP:${OPENVIDU_SECRET} http://${KMS_IP}:3000/media-node/status); do
if [ ${attempt_counter} -eq ${max_attempts} ];then
exit 1
fi
attempt_counter=$(($attempt_counter+1))
sleep 5
done
jq -n \
--arg id "${KMS_ID}" \
--arg ip "${KMS_IP}" \

View File

@ -9,11 +9,11 @@ services:
#
# Default Application
#
# Openvidu-Call Version: 2.16.0
# Openvidu-Call Version: 2.17.0
#
# --------------------------------------------------------------
app:
image: openvidu/openvidu-call:2.16.0
image: openvidu/openvidu-call:2.18.0-beta8
restart: on-failure
network_mode: host
environment:

View File

@ -11,7 +11,7 @@
#
# This file will be overridden when update OpenVidu Platform
#
# Openvidu Version: 2.16.0
# Openvidu Version: 2.17.0
#
# Installation Mode: On Premises
#
@ -22,7 +22,7 @@ version: '3.1'
services:
openvidu-server:
image: openvidu/openvidu-server-pro:2.17.0-dev5
image: openvidu/openvidu-server-pro:2.18.0-beta17
restart: on-failure
network_mode: host
entrypoint: ['/usr/local/bin/entrypoint.sh']
@ -41,7 +41,7 @@ services:
- KMS_URIS=[]
- OPENVIDU_WEBHOOK=false
- OPENVIDU_WEBHOOK_ENDPOINT=http://127.0.0.1:7777/webhook
- OPENVIDU_PRO_REPLICATION_MANAGER_WEBHOOK=http://127.0.0.1:4443/openvidu/replication-manager-webhook?OPENVIDU_SECRET=${OPENVIDU_SECRET}
- MULTI_MASTER_REPLICATION_MANAGER_WEBHOOK=http://127.0.0.1:4443/openvidu/replication-manager-webhook?OPENVIDU_SECRET=${OPENVIDU_SECRET}
- COTURN_REDIS_IP=127.0.0.1
- COTURN_REDIS_PASSWORD=${OPENVIDU_SECRET}
- COTURN_IP=${COTURN_IP:-auto-ipv4}
@ -55,8 +55,8 @@ services:
max-size: "${DOCKER_LOGS_MAX_SIZE:-100M}"
replication-manager:
image: openvidu/replication-manager:1.0.0-dev5
restart: on-failure
image: openvidu/replication-manager:1.0.0-dev6
restart: always
network_mode: host
environment:
- SERVER_PORT=4443
@ -66,15 +66,17 @@ services:
- REDIS_HOST=${RM_REDIS_IP}
- REDIS_PORT=${RM_REDIS_PORT}
- REDIS_PASS=${OPENVIDU_SECRET}
- RM_CLOUDFORMATION_ARN=${RM_CLOUDFORMATION_ARN}
- REDIS_TIMEOUT=5
- REDIS_DB=replicationmanager
- MEDIANODE_AS_NOTIFICATION_QUEUE=${RM_SQS_QUEUE}
- AWS_INSTANCE_ID=${AWS_INSTANCE_ID}
logging:
options:
max-size: "${DOCKER_LOGS_MAX_SIZE:-100M}"
redis:
image: openvidu/openvidu-redis:2.0.0-dev2
image: openvidu/openvidu-redis:2.0.0
restart: always
network_mode: host
environment:
@ -84,18 +86,23 @@ services:
max-size: "${DOCKER_LOGS_MAX_SIZE:-100M}"
coturn:
image: openvidu/openvidu-coturn:3.0.0-dev2
image: openvidu/openvidu-coturn:4.0.0-dev2
restart: on-failure
network_mode: host
environment:
environment:
- REDIS_IP=127.0.0.1
- TURN_LISTEN_PORT=3478
- DB_NAME=0
- DB_PASSWORD=${OPENVIDU_SECRET}
- MIN_PORT=40000
- MAX_PORT=65535
- TURN_PUBLIC_IP=${TURN_PUBLIC_IP:-auto-ipv4}
- ENABLE_COTURN_LOGS=true
command:
- --log-file=stdout
- --external-ip=$$(detect-external-ip)
- --listening-port=3478
- --fingerprint
- --lt-cred-mech
- --min-port=40000
- --max-port=65535
- --realm=openvidu
- --verbose
logging:
options:
max-size: "${DOCKER_LOGS_MAX_SIZE:-100M}"
@ -109,10 +116,15 @@ services:
- .env
environment:
- OPENVIDU_PRO_ELASTICSEARCH_HOST=${OPENVIDU_PRO_ELASTICSEARCH_HOST:-http://127.0.0.1:9200}
- OPENVIDU_PRO_STATS_MONITORING_INTERVAL=${OPENVIDU_PRO_STATS_MONITORING_INTERVAL:-10}
volumes:
- /var/run/docker.sock:/var/run/docker.sock
- ./beats/metricbeat.yml:/usr/share/metricbeat/metricbeat.yml:ro
- /proc:/hostfs/proc:ro
- /sys/fs/cgroup:/hostfs/sys/fs/cgroup:ro
- /:/hostfs:ro
command: >
/bin/bash -c "metricbeat -e -strict.perms=false
/bin/bash -c "metricbeat -e -strict.perms=false -e -system.hostfs=/hostfs
`if [ ! -z $ELASTICSEARCH_USERNAME ]; then echo '-E output.elasticsearch.username=$ELASTICSEARCH_USERNAME'; fi`
`if [ ! -z $ELASTICSEARCH_PASSWORD ]; then echo '-E output.elasticsearch.password=$ELASTICSEARCH_PASSWORD'; fi`"
logging:

View File

@ -19,7 +19,7 @@ fatal_error() {
new_ov_installation() {
printf '\n'
printf '\n ======================================='
printf '\n Install Openvidu PRO %s' "${OPENVIDU_VERSION}"
printf '\n Install OpenVidu Pro %s' "${OPENVIDU_VERSION}"
printf '\n ======================================='
printf '\n'
@ -43,7 +43,7 @@ new_ov_installation() {
chown 1000:1000 "${ELASTICSEARCH_FOLDER}" || fatal_error "Error while changing permission to 'elasticsearch' folder"
# Download necessary files
printf '\n => Downloading Openvidu PRO files:'
printf '\n => Downloading OpenVidu Pro files:'
curl --silent ${DOWNLOAD_URL}/openvidu-server/deployments/pro/docker-compose/aws-asg-openvidu-server-pro/cluster/aws/openvidu_autodiscover.sh \
--output "${AWS_SCRIPTS_FOLDER}/openvidu_autodiscover.sh" || fatal_error "Error when downloading the file 'openvidu_autodiscover.sh'"
@ -108,7 +108,7 @@ new_ov_installation() {
printf '\n'
printf '\n'
printf '\n ======================================='
printf '\n Openvidu PRO successfully installed.'
printf '\n OpenVidu Pro successfully installed.'
printf '\n ======================================='
printf '\n'
printf '\n 1. Go to openvidu folder:'
@ -124,12 +124,23 @@ new_ov_installation() {
printf "\n CAUTION: The folder 'openvidu/elasticsearch' use user and group 1000 permissions. "
printf "\n This folder is necessary for store elasticsearch data."
printf "\n For more information, check:"
printf "\n https://docs.openvidu.io/en/${OPENVIDU_VERSION//v}/openvidu-pro/deployment/on-premises/#deployment-instructions"
printf '\n https://docs.openvidu.io/en/%s/openvidu-pro/deployment/on-premises/#deployment-instructions' "${OPENVIDU_VERSION//v}"
printf '\n'
printf '\n'
exit 0
}
get_previous_env_variable() {
local ENV_VARIABLE_NAME=$1
echo "$(grep -E "${ENV_VARIABLE_NAME}=.*$" "${OPENVIDU_PREVIOUS_FOLDER}/.env" | cut -d'=' -f2)"
}
replace_variable_in_new_env_file() {
local ENV_VARIABLE_NAME=$1
local ENV_VARIABLE_VALUE=$2
[[ -n "${ENV_VARIABLE_VALUE}" ]] && sed -i "s|#${ENV_VARIABLE_NAME}=|${ENV_VARIABLE_NAME}=${ENV_VARIABLE_VALUE}|" "${OPENVIDU_PREVIOUS_FOLDER}/.env-${OPENVIDU_VERSION}"
}
upgrade_ov() {
# Search local Openvidu installation
printf '\n'
@ -156,7 +167,7 @@ upgrade_ov() {
[ -z "${OPENVIDU_PREVIOUS_FOLDER}" ] && fatal_error "No previous Openvidu installation found"
# Uppgrade Openvidu
# Upgrade Openvidu
OPENVIDU_PREVIOUS_VERSION=$(grep 'Openvidu Version:' "${OPENVIDU_PREVIOUS_FOLDER}/docker-compose.yml" | awk '{ print $4 }')
[ -z "${OPENVIDU_PREVIOUS_VERSION}" ] && fatal_error "Can't find previous OpenVidu version"
@ -168,7 +179,7 @@ upgrade_ov() {
printf '\n'
printf '\n ======================================='
printf '\n Upgrade Openvidu PRO %s to %s' "${OPENVIDU_PREVIOUS_VERSION}" "${OPENVIDU_VERSION}"
printf '\n Upgrade OpenVidu Pro %s to %s' "${OPENVIDU_PREVIOUS_VERSION}" "${OPENVIDU_VERSION}"
printf '\n ======================================='
printf '\n'
@ -184,7 +195,7 @@ upgrade_ov() {
mkdir "${TMP_FOLDER}" || fatal_error "Error while creating the folder 'temporal'"
# Download necessary files
printf '\n => Downloading new Openvidu PRO files:'
printf '\n => Downloading new OpenVidu Pro files:'
curl --silent ${DOWNLOAD_URL}/openvidu-server/deployments/pro/docker-compose/aws-asg-openvidu-server-pro/cluster/aws/openvidu_autodiscover.sh \
--output "${TMP_FOLDER}/openvidu_autodiscover.sh" || fatal_error "Error when downloading the file 'openvidu_autodiscover.sh'"
@ -222,8 +233,8 @@ upgrade_ov() {
--output "${TMP_FOLDER}/openvidu" || fatal_error "Error when downloading the file 'openvidu'"
printf '\n - openvidu'
# Dowloading new images and stoped actual Openvidu
printf '\n => Dowloading new images...'
# Downloading new images and stopped actual Openvidu
printf '\n => Downloading new images...'
printf '\n'
sleep 1
@ -231,9 +242,9 @@ upgrade_ov() {
printf '\n'
cd "${TMP_FOLDER}" || fatal_error "Error when moving to 'tmp' folder"
printf '\n'
docker-compose pull | true
docker-compose pull || true
printf '\n => Stoping Openvidu...'
printf '\n => Stopping Openvidu...'
printf '\n'
sleep 1
@ -241,7 +252,7 @@ upgrade_ov() {
printf '\n'
cd "${OPENVIDU_PREVIOUS_FOLDER}" || fatal_error "Error when moving to 'openvidu' folder"
printf '\n'
docker-compose down | true
docker-compose down || true
printf '\n'
printf '\n => Moving to working dir...'
@ -253,7 +264,7 @@ upgrade_ov() {
mv "${OPENVIDU_PREVIOUS_FOLDER}/docker-compose.yml" "${ROLL_BACK_FOLDER}" || fatal_error "Error while moving previous 'docker-compose.yml'"
printf '\n - docker-compose.yml'
if [ ! -z "${USE_OV_CALL}" ]; then
if [ -n "${USE_OV_CALL}" ]; then
mv "${OPENVIDU_PREVIOUS_FOLDER}/docker-compose.override.yml" "${ROLL_BACK_FOLDER}" || fatal_error "Error while moving previous 'docker-compose.override.yml'"
printf '\n - docker-compose.override.yml'
fi
@ -281,7 +292,7 @@ upgrade_ov() {
mv "${TMP_FOLDER}/docker-compose.yml" "${OPENVIDU_PREVIOUS_FOLDER}" || fatal_error "Error while updating 'docker-compose.yml'"
printf '\n - docker-compose.yml'
if [ ! -z "${USE_OV_CALL}" ]; then
if [ -n "${USE_OV_CALL}" ]; then
mv "${TMP_FOLDER}/docker-compose.override.yml" "${OPENVIDU_PREVIOUS_FOLDER}" || fatal_error "Error while updating 'docker-compose.override.yml'"
printf '\n - docker-compose.override.yml'
else
@ -334,18 +345,46 @@ upgrade_ov() {
# Define old mode: On Premise or Cloud Formation
OLD_MODE=$(grep -E "Installation Mode:.*$" "${ROLL_BACK_FOLDER}/docker-compose.yml" | awk '{ print $4,$5 }')
[ ! -z "${OLD_MODE}" ] && sed -i -r "s/Installation Mode:.+/Installation Mode: ${OLD_MODE}/" "${OPENVIDU_PREVIOUS_FOLDER}/docker-compose.yml"
[ -n "${OLD_MODE}" ] && sed -i -r "s/Installation Mode:.+/Installation Mode: ${OLD_MODE}/" "${OPENVIDU_PREVIOUS_FOLDER}/docker-compose.yml"
# In Aws, update AMI ID
AWS_REGION=$(grep -E "AWS_DEFAULT_REGION=.*$" "${OPENVIDU_PREVIOUS_FOLDER}/.env" | cut -d'=' -f2)
if [[ ! -z ${AWS_REGION} ]]; then
# Update .env variables to new .env-version
AWS_REGION=$(get_previous_env_variable AWS_DEFAULT_REGION)
if [[ -n ${AWS_REGION} ]]; then
# Get new AMI ID
NEW_AMI_ID=$(curl https://s3-eu-west-1.amazonaws.com/aws.openvidu.io/CF-OpenVidu-Pro-${OPENVIDU_VERSION//v}.yaml --silent |
sed -n -e '/KMSAMIMAP:/,/Metadata:/ p' |
grep -A 1 ${AWS_REGION} | grep AMI | tr -d " " | cut -d":" -f2)
grep -A 1 "${AWS_REGION}" | grep AMI | tr -d " " | cut -d":" -f2)
[[ -z ${NEW_AMI_ID} ]] && fatal_error "Error while getting new AWS_IMAGE_ID for Media Nodes"
sed -i "s/.*AWS_IMAGE_ID=.*/AWS_IMAGE_ID=${NEW_AMI_ID}/" "${OPENVIDU_PREVIOUS_FOLDER}/.env" || fatal_error "Error while updating new AWS_IMAGE_ID for Media Nodes"
# Get previous values
PREV_AWS_DEFAULT_REGION=$(get_previous_env_variable AWS_DEFAULT_REGION)
PREV_AWS_INSTANCE_TYPE=$(get_previous_env_variable AWS_INSTANCE_TYPE)
PREV_AWS_INSTANCE_ID=$(get_previous_env_variable AWS_INSTANCE_ID)
PREV_AWS_KEY_NAME=$(get_previous_env_variable AWS_KEY_NAME)
PREV_AWS_SUBNET_ID=$(get_previous_env_variable AWS_SUBNET_ID)
PREV_AWS_SECURITY_GROUP=$(get_previous_env_variable AWS_SECURITY_GROUP)
PREV_AWS_STACK_ID=$(get_previous_env_variable AWS_STACK_ID)
PREV_AWS_STACK_NAME=$(get_previous_env_variable AWS_STACK_NAME)
PREV_AWS_CLI_DOCKER_TAG=$(get_previous_env_variable AWS_CLI_DOCKER_TAG)
PREV_AWS_VOLUME_SIZE=$(get_previous_env_variable AWS_VOLUME_SIZE)
# Replace variables in new .env-version file
replace_variable_in_new_env_file "AWS_DEFAULT_REGION" "${PREV_AWS_DEFAULT_REGION}"
replace_variable_in_new_env_file "AWS_INSTANCE_TYPE" "${PREV_AWS_INSTANCE_TYPE}"
replace_variable_in_new_env_file "AWS_INSTANCE_ID" "${PREV_AWS_INSTANCE_ID}"
replace_variable_in_new_env_file "AWS_KEY_NAME" "${PREV_AWS_KEY_NAME}"
replace_variable_in_new_env_file "AWS_SUBNET_ID" "${PREV_AWS_SUBNET_ID}"
replace_variable_in_new_env_file "AWS_SECURITY_GROUP" "${PREV_AWS_SECURITY_GROUP}"
replace_variable_in_new_env_file "AWS_STACK_ID" "${PREV_AWS_STACK_ID}"
replace_variable_in_new_env_file "AWS_STACK_NAME" "${PREV_AWS_STACK_NAME}"
replace_variable_in_new_env_file "AWS_CLI_DOCKER_TAG" "${PREV_AWS_CLI_DOCKER_TAG}"
replace_variable_in_new_env_file "AWS_VOLUME_SIZE" "${PREV_AWS_VOLUME_SIZE}"
# Replace new AMI
replace_variable_in_new_env_file "AWS_IMAGE_ID" "${NEW_AMI_ID}"
fi
# Ready to use
printf '\n'
@ -391,7 +430,7 @@ else
fi
# Check type of installation
if [[ ! -z "$1" && "$1" == "upgrade" ]]; then
if [[ -n "$1" && "$1" == "upgrade" ]]; then
upgrade_ov
else
new_ov_installation

View File

@ -4,7 +4,7 @@ upgrade_ov() {
UPGRADE_SCRIPT_URL="https://s3-eu-west-1.amazonaws.com/aws.openvidu.io/install_openvidu_pro_OVVERSION.sh"
HTTP_STATUS=$(curl -s -o /dev/null -I -w "%{http_code}" ${UPGRADE_SCRIPT_URL//OVVERSION/$1})
printf " => Upgrading Openvidu PRO to '%s' version" "$1"
printf " => Upgrading OpenVidu Pro to '%s' version" "$1"
if [ "$HTTP_STATUS" == "200" ]; then
printf "\n => Downloading and upgrading new version"
@ -13,7 +13,7 @@ upgrade_ov() {
curl --silent ${UPGRADE_SCRIPT_URL//OVVERSION/$1} | bash -s upgrade
else
printf "\n =======¡ERROR!======="
printf "\n Openvidu PRO Version '%s' not exist" "$1"
printf "\n OpenVidu Pro Version '%s' not exist" "$1"
printf "\n"
exit 0
fi
@ -28,7 +28,7 @@ collect_basic_information() {
OV_VERSION=$(grep 'Openvidu Version:' "${OV_FOLDER}/docker-compose.yml" | awk '{ print $4 }')
CONTAINERS=$(docker ps | awk '{if(NR>1) print $NF}')
if [ ! -z "$(grep -E '^ image: openvidu/openvidu-call:.*$' "${OV_FOLDER}/docker-compose.override.yml" | tr -d '[:space:]')" ]; then
if [ -n "$(grep -E '^ image: openvidu/openvidu-call:.*$' "${OV_FOLDER}/docker-compose.override.yml" | tr -d '[:space:]')" ]; then
OV_CALL_VERSION=$(grep -E 'Openvidu-Call Version:' "${OV_FOLDER}/docker-compose.override.yml" | awk '{ print $4 }')
fi
[ -z "${OV_CALL_VERSION}" ] && OV_CALL_VERSION="No present"
@ -71,7 +71,7 @@ generate_report() {
REPORT_CREATION_DATE=$(date +"%d-%m-%Y")
REPORT_CREATION_TIME=$(date +"%H:%M:%S")
REPORT_NAME="openvidu-report-${REPORT_CREATION_DATE}-$(date +"%H-%M").txt"
REPORT_OUPUT="${OV_FOLDER}/${REPORT_NAME}"
REPORT_OUTPUT="${OV_FOLDER}/${REPORT_NAME}"
{
printf "\n ======================================="
@ -150,10 +150,10 @@ generate_report() {
do
printf '\n'
printf "\n ---------------------------------------"
printf "\n %s" $CONTAINER
printf "\n %s" "$CONTAINER"
printf "\n ---------------------------------------"
printf '\n'
docker logs $CONTAINER
docker logs "$CONTAINER"
printf "\n ---------------------------------------"
printf '\n'
printf '\n'
@ -167,19 +167,19 @@ generate_report() {
do
printf '\n'
printf "\n ======================================="
printf "\n %s" $CONTAINER
printf "\n %s" "$CONTAINER"
printf "\n ---------------------------------------"
printf '\n'
docker exec $CONTAINER env
docker exec "$CONTAINER" env
printf "\n ---------------------------------------"
printf '\n'
printf '\n'
done
} >> "${REPORT_OUPUT}" 2>&1
} >> "${REPORT_OUTPUT}" 2>&1
printf "\n Generation of the report completed with success"
printf "\n You can get your report at path '%s'" "${REPORT_OUPUT}"
printf "\n You can get your report at path '%s'" "${REPORT_OUTPUT}"
printf "\n"
}
@ -192,7 +192,7 @@ is_external_url() {
return 1
else
return 0
fi
fi
}
start_openvidu() {
@ -224,9 +224,9 @@ usage() {
printf "\n\nAvailable Commands:"
printf "\n\tstart\t\t\tStart all services"
printf "\n\tstop\t\t\tStop all services"
printf "\n\trestart\t\t\tRestart all stoped and running services"
printf "\n\trestart\t\t\tRestart all stopped and running services"
printf "\n\tlogs\t\t\tShow openvidu-server logs"
printf "\n\tupgrade\t\t\tUpgrade to the lastest Openvidu version"
printf "\n\tupgrade\t\t\tUpgrade to the latest Openvidu version"
printf "\n\tupgrade [version]\tUpgrade to the specific Openvidu version"
printf "\n\tversion\t\t\tShow version of Openvidu Server"
printf "\n\treport\t\t\tGenerate a report with the current status of Openvidu"
@ -268,7 +268,7 @@ case $1 in
UPGRADE_VERSION="$2"
fi
read -r -p " You're about to update Openvidu PRO to '${UPGRADE_VERSION}' version. Are you sure? [y/N]: " response
read -r -p " You're about to update OpenVidu Pro to '${UPGRADE_VERSION}' version. Are you sure? [y/N]: " response
case "$response" in
[yY][eE][sS]|[yY])
upgrade_ov "${UPGRADE_VERSION}"
@ -298,4 +298,4 @@ case $1 in
*)
usage
;;
esac
esac

View File

@ -2,16 +2,15 @@ filebeat.inputs:
- type: container
paths:
- '/var/lib/docker/containers/*/*.log'
multiline.pattern: '^\d*:\d*:\d*'
multiline.negate: true
multiline.match: after
- type: log
paths:
- /opt/openvidu/kurento-logs/*.log
fields:
kurento-media-server: true
ip: ${MEDIA_NODE_IP}
cluster-id: ${CLUSTER_ID}
cluster_id: ${CLUSTER_ID}
node_id: ${NODE_ID}
node_role: medianode
fields_under_root: true
processors:
@ -24,10 +23,12 @@ processors:
fields: ["message"]
target: "json"
overwrite_keys: true
- add_fields:
fields:
ip: ${MEDIA_NODE_IP}
cluster-id: ${CLUSTER_ID}
- drop_event:
when.or:
- contains:
container.image.name: docker.elastic.co/beats/filebeat-oss
- contains:
container.image.name: docker.elastic.co/beats/metricbeat-oss
output:
elasticsearch:
@ -36,6 +37,14 @@ output:
when.or:
- equals:
kurento-media-server: true
- index: "filebeat-media-node-controller-%{+yyyy.MM.dd}"
when.or:
- contains:
container.image.name: openvidu/media-node-controller
- index: "filebeat-openvidu-recording-%{+yyyy.MM.dd}"
when.or:
- contains:
container.image.name: openvidu/openvidu-recording
pipelines:
- pipeline: kurento-pipeline
when.or:

View File

@ -2,7 +2,7 @@ metricbeat.modules:
- module: system
metricsets:
- cpu
#- diskio
- diskio
- memory
- network
- filesystem
@ -26,9 +26,13 @@ metricbeat.modules:
- regexp:
system.filesystem.mount_point: '^/hostfs/(sys|cgroup|proc|dev|etc|host)($|/)'
enabled: true
period: ${OPENVIDU_PRO_CLUSTER_LOAD_INTERVAL}0s
period: ${OPENVIDU_PRO_STATS_MONITORING_INTERVAL}s
cpu.metrics: [normalized_percentages]
fields: {ip: "${MEDIA_NODE_IP}", cluster-id: "${CLUSTER_ID}"}
fields:
ip: "${MEDIA_NODE_IP}"
cluster_id: "${CLUSTER_ID}"
node_id: ${NODE_ID}
node_role: medianode
pipeline:
queue.mem.events: 0
setup.ilm.enabled: false

View File

@ -6,7 +6,7 @@
#
# This docker-compose file coordinates all services of OpenVidu CE Platform.
#
# Openvidu Version: 2.16.0
# Openvidu Version: 2.17.0
#
# Installation Mode: On Premises
#
@ -16,16 +16,17 @@ version: '3.1'
services:
media-node-controller:
image: openvidu/media-node-controller:3.0.0-dev4
image: openvidu/media-node-controller:4.0.0-dev1
restart: always
ulimits:
core: -1
entrypoint: ['/bin/sh', '-c', '/beats/copy_config_files.sh && /usr/local/bin/entrypoint.sh']
environment:
- KMS_IMAGE=kurento/kurento-media-server:6.15.0
- KMS_IMAGE=kurento/kurento-media-server:6.16.0
- METRICBEAT_IMAGE=docker.elastic.co/beats/metricbeat-oss:7.8.0
- FILEBEAT_IMAGE=docker.elastic.co/beats/filebeat-oss:7.8.0
- OPENVIDU_RECORDING_IMAGE=openvidu/openvidu-recording:2.17.0-dev1
- OPENVIDU_RECORDING_IMAGE=openvidu/openvidu-recording:2.17.0
- NO_COLOR=true
ports:
- 3000:3000
volumes:

View File

@ -22,10 +22,10 @@ fatal_error() {
docker_command_by_container_image() {
IMAGE_NAME=$1
COMMAND=$2
if [[ ! -z "${IMAGE_NAME}" ]]; then
CONTAINERS=$(docker ps -a | grep "${IMAGE_NAME}" | awk '{print $1}')
for CONTAINER_ID in ${CONTAINERS[@]}; do
if [[ ! -z "${CONTAINER_ID}" ]] && [[ ! -z "${COMMAND}" ]]; then
if [[ -n "${IMAGE_NAME}" ]]; then
CONTAINERS="$(docker ps -a | grep "${IMAGE_NAME}" | awk '{print $1}')"
for CONTAINER_ID in $CONTAINERS; do
if [[ -n "${CONTAINER_ID}" ]] && [[ -n "${COMMAND}" ]]; then
bash -c "docker ${COMMAND} ${CONTAINER_ID}"
fi
done
@ -35,7 +35,7 @@ docker_command_by_container_image() {
stop_containers() {
printf "Stopping containers..."
for IMAGE in ${IMAGES[@]}; do
for IMAGE in "${IMAGES[@]}"; do
docker_command_by_container_image "${IMAGE}" "rm -f"
done
}
@ -89,15 +89,15 @@ new_media_node_installation() {
# Pull images
printf "\n => Pulling images...\n"
cd "${MEDIA_NODE_FOLDER}" || fatal_error "Error when moving to '${MEDIA_NODE_FOLDER}' folder"
KMS_IMAGE=$(cat docker-compose.yml | grep KMS_IMAGE | cut -d"=" -f2)
METRICBEAT_IMAGE=$(cat docker-compose.yml | grep METRICBEAT_IMAGE | cut -d"=" -f2)
FILEBEAT_IMAGE=$(cat docker-compose.yml | grep FILEBEAT_IMAGE | cut -d"=" -f2)
OPENVIDU_RECORDING_IMAGE=$(cat docker-compose.yml | grep OPENVIDU_RECORDING_IMAGE | cut -d"=" -f2)
docker pull $KMS_IMAGE || fatal "Error while pulling docker image: $KMS_IMAGE"
docker pull $METRICBEAT_IMAGE || fatal "Error while pulling docker image: $METRICBEAT_IMAGE"
docker pull $FILEBEAT_IMAGE || fatal "Error while pulling docker image: $FILEBEAT_IMAGE"
docker pull $OPENVIDU_RECORDING_IMAGE || fatal "Error while pulling docker image: $OPENVIDU_RECORDING_IMAGE"
docker-compose pull | true
KMS_IMAGE=$(grep KMS_IMAGE docker-compose.yml | cut -d"=" -f2)
METRICBEAT_IMAGE=$(grep METRICBEAT_IMAGE docker-compose.yml | cut -d"=" -f2)
FILEBEAT_IMAGE=$(grep FILEBEAT_IMAGE docker-compose.yml | cut -d"=" -f2)
OPENVIDU_RECORDING_IMAGE=$(grep OPENVIDU_RECORDING_IMAGE docker-compose.yml | cut -d"=" -f2)
docker pull "$KMS_IMAGE" || fatal "Error while pulling docker image: $KMS_IMAGE"
docker pull "$METRICBEAT_IMAGE" || fatal "Error while pulling docker image: $METRICBEAT_IMAGE"
docker pull "$FILEBEAT_IMAGE" || fatal "Error while pulling docker image: $FILEBEAT_IMAGE"
docker pull "$OPENVIDU_RECORDING_IMAGE" || fatal "Error while pulling docker image: $OPENVIDU_RECORDING_IMAGE"
docker-compose pull || true
# Ready to use
printf "\n"
@ -119,10 +119,10 @@ new_media_node_installation() {
printf '\n ...'
printf '\n You can also add this node from inspector'
printf '\n'
printf '\n 4. Start or restart OpenVidu Pro and all containers will be provisioned'
printf '\n 4. Start or restart OpenVidu Pro and all containers will be provisioned'
printf '\n automatically to all the media nodes configured in "KMS_URIS"'
printf '\n More info about Media Nodes deployment here:'
printf "\n --> https://docs.openvidu.io/en/${OPENVIDU_VERSION//v}/openvidu-pro/deployment/on-premises/#set-the-number-of-media-nodes-on-startup"
printf '\n --> https://docs.openvidu.io/en/%s/openvidu-pro/deployment/on-premises/#set-the-number-of-media-nodes-on-startup' "${OPENVIDU_VERSION//v}"
printf '\n'
printf '\n'
printf "\n If you want to rollback, all the files from the previous installation have been copied to folder '.old-%s'" "${OPENVIDU_PREVIOUS_VERSION}"
@ -156,7 +156,7 @@ upgrade_media_node() {
[ -z "${MEDIA_NODE_PREVIOUS_FOLDER}" ] && fatal_error "No previous Media Node installation found"
# Uppgrade Media Node
# Upgrade Media Node
OPENVIDU_PREVIOUS_VERSION=$(grep 'Openvidu Version:' "${MEDIA_NODE_PREVIOUS_FOLDER}/docker-compose.yml" | awk '{ print $4 }')
[ -z "${OPENVIDU_PREVIOUS_VERSION}" ] && fatal_error "Can't find previous OpenVidu version"
@ -205,8 +205,8 @@ upgrade_media_node() {
--output "${TMP_FOLDER}/copy_config_files.sh" || fatal_error "Error when downloading the file 'copy_config_files.sh'"
printf '\n - copy_config_files.sh'
# Dowloading new images and stoped actual Media Node
printf '\n => Dowloading new images...'
# Downloading new images and stopped actual Media Node
printf '\n => Downloading new images...'
printf '\n'
sleep 1
@ -214,7 +214,7 @@ upgrade_media_node() {
printf '\n'
cd "${TMP_FOLDER}" || fatal_error "Error when moving to '${TMP_FOLDER}' folder"
printf '\n => Stoping Media Node containers...'
printf '\n => Stopping Media Node containers...'
printf '\n'
sleep 1
@ -222,24 +222,24 @@ upgrade_media_node() {
# Pull images
printf "\n => Pulling images...\n"
KMS_IMAGE=$(cat docker-compose.yml | grep KMS_IMAGE | cut -d"=" -f2)
METRICBEAT_IMAGE=$(cat docker-compose.yml | grep METRICBEAT_IMAGE | cut -d"=" -f2)
FILEBEAT_IMAGE=$(cat docker-compose.yml | grep FILEBEAT_IMAGE | cut -d"=" -f2)
OPENVIDU_RECORDING_IMAGE=$(cat docker-compose.yml | grep OPENVIDU_RECORDING_IMAGE | cut -d"=" -f2)
docker pull $KMS_IMAGE || fatal "Error while pulling docker image: $KMS_IMAGE"
docker pull $METRICBEAT_IMAGE || fatal "Error while pulling docker image: $METRICBEAT_IMAGE"
docker pull $FILEBEAT_IMAGE || fatal "Error while pulling docker image: $FILEBEAT_IMAGE"
docker pull $OPENVIDU_RECORDING_IMAGE || fatal "Error while pulling docker image: $OPENVIDU_RECORDING_IMAGE"
docker-compose pull | true
KMS_IMAGE="$(grep KMS_IMAGE docker-compose.yml | cut -d"=" -f2)"
METRICBEAT_IMAGE="$(grep METRICBEAT_IMAGE docker-compose.yml | cut -d"=" -f2)"
FILEBEAT_IMAGE="$(grep FILEBEAT_IMAGE docker-compose.yml | cut -d"=" -f2)"
OPENVIDU_RECORDING_IMAGE="$(grep OPENVIDU_RECORDING_IMAGE docker-compose.yml | cut -d"=" -f2)"
docker pull "$KMS_IMAGE" || fatal "Error while pulling docker image: $KMS_IMAGE"
docker pull "$METRICBEAT_IMAGE" || fatal "Error while pulling docker image: $METRICBEAT_IMAGE"
docker pull "$FILEBEAT_IMAGE" || fatal "Error while pulling docker image: $FILEBEAT_IMAGE"
docker pull "$OPENVIDU_RECORDING_IMAGE" || fatal "Error while pulling docker image: $OPENVIDU_RECORDING_IMAGE"
docker-compose pull || true
printf '\n => Stoping Media Node...'
printf '\n => Stopping Media Node...'
printf '\n'
sleep 1
printf "\n => Moving to 'openvidu' folder..."
printf '\n'
cd "${MEDIA_NODE_PREVIOUS_FOLDER}" || fatal_error "Error when moving to 'openvidu' folder"
docker-compose down | true
docker-compose down || true
printf '\n'
printf '\n => Moving to working dir...'
@ -290,7 +290,7 @@ upgrade_media_node() {
# Define old mode: On Premise or Cloud Formation
OLD_MODE=$(grep -E "Installation Mode:.*$" "${ROLL_BACK_FOLDER}/docker-compose.yml" | awk '{ print $4,$5 }')
[ ! -z "${OLD_MODE}" ] && sed -i -r "s/Installation Mode:.+/Installation Mode: ${OLD_MODE}/" "${MEDIA_NODE_PREVIOUS_FOLDER}/docker-compose.yml"
[ -n "${OLD_MODE}" ] && sed -i -r "s/Installation Mode:.+/Installation Mode: ${OLD_MODE}/" "${MEDIA_NODE_PREVIOUS_FOLDER}/docker-compose.yml"
# Ready to use
printf '\n'
@ -314,10 +314,10 @@ upgrade_media_node() {
printf '\n ...'
printf '\n You can also add Media Nodes from inspector'
printf '\n'
printf '\n 5. Start or restart OpenVidu Pro and all containers will be provisioned'
printf '\n 5. Start or restart OpenVidu Pro and all containers will be provisioned'
printf '\n automatically to all the media nodes configured in "KMS_URIS"'
printf '\n More info about Media Nodes deployment here:'
printf "\n --> https://docs.openvidu.io/en/${OPENVIDU_VERSION//v}/openvidu-pro/deployment/on-premises/#set-the-number-of-media-nodes-on-startup"
printf '\n --> https://docs.openvidu.io/en/%s/openvidu-pro/deployment/on-premises/#set-the-number-of-media-nodes-on-startup' "${OPENVIDU_VERSION//v}"
printf '\n'
printf '\n'
printf "\n If you want to rollback, all the files from the previous installation have been copied to folder '.old-%s'" "${OPENVIDU_PREVIOUS_VERSION}"
@ -342,7 +342,7 @@ else
fi
# Check type of installation
if [[ ! -z "$1" && "$1" == "upgrade" ]]; then
if [[ -n "$1" && "$1" == "upgrade" ]]; then
upgrade_media_node
else
new_media_node_installation

View File

@ -11,10 +11,10 @@ IMAGES=(
docker_command_by_container_image() {
IMAGE_NAME=$1
COMMAND=$2
if [[ ! -z "${IMAGE_NAME}" ]]; then
CONTAINERS=$(docker ps -a | grep "${IMAGE_NAME}" | awk '{print $1}')
for CONTAINER_ID in ${CONTAINERS[@]}; do
if [[ ! -z "${CONTAINER_ID}" ]] && [[ ! -z "${COMMAND}" ]]; then
if [[ -n "${IMAGE_NAME}" ]]; then
CONTAINERS="$(docker ps -a | grep "${IMAGE_NAME}" | awk '{print $1}')"
for CONTAINER_ID in $CONTAINERS; do
if [[ -n "${CONTAINER_ID}" ]] && [[ -n "${COMMAND}" ]]; then
bash -c "docker ${COMMAND} ${CONTAINER_ID}"
fi
done
@ -24,14 +24,14 @@ docker_command_by_container_image() {
stop_containers() {
printf "Stopping containers..."
for IMAGE in ${IMAGES[@]}; do
for IMAGE in "${IMAGES[@]}"; do
docker_command_by_container_image "${IMAGE}" "rm -f"
done
}
kurento_logs() {
if [[ "$1" == "-f" ]]; then
tail -f /opt/openvidu/kurento-logs/*.log
else
else
cat /opt/openvidu/kurento-logs/*.log
fi
}
@ -99,7 +99,7 @@ generate_report() {
REPORT_CREATION_DATE=$(date +"%d-%m-%Y")
REPORT_CREATION_TIME=$(date +"%H:%M:%S")
REPORT_NAME="media-node-report-${REPORT_CREATION_DATE}-$(date +"%H-%M").txt"
REPORT_OUPUT="${MEDIA_NODE_FOLDER}/${REPORT_NAME}"
REPORT_OUTPUT="${MEDIA_NODE_FOLDER}/${REPORT_NAME}"
CONTAINERS=$(docker ps | awk '{if(NR>1) print $NF}')
{
@ -167,10 +167,10 @@ generate_report() {
do
printf '\n'
printf "\n ---------------------------------------"
printf "\n %s" $CONTAINER
printf "\n %s" "$CONTAINER"
printf "\n ---------------------------------------"
printf '\n'
docker logs $CONTAINER
docker logs "$CONTAINER"
printf "\n ---------------------------------------"
printf '\n'
printf '\n'
@ -178,7 +178,7 @@ generate_report() {
printf '\n'
printf "\n ---------------------------------------"
printf "\n KMS"
printf "\n KMS"
printf "\n ---------------------------------------"
printf '\n'
kurento_logs
@ -194,19 +194,19 @@ generate_report() {
do
printf '\n'
printf "\n ======================================="
printf "\n %s" $CONTAINER
printf "\n %s" "$CONTAINER"
printf "\n ---------------------------------------"
printf '\n'
docker exec $CONTAINER env
docker exec "$CONTAINER" env
printf "\n ---------------------------------------"
printf '\n'
printf '\n'
done
} >> "${REPORT_OUPUT}" 2>&1
} >> "${REPORT_OUTPUT}" 2>&1
printf "\n Generation of the report completed with success"
printf "\n You can get your report at path '%s'" "${REPORT_OUPUT}"
printf "\n You can get your report at path '%s'" "${REPORT_OUTPUT}"
printf "\n"
}
@ -218,7 +218,7 @@ usage() {
printf "\n\trestart\t\t\tRestart media node service"
printf "\n\tlogs [-f]\t\tShow media-node-controller logs."
printf "\n\tkms-logs [-f]\t\tShow kms logs"
printf "\n\tupgrade\t\t\tUpgrade to the lastest Media Node version"
printf "\n\tupgrade\t\t\tUpgrade to the latest Media Node version"
printf "\n\tupgrade [version]\tUpgrade to the specific Media Node version"
printf "\n\tversion\t\t\tShow version of Media Node"
printf "\n\treport\t\t\tGenerate a report with the current status of Media Node"
@ -255,9 +255,9 @@ case $1 in
;;
esac
;;
kms-logs)
kurento_logs $2
kurento_logs "$2"
;;
upgrade)

View File

@ -9,7 +9,7 @@
# For example: 198.51.100.1, or openvidu.example.com
DOMAIN_OR_PUBLIC_IP=
# OpenVidu PRO License
# OpenVidu Pro License
OPENVIDU_PRO_LICENSE=
# OpenVidu SECRET used for apps to connect to OpenVidu server and users to access to OpenVidu Dashboard
@ -42,9 +42,9 @@ LETSENCRYPT_EMAIL=user@example.com
# SDKs, REST clients and browsers will have to connect to this port
# HTTPS_PORT=443
# Old paths are considered now deprecated, but still supported by default.
# OpenVidu Server will log a WARN message every time a deprecated path is called, indicating
# the new path that should be used instead. You can set property SUPPORT_DEPRECATED_API=false
# Old paths are considered now deprecated, but still supported by default.
# OpenVidu Server will log a WARN message every time a deprecated path is called, indicating
# the new path that should be used instead. You can set property SUPPORT_DEPRECATED_API=false
# to stop allowing the use of old paths.
# Default value is true
# SUPPORT_DEPRECATED_API=true
@ -53,7 +53,7 @@ LETSENCRYPT_EMAIL=user@example.com
# Default value is false
# REDIRECT_WWW=false
# How many workers to configure in nginx proxy.
# How many workers to configure in nginx proxy.
# The more workers, the more requests will be handled
# Default value is 10240
# WORKER_CONNECTIONS=10240
@ -88,7 +88,7 @@ OPENVIDU_PRO_CLUSTER_MODE=manual
OPENVIDU_PRO_CLUSTER_ENVIRONMENT=on_premise
# Unique identifier of your cluster. Each OpenVidu Server Pro instance corresponds to one cluster.
# You can launch as many clusters as you want with your license key.
# You can launch as many clusters as you want with your license key.
# Cluster ID will always be stored to disk so restarting OpenVidu Server Pro will keep the same previous cluster ID
# if this configuration parameter is not given a distinct value.
# OPENVIDU_PRO_CLUSTER_ID=
@ -185,10 +185,10 @@ OPENVIDU_PRO_CLUSTER_LOAD_STRATEGY=streams
# and write into the s3 bucket, you don't need this parameter
# OPENVIDU_PRO_AWS_SECRET_KEY=
# AWS region in which the S3 bucket is located (e.g. eu-west-1). If not provided,
# the region will try to be discovered automatically, although this is not always possible.
# AWS region in which the S3 bucket is located (e.g. eu-west-1). If not provided,
# the region will try to be discovered automatically, although this is not always possible.
# This property is only taken into account if OPENVIDU_PRO_RECORDING_STORAGE=s3
# OPENVIDU_PRO_AWS_REGION=
#OPENVIDU_PRO_AWS_REGION=
# Whether to enable recording module or not
OPENVIDU_RECORDING=false
@ -250,6 +250,10 @@ OPENVIDU_STREAMS_VIDEO_MIN_SEND_BANDWIDTH=300
# Default value is false
# OPENVIDU_STREAMS_ALLOW_TRANSCODING=false
# Send openvidu-browser logs of clients to Elasticsearch
# Default value is 'disabled'
# OPENVIDU_BROWSER_LOGS=disabled
# true to enable OpenVidu Webhook service. false' otherwise
# Values: true | false
OPENVIDU_WEBHOOK=false
@ -263,7 +267,7 @@ OPENVIDU_WEBHOOK=false
# List of events that will be sent by OpenVidu Webhook service
# Default value is all available events
OPENVIDU_WEBHOOK_EVENTS=[sessionCreated,sessionDestroyed,participantJoined,participantLeft,webrtcConnectionCreated,webrtcConnectionDestroyed,recordingStatusChanged,filterEventDispatched,mediaNodeStatusChanged]
OPENVIDU_WEBHOOK_EVENTS=[sessionCreated,sessionDestroyed,participantJoined,participantLeft,webrtcConnectionCreated,webrtcConnectionDestroyed,recordingStatusChanged,filterEventDispatched,mediaNodeStatusChanged,nodeCrashed]
# How often the garbage collector of non active sessions runs.
# This helps cleaning up sessions that have been initialized through
@ -324,7 +328,7 @@ ELASTICSEARCH_PASSWORD=
# Uncomment the next line and define this variable with KMS image that you want use
# By default, KMS_IMAGE is defined in media nodes and it does not need to be specified unless
# you want to use a specific version of KMS
# KMS_IMAGE=kurento/kurento-media-server:6.15.0
# KMS_IMAGE=kurento/kurento-media-server:6.16.0
# Uncomment the next line and define this variable to change
# the verbosity level of the logs of KMS
@ -337,6 +341,7 @@ ELASTICSEARCH_PASSWORD=
#AWS_DEFAULT_REGION=
#AWS_IMAGE_ID=
#AWS_INSTANCE_TYPE=
#AWS_INSTANCE_ID=
#AWS_KEY_NAME=
#AWS_SUBNET_ID=
#AWS_SECURITY_GROUP=

View File

@ -2,6 +2,11 @@ filebeat.inputs:
- type: container
paths:
- '/var/lib/docker/containers/*/*.log'
fields:
cluster_id: ${OPENVIDU_PRO_CLUSTER_ID:${DOMAIN_OR_PUBLIC_IP:undefined}}
node_id: master_${AWS_INSTANCE_ID:${OPENVIDU_PRO_CLUSTER_ID:${DOMAIN_OR_PUBLIC_IP:undefined}}}
node_role: masternode
fields_under_root: true
processors:
- add_docker_metadata:
@ -25,9 +30,8 @@ processors:
container.image.name: docker.elastic.co/beats/filebeat-oss
- contains:
container.image.name: docker.elastic.co/beats/metricbeat-oss
- add_fields:
fields:
cluster-id: ${OPENVIDU_PRO_CLUSTER_ID:undefined}
- contains:
container.image.name: openvidu/openvidu-server-pro
output:
elasticsearch:
@ -41,7 +45,10 @@ output:
when.or:
- contains:
container.image.name: openvidu/openvidu-proxy
- index: "filebeat-openvidu-recording-%{+yyyy.MM.dd}"
when.or:
- contains:
container.image.name: openvidu/openvidu-recording
logging.json: true
logging.metrics.enabled: false

View File

@ -1,10 +1,40 @@
metricbeat.modules:
- module: nginx
metricsets: ["stubstatus"]
enabled: true
period: 10s
hosts: ["http://127.0.0.1"]
server_status_path: "nginx_status"
- module: nginx
metricsets: ["stubstatus"]
enabled: true
period: ${OPENVIDU_PRO_STATS_MONITORING_INTERVAL}s
hosts: ["http://127.0.0.1"]
server_status_path: "nginx_status"
- module: system
metricsets:
- cpu
- diskio
- memory
- network
- filesystem
- fsstat
- process_summary
- uptime
filesystem.ignore_types: [nfs, smbfs, autofs, devtmpfs, devpts, hugetlbfs, tmpfs, sysfs, securityfs, cgroup2, cgroup, pstore, debugfs, configfs, fusectl, proc, fuse.lxcfs, squashfs]
processes: ['.*']
processors:
- drop_event:
when:
or:
- regexp:
system.network.name: '^(veth|lo|docker|br-)($|)'
- regexp:
system.filesystem.mount_point: '^/(sys|cgroup|proc|dev|etc|host)($|/)'
- regexp:
system.filesystem.mount_point: '^/hostfs/(sys|cgroup|proc|dev|etc|host)($|/)'
enabled: true
period: ${OPENVIDU_PRO_STATS_MONITORING_INTERVAL}s
cpu.metrics: [normalized_percentages]
fields:
cluster_id: ${OPENVIDU_PRO_CLUSTER_ID:${DOMAIN_OR_PUBLIC_IP:undefined}}
node_id: master_${AWS_INSTANCE_ID:${OPENVIDU_PRO_CLUSTER_ID:${DOMAIN_OR_PUBLIC_IP:undefined}}}
node_role: masternode
output:
elasticsearch:
hosts: ["${OPENVIDU_PRO_ELASTICSEARCH_HOST}"]
setup.ilm.enabled: false

View File

@ -35,6 +35,14 @@ exit_on_error () {
esac
}
# Check custom parameters
if [[ -n "${CUSTOM_INSTANCE_TYPE}" ]]; then
AWS_INSTANCE_TYPE="${CUSTOM_INSTANCE_TYPE}"
fi
if [[ -n "${CUSTOM_VOLUME_SIZE}" ]]; then
AWS_VOLUME_SIZE="${CUSTOM_VOLUME_SIZE}"
fi
docker run --rm amazon/aws-cli:${AWS_CLI_DOCKER_TAG} ec2 run-instances \
--image-id ${AWS_IMAGE_ID} --count 1 \
--instance-type ${AWS_INSTANCE_TYPE} \
@ -51,18 +59,6 @@ docker run --rm amazon/aws-cli:${AWS_CLI_DOCKER_TAG} ec2 wait instance-running -
KMS_IP=$(cat ${OUTPUT} | jq --raw-output ' .Instances[] | .NetworkInterfaces[0] | .PrivateIpAddress')
KMS_ID=$(cat ${OUTPUT} | jq --raw-output ' .Instances[] | .InstanceId')
# Wait media-node controller
attempt_counter=0
max_attempts=10
until $(curl --output /dev/null --silent --head --fail -u OPENVIDUAPP:${OPENVIDU_SECRET} http://${KMS_IP}:3000/media-node/status); do
if [ ${attempt_counter} -eq ${max_attempts} ];then
exit 1
fi
attempt_counter=$(($attempt_counter+1))
sleep 5
done
jq -n \
--arg id "${KMS_ID}" \
--arg ip "${KMS_IP}" \

View File

@ -9,11 +9,11 @@ services:
#
# Default Application
#
# Openvidu-Call Version: 2.16.0
# Openvidu-Call Version: 2.17.0
#
# --------------------------------------------------------------
app:
image: openvidu/openvidu-call:2.16.0
image: openvidu/openvidu-call:2.18.0-beta8
restart: on-failure
network_mode: host
environment:

View File

@ -11,7 +11,7 @@
#
# This file will be overridden when update OpenVidu Platform
#
# Openvidu Version: 2.16.0
# Openvidu Version: 2.17.0
#
# Installation Mode: On Premises
#
@ -22,7 +22,7 @@ version: '3.1'
services:
openvidu-server:
image: openvidu/openvidu-server-pro:2.17.0-dev5
image: openvidu/openvidu-server-pro:2.18.0-beta17
restart: on-failure
network_mode: host
entrypoint: ['/usr/local/bin/entrypoint.sh']
@ -52,7 +52,7 @@ services:
max-size: "${DOCKER_LOGS_MAX_SIZE:-100M}"
redis:
image: openvidu/openvidu-redis:2.0.0-dev2
image: openvidu/openvidu-redis:2.0.0
restart: always
network_mode: host
environment:
@ -62,24 +62,29 @@ services:
max-size: "${DOCKER_LOGS_MAX_SIZE:-100M}"
coturn:
image: openvidu/openvidu-coturn:3.0.0-dev2
image: openvidu/openvidu-coturn:4.0.0-dev2
restart: on-failure
network_mode: host
environment:
environment:
- REDIS_IP=127.0.0.1
- TURN_LISTEN_PORT=3478
- DB_NAME=0
- DB_PASSWORD=${OPENVIDU_SECRET}
- MIN_PORT=40000
- MAX_PORT=65535
- TURN_PUBLIC_IP=${TURN_PUBLIC_IP:-auto-ipv4}
- ENABLE_COTURN_LOGS=true
command:
- --log-file=stdout
- --external-ip=$$(detect-external-ip)
- --listening-port=3478
- --fingerprint
- --lt-cred-mech
- --min-port=40000
- --max-port=65535
- --realm=openvidu
- --verbose
logging:
options:
max-size: "${DOCKER_LOGS_MAX_SIZE:-100M}"
nginx:
image: openvidu/openvidu-proxy:5.0.0-dev2
image: openvidu/openvidu-proxy:6.0.0-dev1
restart: on-failure
network_mode: host
volumes:
@ -120,6 +125,7 @@ services:
command: >
/bin/bash -c "elasticsearch-users useradd ${ELASTICSEARCH_USERNAME}
-p ${ELASTICSEARCH_PASSWORD} -r superuser;
elasticsearch-users passwd ${ELASTICSEARCH_USERNAME} -p ${ELASTICSEARCH_PASSWORD};
docker-entrypoint.sh"
logging:
options:
@ -148,10 +154,15 @@ services:
- .env
environment:
- OPENVIDU_PRO_ELASTICSEARCH_HOST=${OPENVIDU_PRO_ELASTICSEARCH_HOST:-http://127.0.0.1:9200}
- OPENVIDU_PRO_STATS_MONITORING_INTERVAL=${OPENVIDU_PRO_STATS_MONITORING_INTERVAL:-10}
volumes:
- /var/run/docker.sock:/var/run/docker.sock
- ./beats/metricbeat.yml:/usr/share/metricbeat/metricbeat.yml:ro
- /proc:/hostfs/proc:ro
- /sys/fs/cgroup:/hostfs/sys/fs/cgroup:ro
- /:/hostfs:ro
command: >
/bin/bash -c "metricbeat -e -strict.perms=false
/bin/bash -c "metricbeat -e -strict.perms=false -e -system.hostfs=/hostfs
`if [ ! -z $ELASTICSEARCH_USERNAME ]; then echo '-E output.elasticsearch.username=$ELASTICSEARCH_USERNAME'; fi`
`if [ ! -z $ELASTICSEARCH_PASSWORD ]; then echo '-E output.elasticsearch.password=$ELASTICSEARCH_PASSWORD'; fi`"
logging:

View File

@ -19,7 +19,7 @@ fatal_error() {
new_ov_installation() {
printf '\n'
printf '\n ======================================='
printf '\n Install Openvidu PRO %s' "${OPENVIDU_VERSION}"
printf '\n Install OpenVidu Pro %s' "${OPENVIDU_VERSION}"
printf '\n ======================================='
printf '\n'
@ -43,7 +43,7 @@ new_ov_installation() {
chown 1000:1000 "${ELASTICSEARCH_FOLDER}" || fatal_error "Error while changing permission to 'elasticsearch' folder"
# Download necessary files
printf '\n => Downloading Openvidu PRO files:'
printf '\n => Downloading OpenVidu Pro files:'
curl --silent ${DOWNLOAD_URL}/openvidu-server/deployments/pro/docker-compose/openvidu-server-pro/cluster/aws/openvidu_autodiscover.sh \
--output "${AWS_SCRIPTS_FOLDER}/openvidu_autodiscover.sh" || fatal_error "Error when downloading the file 'openvidu_autodiscover.sh'"
@ -108,7 +108,7 @@ new_ov_installation() {
printf '\n'
printf '\n'
printf '\n ======================================='
printf '\n Openvidu PRO successfully installed.'
printf '\n OpenVidu Pro successfully installed.'
printf '\n ======================================='
printf '\n'
printf '\n 1. Go to openvidu folder:'
@ -124,12 +124,23 @@ new_ov_installation() {
printf "\n CAUTION: The folder 'openvidu/elasticsearch' use user and group 1000 permissions. "
printf "\n This folder is necessary for store elasticsearch data."
printf "\n For more information, check:"
printf "\n https://docs.openvidu.io/en/${OPENVIDU_VERSION//v}/openvidu-pro/deployment/on-premises/#deployment-instructions"
printf '\n https://docs.openvidu.io/en/%s/openvidu-pro/deployment/on-premises/#deployment-instructions' "${OPENVIDU_VERSION//v}"
printf '\n'
printf '\n'
exit 0
}
get_previous_env_variable() {
local ENV_VARIABLE_NAME=$1
echo "$(grep -E "${ENV_VARIABLE_NAME}=.*$" "${OPENVIDU_PREVIOUS_FOLDER}/.env" | cut -d'=' -f2)"
}
replace_variable_in_new_env_file() {
local ENV_VARIABLE_NAME=$1
local ENV_VARIABLE_VALUE=$2
[[ -n "${ENV_VARIABLE_VALUE}" ]] && sed -i "s|#${ENV_VARIABLE_NAME}=|${ENV_VARIABLE_NAME}=${ENV_VARIABLE_VALUE}|" "${OPENVIDU_PREVIOUS_FOLDER}/.env-${OPENVIDU_VERSION}"
}
upgrade_ov() {
# Search local Openvidu installation
printf '\n'
@ -156,19 +167,19 @@ upgrade_ov() {
[ -z "${OPENVIDU_PREVIOUS_FOLDER}" ] && fatal_error "No previous Openvidu installation found"
# Uppgrade Openvidu
# Upgrade Openvidu
OPENVIDU_PREVIOUS_VERSION=$(grep 'Openvidu Version:' "${OPENVIDU_PREVIOUS_FOLDER}/docker-compose.yml" | awk '{ print $4 }')
[ -z "${OPENVIDU_PREVIOUS_VERSION}" ] && fatal_error "Can't find previous OpenVidu version"
# In this point using the variable 'OPENVIDU_PREVIOUS_VERSION' we can verify if the upgrade is
# posible or not. If it is not posible launch a warning and stop the upgrade.
if [[ "${OPENVIDU_PREVIOUS_VERSION}" != "${OPENVIDU_UPGRADABLE_VERSION}."* ]]; then
if [[ "${OPENVIDU_PREVIOUS_VERSION}" != "${OPENVIDU_UPGRADABLE_VERSION}."* ]] && [[ "${OPENVIDU_PREVIOUS_VERSION}" != "${OPENVIDU_VERSION//v}"* ]]; then
fatal_error "You can't update from version ${OPENVIDU_PREVIOUS_VERSION} to ${OPENVIDU_VERSION}.\nNever upgrade across multiple major versions."
fi
printf '\n'
printf '\n ======================================='
printf '\n Upgrade Openvidu PRO %s to %s' "${OPENVIDU_PREVIOUS_VERSION}" "${OPENVIDU_VERSION}"
printf '\n Upgrade OpenVidu Pro %s to %s' "${OPENVIDU_PREVIOUS_VERSION}" "${OPENVIDU_VERSION}"
printf '\n ======================================='
printf '\n'
@ -184,7 +195,7 @@ upgrade_ov() {
mkdir "${TMP_FOLDER}" || fatal_error "Error while creating the folder 'temporal'"
# Download necessary files
printf '\n => Downloading new Openvidu PRO files:'
printf '\n => Downloading new OpenVidu Pro files:'
curl --silent ${DOWNLOAD_URL}/openvidu-server/deployments/pro/docker-compose/openvidu-server-pro/cluster/aws/openvidu_autodiscover.sh \
--output "${TMP_FOLDER}/openvidu_autodiscover.sh" || fatal_error "Error when downloading the file 'openvidu_autodiscover.sh'"
@ -222,8 +233,8 @@ upgrade_ov() {
--output "${TMP_FOLDER}/openvidu" || fatal_error "Error when downloading the file 'openvidu'"
printf '\n - openvidu'
# Dowloading new images and stoped actual Openvidu
printf '\n => Dowloading new images...'
# Downloading new images and stopped actual Openvidu
printf '\n => Downloading new images...'
printf '\n'
sleep 1
@ -231,9 +242,9 @@ upgrade_ov() {
printf '\n'
cd "${TMP_FOLDER}" || fatal_error "Error when moving to 'tmp' folder"
printf '\n'
docker-compose pull | true
docker-compose pull || true
printf '\n => Stoping Openvidu...'
printf '\n => Stopping Openvidu...'
printf '\n'
sleep 1
@ -241,7 +252,7 @@ upgrade_ov() {
printf '\n'
cd "${OPENVIDU_PREVIOUS_FOLDER}" || fatal_error "Error when moving to 'openvidu' folder"
printf '\n'
docker-compose down | true
docker-compose down || true
printf '\n'
printf '\n => Moving to working dir...'
@ -253,7 +264,7 @@ upgrade_ov() {
mv "${OPENVIDU_PREVIOUS_FOLDER}/docker-compose.yml" "${ROLL_BACK_FOLDER}" || fatal_error "Error while moving previous 'docker-compose.yml'"
printf '\n - docker-compose.yml'
if [ ! -z "${USE_OV_CALL}" ]; then
if [ -n "${USE_OV_CALL}" ]; then
mv "${OPENVIDU_PREVIOUS_FOLDER}/docker-compose.override.yml" "${ROLL_BACK_FOLDER}" || fatal_error "Error while moving previous 'docker-compose.override.yml'"
printf '\n - docker-compose.override.yml'
fi
@ -281,7 +292,7 @@ upgrade_ov() {
mv "${TMP_FOLDER}/docker-compose.yml" "${OPENVIDU_PREVIOUS_FOLDER}" || fatal_error "Error while updating 'docker-compose.yml'"
printf '\n - docker-compose.yml'
if [ ! -z "${USE_OV_CALL}" ]; then
if [ -n "${USE_OV_CALL}" ]; then
mv "${TMP_FOLDER}/docker-compose.override.yml" "${OPENVIDU_PREVIOUS_FOLDER}" || fatal_error "Error while updating 'docker-compose.override.yml'"
printf '\n - docker-compose.override.yml'
else
@ -334,18 +345,46 @@ upgrade_ov() {
# Define old mode: On Premise or Cloud Formation
OLD_MODE=$(grep -E "Installation Mode:.*$" "${ROLL_BACK_FOLDER}/docker-compose.yml" | awk '{ print $4,$5 }')
[ ! -z "${OLD_MODE}" ] && sed -i -r "s/Installation Mode:.+/Installation Mode: ${OLD_MODE}/" "${OPENVIDU_PREVIOUS_FOLDER}/docker-compose.yml"
[ -n "${OLD_MODE}" ] && sed -i -r "s/Installation Mode:.+/Installation Mode: ${OLD_MODE}/" "${OPENVIDU_PREVIOUS_FOLDER}/docker-compose.yml"
# In Aws, update AMI ID
AWS_REGION=$(grep -E "AWS_DEFAULT_REGION=.*$" "${OPENVIDU_PREVIOUS_FOLDER}/.env" | cut -d'=' -f2)
if [[ ! -z ${AWS_REGION} ]]; then
# Update .env variables to new .env-version
AWS_REGION=$(get_previous_env_variable AWS_DEFAULT_REGION)
if [[ -n ${AWS_REGION} ]]; then
# Get new AMI ID
NEW_AMI_ID=$(curl https://s3-eu-west-1.amazonaws.com/aws.openvidu.io/CF-OpenVidu-Pro-${OPENVIDU_VERSION//v}.yaml --silent |
sed -n -e '/KMSAMIMAP:/,/Metadata:/ p' |
grep -A 1 ${AWS_REGION} | grep AMI | tr -d " " | cut -d":" -f2)
grep -A 1 "${AWS_REGION}" | grep AMI | tr -d " " | cut -d":" -f2)
[[ -z ${NEW_AMI_ID} ]] && fatal_error "Error while getting new AWS_IMAGE_ID for Media Nodes"
sed -i "s/.*AWS_IMAGE_ID=.*/AWS_IMAGE_ID=${NEW_AMI_ID}/" "${OPENVIDU_PREVIOUS_FOLDER}/.env" || fatal_error "Error while updating new AWS_IMAGE_ID for Media Nodes"
# Get previous values
PREV_AWS_DEFAULT_REGION=$(get_previous_env_variable AWS_DEFAULT_REGION)
PREV_AWS_INSTANCE_TYPE=$(get_previous_env_variable AWS_INSTANCE_TYPE)
PREV_AWS_INSTANCE_ID=$(get_previous_env_variable AWS_INSTANCE_ID)
PREV_AWS_KEY_NAME=$(get_previous_env_variable AWS_KEY_NAME)
PREV_AWS_SUBNET_ID=$(get_previous_env_variable AWS_SUBNET_ID)
PREV_AWS_SECURITY_GROUP=$(get_previous_env_variable AWS_SECURITY_GROUP)
PREV_AWS_STACK_ID=$(get_previous_env_variable AWS_STACK_ID)
PREV_AWS_STACK_NAME=$(get_previous_env_variable AWS_STACK_NAME)
PREV_AWS_CLI_DOCKER_TAG=$(get_previous_env_variable AWS_CLI_DOCKER_TAG)
PREV_AWS_VOLUME_SIZE=$(get_previous_env_variable AWS_VOLUME_SIZE)
# Replace variables in new .env-version file
replace_variable_in_new_env_file "AWS_DEFAULT_REGION" "${PREV_AWS_DEFAULT_REGION}"
replace_variable_in_new_env_file "AWS_INSTANCE_TYPE" "${PREV_AWS_INSTANCE_TYPE}"
replace_variable_in_new_env_file "AWS_INSTANCE_ID" "${PREV_AWS_INSTANCE_ID}"
replace_variable_in_new_env_file "AWS_KEY_NAME" "${PREV_AWS_KEY_NAME}"
replace_variable_in_new_env_file "AWS_SUBNET_ID" "${PREV_AWS_SUBNET_ID}"
replace_variable_in_new_env_file "AWS_SECURITY_GROUP" "${PREV_AWS_SECURITY_GROUP}"
replace_variable_in_new_env_file "AWS_STACK_ID" "${PREV_AWS_STACK_ID}"
replace_variable_in_new_env_file "AWS_STACK_NAME" "${PREV_AWS_STACK_NAME}"
replace_variable_in_new_env_file "AWS_CLI_DOCKER_TAG" "${PREV_AWS_CLI_DOCKER_TAG}"
replace_variable_in_new_env_file "AWS_VOLUME_SIZE" "${PREV_AWS_VOLUME_SIZE}"
# Replace new AMI
replace_variable_in_new_env_file "AWS_IMAGE_ID" "${NEW_AMI_ID}"
fi
# Ready to use
printf '\n'
@ -391,7 +430,7 @@ else
fi
# Check type of installation
if [[ ! -z "$1" && "$1" == "upgrade" ]]; then
if [[ -n "$1" && "$1" == "upgrade" ]]; then
upgrade_ov
else
new_ov_installation

View File

@ -4,7 +4,7 @@ upgrade_ov() {
UPGRADE_SCRIPT_URL="https://s3-eu-west-1.amazonaws.com/aws.openvidu.io/install_openvidu_pro_OVVERSION.sh"
HTTP_STATUS=$(curl -s -o /dev/null -I -w "%{http_code}" ${UPGRADE_SCRIPT_URL//OVVERSION/$1})
printf " => Upgrading Openvidu PRO to '%s' version" "$1"
printf " => Upgrading OpenVidu Pro to '%s' version" "$1"
if [ "$HTTP_STATUS" == "200" ]; then
printf "\n => Downloading and upgrading new version"
@ -13,7 +13,7 @@ upgrade_ov() {
curl --silent ${UPGRADE_SCRIPT_URL//OVVERSION/$1} | bash -s upgrade
else
printf "\n =======¡ERROR!======="
printf "\n Openvidu PRO Version '%s' not exist" "$1"
printf "\n OpenVidu Pro Version '%s' not exist" "$1"
printf "\n"
exit 0
fi
@ -28,7 +28,7 @@ collect_basic_information() {
OV_VERSION=$(grep 'Openvidu Version:' "${OV_FOLDER}/docker-compose.yml" | awk '{ print $4 }')
CONTAINERS=$(docker ps | awk '{if(NR>1) print $NF}')
if [ ! -z "$(grep -E '^ image: openvidu/openvidu-call:.*$' "${OV_FOLDER}/docker-compose.override.yml" | tr -d '[:space:]')" ]; then
if [ -n "$(grep -E '^ image: openvidu/openvidu-call:.*$' "${OV_FOLDER}/docker-compose.override.yml" | tr -d '[:space:]')" ]; then
OV_CALL_VERSION=$(grep -E 'Openvidu-Call Version:' "${OV_FOLDER}/docker-compose.override.yml" | awk '{ print $4 }')
fi
[ -z "${OV_CALL_VERSION}" ] && OV_CALL_VERSION="No present"
@ -71,7 +71,7 @@ generate_report() {
REPORT_CREATION_DATE=$(date +"%d-%m-%Y")
REPORT_CREATION_TIME=$(date +"%H:%M:%S")
REPORT_NAME="openvidu-report-${REPORT_CREATION_DATE}-$(date +"%H-%M").txt"
REPORT_OUPUT="${OV_FOLDER}/${REPORT_NAME}"
REPORT_OUTPUT="${OV_FOLDER}/${REPORT_NAME}"
{
printf "\n ======================================="
@ -150,10 +150,10 @@ generate_report() {
do
printf '\n'
printf "\n ---------------------------------------"
printf "\n %s" $CONTAINER
printf "\n %s" "$CONTAINER"
printf "\n ---------------------------------------"
printf '\n'
docker logs $CONTAINER
docker logs "$CONTAINER"
printf "\n ---------------------------------------"
printf '\n'
printf '\n'
@ -167,19 +167,19 @@ generate_report() {
do
printf '\n'
printf "\n ======================================="
printf "\n %s" $CONTAINER
printf "\n %s" "$CONTAINER"
printf "\n ---------------------------------------"
printf '\n'
docker exec $CONTAINER env
docker exec "$CONTAINER" env
printf "\n ---------------------------------------"
printf '\n'
printf '\n'
done
} >> "${REPORT_OUPUT}" 2>&1
} >> "${REPORT_OUTPUT}" 2>&1
printf "\n Generation of the report completed with success"
printf "\n You can get your report at path '%s'" "${REPORT_OUPUT}"
printf "\n You can get your report at path '%s'" "${REPORT_OUTPUT}"
printf "\n"
}
@ -192,7 +192,7 @@ is_external_url() {
return 1
else
return 0
fi
fi
}
start_openvidu() {
@ -224,9 +224,9 @@ usage() {
printf "\n\nAvailable Commands:"
printf "\n\tstart\t\t\tStart all services"
printf "\n\tstop\t\t\tStop all services"
printf "\n\trestart\t\t\tRestart all stoped and running services"
printf "\n\trestart\t\t\tRestart all stopped and running services"
printf "\n\tlogs\t\t\tShow openvidu-server logs"
printf "\n\tupgrade\t\t\tUpgrade to the lastest Openvidu version"
printf "\n\tupgrade\t\t\tUpgrade to the latest Openvidu version"
printf "\n\tupgrade [version]\tUpgrade to the specific Openvidu version"
printf "\n\tversion\t\t\tShow version of Openvidu Server"
printf "\n\treport\t\t\tGenerate a report with the current status of Openvidu"
@ -268,7 +268,7 @@ case $1 in
UPGRADE_VERSION="$2"
fi
read -r -p " You're about to update Openvidu PRO to '${UPGRADE_VERSION}' version. Are you sure? [y/N]: " response
read -r -p " You're about to update OpenVidu Pro to '${UPGRADE_VERSION}' version. Are you sure? [y/N]: " response
case "$response" in
[yY][eE][sS]|[yY])
upgrade_ov "${UPGRADE_VERSION}"
@ -298,4 +298,4 @@ case $1 in
*)
usage
;;
esac
esac

View File

@ -1,14 +1,17 @@
FROM ubuntu:16.04
FROM coturn/coturn:4.5.2-alpine
RUN apt-get update \
&& apt-get install -y coturn curl dnsutils
USER root
COPY ./configuration-files.sh /tmp/
COPY ./entrypoint.sh /usr/local/bin
COPY ./discover_my_public_ip.sh /usr/local/bin
RUN apk add --no-cache bind-tools
RUN chmod +x /tmp/configuration-files.sh \
&& chmod +x /usr/local/bin/entrypoint.sh \
&& chmod +x /usr/local/bin/discover_my_public_ip.sh
# Override detect-external-ip.sh script
COPY ./detect-external-ip.sh /usr/local/bin/detect-external-ip.sh
COPY ./docker-entrypoint.sh /usr/local/bin/docker-entrypoint.sh
ENTRYPOINT [ "/usr/local/bin/entrypoint.sh" ]
RUN chmod +x /usr/local/bin/detect-external-ip.sh /usr/local/bin/docker-entrypoint.sh && \
chown -R nobody:nogroup /var/lib/coturn/ && \
touch /turnserver.conf && chown nobody:nogroup /turnserver.conf
USER nobody:nogroup
ENTRYPOINT ["docker-entrypoint.sh"]
CMD ["--log-file=stdout", "--external-ip=$(detect-external-ip)"]

View File

@ -1,42 +1,4 @@
# Coturn OpenVidu
# OpenVidu Coturn
This is a docker image to launch a coturn server. Environment variables can be defined to modify the files `/etc/default/coturn` and `cat>/etc/turnserver.conf`.
## Environment variables
### Turn configuration
- TURN_PUBLIC_IP: Public ip where coturn will be placed. If this environment variable is not setted, it will get the ip returned by `curl ifconfig.co`.
- TURN_LISTEN_PORT: Port where turn will be listening.
### Turn credentials
- REDIS_IP: Redis where credentials are stored
- DB_NAME: Name of the database in redis
- DB_PASSWORD: Password of the redis database
# Execution example
Actual version of OpenVidu need to be located in the same node because tokens sends the url for turn/stun connections with the host url.
## Execute turn locally next to the redis database
You need to have a redis database running:
```
docker run --rm --name some-redis -d -p 6379:6379 redis
```
Get the ip of the container and after that, run coturn, you can use url as ip too, in this example I am running coturn with nip.io:
```
docker run -it -e REDIS_IP=172.17.0.2 -e DB_NAME=0 -e DB_PASSWORD=turn -e MIN_PORT=40000 -e MAX_PORT=65535 -e TURN_PUBLIC_IP=auto -e TURN_LISTEN_PORT=3478 --network=host openvidu/openvidu-coturn
```
## Execute turn locally with fixed username and password
```
docker run -it -e TURN_PUBLIC_IP=auto -e TURN_USERNAME_PASSWORD=<USER>:<PASSWORD> -e MIN_PORT=40000 -e MAX_PORT=65535 -e TURN_LISTEN_PORT=3478 --network=host openvidu/openvidu-coturn
```
# Kubernetes
TODO
This is a minor modification from the official [coturn/coturn](https://hub.docker.com/r/coturn/coturn) image.
It just replace the `detect-external-ip.sh` with a custom one to use DNS to resolve getting the public IP.

View File

@ -1,31 +0,0 @@
#!/bin/bash
# Enable turn
cat>/etc/default/coturn<<EOF
TURNSERVER_ENABLED=1
EOF
# Turn server configuration
cat>/etc/turnserver.conf<<EOF
listening-port=${TURN_LISTEN_PORT}
fingerprint
lt-cred-mech
max-port=${MAX_PORT:-65535}
min-port=${MIN_PORT:-40000}
simple-log
pidfile="/var/run/turnserver.pid"
realm=openvidu
verbose
EOF
if [[ ! -z "${TURN_PUBLIC_IP}" ]]; then
echo "external-ip=${TURN_PUBLIC_IP}" >> /etc/turnserver.conf
fi
if [[ ! -z "${REDIS_IP}" ]] && [[ ! -z "${DB_NAME}" ]] && [[ ! -z "${DB_PASSWORD}" ]]; then
echo "redis-userdb=\"ip=${REDIS_IP} dbname=${DB_NAME} password=${DB_PASSWORD} connect_timeout=30\"" >> /etc/turnserver.conf
fi
if [[ ! -z "${TURN_USERNAME_PASSWORD}" ]]; then
echo "user=${TURN_USERNAME_PASSWORD}" >> /etc/turnserver.conf
fi

View File

@ -1,10 +1,8 @@
#!/bin/bash
VERSION=$1
if [[ ! -z $VERSION ]]; then
cp ../utils/discover_my_public_ip.sh ./discover_my_public_ip.sh
docker build --rm -t openvidu/openvidu-coturn:$VERSION .
rm ./discover_my_public_ip.sh
docker build --pull --no-cache --rm=true -t openvidu/openvidu-coturn:$VERSION .
else
echo "Error: You need to specify a version as first argument"
fi
fi

View File

@ -0,0 +1,115 @@
#!/usr/bin/env sh
# shellcheck shell=dash
#/ Use DNS to find out about the external IP of the running system.
#/
#/ This script is useful when running from a machine that sits behind a NAT.
#/ Due to how NAT works, machines behind it belong to an internal or private
#/ subnet, with a different address space than the external or public side.
#/
#/ Typically it is possible to make an HTTP request to a number of providers
#/ that offer the external IP in their response body (eg: ifconfig.me). However,
#/ why do a slow and heavy HTTP request, when DNS exists and is much faster?
#/ Well established providers such as OpenDNS or Google offer special hostnames
#/ that, when resolved, will actually return the IP address of the caller.
#/
#/ https://unix.stackexchange.com/questions/22615/how-can-i-get-my-external-ip-address-in-a-shell-script/81699#81699
#/
#/
#/ Arguments
#/ ---------
#/
#/ --ipv4
#/
#/ Find the external IPv4 address.
#/ Optional. Default: Enabled.
#/
#/ --ipv6
#/
#/ Find the external IPv6 address.
#/ Optional. Default: Disabled.
# Shell setup
# ===========
# Shell options for strict error checking.
for OPTION in errexit errtrace pipefail nounset; do
set -o | grep -wq "$OPTION" && set -o "$OPTION"
done
# Trace all commands (to stderr).
#set -o xtrace
# Shortcut: REAL_EXTERNAL_IP
# ==========================
if [ -n "${REAL_EXTERNAL_IP:-}" ]; then
echo "$REAL_EXTERNAL_IP"
exit 0
fi
# Parse call arguments
# ====================
CFG_IPV4="true"
while [ $# -gt 0 ]; do
case "${1-}" in
--ipv4) CFG_IPV4="true" ;;
--ipv6) CFG_IPV4="false" ;;
*)
echo "Invalid argument: '${1-}'" >&2
exit 1
;;
esac
shift
done
# Discover the external IP address
# ================================
if [ "$CFG_IPV4" = "true" ]; then
COMMANDS='dig @resolver1.opendns.com myip.opendns.com A -4 +short
dig @ns1.google.com o-o.myaddr.l.google.com TXT -4 +short | tr -d \"
dig @1.1.1.1 whoami.cloudflare TXT CH -4 +short | tr -d \"
dig @ns1-1.akamaitech.net whoami.akamai.net A -4 +short'
is_valid_ip() {
# Check if the input looks like an IPv4 address.
# Doesn't check if the actual values are valid; assumes they are.
echo "$1" | grep -Eq '^([0-9]{1,3}\.){3}[0-9]{1,3}$'
}
else
COMMANDS='dig @resolver1.opendns.com myip.opendns.com AAAA -6 +short
dig @ns1.google.com o-o.myaddr.l.google.com TXT -6 +short | tr -d \"
dig @2606:4700:4700::1111 whoami.cloudflare TXT CH -6 +short | tr -d \"'
is_valid_ip() {
# Check if the input looks like an IPv6 address.
# It's almost impossible to check the IPv6 representation because it
# varies wildly, so just check that there are at least 2 colons.
[ "$(echo "$1" | awk -F':' '{print NF-1}')" -ge 2 ]
}
fi
echo "$COMMANDS" | while read -r COMMAND; do
if IP="$(eval "$COMMAND")" && is_valid_ip "$IP"; then
echo "$IP"
exit 100 # Exits the pipe subshell.
fi
done
if [ $? -eq 100 ]; then
exit 0
else
echo "[$0] All providers failed" >&2
exit 1
fi

View File

@ -0,0 +1,11 @@
#!/bin/sh
if [ ! -z "${REDIS_IP}" ] && [ ! -z "${DB_NAME}" ] && [ ! -z "${DB_PASSWORD}" ]; then
echo "redis-userdb=\"ip=${REDIS_IP} dbname=${DB_NAME} password=${DB_PASSWORD} connect_timeout=30\"" > turnserver.conf
fi
# If command starts with an option, prepend with turnserver binary.
if [ "${1:0:1}" == '-' ]; then
set -- turnserver "$@"
fi
exec $(eval "echo $@")

View File

@ -1,31 +0,0 @@
#!/bin/bash
# Set debug mode
DEBUG=${DEBUG:-false}
[ "$DEBUG" == "true" ] && set -x
#Check parameters
[[ "${TURN_PUBLIC_IP}" == "auto-ipv4" ]] && export TURN_PUBLIC_IP=$(/usr/local/bin/discover_my_public_ip.sh)
[[ "${TURN_PUBLIC_IP}" == "auto-ipv6" ]] && export TURN_PUBLIC_IP=$(/usr/local/bin/discover_my_public_ip.sh --ipv6)
[[ -z "${ENABLE_COTURN_LOGS}" ]] && export ENABLE_COTURN_LOGS=true
echo "TURN public IP: ${TURN_PUBLIC_IP:-"empty"}"
[[ ! -z "${TURN_LISTEN_PORT}" ]] && echo "TURN listening port: ${TURN_LISTEN_PORT}" ||
{ echo "TURN_LISTEN_PORT environment variable is not defined"; exit 1; }
[[ ! -z "${MIN_PORT}" ]] && echo "Defined min port coturn: ${MIN_PORT}" || echo "Min port coturn: 40000"
[[ ! -z "${MAX_PORT}" ]] && echo "Defined max port coturn: ${MAX_PORT}" || echo "Max port coturn: 65535"
# Load configuration files of coturn
source /tmp/configuration-files.sh
# Remove temp file with configuration parameters
rm /tmp/configuration-files.sh
if [[ "${ENABLE_COTURN_LOGS}" == "true" ]]; then
/usr/bin/turnserver -c /etc/turnserver.conf -v --log-file /dev/null
else
/usr/bin/turnserver -c /etc/turnserver.conf -v --log-file /dev/null --no-stdout-log
fi

View File

@ -1,4 +1,4 @@
FROM nginx:1.18.0-alpine
FROM nginx:1.20.0-alpine
# Install required software
RUN apk update && \

View File

@ -2,7 +2,7 @@ VERSION=$1
if [[ ! -z $VERSION ]]; then
cp ../utils/discover_my_public_ip.sh ./discover_my_public_ip.sh
docker build -t openvidu/openvidu-proxy:$VERSION .
docker build --pull --no-cache --rm=true -t openvidu/openvidu-proxy:$VERSION .
rm ./discover_my_public_ip.sh
else

View File

@ -14,14 +14,31 @@
proxy_pass http://openviduserver;
}
location ~ /openvidu$ {
proxy_pass http://openviduserver;
}
location /kibana {
{rules_access_dashboard}
deny all;
rewrite ^/kibana/(.*)$ /$1 break;
proxy_pass http://kibana/;
}
location ~ ^/openvidu/elasticsearch$ {
{rules_access_dashboard}
deny all;
rewrite ^/openvidu/elasticsearch$ / break;
proxy_pass http://elasticsearch;
}
location ~ ^/openvidu/elasticsearch/.*$ {
{rules_access_dashboard}
deny all;
rewrite ^/openvidu/elasticsearch(.*)$ $1 break;
proxy_pass http://elasticsearch;
}
location ~ /openvidu$ {
proxy_pass http://openviduserver;
}

View File

@ -8,6 +8,10 @@ upstream kibana {
server localhost:5601;
}
upstream elasticsearch {
server localhost:9200;
}
upstream openviduserver {
server localhost:5443;
}

View File

@ -43,6 +43,7 @@ CERTIFICATES_CONF="${CERTIFICATES_LIVE_FOLDER}/certificates.conf"
[ -z "${REDIRECT_WWW}" ] && export REDIRECT_WWW=false
[ -z "${PROXY_MODE}" ] && export PROXY_MODE=CE
[ -z "${WORKER_CONNECTIONS}" ] && export WORKER_CONNECTIONS=10240
[ -z "${CLIENT_MAX_BODY_SIZE}" ] && export CLIENT_MAX_BODY_SIZE=200M
[ -z "${PUBLIC_IP}" ] && export PUBLIC_IP=auto-ipv4
[ -z "${ALLOWED_ACCESS_TO_DASHBOARD}" ] && export ALLOWED_ACCESS_TO_DASHBOARD=all
[ -z "${ALLOWED_ACCESS_TO_RESTAPI}" ] && export ALLOWED_ACCESS_TO_RESTAPI=all
@ -77,6 +78,7 @@ printf "\n"
# Override worker connections
sed -i "s/{worker_connections}/${WORKER_CONNECTIONS}/g" /etc/nginx/nginx.conf
sed -i "s/{client_max_body_size}/${CLIENT_MAX_BODY_SIZE}/g" /etc/nginx/nginx.conf
printf "\n Configure %s domain..." "${DOMAIN_OR_PUBLIC_IP}"
OLD_DOMAIN_OR_PUBLIC_IP=$(head -n 1 "${CERTIFICATES_CONF}" | cut -f1 -d$'\t')

View File

@ -29,6 +29,8 @@ http {
server_tokens off;
client_max_body_size {client_max_body_size};
include /etc/nginx/conf.d/*.conf;
include /etc/nginx/vhost.d/*.conf;
}

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