Merge pull request #1 from techcouncilbox/master

update from original repo
pull/88/head
miguelrcDEV 2018-03-19 12:57:00 +01:00 committed by GitHub
commit 517edd0ac6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
130 changed files with 46631 additions and 37592 deletions

10
.gitignore vendored
View File

@ -16,7 +16,13 @@ nbactions.xml
*bower_components/
.externalToolBuilders
*bin/
/.vscode
*/.vscode/*
*/.sts4-cache/*
*/.project
*/.classpath
*/.settings/*
.idea/
log/
\.vscode/
*.iml

View File

@ -8,7 +8,7 @@
"sdp-translator": "0.1.24",
"ua-parser-js": "0.7.17",
"uuid": "3.1.0",
"webrtc-adapter": "6.0.4",
"webrtc-adapter": "6.1.1",
"wolfy87-eventemitter": "5.2.4"
},
"description": "OpenVidu Browser",
@ -28,10 +28,9 @@
"scripts": {
"browserify": "VERSION=${VERSION:-}; cd ts/OpenVidu && browserify Main.ts -p [ tsify ] --exclude kurento-browser-extensions --debug -o ../../static/js/openvidu-browser-$VERSION.js -v",
"browserify-prod": "VERSION=${VERSION:-}; cd ts/OpenVidu && browserify --debug Main.ts -p [ tsify ] --exclude kurento-browser-extensions | uglifyjs --source-map content=inline --output ../../static/js/openvidu-browser-$VERSION.min.js",
"prepublish": "cd ts/OpenViduInternal && tsc && cd ../OpenVidu && tsc && cd ../.. && tsc --declaration ts/OpenVidu/index.ts --outDir lib --sourceMap && tsc --declaration ts/OpenVidu/Main.ts --outDir lib --sourceMap",
"test": "echo \"Error: no test specified\" && exit 1",
"updatetsc": "cd ts/OpenViduInternal && tsc && cd ../OpenVidu && tsc && cd ../.. && tsc --declaration ts/OpenVidu/index.ts --outDir lib --sourceMap && tsc --declaration ts/OpenVidu/Main.ts --outDir lib --sourceMap"
"updatetsc": "cd ts/OpenViduInternal && tsc && cd ../OpenVidu && tsc && cd ../.. && tsc --declaration ts/OpenVidu/index.ts --outDir lib --sourceMap --lib dom,es5,es2015.promise,scripthost && tsc --declaration ts/OpenVidu/Main.ts --outDir lib --sourceMap --lib dom,es5,es2015.promise,scripthost"
},
"types": "lib/OpenVidu/index.d.ts",
"version": "1.7.0"
"version": "1.8.0"
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

View File

@ -19,11 +19,13 @@ import { OpenViduInternal } from '../OpenViduInternal/OpenViduInternal';
import { Session } from './Session';
import { Publisher } from './Publisher';
import { OpenViduError, OpenViduErrorName } from '../OpenViduInternal/OpenViduError';
import { OutboundStreamOptions } from '../OpenViduInternal/index';
import { Stream } from '../OpenViduInternal/Stream';
import { LocalRecorder } from '../OpenViduInternal/LocalRecorder';
import * as adapter from 'webrtc-adapter';
import * as screenSharing from '../ScreenSharing/Screen-Capturing.js';
import * as screenSharingAuto from '../ScreenSharing/Screen-Capturing-Auto.js';
import * as DetectRTC from '../KurentoUtils/DetectRTC';
if (window) {
window["adapter"] = adapter;
@ -42,14 +44,10 @@ export class OpenVidu {
initSession(sessionId: string): Session;
initSession(param1, param2?): any {
if (this.checkSystemRequirements()) {
if (typeof param2 == "string") {
return new Session(this.openVidu.initSession(param2), this);
} else {
return new Session(this.openVidu.initSession(param1), this);
}
if (typeof param2 == "string") {
return new Session(this.openVidu.initSession(param2), this);
} else {
alert("Browser not supported");
return new Session(this.openVidu.initSession(param1), this);
}
}
@ -58,139 +56,139 @@ export class OpenVidu {
initPublisher(parentId: string, cameraOptions: any, callback: any): Publisher;
initPublisher(parentId: string, cameraOptions?: any, callback?: Function): any {
if (this.checkSystemRequirements()) {
let publisher: Publisher;
if (cameraOptions != null) {
cameraOptions.audio = cameraOptions.audio != null ? cameraOptions.audio : true;
cameraOptions.video = cameraOptions.video != null ? cameraOptions.video : true;
let publisher: Publisher;
if (cameraOptions != null) {
if (!cameraOptions.screen) {
cameraOptions.audio = cameraOptions.audio != null ? cameraOptions.audio : true;
cameraOptions.video = cameraOptions.video != null ? cameraOptions.video : true;
// Webcam and/or microphone is being requested
if (!cameraOptions.screen) {
let cameraOptionsAux = {
sendAudio: cameraOptions.audio != null ? cameraOptions.audio : true,
sendVideo: cameraOptions.video != null ? cameraOptions.video : true,
activeAudio: cameraOptions.audioActive != null ? cameraOptions.audioActive : true,
activeVideo: cameraOptions.videoActive != null ? cameraOptions.videoActive : true,
dataChannel: true,
mediaConstraints: this.openVidu.generateMediaConstraints(cameraOptions)
};
cameraOptions = cameraOptionsAux;
// Webcam and/or microphone is being requested
publisher = new Publisher(this.openVidu.initPublisherTagged(parentId, cameraOptions, true, callback), parentId, false);
console.info("'Publisher' initialized");
return publisher;
} else {
publisher = new Publisher(this.openVidu.initPublisherScreen(parentId, true, callback), parentId, true);
if (adapter.browserDetails.browser === 'firefox' && adapter.browserDetails.version >= 52) {
screenSharingAuto.getScreenId((error, sourceId, screenConstraints) => {
cameraOptions = {
sendAudio: cameraOptions.audio,
sendVideo: cameraOptions.video,
activeAudio: cameraOptions.audioActive != null ? cameraOptions.audioActive : true,
activeVideo: cameraOptions.videoActive != null ? cameraOptions.videoActive : true,
dataChannel: true,
mediaConstraints: {
video: screenConstraints.video,
audio: false
}
}
publisher.stream.configureScreenOptions(cameraOptions);
console.info("'Publisher' initialized");
publisher.stream.ee.emitEvent('can-request-screen');
});
return publisher;
} else if (adapter.browserDetails.browser === 'chrome') {
// Screen is being requested
/*screenSharing.isChromeExtensionAvailable((availability) => {
switch (availability) {
case 'available':
console.warn('EXTENSION AVAILABLE!!!');
screenSharing.getScreenConstraints((error, screenConstraints) => {
if (!error) {
console.warn(screenConstraints);
}
});
break;
case 'unavailable':
console.warn('EXTENSION NOT AVAILABLE!!!');
break;
case 'isFirefox':
console.warn('IT IS FIREFOX!!!');
screenSharing.getScreenConstraints((error, screenConstraints) => {
if (!error) {
console.warn(screenConstraints);
}
});
break;
}
});*/
screenSharingAuto.getScreenId((error, sourceId, screenConstraints) => {
if (error === 'not-installed') {
let error = new OpenViduError(OpenViduErrorName.SCREEN_EXTENSION_NOT_INSTALLED, 'https://chrome.google.com/webstore/detail/screen-capturing/ajhifddimkapgcifgcodmmfdlknahffk');
console.error(error);
if (callback) callback(error);
return;
} else if (error === 'permission-denied') {
let error = new OpenViduError(OpenViduErrorName.SCREEN_CAPTURE_DENIED, 'You must allow access to one window of your desktop');
console.error(error);
if (callback) callback(error);
return;
}
cameraOptions = {
sendAudio: cameraOptions.audio != null ? cameraOptions.audio : true,
sendVideo: cameraOptions.video != null ? cameraOptions.video : true,
activeAudio: cameraOptions.audioActive != null ? cameraOptions.audioActive : true,
activeVideo: cameraOptions.videoActive != null ? cameraOptions.videoActive : true,
dataChannel: true,
mediaConstraints: {
video: screenConstraints.video,
audio: false
}
}
publisher.stream.configureScreenOptions(cameraOptions);
publisher.stream.ee.emitEvent('can-request-screen');
}, (error) => {
console.error('getScreenId error', error);
return;
});
console.info("'Publisher' initialized");
return publisher;
} else {
console.error('Screen sharing not supported on ' + adapter.browserDetails.browser);
}
}
} else {
cameraOptions = {
sendAudio: true,
sendVideo: true,
activeAudio: true,
activeVideo: true,
let cameraOptionsAux = {
sendAudio: cameraOptions.audio != null ? cameraOptions.audio : true,
sendVideo: cameraOptions.video != null ? cameraOptions.video : true,
activeAudio: cameraOptions.audioActive != null ? cameraOptions.audioActive : true,
activeVideo: cameraOptions.videoActive != null ? cameraOptions.videoActive : true,
dataChannel: true,
mediaConstraints: {
audio: true,
video: { width: { ideal: 1280 } }
}
}
mediaConstraints: this.openVidu.generateMediaConstraints(cameraOptions)
};
cameraOptions = cameraOptionsAux;
publisher = new Publisher(this.openVidu.initPublisherTagged(parentId, cameraOptions, true, callback), parentId, false);
console.info("'Publisher' initialized");
return publisher;
} else {
// Screen share is being requested
publisher = new Publisher(this.openVidu.initPublisherScreen(parentId, true, callback), parentId, true);
if (DetectRTC.browser.name === 'Firefox' && DetectRTC.browser.version >= 52) {
screenSharingAuto.getScreenId((error, sourceId, screenConstraints) => {
cameraOptions = {
sendAudio: cameraOptions.audio,
sendVideo: cameraOptions.video,
activeAudio: cameraOptions.audioActive != null ? cameraOptions.audioActive : true,
activeVideo: cameraOptions.videoActive != null ? cameraOptions.videoActive : true,
dataChannel: true,
mediaConstraints: {
video: screenConstraints.video,
audio: false
}
}
publisher.stream.configureScreenOptions(cameraOptions);
console.info("'Publisher' initialized");
publisher.stream.ee.emitEvent('can-request-screen');
});
return publisher;
} else if (DetectRTC.browser.name === 'Chrome') {
// Screen is being requested
/*screenSharing.isChromeExtensionAvailable((availability) => {
switch (availability) {
case 'available':
console.warn('EXTENSION AVAILABLE!!!');
screenSharing.getScreenConstraints((error, screenConstraints) => {
if (!error) {
console.warn(screenConstraints);
}
});
break;
case 'unavailable':
console.warn('EXTENSION NOT AVAILABLE!!!');
break;
case 'isFirefox':
console.warn('IT IS FIREFOX!!!');
screenSharing.getScreenConstraints((error, screenConstraints) => {
if (!error) {
console.warn(screenConstraints);
}
});
break;
}
});*/
screenSharingAuto.getScreenId((error, sourceId, screenConstraints) => {
if (error === 'not-installed') {
let error = new OpenViduError(OpenViduErrorName.SCREEN_EXTENSION_NOT_INSTALLED, 'https://chrome.google.com/webstore/detail/screen-capturing/ajhifddimkapgcifgcodmmfdlknahffk');
console.error(error);
if (callback) callback(error);
return;
} else if (error === 'permission-denied') {
let error = new OpenViduError(OpenViduErrorName.SCREEN_CAPTURE_DENIED, 'You must allow access to one window of your desktop');
console.error(error);
if (callback) callback(error);
return;
}
cameraOptions = {
sendAudio: cameraOptions.audio != null ? cameraOptions.audio : true,
sendVideo: cameraOptions.video != null ? cameraOptions.video : true,
activeAudio: cameraOptions.audioActive != null ? cameraOptions.audioActive : true,
activeVideo: cameraOptions.videoActive != null ? cameraOptions.videoActive : true,
dataChannel: true,
mediaConstraints: {
video: screenConstraints.video,
audio: false
}
}
publisher.stream.configureScreenOptions(cameraOptions);
publisher.stream.ee.emitEvent('can-request-screen');
}, (error) => {
console.error('getScreenId error', error);
return;
});
console.info("'Publisher' initialized");
return publisher;
} else {
console.error('Screen sharing not supported on ' + DetectRTC.browser.name);
if (!!callback) callback(new OpenViduError(OpenViduErrorName.SCREEN_SHARING_NOT_SUPPORTED, 'Screen sharing not supported on ' + DetectRTC.browser.name + ' ' + DetectRTC.browser.version));
}
}
} else {
alert("Browser not supported");
cameraOptions = {
sendAudio: true,
sendVideo: true,
activeAudio: true,
activeVideo: true,
dataChannel: true,
mediaConstraints: {
audio: true,
video: { width: { ideal: 1280 } }
}
}
publisher = new Publisher(this.openVidu.initPublisherTagged(parentId, cameraOptions, true, callback), parentId, false);
console.info("'Publisher' initialized");
return publisher;
}
}
@ -201,7 +199,7 @@ export class OpenVidu {
return publisher;
} else {
publisher = new Publisher(this.openVidu.initPublisherScreen(publisher.stream.getParentId(), false), publisher.stream.getParentId(), true);
if (adapter.browserDetails.browser === 'firefox' && adapter.browserDetails.version >= 52) {
if (DetectRTC.browser.name === 'Firefox' && DetectRTC.browser.version >= 52) {
screenSharingAuto.getScreenId((error, sourceId, screenConstraints) => {
publisher.stream.outboundOptions.mediaConstraints.video = screenConstraints.video;
@ -211,7 +209,7 @@ export class OpenVidu {
publisher.stream.ee.emitEvent('can-request-screen');
});
return publisher;
} else if (adapter.browserDetails.browser === 'chrome') {
} else if (DetectRTC.browser.name === 'Chrome') {
screenSharingAuto.getScreenId((error, sourceId, screenConstraints) => {
if (error === 'not-installed') {
let error = new OpenViduError(OpenViduErrorName.SCREEN_EXTENSION_NOT_INSTALLED, 'https://chrome.google.com/webstore/detail/screen-capturing/ajhifddimkapgcifgcodmmfdlknahffk');
@ -233,23 +231,21 @@ export class OpenVidu {
console.info("'Publisher' initialized");
return publisher;
} else {
console.error('Screen sharing not supported on ' + adapter.browserDetails.browser);
console.error('Screen sharing not supported on ' + DetectRTC.browser.name);
}
}
}
checkSystemRequirements(): number {
let browser = adapter.browserDetails.browser;
let version = adapter.browserDetails.version;
//Bug fix: 'navigator.userAgent' in Firefox for Ubuntu 14.04 does not return "Firefox/[version]" in the string, so version returned is null
if ((browser == 'firefox') && (version == null)) {
return 1;
}
if (((browser == 'chrome') && (version >= 28)) || ((browser == 'edge') && (version >= 12)) || ((browser == 'firefox') && (version >= 22))) {
return 1;
} else {
let defaultWebRTCSupport: boolean = DetectRTC.isWebRTCSupported;
let browser = DetectRTC.browser.name;
let version = DetectRTC.browser.version;
if ((browser !== 'Chrome') && (browser !== 'Firefox') && (browser !== 'Opera')) {
return 0;
} else {
return defaultWebRTCSupport ? 1 : 0;
}
}
@ -269,4 +265,8 @@ export class OpenVidu {
console.warn = function () { };
}
initLocalRecorder(stream: Stream): LocalRecorder {
return new LocalRecorder(stream);
}
}

View File

@ -27,7 +27,11 @@ export class Publisher {
// Listens to the deactivation of the default behaviour upon the deletion of a Stream object
this.ee.addListener('stream-destroyed-default', event => {
event.stream.removeVideo();
let s: Stream = event.stream;
s.addOnceEventListener('video-removed', () => {
this.ee.emitEvent('videoElementDestroyed');
});
s.removeVideo();
});
if (document.getElementById(parentId) != null) {
@ -44,7 +48,7 @@ export class Publisher {
}
destroy() {
this.session.unpublish(this);
if (!!this.session) this.session.unpublish(this);
this.stream.dispose();
this.stream.removeVideo(this.element);
return this;

View File

@ -1,12 +1,14 @@
import { SessionInternal, SessionOptions, SignalOptions } from '../OpenViduInternal/SessionInternal';
import { Stream } from '../OpenViduInternal/Stream';
import { Connection } from "../OpenViduInternal/Connection";
import { Connection } from '../OpenViduInternal/Connection';
import { OpenViduError, OpenViduErrorName } from '../OpenViduInternal/OpenViduError';
import { OpenVidu } from './OpenVidu';
import { Publisher } from './Publisher';
import { Subscriber } from './Subscriber';
import EventEmitter = require('wolfy87-eventemitter');
import * as DetectRTC from '../KurentoUtils/DetectRTC';
export class Session {
@ -26,10 +28,10 @@ export class Session {
// Listens to the deactivation of the default behaviour upon the disconnection of a Session
this.session.addEventListener('session-disconnected-default', () => {
let s: Stream;
for (s of this.openVidu.openVidu.getRemoteStreams()) {
s.removeVideo();
for (let streamId in this.session.getRemoteStreams()) {
this.session.getRemoteStreams()[streamId].removeVideo();
}
if (this.connection) {
if (this.connection && (Object.keys(this.connection.getStreams()).length > 0)) {
for (let streamId in this.connection.getStreams()) {
this.connection.getStreams()[streamId].removeVideo();
}
@ -48,21 +50,29 @@ export class Session {
connect(param1, param2, param3?) {
// Early configuration to deactivate automatic subscription to streams
if (param3) {
this.session.configure({
sessionId: this.session.getSessionId(),
participantId: param1,
metadata: this.session.stringClientMetadata(param2),
subscribeToStreams: false
});
this.session.connect(param1, param3);
if (this.openVidu.checkSystemRequirements()) {
this.session.configure({
sessionId: this.session.getSessionId(),
participantId: param1,
metadata: this.session.stringClientMetadata(param2),
subscribeToStreams: false
});
this.session.connect(param1, param3);
} else {
param3(new OpenViduError(OpenViduErrorName.BROWSER_NOT_SUPPORTED, 'Browser ' + DetectRTC.browser.name + ' ' + DetectRTC.browser.version + ' is not supported in OpenVidu'));
}
} else {
this.session.configure({
sessionId: this.session.getSessionId(),
participantId: param1,
metadata: '',
subscribeToStreams: false
});
this.session.connect(param1, param2);
if (this.openVidu.checkSystemRequirements()) {
this.session.configure({
sessionId: this.session.getSessionId(),
participantId: param1,
metadata: '',
subscribeToStreams: false
});
this.session.connect(param1, param2);
} else {
param2(new OpenViduError(OpenViduErrorName.BROWSER_NOT_SUPPORTED, 'Browser ' + DetectRTC.browser.name + ' ' + DetectRTC.browser.version + ' is not supported in OpenVidu'));
}
}
}
@ -99,9 +109,10 @@ export class Session {
}
}
}
private streamPublish(publisher: Publisher) {
publisher.session = this;
this.connection.addStream(publisher.stream);
publisher.stream.publish();
}

View File

@ -15,6 +15,11 @@ export class Subscriber {
if (document.getElementById(parentId) != null) {
this.element = document.getElementById(parentId)!!;
}
// Listens to deletion of the HTML video element of the Subscriber
this.stream.addEventListener('video-removed', () => {
this.ee.emitEvent('videoElementDestroyed');
});
}
on(eventName: string, callback) {
@ -33,7 +38,6 @@ export class Subscriber {
}]);
} else {
this.stream.addOnceEventListener('video-element-created-by-stream', element => {
console.warn("Subscriber emitting videoElementCreated");
this.id = element.id;
this.ee.emitEvent('videoElementCreated', [{
element: element

View File

@ -4,3 +4,4 @@ export * from './Publisher';
export * from './Subscriber';
export * from '../OpenViduInternal/Stream';
export * from '../OpenViduInternal/Connection';
export * from '../OpenViduInternal/LocalRecorder';

View File

@ -21,7 +21,8 @@
"outDir": "../../lib",
"emitBOM": false,
"preserveConstEnums": true,
"sourceMap": true
"sourceMap": true,
"lib": ["dom","es5","es2015.promise","scripthost"]
},
//"buildOnSave": true,
"compileOnSave": true

View File

@ -22,7 +22,7 @@ export class Connection {
console.info( "'Connection' created (" + ( local ? "local" : "remote" ) + ")" + ( local ? "" : ", with 'connectionId' [" + (options ? options.id : '') + "] " ));
if ( options ) {
if ( options && !local ) {
this.connectionId = options.id;
if (options.metadata) {
@ -36,13 +36,14 @@ export class Connection {
}
addStream( stream: Stream ) {
stream.connection = this;
this.streams[stream.streamId] = stream;
this.room.getStreams()[stream.streamId] = stream;
//this.room.getStreams()[stream.streamId] = stream;
}
removeStream( key: string ) {
delete this.streams[key];
delete this.room.getStreams()[key];
//delete this.room.getStreams()[key];
delete this.inboundStreamsOpts;
}

View File

@ -0,0 +1,274 @@
import { Stream } from "./Stream";
declare var MediaRecorder: any;
export const enum LocalRecoderState {
READY = 'READY',
RECORDING = 'RECORDING',
PAUSED = 'PAUSED',
FINISHED = 'FINISHED'
}
export class LocalRecorder {
state: LocalRecoderState;
private stream: Stream;
private connectionId: string;
private mediaRecorder: any;
private chunks: any[] = [];
private blob: Blob;
private count: number = 0;
private id: string;
private videoPreviewSrc: string;
private htmlParentElementId: string;
private videoPreview: HTMLVideoElement;
constructor(stream: Stream) {
this.stream = stream;
this.connectionId = (!!this.stream.connection) ? this.stream.connection.connectionId : 'default-connection';
this.id = this.stream.streamId + '_' + this.connectionId + '_localrecord';
this.state = LocalRecoderState.READY;
}
record() {
if (typeof MediaRecorder === 'undefined') {
console.error('MediaRecorder not supported on your browser. See compatibility in https://caniuse.com/#search=MediaRecorder');
throw (Error('MediaRecorder not supported on your browser. See compatibility in https://caniuse.com/#search=MediaRecorder'));
}
if (this.state !== LocalRecoderState.READY) {
throw (Error('\'LocalRecord.record()\' needs \'LocalRecord.state\' to be \'READY\' (current value: \'' + this.state + '\'). Call \'LocalRecorder.clean()\' or init a new LocalRecorder before'));
}
console.log("Starting local recording of stream '" + this.stream.streamId + "' of connection '" + this.connectionId + "'");
if (typeof MediaRecorder.isTypeSupported == 'function') {
let options;
if (MediaRecorder.isTypeSupported('video/webm;codecs=vp9')) {
options = { mimeType: 'video/webm;codecs=vp9' };
} else if (MediaRecorder.isTypeSupported('video/webm;codecs=h264')) {
options = { mimeType: 'video/webm;codecs=h264' };
} else if (MediaRecorder.isTypeSupported('video/webm;codecs=vp8')) {
options = { mimeType: 'video/webm;codecs=vp8' };
}
console.log('Using mimeType ' + options.mimeType);
this.mediaRecorder = new MediaRecorder(this.stream.getMediaStream(), options);
} else {
console.warn('isTypeSupported is not supported, using default codecs for browser');
this.mediaRecorder = new MediaRecorder(this.stream.getMediaStream());
}
this.mediaRecorder.start(10);
this.mediaRecorder.ondataavailable = (e) => {
this.chunks.push(e.data);
};
this.mediaRecorder.onerror = (e) => {
console.error('MediaRecorder error: ', e);
};
this.mediaRecorder.onstart = () => {
console.log('MediaRecorder started (state=' + this.mediaRecorder.state + ")");
};
this.mediaRecorder.onstop = () => {
this.onStopDefault();
};
this.mediaRecorder.onpause = () => {
console.log('MediaRecorder paused (state=' + this.mediaRecorder.state + ")");
}
this.mediaRecorder.onresume = () => {
console.log('MediaRecorder resumed (state=' + this.mediaRecorder.state + ")");
}
this.mediaRecorder.onwarning = (e) => {
console.log('MediaRecorder warning: ' + e);
};
this.state = LocalRecoderState.RECORDING;
}
stop(): Promise<any> {
return new Promise((resolve, reject) => {
try {
if (this.state === LocalRecoderState.READY || this.state === LocalRecoderState.FINISHED) {
throw (Error('\'LocalRecord.stop()\' needs \'LocalRecord.state\' to be \'RECORDING\' or \'PAUSED\' (current value: \'' + this.state + '\'). Call \'LocalRecorder.start()\' before'));
}
this.mediaRecorder.onstop = () => {
this.onStopDefault();
resolve();
}
} catch (e) {
reject(e);
}
try {
this.mediaRecorder.stop();
} catch (e) {
reject(e);
}
});
}
pause() {
if (this.state !== LocalRecoderState.RECORDING) {
throw (Error('\'LocalRecord.pause()\' needs \'LocalRecord.state\' to be \'RECORDING\' (current value: \'' + this.state + '\'). Call \'LocalRecorder.start()\' or \'LocalRecorder.resume()\' before'));
}
this.mediaRecorder.pause();
this.state = LocalRecoderState.PAUSED;
}
resume() {
if (this.state !== LocalRecoderState.PAUSED) {
throw (Error('\'LocalRecord.resume()\' needs \'LocalRecord.state\' to be \'PAUSED\' (current value: \'' + this.state + '\'). Call \'LocalRecorder.pause()\' before'));
}
this.mediaRecorder.resume();
this.state = LocalRecoderState.RECORDING;
}
preview(parentElement): HTMLVideoElement {
if (this.state !== LocalRecoderState.FINISHED) {
throw (Error('\'LocalRecord.preview()\' needs \'LocalRecord.state\' to be \'FINISHED\' (current value: \'' + this.state + '\'). Call \'LocalRecorder.stop()\' before'));
}
this.videoPreview = document.createElement('video');
this.videoPreview.id = this.id;
this.videoPreview.autoplay = true;
if (typeof parentElement === "string") {
this.htmlParentElementId = parentElement;
let parentElementDom = document.getElementById(parentElement);
if (parentElementDom) {
this.videoPreview = parentElementDom.appendChild(this.videoPreview);
}
} else {
this.htmlParentElementId = parentElement.id;
this.videoPreview = parentElement.appendChild(this.videoPreview);
}
this.videoPreview.src = this.videoPreviewSrc;
return this.videoPreview;
}
clean() {
let f = () => {
delete this.blob;
this.chunks = [];
this.count = 0;
delete this.mediaRecorder;
this.state = LocalRecoderState.READY;
}
if (this.state === LocalRecoderState.RECORDING || this.state === LocalRecoderState.PAUSED) {
this.stop().then(() => f()).catch(() => f());
} else {
f();
}
}
download() {
if (this.state !== LocalRecoderState.FINISHED) {
throw (Error('\'LocalRecord.download()\' needs \'LocalRecord.state\' to be \'FINISHED\' (current value: \'' + this.state + '\'). Call \'LocalRecorder.stop()\' before'));
} else {
let a: HTMLAnchorElement = document.createElement("a");
a.style.display = 'none';
document.body.appendChild(a);
let url = window.URL.createObjectURL(this.blob);
a.href = url;
a.download = this.id + '.webm';
a.click();
window.URL.revokeObjectURL(url);
document.body.removeChild(a);
}
}
getBlob(): Blob {
if (this.state !== LocalRecoderState.FINISHED) {
throw (Error('Call \'LocalRecord.stop()\' before getting Blob file'));
} else {
return this.blob;
}
}
uploadAsBinary(endpoint: string, headers?: any): Promise<any> {
return new Promise((resolve, reject) => {
if (this.state !== LocalRecoderState.FINISHED) {
reject(Error('\'LocalRecord.uploadAsBinary()\' needs \'LocalRecord.state\' to be \'FINISHED\' (current value: \'' + this.state + '\'). Call \'LocalRecorder.stop()\' before'));
} else {
let http = new XMLHttpRequest();
http.open("POST", endpoint, true);
if (typeof headers === 'object') {
for (let key of Object.keys(headers)) {
http.setRequestHeader(key, headers[key]);
}
}
http.onreadystatechange = () => {
if (http.readyState === 4) {
if (http.status.toString().charAt(0) === '2') {
// Success response from server (HTTP status standard: 2XX is success)
resolve(http.responseText);
} else {
reject(Error("Upload error: " + http.status));
}
}
}
http.send(this.blob);
}
});
}
uploadAsMultipartfile(endpoint: string, headers?: any): Promise<any> {
return new Promise((resolve, reject) => {
if (this.state !== LocalRecoderState.FINISHED) {
reject(Error('\'LocalRecord.uploadAsMultipartfile()\' needs \'LocalRecord.state\' to be \'FINISHED\' (current value: \'' + this.state + '\'). Call \'LocalRecorder.stop()\' before'));
} else {
let http = new XMLHttpRequest();
http.open("POST", endpoint, true);
if (typeof headers === 'object') {
for (let key of Object.keys(headers)) {
http.setRequestHeader(key, headers[key]);
}
}
let sendable = new FormData();
sendable.append("file", this.blob, this.id + ".webm");
http.onreadystatechange = () => {
if (http.readyState === 4) {
if (http.status.toString().charAt(0) === '2') {
// Success response from server (HTTP status standard: 2XX is success)
resolve(http.responseText);
} else {
reject(Error("Upload error: " + http.status));
}
}
}
http.send(sendable);
}
});
}
private onStopDefault() {
console.log('MediaRecorder stopped (state=' + this.mediaRecorder.state + ")");
this.blob = new Blob(this.chunks, { type: "video/webm" });
this.chunks = [];
this.videoPreviewSrc = window.URL.createObjectURL(this.blob);
this.state = LocalRecoderState.FINISHED;
}
}

View File

@ -1,9 +1,11 @@
export const enum OpenViduErrorName {
BROWSER_NOT_SUPPORTED = 'BROWSER_NOT_SUPPORTED',
CAMERA_ACCESS_DENIED = 'CAMERA_ACCESS_DENIED',
MICROPHONE_ACCESS_DENIED = 'MICROPHONE_ACCESS_DENIED',
SCREEN_CAPTURE_DENIED = 'SCREEN_CAPTURE_DENIED',
NO_VIDEO_DEVICE = 'NO_VIDEO_DEVICE',
NO_INPUT_DEVICE = 'NO_INPUT_DEVICE',
SCREEN_SHARING_NOT_SUPPORTED = 'SCREEN_SHARING_NOT_SUPPORTED',
SCREEN_EXTENSION_NOT_INSTALLED = 'SCREEN_EXTENSION_NOT_INSTALLED',
GENERIC_ERROR = 'GENERIC_ERROR'
}

View File

@ -15,6 +15,7 @@
*
*/
import { SessionInternal, SessionOptions } from './SessionInternal';
import { Connection } from './Connection';
import { OpenViduError, OpenViduErrorName } from './OpenViduError';
import { Stream, OutboundStreamOptions } from './Stream';
import * as RpcBuilder from '../KurentoUtils/kurento-jsonrpc';
@ -29,7 +30,6 @@ export class OpenViduInternal {
private rpcParams: any;
private callback: Callback<OpenViduInternal>;
private localStream: Stream;
private remoteStreams: Stream[] = [];
private secret: string;
private recorder: boolean = false;
@ -45,7 +45,6 @@ export class OpenViduInternal {
if (newStream) {
if (cameraOptions == null) {
cameraOptions = {
connection: this.session.getLocalParticipant(),
sendAudio: true,
sendVideo: true,
activeAudio: true,
@ -56,8 +55,6 @@ export class OpenViduInternal {
video: { width: { ideal: 1280 } }
}
}
} else {
cameraOptions.connection = this.session.getLocalParticipant();
}
this.localStream = new Stream(this, true, this.session, cameraOptions);
}
@ -139,7 +136,7 @@ export class OpenViduInternal {
return this.localStream;
}
cameraReady(localStream: Stream, parentId: string): HTMLVideoElement {
cameraReady(localStream: Stream, parentId: string) {
this.localStream = localStream;
let videoElement = this.localStream.playOnlyVideo(parentId, null);
this.localStream.emitStreamReadyEvent();
@ -150,9 +147,6 @@ export class OpenViduInternal {
return this.localStream;
}
getRemoteStreams() {
return this.remoteStreams;
}
/* NEW METHODS */
getWsUri() {
@ -236,7 +230,7 @@ export class OpenViduInternal {
if (this.session !== undefined && this.session instanceof SessionInternal) {
return true;
} else {
console.warn('Room instance not found');
console.warn('Session instance not found');
return false;
}
}

View File

@ -5,6 +5,8 @@ import { Publisher } from '../OpenVidu/Publisher';
import EventEmitter = require('wolfy87-eventemitter');
type ObjMap<T> = { [s: string]: T; }
const SECRET_PARAM = '?secret=';
const RECORDER_PARAM = '&recorder=';
@ -28,7 +30,7 @@ export class SessionInternal {
private id: string;
private sessionId: string;
private ee = new EventEmitter();
private streams = {};
private remoteStreams: ObjMap<Stream> = {};
private participants = {};
private publishersSpeaking: Connection[] = [];
private connected = false;
@ -41,7 +43,7 @@ export class SessionInternal {
constructor(private openVidu: OpenViduInternal, sessionId: string) {
this.sessionId = this.getUrlWithoutSecret(sessionId);
this.localParticipant = new Connection(this.openVidu, true, this);
if (!this.openVidu.getWsUri()) {
if (!this.openVidu.getWsUri() && !!sessionId) {
this.processOpenViduUrl(sessionId);
}
}
@ -129,7 +131,7 @@ export class SessionInternal {
if (this.localParticipant) {
if (Object.keys(this.localParticipant.getStreams()).some(streamId =>
this.streams[streamId].isDataChannelEnabled())) {
this.remoteStreams[streamId].isDataChannelEnabled())) {
joinParams.dataChannels = true;
}
}
@ -191,8 +193,8 @@ export class SessionInternal {
for (let stream of roomEvent.streams) {
this.ee.emitEvent('streamCreated', [{ stream }]);
// Adding the remote stream to the OpenVidu object
this.openVidu.getRemoteStreams().push(stream);
// Store the remote stream
this.remoteStreams[stream.streamId] = stream;
}
callback(undefined);
@ -214,6 +216,10 @@ export class SessionInternal {
this.updateSpeakerInterval = options.updateSpeakerInterval || 1500;
this.thresholdSpeaker = options.thresholdSpeaker || -50;
this.activateUpdateMainSpeaker();
if (!this.openVidu.getWsUri()) {
this.processOpenViduUrl(options.sessionId);
}
}
getId() {
@ -269,14 +275,14 @@ export class SessionInternal {
this.openVidu.sendRequest('unsubscribeFromVideo', {
sender: stream.connection.connectionId
},
(error, response) => {
if (error) {
console.error("Error unsubscribing from Subscriber", error);
} else {
console.info("Unsubscribed correctly from " + stream.connection.connectionId);
}
stream.dispose();
});
(error, response) => {
if (error) {
console.error("Error unsubscribing from Subscriber", error);
} else {
console.info("Unsubscribed correctly from " + stream.connection.connectionId);
}
stream.dispose();
});
}
onParticipantPublished(response: ConnectionOptions) {
@ -301,22 +307,26 @@ export class SessionInternal {
console.debug("Remote Connection found in connections list by its id [" + pid + "]");
}
this.participants[pid] = connection;
this.ee.emitEvent('participant-published', [{ connection }]);
let streams = connection.getStreams();
for (let key in streams) {
let stream = streams[key];
if (this.subscribeToStreams) {
stream.subscribe();
}
this.ee.emitEvent('streamCreated', [{ stream }]);
// Adding the remote stream to the OpenVidu object
this.openVidu.getRemoteStreams().push(stream);
if (!this.remoteStreams[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
if (this.subscribeToStreams) {
stream.subscribe();
}
this.ee.emitEvent('streamCreated', [{ stream }]);
// Store the remote stream
this.remoteStreams[stream.streamId] = stream;
}
}
}
@ -335,14 +345,12 @@ export class SessionInternal {
stream: streams[key]
}]);
// Deleting the removed stream from the OpenVidu object
let index = this.openVidu.getRemoteStreams().indexOf(streams[key]);
let stream = this.openVidu.getRemoteStreams()[index];
// Deleting the remote stream
let streamId: string = streams[key].streamId;
let stream: Stream = this.remoteStreams[streamId];
stream.dispose();
this.openVidu.getRemoteStreams().splice(index, 1);
delete this.streams[stream.streamId];
delete this.remoteStreams[stream.streamId];
connection.removeStream(stream.streamId);
}
@ -383,7 +391,6 @@ export class SessionInternal {
let connection: Connection = this.participants[msg.name];
if (connection !== undefined) {
delete this.participants[msg.name];
this.ee.emitEvent('participant-left', [{
connection: connection
@ -399,13 +406,15 @@ export class SessionInternal {
stream: streams[key]
}]);
// Deleting the removed stream from the OpenVidu object
let index = this.openVidu.getRemoteStreams().indexOf(streams[key]);
this.openVidu.getRemoteStreams().splice(index, 1);
// Deleting the remote stream
let streamId: string = streams[key].streamId;
delete this.remoteStreams[streamId];
}
connection.dispose();
delete this.participants[msg.name];
this.ee.emitEvent('connectionDestroyed', [{
connection: connection
}]);
@ -472,21 +481,21 @@ export class SessionInternal {
onRoomClosed(msg) {
console.info("Room closed: " + JSON.stringify(msg));
console.info("Session closed: " + JSON.stringify(msg));
let room = msg.room;
if (room !== undefined) {
this.ee.emitEvent('room-closed', [{
room: room
}]);
} else {
console.warn("Room undefined in on room closed", msg);
console.warn("Session undefined on session closed", msg);
}
}
onLostConnection() {
if (!this.connected) {
console.warn('Not connected to room: if you are not debugging, this is probably a certificate error');
console.warn('Not connected to session: if you are not debugging, this is probably a certificate error');
if (window.confirm('If you are not debugging, this is probably a certificate error at \"' + this.openVidu.getOpenViduServerURL() + '\"\n\nClick OK to navigate and accept it')) {
location.assign(this.openVidu.getOpenViduServerURL() + '/accept-certificate');
};
@ -498,7 +507,7 @@ export class SessionInternal {
if (room !== undefined) {
this.ee.emitEvent('lost-connection', [{ room }]);
} else {
console.warn('Room undefined when lost connection');
console.warn('Session undefined when lost connection');
}
}
@ -580,7 +589,7 @@ export class SessionInternal {
return;
} else if (stream.connection !== this.localParticipant) {
console.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);
"Only moderators can force unpublish on remote Streams via 'forceUnpublish' method", stream);
return;
} else {
stream.dispose();
@ -610,8 +619,8 @@ export class SessionInternal {
}
}
getStreams() {
return this.streams;
getRemoteStreams() {
return this.remoteStreams;
}
addParticipantSpeaking(participantId) {

View File

@ -9,10 +9,12 @@ import { Connection } from './Connection';
import { SessionInternal } from './SessionInternal';
import { OpenViduInternal, Callback } from './OpenViduInternal';
import { OpenViduError, OpenViduErrorName } from './OpenViduError';
import { WebRtcStats } from './WebRtcStats';
import EventEmitter = require('wolfy87-eventemitter');
import * as kurentoUtils from '../KurentoUtils/kurento-utils-js';
import * as adapter from 'webrtc-adapter';
declare var navigator: any;
declare var RTCSessionDescription: any;
@ -50,7 +52,6 @@ export interface InboundStreamOptions {
export interface OutboundStreamOptions {
activeAudio: boolean;
activeVideo: boolean;
connection: Connection;
dataChannel: boolean;
mediaConstraints: any;
sendAudio: boolean;
@ -87,8 +88,11 @@ export class Stream {
public isScreenRequestedReady: boolean = false;
private isScreenRequested = false;
private webRtcStats: WebRtcStats;
constructor(private openVidu: OpenViduInternal, private local: boolean, private room: SessionInternal, options: any) {
if (options !== 'screen-options') {
// Outbound stream (not screen share) or Inbound stream
if ('id' in options) {
this.inboundOptions = options;
} else {
@ -96,11 +100,15 @@ export class Stream {
}
this.streamId = (options.id != null) ? options.id : ((options.sendVideo) ? "CAMERA" : "MICRO");
this.typeOfVideo = (options.typeOfVideo != null) ? options.typeOfVideo : '';
this.connection = options.connection;
if ('recvAudio' in options) {
// Set Connection for an Inbound stream (for Outbound streams will be set on Session.Publish(Publisher))
this.connection = options.connection;
}
} else {
// Outbound stream for screen share
this.isScreenRequested = true;
this.typeOfVideo = 'SCREEN';
this.connection = this.room.getLocalParticipant();
}
this.addEventListener('mediastream-updated', () => {
if (this.video) this.video.srcObject = this.mediaStream;
@ -120,12 +128,14 @@ export class Stream {
if (this.video) {
if (typeof parentElement === "string") {
document.getElementById(parentElement)!.removeChild(this.video);
this.ee.emitEvent('video-removed');
} else if (parentElement instanceof Element) {
parentElement.removeChild(this.video);
}
else if (!parentElement) {
this.ee.emitEvent('video-removed');
} else if (!parentElement) {
if (document.getElementById(this.parentId)) {
document.getElementById(this.parentId)!.removeChild(this.video);
this.ee.emitEvent('video-removed');
}
}
delete this.video;
@ -136,8 +146,8 @@ export class Stream {
return this.video;
}
setVideoElement(video: HTMLVideoElement) {
this.video = video;
setVideoElement(video) {
if (!!video) this.video = video;
}
getParentId() {
@ -225,6 +235,10 @@ export class Stream {
return this.wp;
}
getRTCPeerConnection() {
return this.wp.peerConnection;
}
addEventListener(eventName: string, listener: any) {
this.ee.addListener(eventName, listener);
}
@ -253,45 +267,49 @@ export class Stream {
}
playOnlyVideo(parentElement, thumbnailId) {
if (!!parentElement) {
this.video = document.createElement('video');
this.video = document.createElement('video');
this.video.id = (this.local ? 'local-' : 'remote-') + 'video-' + this.streamId;
this.video.autoplay = true;
this.video.controls = false;
this.ee.emitEvent('mediastream-updated');
this.video.id = (this.local ? 'local-' : 'remote-') + 'video-' + this.streamId;
this.video.autoplay = true;
this.video.controls = false;
this.ee.emitEvent('mediastream-updated');
if (this.local && !this.displayMyRemote()) {
this.video.muted = true;
this.video.oncanplay = () => {
console.info("Local 'Stream' with id [" + this.streamId + "] video is now playing");
this.ee.emitEvent('video-is-playing', [{
element: this.video
}]);
};
} else {
this.video.title = this.streamId;
}
if (typeof parentElement === "string") {
this.parentId = parentElement;
let parentElementDom = document.getElementById(parentElement);
if (parentElementDom) {
this.video = parentElementDom.appendChild(this.video);
this.ee.emitEvent('video-element-created-by-stream', [{
element: this.video
}]);
this.isVideoELementCreated = true;
if (this.local && !this.displayMyRemote()) {
this.video.muted = true;
this.video.oncanplay = () => {
console.info("Local 'Stream' with id [" + this.streamId + "] video is now playing");
this.ee.emitEvent('video-is-playing', [{
element: this.video
}]);
};
} else {
this.video.title = this.streamId;
}
} else {
this.parentId = parentElement.id;
this.video = parentElement.appendChild(this.video);
if (typeof parentElement === "string") {
this.parentId = parentElement;
let parentElementDom = document.getElementById(parentElement);
if (parentElementDom) {
this.video = parentElementDom.appendChild(this.video);
this.ee.emitEvent('video-element-created-by-stream', [{
element: this.video
}]);
this.isVideoELementCreated = true;
}
} else {
this.parentId = parentElement.id;
this.video = parentElement.appendChild(this.video);
}
this.isReadyToPublish = true;
return this.video;
}
this.isReadyToPublish = true;
return this.video;
return null;
}
playThumbnail(thumbnailId) {
@ -324,14 +342,8 @@ export class Stream {
return this.connection;
}
getRTCPeerConnection() {
return this.getWebRtcPeer().peerConnection;
}
requestCameraAccess(callback: Callback<Stream>) {
this.connection.addStream(this);
let constraints = this.outboundOptions.mediaConstraints;
/*let constraints2 = {
@ -433,7 +445,7 @@ export class Stream {
doLoopback: this.displayMyRemote() || false,
audioActive: this.outboundOptions.sendAudio,
videoActive: this.outboundOptions.sendVideo,
typeOfVideo: ((this.outboundOptions.sendVideo) ? ((this.isScreenRequested) ? 'SCREEN' :'CAMERA') : '')
typeOfVideo: ((this.outboundOptions.sendVideo) ? ((this.isScreenRequested) ? 'SCREEN' : 'CAMERA') : '')
}, (error, response) => {
if (error) {
console.error("Error on publishVideo: " + JSON.stringify(error));
@ -594,26 +606,32 @@ export class Stream {
}
}
// let thumbnailId = this.video.thumb;
this.video.oncanplay = () => {
if (this.local && this.displayMyRemote()) {
console.info("Your own remote 'Stream' with id [" + this.streamId + "] video is now playing");
this.ee.emitEvent('remote-video-is-playing', [{
element: this.video
}]);
} else if (!this.local && !this.displayMyRemote()) {
console.info("Remote 'Stream' with id [" + this.streamId + "] video is now playing");
this.ee.emitEvent('video-is-playing', [{
element: this.video
}]);
}
//show(thumbnailId);
//this.hideSpinner(this.streamId);
};
if (!!this.video) {
// let thumbnailId = this.video.thumb;
this.video.oncanplay = () => {
if (this.local && this.displayMyRemote()) {
console.info("Your own remote 'Stream' with id [" + this.streamId + "] video is now playing");
this.ee.emitEvent('remote-video-is-playing', [{
element: this.video
}]);
} else if (!this.local && !this.displayMyRemote()) {
console.info("Remote 'Stream' with id [" + this.streamId + "] video is now playing");
this.ee.emitEvent('video-is-playing', [{
element: this.video
}]);
}
//show(thumbnailId);
//this.hideSpinner(this.streamId);
};
}
this.room.emitEvent('stream-subscribed', [{
stream: this
}]);
}
this.initWebRtcStats();
}, error => {
console.error(this.streamId + ": Error setting SDP to the peer connection: "
+ JSON.stringify(error));
@ -668,6 +686,8 @@ export class Stream {
this.speechEvent.stop();
}
this.stopWebRtcStats();
console.info((this.local ? "Local " : "Remote ") + "'Stream' with id [" + this.streamId + "]' has been succesfully disposed");
}
@ -675,4 +695,16 @@ export class Stream {
this.outboundOptions = options;
this.streamId = "SCREEN";
}
private initWebRtcStats(): void {
this.webRtcStats = new WebRtcStats(this);
this.webRtcStats.initWebRtcStats();
}
private stopWebRtcStats() {
if (this.webRtcStats != null && this.webRtcStats.isEnabled()) {
this.webRtcStats.stopWebRtcStats();
}
}
}

View File

@ -0,0 +1,326 @@
import { Stream } from './Stream';
import * as adapter from 'webrtc-adapter';
import * as DetectRTC from '../KurentoUtils/DetectRTC';
export class WebRtcStats {
private webRtcStatsEnabled: boolean = false;
private webRtcStatsIntervalId: number;
private statsInterval: number = 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
}
}
}
constructor(private stream: Stream) { }
public isEnabled(): boolean {
return this.webRtcStatsEnabled;
}
public initWebRtcStats(): void {
let elastestInstrumentation = localStorage.getItem('elastest-instrumentation');
if (elastestInstrumentation) {
// ElasTest instrumentation object found in local storage
console.warn("WebRtc stats enabled for stream " + this.stream.streamId + " of connection " + this.stream.connection.connectionId);
this.webRtcStatsEnabled = true;
let instrumentation = JSON.parse(elastestInstrumentation);
this.statsInterval = instrumentation.webrtc.interval; // Interval in seconds
console.warn("localStorage item: " + JSON.stringify(instrumentation));
this.webRtcStatsIntervalId = setInterval(() => {
this.sendStatsToHttpEndpoint(instrumentation);
}, this.statsInterval * 1000);
return;
}
console.debug("WebRtc stats not enabled");
}
public stopWebRtcStats() {
if (this.webRtcStatsEnabled) {
clearInterval(this.webRtcStatsIntervalId);
console.warn("WebRtc stats stopped for disposed stream " + this.stream.streamId + " of connection " + this.stream.connection.connectionId);
}
}
private sendStatsToHttpEndpoint(instrumentation): void {
let sendPost = (json) => {
let http: XMLHttpRequest = new XMLHttpRequest();
let 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) {
console.log("WebRtc stats succesfully sent to " + url + " for stream " + this.stream.streamId + " of connection " + this.stream.connection.connectionId);
}
}
http.send(json);
}
let f = (stats) => {
if (DetectRTC.browser.name === 'Firefox') {
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')
)) {
let metricId = 'webrtc_inbound_' + stat.mediaType + '_' + stat.ssrc;
let jitter = stat.jitter * 1000;
let metrics = {
"bytesReceived": (stat.bytesReceived - this.stats.inbound[stat.mediaType].bytesReceived) / this.statsInterval,
"jitter": jitter,
"packetsReceived": (stat.packetsReceived - this.stats.inbound[stat.mediaType].packetsReceived) / this.statsInterval,
"packetsLost": (stat.packetsLost - this.stats.inbound[stat.mediaType].packetsLost) / this.statsInterval
};
let 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",
"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')
)) {
let metricId = 'webrtc_outbound_' + stat.mediaType + '_' + stat.ssrc;
let metrics = {
"bytesSent": (stat.bytesSent - this.stats.outbound[stat.mediaType].bytesSent) / this.statsInterval,
"packetsSent": (stat.packetsSent - this.stats.outbound[stat.mediaType].packetsSent) / this.statsInterval
};
let 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",
"type": metricId,
"stream_type": "composed_metrics",
"units": units
}
json[metricId] = metrics;
sendPost(JSON.stringify(json));
}
});
} else if (DetectRTC.browser.name === 'Chrome') {
for (let key of Object.keys(stats)) {
let 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
let metricId = 'webrtc_inbound_' + stat.mediaType + '_' + stat.ssrc;
let 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
};
let 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",
"type": metricId,
"stream_type": "composed_metrics",
"units": units
}
json[metricId] = metrics;
sendPost(JSON.stringify(json));
} else if ('bytesSent' in stat) {
// outbound-rtp
let metricId = 'webrtc_outbound_' + stat.mediaType + '_' + stat.ssrc;
let metrics = {
"bytesSent": (stat.bytesSent - this.stats.outbound[stat.mediaType].bytesSent) / this.statsInterval,
"packetsSent": (stat.packetsSent - this.stats.outbound[stat.mediaType].packetsSent) / this.statsInterval
};
let 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",
"type": metricId,
"stream_type": "composed_metrics",
"units": units
}
json[metricId] = metrics;
sendPost(JSON.stringify(json));
}
}
}
}
};
this.getStatsAgnostic(this.stream.getRTCPeerConnection(), null, f, (error) => { console.log(error) });
}
private standardizeReport(response) {
if (DetectRTC.browser.name === 'Firefox') {
return response;
}
var standardReport = {};
response.result().forEach(function (report) {
var standardStats = {
id: report.id,
timestamp: report.timestamp,
type: report.type
};
report.names().forEach(function (name) {
standardStats[name] = report.stat(name);
});
standardReport[standardStats.id] = standardStats;
});
return standardReport;
}
private getStatsAgnostic(pc, selector, successCb, failureCb) {
if (DetectRTC.browser.name === 'Firefox') {
// getStats takes args in different order in Chrome and Firefox
return pc.getStats(selector, (response) => {
var report = this.standardizeReport(response);
successCb(report);
}, failureCb);
} else if (DetectRTC.browser.name === 'Chrome') {
// In Chrome, the first two arguments are reversed
return pc.getStats((response) => {
var report = this.standardizeReport(response);
successCb(report);
}, selector, failureCb);
}
}
}

View File

@ -2,4 +2,5 @@ export * from './OpenViduInternal';
export * from './Connection';
export * from './SessionInternal';
export * from './Stream';
export * from './LocalRecorder';
export * from './OpenViduError';

View File

@ -21,8 +21,9 @@
"outDir": "../../lib/OpenViduInternal",
"emitBOM": false,
"preserveConstEnums": true,
"sourceMap": true
"sourceMap": true,
"lib": ["dom","es5","es2015.promise","scripthost"]
},
//"buildOnSave": true,
"compileOnSave":true
"compileOnSave": true
}

View File

@ -1,26 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<classpath>
<classpathentry kind="src" output="target/classes" path="src/main/java">
<attributes>
<attribute name="optional" value="true"/>
<attribute name="maven.pomderived" value="true"/>
</attributes>
</classpathentry>
<classpathentry kind="src" output="target/test-classes" path="src/test/java">
<attributes>
<attribute name="optional" value="true"/>
<attribute name="maven.pomderived" value="true"/>
</attributes>
</classpathentry>
<classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-1.8">
<attributes>
<attribute name="maven.pomderived" value="true"/>
</attributes>
</classpathentry>
<classpathentry kind="con" path="org.eclipse.m2e.MAVEN2_CLASSPATH_CONTAINER">
<attributes>
<attribute name="maven.pomderived" value="true"/>
</attributes>
</classpathentry>
<classpathentry kind="output" path="target/classes"/>
</classpath>

View File

@ -1,23 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<projectDescription>
<name>kurento-room-client-openvic</name>
<comment></comment>
<projects>
</projects>
<buildSpec>
<buildCommand>
<name>org.eclipse.jdt.core.javabuilder</name>
<arguments>
</arguments>
</buildCommand>
<buildCommand>
<name>org.eclipse.m2e.core.maven2Builder</name>
<arguments>
</arguments>
</buildCommand>
</buildSpec>
<natures>
<nature>org.eclipse.jdt.core.javanature</nature>
<nature>org.eclipse.m2e.core.maven2Nature</nature>
</natures>
</projectDescription>

View File

@ -1,4 +0,0 @@
eclipse.preferences.version=1
encoding//src/main/java=UTF-8
encoding//src/test/java=UTF-8
encoding/<project>=UTF-8

View File

@ -1,5 +0,0 @@
eclipse.preferences.version=1
org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.8
org.eclipse.jdt.core.compiler.compliance=1.8
org.eclipse.jdt.core.compiler.problem.forbiddenReference=warning
org.eclipse.jdt.core.compiler.source=1.8

View File

@ -1,4 +0,0 @@
activeProfiles=
eclipse.preferences.version=1
resolveWorkspaceProjects=true
version=1

View File

@ -20,59 +20,60 @@ package io.openvidu.client;
import org.kurento.jsonrpc.JsonRpcErrorException;
public class OpenViduException extends JsonRpcErrorException {
private static final long serialVersionUID = 1L;
private static final long serialVersionUID = 1L;
public static enum Code {
GENERIC_ERROR_CODE(999),
public static enum Code {
GENERIC_ERROR_CODE(999),
TRANSPORT_ERROR_CODE(803), TRANSPORT_RESPONSE_ERROR_CODE(802), TRANSPORT_REQUEST_ERROR_CODE(
801),
TRANSPORT_ERROR_CODE(803), TRANSPORT_RESPONSE_ERROR_CODE(802), TRANSPORT_REQUEST_ERROR_CODE(801),
MEDIA_MUTE_ERROR_CODE(307), MEDIA_NOT_A_WEB_ENDPOINT_ERROR_CODE(
306), MEDIA_RTP_ENDPOINT_ERROR_CODE(305), MEDIA_WEBRTC_ENDPOINT_ERROR_CODE(
304), MEDIA_ENDPOINT_ERROR_CODE(
303), MEDIA_SDP_ERROR_CODE(302), MEDIA_GENERIC_ERROR_CODE(301),
MEDIA_MUTE_ERROR_CODE(307), MEDIA_NOT_A_WEB_ENDPOINT_ERROR_CODE(306), MEDIA_RTP_ENDPOINT_ERROR_CODE(
305), MEDIA_WEBRTC_ENDPOINT_ERROR_CODE(
304), MEDIA_ENDPOINT_ERROR_CODE(303), MEDIA_SDP_ERROR_CODE(302), MEDIA_GENERIC_ERROR_CODE(301),
ROOM_CANNOT_BE_CREATED_ERROR_CODE(204), ROOM_CLOSED_ERROR_CODE(203), ROOM_NOT_FOUND_ERROR_CODE(
202), ROOM_GENERIC_ERROR_CODE(201),
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_NOT_FOUND_ERROR_CODE(102), USER_GENERIC_ERROR_CODE(101),
USER_UNAUTHORIZED_ERROR_CODE(401), ROLE_NOT_FOUND_ERROR_CODE(402),
SESSIONID_CANNOT_BE_CREATED_ERROR_CODE(403), TOKEN_CANNOT_BE_CREATED_ERROR_CODE(404),
USER_METADATA_FORMAT_INVALID_ERROR_CODE(500),
SIGNAL_FORMAT_INVALID_ERROR_CODE(600), SIGNAL_TO_INVALID_ERROR_CODE(601),
SIGNAL_MESSAGE_INVALID_ERROR_CODE(602);
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(101),
private int value;
USER_UNAUTHORIZED_ERROR_CODE(401), ROLE_NOT_FOUND_ERROR_CODE(402), SESSIONID_CANNOT_BE_CREATED_ERROR_CODE(
403), TOKEN_CANNOT_BE_CREATED_ERROR_CODE(404),
private Code(int value) {
this.value = value;
}
USER_METADATA_FORMAT_INVALID_ERROR_CODE(500),
public int getValue() {
return this.value;
}
}
SIGNAL_FORMAT_INVALID_ERROR_CODE(600), SIGNAL_TO_INVALID_ERROR_CODE(601), SIGNAL_MESSAGE_INVALID_ERROR_CODE(
602),
private Code code = Code.GENERIC_ERROR_CODE;
RECORDING_FILE_EMPTY_ERROR(707), RECORDING_DELETE_ERROR_CODE(706), RECORDING_LIST_ERROR_CODE(
705), RECORDING_STOP_ERROR_CODE(704), RECORDING_START_ERROR_CODE(
703), RECORDING_REPORT_ERROR_CODE(702), RECORDING_COMPLETION_ERROR_CODE(701);
public OpenViduException(Code code, String message) {
super(code.getValue(), message);
this.code = code;
}
private int value;
public int getCodeValue() {
return code.getValue();
}
private Code(int value) {
this.value = value;
}
@Override
public String toString() {
return "CODE: " + getCodeValue() + ". EXCEPTION: " + super.toString();
}
public int getValue() {
return this.value;
}
}
private Code code = Code.GENERIC_ERROR_CODE;
public OpenViduException(Code code, String message) {
super(code.getValue(), message);
this.code = code;
}
public int getCodeValue() {
return code.getValue();
}
@Override
public String toString() {
return "CODE: " + getCodeValue() + ". EXCEPTION: " + super.toString();
}
}

View File

@ -9,7 +9,7 @@
</parent>
<artifactId>openvidu-java-client</artifactId>
<version>1.7.0</version>
<version>1.8.0</version>
<packaging>jar</packaging>
<name>OpenVidu Java Client</name>
@ -161,10 +161,10 @@
<version>1.6.8</version>
<extensions>true</extensions>
<configuration>
<serverId>ossrh</serverId>
<nexusUrl>https://oss.sonatype.org/</nexusUrl>
<autoReleaseAfterClose>false</autoReleaseAfterClose>
</configuration>
<serverId>ossrh</serverId>
<nexusUrl>https://oss.sonatype.org/</nexusUrl>
<autoReleaseAfterClose>true</autoReleaseAfterClose>
</configuration>
</plugin>
<plugin>
<artifactId>maven-resources-plugin</artifactId>

View File

@ -0,0 +1,81 @@
package io.openvidu.java.client;
import org.json.simple.JSONObject;
public class Archive {
public enum Status {
starting, // The recording is starting (cannot be stopped)
started, // The recording has started and is going on
stopped, // The recording has finished OK
available, // The recording is available for downloading. This status is reached for all
// stopped recordings if property 'openvidu.recording.free-access' is true
failed; // The recording has failed
}
private Archive.Status status;
private String id;
private String name;
private String sessionId;
private long createdAt; // milliseconds (UNIX Epoch time)
private long size = 0; // bytes
private double duration = 0; // seconds
private String url;
private boolean hasAudio = true;
private boolean hasVideo = true;
public Archive(JSONObject json) {
this.id = (String) json.get("id");
this.name = (String) json.get("name");
this.sessionId = (String) json.get("sessionId");
this.createdAt = (long) json.get("createdAt");
this.size = (long) json.get("size");
this.duration = (double) json.get("duration");
this.url = (String) json.get("url");
this.hasAudio = (boolean) json.get("hasAudio");
this.hasVideo = (boolean) json.get("hasVideo");
this.status = Archive.Status.valueOf((String) json.get("status"));
}
public Archive.Status getStatus() {
return status;
}
public String getId() {
return id;
}
public String getName() {
return name;
}
public String getSessionId() {
return sessionId;
}
public long getCreatedAt() {
return createdAt;
}
public long getSize() {
return size;
}
public double getDuration() {
return duration;
}
public String getUrl() {
return url;
}
public boolean hasAudio() {
return hasAudio;
}
public boolean hasVideo() {
return hasVideo;
}
}

View File

@ -1,7 +1,7 @@
package io.openvidu.java.client;
public enum ArchiveLayout {
BEST_FIT,
BEST_FIT, // All the videos are evenly distributed, taking up as much space as possible
PICTURE_IN_PICTURE,
VERTICAL_PRESENTATION,
HORIZONTAL_PRESENTATION

View File

@ -1,18 +1,38 @@
package io.openvidu.java.client;
import java.io.IOException;
import java.security.KeyManagementException;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.List;
import javax.net.ssl.SSLContext;
import org.apache.http.HttpHeaders;
import org.apache.http.HttpResponse;
import org.apache.http.auth.AuthScope;
import org.apache.http.auth.UsernamePasswordCredentials;
import org.apache.http.client.CredentialsProvider;
import org.apache.http.client.HttpClient;
import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
import org.apache.http.conn.ssl.TrustSelfSignedStrategy;
import org.apache.http.client.methods.HttpDelete;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.conn.ssl.NoopHostnameVerifier;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.BasicCredentialsProvider;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.ssl.SSLContextBuilder;
import org.apache.http.ssl.TrustStrategy;
import org.apache.http.util.EntityUtils;
import org.json.simple.JSONArray;
import org.json.simple.JSONObject;
import org.json.simple.parser.JSONParser;
import org.json.simple.parser.ParseException;
import io.openvidu.java.client.OpenViduException.Code;
public class OpenVidu {
@ -20,51 +40,155 @@ public class OpenVidu {
private String secret;
private HttpClient myHttpClient;
public OpenVidu(String urlOpenViduServer, String secret) {
this.urlOpenViduServer = urlOpenViduServer;
if (!this.urlOpenViduServer.endsWith("/")){
this.urlOpenViduServer += "/";
}
final static String API_RECORDINGS = "api/recordings";
final static String API_RECORDINGS_START = "/start";
final static String API_RECORDINGS_STOP = "/stop";
this.secret = secret;
CredentialsProvider provider = new BasicCredentialsProvider();
UsernamePasswordCredentials credentials = new UsernamePasswordCredentials("OPENVIDUAPP", this.secret);
provider.setCredentials(AuthScope.ANY, credentials);
SSLContextBuilder builder = new SSLContextBuilder();
try {
builder.loadTrustMaterial(null, new TrustSelfSignedStrategy());
SSLConnectionSocketFactory sslsf = new SSLConnectionSocketFactory(builder.build(),
SSLConnectionSocketFactory.ALLOW_ALL_HOSTNAME_VERIFIER);
/*SSLConnectionSocketFactory sslsf = new SSLConnectionSocketFactory(
builder.build());*/
this.myHttpClient = HttpClients.custom().setSSLSocketFactory(
sslsf).setDefaultCredentialsProvider(provider).build();
} catch (NoSuchAlgorithmException | KeyStoreException | KeyManagementException e) {
// TODO Auto-generated catch block
e.printStackTrace();
public OpenVidu(String urlOpenViduServer, String secret) {
this.urlOpenViduServer = urlOpenViduServer;
if (!this.urlOpenViduServer.endsWith("/")) {
this.urlOpenViduServer += "/";
}
this.secret = secret;
TrustStrategy trustStrategy = new TrustStrategy() {
@Override
public boolean isTrusted(X509Certificate[] chain, String authType) throws CertificateException {
return true;
}
}
public Session createSession() throws OpenViduException {
Session s = new Session(myHttpClient, urlOpenViduServer);
return s;
}
public Session createSession(SessionProperties properties) throws OpenViduException {
Session s = new Session(myHttpClient, urlOpenViduServer, properties);
return s;
}
public void startArchive(String sessionId) {
// TODO: REST POST to start recording in OpenVidu Server
}
public void stopArchive(String sessionId) {
// TODO: REST POST to end recording in OpenVidu Server
}
};
CredentialsProvider provider = new BasicCredentialsProvider();
UsernamePasswordCredentials credentials = new UsernamePasswordCredentials("OPENVIDUAPP", this.secret);
provider.setCredentials(AuthScope.ANY, credentials);
SSLContext sslContext;
try {
sslContext = new SSLContextBuilder().loadTrustMaterial(null, trustStrategy).build();
} catch (KeyManagementException | NoSuchAlgorithmException | KeyStoreException e) {
throw new RuntimeException(e);
}
this.myHttpClient = HttpClients.custom().setSSLHostnameVerifier(NoopHostnameVerifier.INSTANCE)
.setSSLContext(sslContext).setDefaultCredentialsProvider(provider).build();
}
public Session createSession() throws OpenViduException {
Session s = new Session(myHttpClient, urlOpenViduServer);
return s;
}
public Session createSession(SessionProperties properties) throws OpenViduException {
Session s = new Session(myHttpClient, urlOpenViduServer, properties);
return s;
}
@SuppressWarnings("unchecked")
public Archive startRecording(String sessionId) throws OpenViduException {
try {
HttpPost request = new HttpPost(this.urlOpenViduServer + API_RECORDINGS + API_RECORDINGS_START);
JSONObject json = new JSONObject();
json.put("session", sessionId);
StringEntity params = new StringEntity(json.toString());
request.setHeader(HttpHeaders.CONTENT_TYPE, "application/json");
request.setEntity(params);
HttpResponse response = myHttpClient.execute(request);
int statusCode = response.getStatusLine().getStatusCode();
if ((statusCode == org.apache.http.HttpStatus.SC_OK)) {
return new Archive(OpenVidu.httpResponseToJson(response));
} else {
throw new OpenViduException(Code.RECORDING_START_ERROR_CODE, Integer.toString(statusCode));
}
} catch (Exception e) {
throw new OpenViduException(Code.RECORDING_START_ERROR_CODE,
"Unable to start recording for session '" + sessionId + "': " + e.getMessage());
}
}
public Archive stopRecording(String recordingId) throws OpenViduException {
try {
HttpPost request = new HttpPost(
this.urlOpenViduServer + API_RECORDINGS + API_RECORDINGS_STOP + "/" + recordingId);
HttpResponse response = myHttpClient.execute(request);
int statusCode = response.getStatusLine().getStatusCode();
if ((statusCode == org.apache.http.HttpStatus.SC_OK)) {
return new Archive(OpenVidu.httpResponseToJson(response));
} else {
throw new OpenViduException(Code.RECORDING_STOP_ERROR_CODE, Integer.toString(statusCode));
}
} catch (Exception e) {
throw new OpenViduException(Code.RECORDING_STOP_ERROR_CODE,
"Unable to stop recording '" + recordingId + "': " + e.getMessage());
}
}
public Archive getRecording(String recordingId) throws OpenViduException {
try {
HttpGet request = new HttpGet(this.urlOpenViduServer + API_RECORDINGS + "/" + recordingId);
HttpResponse response = myHttpClient.execute(request);
int statusCode = response.getStatusLine().getStatusCode();
if ((statusCode == org.apache.http.HttpStatus.SC_OK)) {
return new Archive(OpenVidu.httpResponseToJson(response));
} else {
throw new OpenViduException(Code.RECORDING_LIST_ERROR_CODE, Integer.toString(statusCode));
}
} catch (Exception e) {
throw new OpenViduException(Code.RECORDING_LIST_ERROR_CODE, "Unable to get recording '" + recordingId + "': " + e.getMessage());
}
}
@SuppressWarnings("unchecked")
public List<Archive> listRecordings() throws OpenViduException {
try {
HttpGet request = new HttpGet(this.urlOpenViduServer + API_RECORDINGS);
HttpResponse response = myHttpClient.execute(request);
int statusCode = response.getStatusLine().getStatusCode();
if ((statusCode == org.apache.http.HttpStatus.SC_OK)) {
List<Archive> archives = new ArrayList<>();
JSONObject json = OpenVidu.httpResponseToJson(response);
JSONArray array = (JSONArray) json.get("items");
array.forEach(item -> {
archives.add(new Archive((JSONObject) item));
});
return archives;
} else {
throw new OpenViduException(Code.RECORDING_LIST_ERROR_CODE, Integer.toString(statusCode));
}
} catch (Exception e) {
throw new OpenViduException(Code.RECORDING_LIST_ERROR_CODE, "Unable to list recordings: " + e.getMessage());
}
}
public void deleteRecording(String recordingId) throws OpenViduException {
try {
HttpDelete request = new HttpDelete(this.urlOpenViduServer + API_RECORDINGS + "/" + recordingId);
HttpResponse response = myHttpClient.execute(request);
int statusCode = response.getStatusLine().getStatusCode();
if (!(statusCode == org.apache.http.HttpStatus.SC_NO_CONTENT)) {
throw new OpenViduException(Code.RECORDING_DELETE_ERROR_CODE, Integer.toString(statusCode));
}
} catch (Exception e) {
throw new OpenViduException(Code.RECORDING_DELETE_ERROR_CODE,
"Unable to delete recording '" + recordingId + "': " + e.getMessage());
}
}
public static JSONObject httpResponseToJson(HttpResponse response) throws ParseException, IOException {
JSONParser parser = new JSONParser();
return (JSONObject) parser.parse(EntityUtils.toString(response.getEntity()));
}
}

View File

@ -18,60 +18,64 @@
package io.openvidu.java.client;
public class OpenViduException extends RuntimeException {
private static final long serialVersionUID = 1L;
private static final long serialVersionUID = 1L;
public static enum Code {
GENERIC_ERROR_CODE(999),
public static enum Code {
GENERIC_ERROR_CODE(999),
TRANSPORT_ERROR_CODE(803), TRANSPORT_RESPONSE_ERROR_CODE(802), TRANSPORT_REQUEST_ERROR_CODE(
801),
TRANSPORT_ERROR_CODE(803), TRANSPORT_RESPONSE_ERROR_CODE(802), TRANSPORT_REQUEST_ERROR_CODE(801),
MEDIA_MUTE_ERROR_CODE(307), MEDIA_NOT_A_WEB_ENDPOINT_ERROR_CODE(
306), MEDIA_RTP_ENDPOINT_ERROR_CODE(305), MEDIA_WEBRTC_ENDPOINT_ERROR_CODE(
304), MEDIA_ENDPOINT_ERROR_CODE(
303), MEDIA_SDP_ERROR_CODE(302), MEDIA_GENERIC_ERROR_CODE(301),
MEDIA_MUTE_ERROR_CODE(307), MEDIA_NOT_A_WEB_ENDPOINT_ERROR_CODE(306), MEDIA_RTP_ENDPOINT_ERROR_CODE(
305), MEDIA_WEBRTC_ENDPOINT_ERROR_CODE(
304), MEDIA_ENDPOINT_ERROR_CODE(303), MEDIA_SDP_ERROR_CODE(302), MEDIA_GENERIC_ERROR_CODE(301),
ROOM_CANNOT_BE_CREATED_ERROR_CODE(204), ROOM_CLOSED_ERROR_CODE(203), ROOM_NOT_FOUND_ERROR_CODE(
202), ROOM_GENERIC_ERROR_CODE(201),
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_NOT_FOUND_ERROR_CODE(102), USER_GENERIC_ERROR_CODE(101),
USER_UNAUTHORIZED_ERROR_CODE(401), ROLE_NOT_FOUND_ERROR_CODE(402),
SESSIONID_CANNOT_BE_CREATED_ERROR_CODE(403), TOKEN_CANNOT_BE_CREATED_ERROR_CODE(404),
USER_METADATA_FORMAT_INVALID_ERROR_CODE(500);
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(101),
private int value;
USER_UNAUTHORIZED_ERROR_CODE(401), ROLE_NOT_FOUND_ERROR_CODE(402), SESSIONID_CANNOT_BE_CREATED_ERROR_CODE(
403), TOKEN_CANNOT_BE_CREATED_ERROR_CODE(404),
private Code(int value) {
this.value = value;
}
USER_METADATA_FORMAT_INVALID_ERROR_CODE(500),
public int getValue() {
return this.value;
}
}
SIGNAL_FORMAT_INVALID_ERROR_CODE(600), SIGNAL_TO_INVALID_ERROR_CODE(601), SIGNAL_MESSAGE_INVALID_ERROR_CODE(
602),
private Code code = Code.GENERIC_ERROR_CODE;
RECORDING_FILE_EMPTY_ERROR(707), RECORDING_DELETE_ERROR_CODE(706), RECORDING_LIST_ERROR_CODE(
705), RECORDING_STOP_ERROR_CODE(704), RECORDING_START_ERROR_CODE(
703), RECORDING_REPORT_ERROR_CODE(702), RECORDING_COMPLETION_ERROR_CODE(701);
public OpenViduException(Code code, String message) {
super(message);
this.code = code;
}
private int value;
public Code getCode() {
return code;
}
private Code(int value) {
this.value = value;
}
public int getCodeValue() {
return code.getValue();
}
public int getValue() {
return this.value;
}
}
@Override
public String toString() {
return "Code: " + getCodeValue() + " " + super.toString();
}
private Code code = Code.GENERIC_ERROR_CODE;
public OpenViduException(Code code, String message) {
super(message);
this.code = code;
}
public Code getCode() {
return code;
}
public int getCodeValue() {
return code.getValue();
}
@Override
public String toString() {
return "Code: " + getCodeValue() + " " + super.toString();
}
}

View File

@ -1,16 +1,11 @@
package io.openvidu.java.client;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import org.apache.http.HttpHeaders;
import org.apache.http.HttpResponse;
import org.apache.http.client.HttpClient;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.entity.StringEntity;
import org.json.simple.JSONObject;
import org.json.simple.parser.JSONParser;
import org.json.simple.parser.ParseException;
import io.openvidu.java.client.OpenViduException.Code;
@ -20,6 +15,9 @@ public class Session {
private String urlOpenViduServer;
private String sessionId;
private SessionProperties properties;
final static String API_SESSIONS = "api/sessions";
final static String API_TOKENS = "api/tokens";
protected Session(HttpClient httpClient, String urlOpenViduServer) throws OpenViduException {
this.httpClient = httpClient;
@ -35,6 +33,7 @@ public class Session {
this.sessionId = this.getSessionId();
}
@SuppressWarnings("unchecked")
public String getSessionId() throws OpenViduException {
if (this.hasSessionId()) {
@ -42,15 +41,15 @@ public class Session {
}
try {
HttpPost request = new HttpPost(this.urlOpenViduServer + API_SESSIONS);
JSONObject json = new JSONObject();
json.put("archiveMode", properties.archiveMode().name());
json.put("archiveLayout", properties.archiveLayout().name());
json.put("archiveMode", properties.archiveMode().name());
json.put("mediaMode", properties.mediaMode().name());
HttpPost request = new HttpPost(this.urlOpenViduServer + "api/sessions");
StringEntity params = new StringEntity(json.toString());
request.addHeader("content-type", "application/json");
request.setHeader(HttpHeaders.CONTENT_TYPE, "application/json");
request.setEntity(params);
HttpResponse response = httpClient.execute(request);
@ -58,7 +57,7 @@ public class Session {
if ((statusCode == org.apache.http.HttpStatus.SC_OK)) {
System.out.println("Returning a SESSIONID");
String id = "";
id = this.httpResponseToString(response);
id = (String) OpenVidu.httpResponseToJson(response).get("id");
this.sessionId = id;
return id;
} else {
@ -75,6 +74,7 @@ public class Session {
return this.generateToken(new TokenOptions.Builder().role(OpenViduRole.PUBLISHER).build());
}
@SuppressWarnings("unchecked")
public String generateToken(TokenOptions tokenOptions) throws OpenViduException {
if (!this.hasSessionId()) {
@ -82,15 +82,15 @@ public class Session {
}
try {
HttpPost request = new HttpPost(this.urlOpenViduServer + API_TOKENS);
JSONObject json = new JSONObject();
json.put("session", this.sessionId);
json.put("role", tokenOptions.getRole().name());
json.put("data", tokenOptions.getData());
HttpPost request = new HttpPost(this.urlOpenViduServer + "api/tokens");
StringEntity params = new StringEntity(json.toString());
request.addHeader("content-type", "application/json");
request.setHeader(HttpHeaders.CONTENT_TYPE, "application/json");
request.setEntity(params);
HttpResponse response = httpClient.execute(request);
@ -98,7 +98,7 @@ public class Session {
int statusCode = response.getStatusLine().getStatusCode();
if ((statusCode == org.apache.http.HttpStatus.SC_OK)) {
System.out.println("Returning a TOKEN");
return this.httpResponseToString(response);
return (String) OpenVidu.httpResponseToJson(response).get("id");
} else {
throw new OpenViduException(Code.TOKEN_CANNOT_BE_CREATED_ERROR_CODE, Integer.toString(statusCode));
}
@ -118,17 +118,6 @@ public class Session {
return this.sessionId;
}
private String httpResponseToString(HttpResponse response) throws IOException, ParseException {
BufferedReader rd = new BufferedReader(new InputStreamReader(response.getEntity().getContent()));
StringBuffer buf = new StringBuffer();
String line = "";
while ((line = rd.readLine()) != null) {
buf.append(line);
}
JSONParser parser = new JSONParser();
return ((String) ((JSONObject) parser.parse(buf.toString())).get("id"));
}
private boolean hasSessionId() {
return (this.sessionId != null && !this.sessionId.isEmpty());
}

View File

@ -1,10 +1,21 @@
import { Session } from "./Session";
import { SessionProperties } from "./SessionProperties";
import { Archive } from "./Archive";
export declare class OpenVidu {
private urlOpenViduServer;
private secret;
private static readonly API_RECORDINGS;
private static readonly API_RECORDINGS_START;
private static readonly API_RECORDINGS_STOP;
private hostname;
private port;
private basicAuth;
constructor(urlOpenViduServer: string, secret: string);
createSession(properties?: SessionProperties): Session;
startArchive(sessionId: string): void;
stopArchive(sessionId: string): void;
startRecording(sessionId: string): Promise<Archive>;
stopRecording(recordingId: string): Promise<Archive>;
getRecording(recordingId: string): Promise<Archive>;
listRecordings(): Promise<Archive[]>;
deleteRecording(recordingId: string): Promise<Error>;
private getBasicAuth(secret);
private setHostnameAndPort();
}

View File

@ -1,20 +1,231 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
var Session_1 = require("./Session");
var Archive_1 = require("./Archive");
var https = require('https');
var OpenVidu = /** @class */ (function () {
function OpenVidu(urlOpenViduServer, secret) {
this.urlOpenViduServer = urlOpenViduServer;
this.secret = secret;
this.setHostnameAndPort();
this.basicAuth = this.getBasicAuth(secret);
}
OpenVidu.prototype.createSession = function (properties) {
return new Session_1.Session(this.urlOpenViduServer, this.secret, properties);
return new Session_1.Session(this.hostname, this.port, this.basicAuth, properties);
};
OpenVidu.prototype.startArchive = function (sessionId) {
// TODO: REST POST to start recording in OpenVidu Server
OpenVidu.prototype.startRecording = function (sessionId) {
var _this = this;
return new Promise(function (resolve, reject) {
var requestBody = JSON.stringify({
'session': sessionId
});
var options = {
hostname: _this.hostname,
port: _this.port,
path: OpenVidu.API_RECORDINGS + OpenVidu.API_RECORDINGS_START,
method: 'POST',
headers: {
'Authorization': _this.basicAuth,
'Content-Type': 'application/json',
'Content-Length': Buffer.byteLength(requestBody)
}
};
var req = https.request(options, function (res) {
var body = '';
res.on('data', function (d) {
// Continuously update stream with data
body += d;
});
res.on('end', function () {
if (res.statusCode === 200) {
// SUCCESS response from openvidu-server (Archive in JSON format). Resolve new Archive
resolve(new Archive_1.Archive(JSON.parse(body)));
}
else {
// ERROR response from openvidu-server. Resolve HTTP status
reject(new Error(res.statusCode));
}
});
});
req.on('error', function (e) {
reject(new Error(e));
});
req.write(requestBody);
req.end();
});
};
OpenVidu.prototype.stopArchive = function (sessionId) {
// TODO: REST POST to end recording in OpenVidu Server
OpenVidu.prototype.stopRecording = function (recordingId) {
var _this = this;
return new Promise(function (resolve, reject) {
var options = {
hostname: _this.hostname,
port: _this.port,
path: OpenVidu.API_RECORDINGS + OpenVidu.API_RECORDINGS_STOP + '/' + recordingId,
method: 'POST',
headers: {
'Authorization': _this.basicAuth,
'Content-Type': 'application/x-www-form-urlencoded'
}
};
var req = https.request(options, function (res) {
var body = '';
res.on('data', function (d) {
// Continuously update stream with data
body += d;
});
res.on('end', function () {
if (res.statusCode === 200) {
// SUCCESS response from openvidu-server (Archive in JSON format). Resolve new Archive
resolve(new Archive_1.Archive(JSON.parse(body)));
}
else {
// ERROR response from openvidu-server. Resolve HTTP status
reject(new Error(res.statusCode));
}
});
});
req.on('error', function (e) {
reject(new Error(e));
});
//req.write();
req.end();
});
};
OpenVidu.prototype.getRecording = function (recordingId) {
var _this = this;
return new Promise(function (resolve, reject) {
var options = {
hostname: _this.hostname,
port: _this.port,
path: OpenVidu.API_RECORDINGS + '/' + recordingId,
method: 'GET',
headers: {
'Authorization': _this.basicAuth,
'Content-Type': 'application/x-www-form-urlencoded'
}
};
var req = https.request(options, function (res) {
var body = '';
res.on('data', function (d) {
// Continuously update stream with data
body += d;
});
res.on('end', function () {
if (res.statusCode === 200) {
// SUCCESS response from openvidu-server (Archive in JSON format). Resolve new Archive
resolve(new Archive_1.Archive(JSON.parse(body)));
}
else {
// ERROR response from openvidu-server. Resolve HTTP status
reject(new Error(res.statusCode));
}
});
});
req.on('error', function (e) {
reject(new Error(e));
});
//req.write();
req.end();
});
};
OpenVidu.prototype.listRecordings = function () {
var _this = this;
return new Promise(function (resolve, reject) {
var options = {
hostname: _this.hostname,
port: _this.port,
path: OpenVidu.API_RECORDINGS,
method: 'GET',
headers: {
'Authorization': _this.basicAuth,
'Content-Type': 'application/x-www-form-urlencoded'
}
};
var req = https.request(options, function (res) {
var body = '';
res.on('data', function (d) {
// Continuously update stream with data
body += d;
});
res.on('end', function () {
if (res.statusCode === 200) {
// SUCCESS response from openvidu-server (JSON arrays of Archives in JSON format). Resolve list of new Archives
var archiveArray = [];
var responseItems = JSON.parse(body)['items'];
for (var i = 0; i < responseItems.length; i++) {
archiveArray.push(new Archive_1.Archive(responseItems[i]));
}
resolve(archiveArray);
}
else {
// ERROR response from openvidu-server. Resolve HTTP status
reject(new Error(res.statusCode));
}
});
});
req.on('error', function (e) {
reject(new Error(e));
});
//req.write();
req.end();
});
};
OpenVidu.prototype.deleteRecording = function (recordingId) {
var _this = this;
return new Promise(function (resolve, reject) {
var options = {
hostname: _this.hostname,
port: _this.port,
path: OpenVidu.API_RECORDINGS + '/' + recordingId,
method: 'DELETE',
headers: {
'Authorization': _this.basicAuth,
'Content-Type': 'application/x-www-form-urlencoded'
}
};
var req = https.request(options, function (res) {
var body = '';
res.on('data', function (d) {
// Continuously update stream with data
body += d;
});
res.on('end', function () {
if (res.statusCode === 204) {
// SUCCESS response from openvidu-server. Resolve undefined
resolve(undefined);
}
else {
// ERROR response from openvidu-server. Resolve HTTP status
reject(new Error(res.statusCode));
}
});
});
req.on('error', function (e) {
reject(new Error(e));
});
//req.write();
req.end();
});
};
OpenVidu.prototype.getBasicAuth = function (secret) {
return 'Basic ' + (new Buffer('OPENVIDUAPP:' + secret).toString('base64'));
};
OpenVidu.prototype.setHostnameAndPort = function () {
var urlSplitted = this.urlOpenViduServer.split(':');
if (urlSplitted.length === 3) {
this.hostname = this.urlOpenViduServer.split(':')[1].replace(/\//g, '');
this.port = parseInt(this.urlOpenViduServer.split(':')[2].replace(/\//g, ''));
}
else if (urlSplitted.length == 2) {
this.hostname = this.urlOpenViduServer.split(':')[0].replace(/\//g, '');
this.port = parseInt(this.urlOpenViduServer.split(':')[1].replace(/\//g, ''));
}
else {
console.error("URL format incorrect: it must contain hostname and port (current value: '" + this.urlOpenViduServer + "')");
}
};
OpenVidu.API_RECORDINGS = '/api/recordings';
OpenVidu.API_RECORDINGS_START = '/start';
OpenVidu.API_RECORDINGS_STOP = '/stop';
return OpenVidu;
}());
exports.OpenVidu = OpenVidu;

File diff suppressed because one or more lines are too long

View File

@ -1,19 +1,16 @@
import { TokenOptions } from './TokenOptions';
import { SessionProperties } from './SessionProperties';
export declare class Session {
private urlOpenViduServer;
private secret;
private sessionIdURL;
private tokenURL;
private sessionId;
private properties;
private hostname;
private port;
constructor(urlOpenViduServer: string, secret: string, properties?: SessionProperties);
private basicAuth;
private static readonly API_SESSIONS;
private static readonly API_TOKENS;
private sessionId;
private properties;
constructor(hostname: string, port: number, basicAuth: string, properties?: SessionProperties);
getSessionId(callback: Function): void;
generateToken(callback: Function): any;
generateToken(tokenOptions: TokenOptions, callback: Function): any;
getProperties(): SessionProperties;
private getBasicAuth();
private setHostnameAndPort();
}

View File

@ -4,11 +4,10 @@ var OpenViduRole_1 = require("./OpenViduRole");
var SessionProperties_1 = require("./SessionProperties");
var https = require('https');
var Session = /** @class */ (function () {
function Session(urlOpenViduServer, secret, properties) {
this.urlOpenViduServer = urlOpenViduServer;
this.secret = secret;
this.sessionIdURL = '/api/sessions';
this.tokenURL = '/api/tokens';
function Session(hostname, port, basicAuth, properties) {
this.hostname = hostname;
this.port = port;
this.basicAuth = basicAuth;
this.sessionId = "";
if (properties == null) {
this.properties = new SessionProperties_1.SessionProperties.Builder().build();
@ -16,7 +15,6 @@ var Session = /** @class */ (function () {
else {
this.properties = properties;
}
this.setHostnameAndPort();
}
Session.prototype.getSessionId = function (callback) {
var _this = this;
@ -32,10 +30,10 @@ var Session = /** @class */ (function () {
var options = {
hostname: this.hostname,
port: this.port,
path: this.sessionIdURL,
path: Session.API_SESSIONS,
method: 'POST',
headers: {
'Authorization': this.getBasicAuth(),
'Authorization': this.basicAuth,
'Content-Type': 'application/json',
'Content-Length': Buffer.byteLength(requestBody)
}
@ -47,10 +45,16 @@ var Session = /** @class */ (function () {
body += d;
});
res.on('end', function () {
// Data reception is done
var parsed = JSON.parse(body);
_this.sessionId = parsed.id;
callback(parsed.id);
if (res.statusCode === 200) {
// SUCCESS response from openvidu-server. Resolve sessionId
var parsed = JSON.parse(body);
_this.sessionId = parsed.id;
callback(parsed.id);
}
else {
// ERROR response from openvidu-server. Resolve HTTP status
console.error(res.statusCode);
}
});
});
req.on('error', function (e) {
@ -79,10 +83,10 @@ var Session = /** @class */ (function () {
var options = {
hostname: this.hostname,
port: this.port,
path: this.tokenURL,
path: Session.API_TOKENS,
method: 'POST',
headers: {
'Authorization': this.getBasicAuth(),
'Authorization': this.basicAuth,
'Content-Type': 'application/json',
'Content-Length': Buffer.byteLength(requestBody)
}
@ -94,9 +98,15 @@ var Session = /** @class */ (function () {
body += d;
});
res.on('end', function () {
// Data reception is done
var parsed = JSON.parse(body);
callback(parsed.id);
if (res.statusCode === 200) {
// SUCCESS response from openvidu-server. Resolve token
var parsed = JSON.parse(body);
callback(parsed.id);
}
else {
// ERROR response from openvidu-server. Resolve HTTP status
console.error(res.statusCode);
}
});
});
req.on('error', function (e) {
@ -108,23 +118,8 @@ var Session = /** @class */ (function () {
Session.prototype.getProperties = function () {
return this.properties;
};
Session.prototype.getBasicAuth = function () {
return 'Basic ' + (new Buffer('OPENVIDUAPP:' + this.secret).toString('base64'));
};
Session.prototype.setHostnameAndPort = function () {
var urlSplitted = this.urlOpenViduServer.split(':');
if (urlSplitted.length === 3) {
this.hostname = this.urlOpenViduServer.split(':')[1].replace(/\//g, '');
this.port = parseInt(this.urlOpenViduServer.split(':')[2].replace(/\//g, ''));
}
else if (urlSplitted.length == 2) {
this.hostname = this.urlOpenViduServer.split(':')[0].replace(/\//g, '');
this.port = parseInt(this.urlOpenViduServer.split(':')[1].replace(/\//g, ''));
}
else {
console.error("URL format incorrect: it must contain hostname and port (current value: '" + this.urlOpenViduServer + "')");
}
};
Session.API_SESSIONS = '/api/sessions';
Session.API_TOKENS = '/api/tokens';
return Session;
}());
exports.Session = Session;

View File

@ -1 +1 @@
{"version":3,"file":"Session.js","sourceRoot":"","sources":["../src/Session.ts"],"names":[],"mappings":"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"}
{"version":3,"file":"Session.js","sourceRoot":"","sources":["../src/Session.ts"],"names":[],"mappings":"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"}

View File

@ -1,5 +1,6 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
var OpenViduRole_1 = require("./OpenViduRole");
var TokenOptions = /** @class */ (function () {
function TokenOptions(data, role) {
this.data = data;
@ -17,6 +18,8 @@ exports.TokenOptions = TokenOptions;
(function (TokenOptions) {
var Builder = /** @class */ (function () {
function Builder() {
this.dataProp = '';
this.roleProp = OpenViduRole_1.OpenViduRole.PUBLISHER;
}
Builder.prototype.build = function () {
return new TokenOptions(this.dataProp, this.roleProp);

View File

@ -1 +1 @@
{"version":3,"file":"TokenOptions.js","sourceRoot":"","sources":["../src/TokenOptions.ts"],"names":[],"mappings":";;AAEA;IAEI,sBAAoB,IAAY,EAAU,IAAkB;QAAxC,SAAI,GAAJ,IAAI,CAAQ;QAAU,SAAI,GAAJ,IAAI,CAAc;IAAI,CAAC;IAEjE,8BAAO,GAAP;QACI,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC;IACrB,CAAC;IAED,8BAAO,GAAP;QACI,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC;IACrB,CAAC;IACL,mBAAC;AAAD,CAAC,AAXD,IAWC;AAXY,oCAAY;AAazB,WAAiB,YAAY;IACzB;QAAA;QAmBA,CAAC;QAdG,uBAAK,GAAL;YACI,MAAM,CAAC,IAAI,YAAY,CAAC,IAAI,CAAC,QAAQ,EAAE,IAAI,CAAC,QAAQ,CAAC,CAAC;QAC1D,CAAC;QAED,sBAAI,GAAJ,UAAK,IAAY;YACb,IAAI,CAAC,QAAQ,GAAG,IAAI,CAAC;YACrB,MAAM,CAAC,IAAI,CAAC;QAChB,CAAC;QAED,sBAAI,GAAJ,UAAK,IAAkB;YACnB,IAAI,CAAC,QAAQ,GAAG,IAAI,CAAC;YACrB,MAAM,CAAC,IAAI,CAAC;QAChB,CAAC;QAEL,cAAC;IAAD,CAAC,AAnBD,IAmBC;IAnBY,oBAAO,UAmBnB,CAAA;IAAA,CAAC;AACN,CAAC,EArBgB,YAAY,GAAZ,oBAAY,KAAZ,oBAAY,QAqB5B;AAlCY,oCAAY"}
{"version":3,"file":"TokenOptions.js","sourceRoot":"","sources":["../src/TokenOptions.ts"],"names":[],"mappings":";;AAAA,+CAA8C;AAE9C;IAEI,sBAAoB,IAAY,EAAU,IAAkB;QAAxC,SAAI,GAAJ,IAAI,CAAQ;QAAU,SAAI,GAAJ,IAAI,CAAc;IAAI,CAAC;IAEjE,8BAAO,GAAP;QACI,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC;IACrB,CAAC;IAED,8BAAO,GAAP;QACI,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC;IACrB,CAAC;IACL,mBAAC;AAAD,CAAC,AAXD,IAWC;AAXY,oCAAY;AAazB,WAAiB,YAAY;IACzB;QAAA;YAEY,aAAQ,GAAW,EAAE,CAAC;YACtB,aAAQ,GAAiB,2BAAY,CAAC,SAAS,CAAC;QAgB5D,CAAC;QAdG,uBAAK,GAAL;YACI,MAAM,CAAC,IAAI,YAAY,CAAC,IAAI,CAAC,QAAQ,EAAE,IAAI,CAAC,QAAQ,CAAC,CAAC;QAC1D,CAAC;QAED,sBAAI,GAAJ,UAAK,IAAY;YACb,IAAI,CAAC,QAAQ,GAAG,IAAI,CAAC;YACrB,MAAM,CAAC,IAAI,CAAC;QAChB,CAAC;QAED,sBAAI,GAAJ,UAAK,IAAkB;YACnB,IAAI,CAAC,QAAQ,GAAG,IAAI,CAAC;YACrB,MAAM,CAAC,IAAI,CAAC;QAChB,CAAC;QAEL,cAAC;IAAD,CAAC,AAnBD,IAmBC;IAnBY,oBAAO,UAmBnB,CAAA;IAAA,CAAC;AACN,CAAC,EArBgB,YAAY,GAAZ,oBAAY,KAAZ,oBAAY,QAqB5B;AAlCY,oCAAY"}

View File

@ -1,8 +1,9 @@
export * from './OpenVidu';
export * from './OpenViduRole';
export * from './Session';
export * from './SessionProperties';
export * from './TokenOptions';
export * from './MediaMode';
export * from './ArchiveLayout';
export * from './ArchiveMode';
export * from './SessionProperties';
export * from './Archive';

View File

@ -6,9 +6,10 @@ Object.defineProperty(exports, "__esModule", { value: true });
__export(require("./OpenVidu"));
__export(require("./OpenViduRole"));
__export(require("./Session"));
__export(require("./SessionProperties"));
__export(require("./TokenOptions"));
__export(require("./MediaMode"));
__export(require("./ArchiveLayout"));
__export(require("./ArchiveMode"));
__export(require("./SessionProperties"));
__export(require("./Archive"));
//# sourceMappingURL=index.js.map

View File

@ -1 +1 @@
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";;;;;AAAA,gCAA2B;AAC3B,oCAA+B;AAC/B,+BAA0B;AAC1B,oCAA+B;AAC/B,iCAA4B;AAC5B,qCAAgC;AAChC,mCAA8B;AAC9B,yCAAoC"}
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";;;;;AAAA,gCAA2B;AAC3B,oCAA+B;AAC/B,+BAA0B;AAC1B,yCAAoC;AACpC,oCAA+B;AAC/B,iCAA4B;AAC5B,qCAAgC;AAChC,mCAA8B;AAC9B,+BAA0B"}

View File

@ -1,6 +1,6 @@
{
"name": "openvidu-node-client",
"version": "1.7.0",
"version": "1.8.0",
"description": "OpenVidu Node Client",
"author": "OpenVidu",
"license": "Apache-2.0",
@ -16,9 +16,8 @@
"dependencies": {},
"devDependencies": {
"@types/node": "9.4.0",
"codelyzer": "4.1.0",
"ts-node": "4.1.0",
"tslint": "5.9.1",
"typescript": "2.6.2"
}
}
}

View File

@ -0,0 +1,77 @@
export class Archive {
private id: string;
private name: string;
private sessionId: string;
private createdAt: number;
private size: number = 0;
private duration: number = 0;
private url: string;
private hasaudio: boolean = true;
private hasvideo: boolean = true;
private status: Archive.Status;
constructor(json: JSON) {
this.id = json['id'];
this.name = json['name'];
this.sessionId = json['sessionId'];
this.createdAt = json['createdAt'];
this.size = json['size'];
this.duration = json['duration'];
this.url = json['url'];
this.hasaudio = json['hasAudio'];
this.hasvideo = json['hasVideo'];
this.status = json['status'];
}
public getStatus(): Archive.Status {
return this.status;
}
public getId(): string {
return this.id;
}
public getName(): string {
return this.name;
}
public getSessionId(): string {
return this.sessionId;
}
public getCreatedAt(): number {
return this.createdAt;
}
public getSize(): number {
return this.size;
}
public getDuration(): number {
return this.duration;
}
public getUrl(): string {
return this.url;
}
public hasAudio(): boolean {
return this.hasaudio;
}
public hasVideo(): boolean {
return this.hasvideo;
}
}
export namespace Archive {
export enum Status {
starting, // The recording is starting (cannot be stopped)
started, // The recording has started and is going on
stopped, // The recording has finished OK
available, // The recording is available for downloading. This status is reached for all
// stopped recordings if property 'openvidu.recording.free-access' is true
failed // The recording has failed
}
}

View File

@ -1,5 +1,5 @@
export enum ArchiveLayout {
BEST_FIT = 'BEST_FIT',
BEST_FIT = 'BEST_FIT', // All the videos are evenly distributed, taking up as much space as possible
PICTURE_IN_PICTURE = 'PICTURE_IN_PICTURE',
VERTICAL_PRESENTATION = 'VERTICAL_PRESENTATION',
HORIZONTAL_PRESENTATION = 'VERTICAL_PRESENTATION'

View File

@ -1,20 +1,249 @@
import { Session } from "./Session";
import { SessionProperties } from "./SessionProperties";
import { Archive } from "./Archive";
declare const Buffer;
let https = require('https');
export class OpenVidu {
constructor(private urlOpenViduServer: string, private secret: string) { }
private static readonly API_RECORDINGS: string = '/api/recordings';
private static readonly API_RECORDINGS_START: string = '/start';
private static readonly API_RECORDINGS_STOP: string = '/stop';
private hostname: string;
private port: number;
private basicAuth: string;
constructor(private urlOpenViduServer: string, secret: string) {
this.setHostnameAndPort();
this.basicAuth = this.getBasicAuth(secret);
}
public createSession(properties?: SessionProperties): Session {
return new Session(this.urlOpenViduServer, this.secret, properties);
return new Session(this.hostname, this.port, this.basicAuth, properties);
}
public startArchive(sessionId: string) {
// TODO: REST POST to start recording in OpenVidu Server
public startRecording(sessionId: string): Promise<Archive> {
return new Promise<Archive>((resolve, reject) => {
let requestBody = JSON.stringify({
'session': sessionId
});
let options = {
hostname: this.hostname,
port: this.port,
path: OpenVidu.API_RECORDINGS + OpenVidu.API_RECORDINGS_START,
method: 'POST',
headers: {
'Authorization': this.basicAuth,
'Content-Type': 'application/json',
'Content-Length': Buffer.byteLength(requestBody)
}
}
const req = https.request(options, (res) => {
let body = '';
res.on('data', (d) => {
// Continuously update stream with data
body += d;
});
res.on('end', () => {
if (res.statusCode === 200) {
// SUCCESS response from openvidu-server (Archive in JSON format). Resolve new Archive
resolve(new Archive(JSON.parse(body)));
} else {
// ERROR response from openvidu-server. Resolve HTTP status
reject(new Error(res.statusCode));
}
});
});
req.on('error', (e) => {
reject(new Error(e));
});
req.write(requestBody);
req.end();
});
}
public stopArchive(sessionId: string) {
// TODO: REST POST to end recording in OpenVidu Server
public stopRecording(recordingId: string): Promise<Archive> {
return new Promise<Archive>((resolve, reject) => {
let options = {
hostname: this.hostname,
port: this.port,
path: OpenVidu.API_RECORDINGS + OpenVidu.API_RECORDINGS_STOP + '/' + recordingId,
method: 'POST',
headers: {
'Authorization': this.basicAuth,
'Content-Type': 'application/x-www-form-urlencoded'
}
}
const req = https.request(options, (res) => {
let body = '';
res.on('data', (d) => {
// Continuously update stream with data
body += d;
});
res.on('end', () => {
if (res.statusCode === 200) {
// SUCCESS response from openvidu-server (Archive in JSON format). Resolve new Archive
resolve(new Archive(JSON.parse(body)));
} else {
// ERROR response from openvidu-server. Resolve HTTP status
reject(new Error(res.statusCode));
}
});
});
req.on('error', (e) => {
reject(new Error(e));
});
//req.write();
req.end();
});
}
public getRecording(recordingId: string): Promise<Archive> {
return new Promise<Archive>((resolve, reject) => {
let options = {
hostname: this.hostname,
port: this.port,
path: OpenVidu.API_RECORDINGS + '/' + recordingId,
method: 'GET',
headers: {
'Authorization': this.basicAuth,
'Content-Type': 'application/x-www-form-urlencoded'
}
}
const req = https.request(options, (res) => {
let body = '';
res.on('data', (d) => {
// Continuously update stream with data
body += d;
});
res.on('end', () => {
if (res.statusCode === 200) {
// SUCCESS response from openvidu-server (Archive in JSON format). Resolve new Archive
resolve(new Archive(JSON.parse(body)));
} else {
// ERROR response from openvidu-server. Resolve HTTP status
reject(new Error(res.statusCode));
}
});
});
req.on('error', (e) => {
reject(new Error(e));
});
//req.write();
req.end();
});
}
public listRecordings(): Promise<Archive[]> {
return new Promise<Archive[]>((resolve, reject) => {
let options = {
hostname: this.hostname,
port: this.port,
path: OpenVidu.API_RECORDINGS,
method: 'GET',
headers: {
'Authorization': this.basicAuth,
'Content-Type': 'application/x-www-form-urlencoded'
}
}
const req = https.request(options, (res) => {
let body = '';
res.on('data', (d) => {
// Continuously update stream with data
body += d;
});
res.on('end', () => {
if (res.statusCode === 200) {
// SUCCESS response from openvidu-server (JSON arrays of Archives in JSON format). Resolve list of new Archives
let archiveArray: Archive[] = [];
let responseItems = JSON.parse(body)['items'];
for (let i = 0; i < responseItems.length; i++) {
archiveArray.push(new Archive(responseItems[i]));
}
resolve(archiveArray);
} else {
// ERROR response from openvidu-server. Resolve HTTP status
reject(new Error(res.statusCode));
}
});
});
req.on('error', (e) => {
reject(new Error(e));
});
//req.write();
req.end();
});
}
public deleteRecording(recordingId: string): Promise<Error> {
return new Promise<Error>((resolve, reject) => {
let options = {
hostname: this.hostname,
port: this.port,
path: OpenVidu.API_RECORDINGS + '/' + recordingId,
method: 'DELETE',
headers: {
'Authorization': this.basicAuth,
'Content-Type': 'application/x-www-form-urlencoded'
}
}
const req = https.request(options, (res) => {
let body = '';
res.on('data', (d) => {
// Continuously update stream with data
body += d;
});
res.on('end', () => {
if (res.statusCode === 204) {
// SUCCESS response from openvidu-server. Resolve undefined
resolve(undefined);
} else {
// ERROR response from openvidu-server. Resolve HTTP status
reject(new Error(res.statusCode));
}
});
});
req.on('error', (e) => {
reject(new Error(e));
});
//req.write();
req.end();
});
}
private getBasicAuth(secret: string): string {
return 'Basic ' + (new Buffer('OPENVIDUAPP:' + secret).toString('base64'));
}
private setHostnameAndPort(): void {
let urlSplitted = this.urlOpenViduServer.split(':');
if (urlSplitted.length === 3) { // URL has format: http:// + hostname + :port
this.hostname = this.urlOpenViduServer.split(':')[1].replace(/\//g, '');
this.port = parseInt(this.urlOpenViduServer.split(':')[2].replace(/\//g, ''));
} else if (urlSplitted.length == 2) { // URL has format: hostname + :port
this.hostname = this.urlOpenViduServer.split(':')[0].replace(/\//g, '');
this.port = parseInt(this.urlOpenViduServer.split(':')[1].replace(/\//g, ''));
} else {
console.error("URL format incorrect: it must contain hostname and port (current value: '" + this.urlOpenViduServer + "')");
}
}
}

View File

@ -9,20 +9,18 @@ let https = require('https');
export class Session {
private sessionIdURL: string = '/api/sessions';
private tokenURL: string = '/api/tokens';
private static readonly API_SESSIONS: string = '/api/sessions';
private static readonly API_TOKENS: string = '/api/tokens';
private sessionId: string = "";
private properties: SessionProperties;
private hostname: string;
private port: number;
constructor(private urlOpenViduServer: string, private secret: string, properties?: SessionProperties) {
constructor(private hostname: string, private port: number, private basicAuth: string, properties?: SessionProperties) {
if (properties == null) {
this.properties = new SessionProperties.Builder().build();
} else {
this.properties = properties;
}
this.setHostnameAndPort();
}
public getSessionId(callback: Function) {
@ -41,10 +39,10 @@ export class Session {
let options = {
hostname: this.hostname,
port: this.port,
path: this.sessionIdURL,
path: Session.API_SESSIONS,
method: 'POST',
headers: {
'Authorization': this.getBasicAuth(),
'Authorization': this.basicAuth,
'Content-Type': 'application/json',
'Content-Length': Buffer.byteLength(requestBody)
}
@ -56,10 +54,15 @@ export class Session {
body += d;
});
res.on('end', () => {
// Data reception is done
let parsed = JSON.parse(body);
this.sessionId = parsed.id;
callback(parsed.id);
if (res.statusCode === 200) {
// SUCCESS response from openvidu-server. Resolve sessionId
let parsed = JSON.parse(body);
this.sessionId = parsed.id;
callback(parsed.id);
} else {
// ERROR response from openvidu-server. Resolve HTTP status
console.error(res.statusCode);
}
});
});
@ -94,10 +97,10 @@ export class Session {
let options = {
hostname: this.hostname,
port: this.port,
path: this.tokenURL,
path: Session.API_TOKENS,
method: 'POST',
headers: {
'Authorization': this.getBasicAuth(),
'Authorization': this.basicAuth,
'Content-Type': 'application/json',
'Content-Length': Buffer.byteLength(requestBody)
}
@ -109,9 +112,14 @@ export class Session {
body += d;
});
res.on('end', () => {
// Data reception is done
let parsed = JSON.parse(body);
callback(parsed.id);
if (res.statusCode === 200) {
// SUCCESS response from openvidu-server. Resolve token
let parsed = JSON.parse(body);
callback(parsed.id);
} else {
// ERROR response from openvidu-server. Resolve HTTP status
console.error(res.statusCode);
}
});
});
@ -123,24 +131,7 @@ export class Session {
}
public getProperties(): SessionProperties {
return this.properties;
}
private getBasicAuth() {
return 'Basic ' + (new Buffer('OPENVIDUAPP:' + this.secret).toString('base64'));
}
private setHostnameAndPort() {
let urlSplitted = this.urlOpenViduServer.split(':');
if (urlSplitted.length === 3) { // URL has format: http:// + hostname + :port
this.hostname = this.urlOpenViduServer.split(':')[1].replace(/\//g, '');
this.port = parseInt(this.urlOpenViduServer.split(':')[2].replace(/\//g, ''));
} else if (urlSplitted.length == 2) { // URL has format: hostname + :port
this.hostname = this.urlOpenViduServer.split(':')[0].replace(/\//g, '');
this.port = parseInt(this.urlOpenViduServer.split(':')[1].replace(/\//g, ''));
} else {
console.error("URL format incorrect: it must contain hostname and port (current value: '" + this.urlOpenViduServer + "')");
}
return this.properties;
}
}

View File

@ -16,8 +16,8 @@ export class TokenOptions {
export namespace TokenOptions {
export class Builder {
private dataProp: string;
private roleProp: OpenViduRole;
private dataProp: string = '';
private roleProp: OpenViduRole = OpenViduRole.PUBLISHER;
build(): TokenOptions {
return new TokenOptions(this.dataProp, this.roleProp);

View File

@ -1,8 +1,9 @@
export * from './OpenVidu';
export * from './OpenViduRole';
export * from './Session';
export * from './SessionProperties';
export * from './TokenOptions';
export * from './MediaMode';
export * from './ArchiveLayout';
export * from './ArchiveMode';
export * from './SessionProperties';
export * from './Archive';

View File

@ -1,41 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<classpath>
<classpathentry kind="src" output="target/classes" path="src/main/java">
<attributes>
<attribute name="optional" value="true"/>
<attribute name="maven.pomderived" value="true"/>
</attributes>
</classpathentry>
<classpathentry excluding="**" kind="src" output="target/classes" path="src/main/resources">
<attributes>
<attribute name="maven.pomderived" value="true"/>
</attributes>
</classpathentry>
<classpathentry kind="src" output="target/test-classes" path="src/test/java">
<attributes>
<attribute name="optional" value="true"/>
<attribute name="maven.pomderived" value="true"/>
</attributes>
</classpathentry>
<classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-1.8">
<attributes>
<attribute name="maven.pomderived" value="true"/>
</attributes>
</classpathentry>
<classpathentry kind="con" path="org.eclipse.m2e.MAVEN2_CLASSPATH_CONTAINER">
<attributes>
<attribute name="maven.pomderived" value="true"/>
</attributes>
</classpathentry>
<classpathentry excluding="**" kind="src" output="target/test-classes" path="src/test/resources">
<attributes>
<attribute name="maven.pomderived" value="true"/>
</attributes>
</classpathentry>
<classpathentry kind="src" path=".apt_generated">
<attributes>
<attribute name="optional" value="true"/>
</attributes>
</classpathentry>
<classpathentry kind="output" path="target/classes"/>
</classpath>

View File

@ -1,40 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<projectDescription>
<name>kurento-room-server</name>
<comment></comment>
<projects>
</projects>
<buildSpec>
<buildCommand>
<name>org.eclipse.wst.common.project.facet.core.builder</name>
<arguments>
</arguments>
</buildCommand>
<buildCommand>
<name>org.eclipse.jdt.core.javabuilder</name>
<arguments>
</arguments>
</buildCommand>
<buildCommand>
<name>org.springframework.ide.eclipse.core.springbuilder</name>
<arguments>
</arguments>
</buildCommand>
<buildCommand>
<name>org.springframework.ide.eclipse.boot.validation.springbootbuilder</name>
<arguments>
</arguments>
</buildCommand>
<buildCommand>
<name>org.eclipse.m2e.core.maven2Builder</name>
<arguments>
</arguments>
</buildCommand>
</buildSpec>
<natures>
<nature>org.springframework.ide.eclipse.core.springnature</nature>
<nature>org.eclipse.jdt.core.javanature</nature>
<nature>org.eclipse.m2e.core.maven2Nature</nature>
<nature>org.eclipse.wst.common.project.facet.core.nature</nature>
</natures>
</projectDescription>

View File

@ -1,6 +0,0 @@
eclipse.preferences.version=1
encoding//src/main/java=UTF-8
encoding//src/main/resources=UTF-8
encoding//src/test/java=UTF-8
encoding//src/test/resources=UTF-8
encoding/<project>=UTF-8

View File

@ -1,6 +0,0 @@
eclipse.preferences.version=1
org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.8
org.eclipse.jdt.core.compiler.compliance=1.8
org.eclipse.jdt.core.compiler.problem.forbiddenReference=warning
org.eclipse.jdt.core.compiler.processAnnotations=enabled
org.eclipse.jdt.core.compiler.source=1.8

View File

@ -1,4 +0,0 @@
activeProfiles=
eclipse.preferences.version=1
resolveWorkspaceProjects=true
version=1

View File

@ -13,6 +13,9 @@ RUN add-apt-repository ppa:mc3man/xerus-media
RUN apt-get update
RUN apt-get install -y ffmpeg pulseaudio xvfb
# Install jq for managing JSON
RUN apt-get install -y jq
# Clean
RUN apt-get autoclean

122
openvidu-server/docker/openvidu-recording/entrypoint.sh Normal file → Executable file
View File

@ -1,47 +1,131 @@
#!/bin/bash
adduser --uid $USER_ID --disabled-password --gecos "" myuser
### Variables ###
CURRENT_UID="$(id -u $USER)"
if [[ $CURRENT_UID != $USER_ID ]]; then
adduser --uid $USER_ID --disabled-password --gecos "" myuser
fi
URL="${URL:-https://www.youtube.com/watch?v=JMuzlEQz3uo}"
RESOLUTION="${RESOLUTION:-1920x1080}"
FRAMERATE="${FRAMERATE:-30}"
FRAMERATE="${FRAMERATE:-24}"
VIDEO_SIZE="$RESOLUTION"
ARRAY=(${VIDEO_SIZE//x/ })
VIDEO_NAME="${VIDEO_NAME:-video}"
VIDEO_FORMAT="${VIDEO_FORMAT:-mp4}"
RECORDING_JSON="'${RECORDING_JSON}'"
echo "----------------------------------------"
echo "Recording URL -> $URL"
echo "----------------------------------------"
### Store Recording json data ###
if [[ $CURRENT_UID != $USER_ID ]]; then
su myuser -c "echo ${RECORDING_JSON} > /recordings/.recording.${VIDEO_NAME}"
else
echo ${RECORDING_JSON} > /recordings/.recording.${VIDEO_NAME}
fi
### Get a free display identificator ###
DISPLAY_NUM=99
DONE="no"
while [ "$DONE" == "no" ]
do
out=$(xdpyinfo -display :$DISPLAY_NUM 2>&1)
if [[ "$out" == name* ]] || [[ "$out" == Invalid* ]]
then
# command succeeded; or failed with access error; display exists
(( DISPLAY_NUM+=1 ))
else
# display doesn't exist
DONE="yes"
fi
out=$(xdpyinfo -display :$DISPLAY_NUM 2>&1)
if [[ "$out" == name* ]] || [[ "$out" == Invalid* ]]
then
# Command succeeded; or failed with access error; display exists
(( DISPLAY_NUM+=1 ))
else
# Display doesn't exist
DONE="yes"
fi
done
echo "First available display -> :$DISPLAY_NUM"
echo "----------------------------------------"
su myuser -c "pulseaudio -D"
### Start pulseaudio ###
if [[ $CURRENT_UID != $USER_ID ]]; then
su myuser -c "pulseaudio -D"
else
pulseaudio -D
fi
### Start Chrome in headless mode with xvfb, using the display num previously obtained ###
touch xvfb.log
chmod 777 xvfb.log
su myuser -c "xvfb-run --server-num=${DISPLAY_NUM} --server-args='-ac -screen 0 ${RESOLUTION}x24 -noreset' google-chrome -no-sandbox -disable-infobars -window-size=${ARRAY[0]},${ARRAY[1]} -start-fullscreen -no-first-run -ignore-certificate-errors $URL &> xvfb.log &"
sleep 3
if [[ $CURRENT_UID != $USER_ID ]]; then
su myuser -c "xvfb-run --server-num=${DISPLAY_NUM} --server-args='-ac -screen 0 ${RESOLUTION}x24 -noreset' google-chrome -no-sandbox -test-type -disable-infobars -window-size=${ARRAY[0]},${ARRAY[1]} -no-first-run -ignore-certificate-errors --kiosk $URL &> xvfb.log &"
else
xvfb-run --server-num=${DISPLAY_NUM} --server-args="-ac -screen 0 ${RESOLUTION}x24 -noreset" google-chrome -no-sandbox -test-type -disable-infobars -window-size=${ARRAY[0]},${ARRAY[1]} -no-first-run -ignore-certificate-errors --kiosk $URL &> xvfb.log &
fi
touch stop
chmod 777 /recordings
su myuser -c "<./stop ffmpeg -y -video_size $RESOLUTION -framerate $FRAMERATE -f x11grab -i :${DISPLAY_NUM} -f pulse -ac 2 -i default /recordings/${VIDEO_NAME}.${VIDEO_FORMAT}"
sleep 2
### Start recording with ffmpeg ###
if [[ $CURRENT_UID != $USER_ID ]]; then
su myuser -c "<./stop ffmpeg -y -f alsa -i pulse -f x11grab -framerate 25 -video_size $RESOLUTION -i :${DISPLAY_NUM} -c:a libfdk_aac -c:v libx264 -preset ultrafast -crf 28 -refs 4 -qmin 4 -pix_fmt yuv420p -filter:v fps=25 '/recordings/${VIDEO_NAME}.${VIDEO_FORMAT}'"
else
<./stop ffmpeg -y -f alsa -i pulse -f x11grab -framerate 25 -video_size $RESOLUTION -i :${DISPLAY_NUM} -c:a libfdk_aac -c:v libx264 -preset ultrafast -crf 28 -refs 4 -qmin 4 -pix_fmt yuv420p -filter:v fps=25 "/recordings/${VIDEO_NAME}.${VIDEO_FORMAT}"
fi
### Generate video report file ###
if [[ $CURRENT_UID != $USER_ID ]]; then
su myuser -c "ffprobe -v quiet -print_format json -show_format -show_streams /recordings/${VIDEO_NAME}.${VIDEO_FORMAT} > /recordings/${VIDEO_NAME}.info"
else
ffprobe -v quiet -print_format json -show_format -show_streams /recordings/${VIDEO_NAME}.${VIDEO_FORMAT} > /recordings/${VIDEO_NAME}.info
fi
### Update Recording json data ###
if [[ $CURRENT_UID != $USER_ID ]]; then
TMP=$(su myuser -c "mktemp /recordings/.${VIDEO_NAME}.XXXXXXXXXXXXXXXXXXXXXXX.if.json")
INFO=$(su myuser -c "cat /recordings/${VIDEO_NAME}.info | jq '.'")
HAS_AUDIO_AUX=$(su myuser -c "echo '$INFO' | jq '.streams[] | select(.codec_type == \"audio\")'")
if [ -z "$HAS_AUDIO_AUX" ]; then HAS_AUDIO=false; else HAS_AUDIO=true; fi
HAS_VIDEO_AUX=$(su myuser -c "echo '$INFO' | jq '.streams[] | select(.codec_type == \"video\")'")
if [ -z "$HAS_VIDEO_AUX" ]; then HAS_VIDEO=false; else HAS_VIDEO=true; fi
SIZE=$(su myuser -c "echo '$INFO' | jq '.format.size | tonumber'")
DURATION=$(su myuser -c "echo '$INFO' | jq '.format.duration | tonumber'")
STATUS="stopped"
# su myuser -c "echo 'TMP=${TMP}, SIZE=${SIZE}, DURATION=${DURATION}, STATUS=${STATUS}, HAS_AUDIO=${HAS_AUDIO}, HAS_VIDEO=${HAS_VIDEO}' > /recordings/.${VIDEO_NAME}.if.vars"
su myuser -c "jq -c -r \".hasAudio=${HAS_AUDIO} | .hasVideo=${HAS_VIDEO} | .duration=${DURATION} | .size=${SIZE} | .status=\\\"${STATUS}\\\"\" \"/recordings/.recording.${VIDEO_NAME}\" > ${TMP} && mv ${TMP} \"/recordings/.recording.${VIDEO_NAME}\""
else
TMP=$(mktemp /recordings/.${VIDEO_NAME}.XXXXXXXXXXXXXXXXXXXXXXX.if.json)
INFO=$(cat /recordings/${VIDEO_NAME}.info | jq '.')
HAS_AUDIO_AUX=$(echo "$INFO" | jq '.streams[] | select(.codec_type == "audio")')
if [ -z "$HAS_AUDIO_AUX" ]; then HAS_AUDIO=false; else HAS_AUDIO=true; fi
HAS_VIDEO_AUX=$(echo "$INFO" | jq '.streams[] | select(.codec_type == "video")')
if [ -z "$HAS_VIDEO_AUX" ]; then HAS_VIDEO=false; else HAS_VIDEO=true; fi
SIZE=$(echo "$INFO" | jq '.format.size | tonumber')
DURATION=$(echo "$INFO" | jq '.format.duration | tonumber')
STATUS="stopped"
# echo "TMP=${TMP}, SIZE=${SIZE}, DURATION=${DURATION}, STATUS=${STATUS}, HAS_AUDIO=${HAS_AUDIO}, HAS_VIDEO=${HAS_VIDEO}" > /recordings/.${VIDEO_NAME}.if.vars
jq -c -r ".hasAudio=${HAS_AUDIO} | .hasVideo=${HAS_VIDEO} | .duration=${DURATION} | .size=${SIZE} | .status=\"${STATUS}\"" "/recordings/.recording.${VIDEO_NAME}" > ${TMP} && mv ${TMP} "/recordings/.recording.${VIDEO_NAME}"
fi

View File

@ -14,12 +14,15 @@ COPY kms.sh /kms.sh
# Install Java
RUN apt-get update && apt-get install -y openjdk-8-jdk && rm -rf /var/lib/apt/lists/*
# Configure Supervisor
RUN mkdir -p /var/log/supervisor
COPY supervisord.conf /etc/supervisor/conf.d/supervisord.conf
# Install supervisor
RUN apt-get update && apt-get install -y supervisor && rm -rf /var/lib/apt/lists/*
# Install OpenVidu Server
# Configure supervisor
RUN mkdir -p /var/log/supervisor
COPY supervisord.conf /etc/supervisor/conf.d/supervisord.conf
# Copy OpenVidu Server
COPY openvidu-server-cbx.jar openvidu-server-cbx.jar
EXPOSE 8888

View File

@ -9,5 +9,5 @@ command=/bin/bash /kms.sh
redirect_stderr=true
[program:openvidu-server]
command=/bin/bash -c "java -jar /openvidu-server-cbx.jar"
redirect_stderr=true
command=/bin/bash -c "java -jar -Dopenvidu.publicurl=docker-local /openvidu-server-cbx.jar"
redirect_stderr=true

View File

@ -4,10 +4,12 @@ MAINTAINER miguel.rodriguez@cocodin.com
# Install Java
RUN apt-get update && apt-get install -y openjdk-8-jdk && rm -rf /var/lib/apt/lists/*
# Configure Supervisor
# Install supervisor
RUN apt-get update && apt-get install -y supervisor && rm -rf /var/lib/apt/lists/*
# Configure supervisor
RUN mkdir -p /var/log/supervisor
COPY supervisord.conf /etc/supervisor/conf.d/supervisord.conf
RUN apt-get update && apt-get install -y supervisor && rm -rf /var/lib/apt/lists/*
# Copy OpenVidu Server
COPY openvidu-server-cbx.jar /

View File

@ -5,5 +5,6 @@ pidfile=/var/run/supervisord.pid;
loglevel=debug
[program:openvidu-server]
command=/bin/bash -c "java -jar /openvidu-server-cbx.jar"
command=/bin/bash -c "java -jar -Dopenvidu.publicurl=docker-local /openvidu-server-cbx.jar"
redirect_stderr=true

View File

@ -12,7 +12,7 @@
<packaging>jar</packaging>
<name>OpenVidu Server</name>
<version>1.7.0</version>
<version>1.8.0</version>
<description>OpenVidu Server</description>
<url>https://github.com/OpenVidu/openvidu</url>
@ -209,7 +209,17 @@
<dependency>
<groupId>io.openvidu</groupId>
<artifactId>openvidu-java-client</artifactId>
<version>1.7.0</version>
<version>1.8.0</version>
</dependency>
<dependency>
<groupId>org.codehaus.janino</groupId>
<artifactId>janino</artifactId>
<version>3.0.7</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.7</version>
</dependency>
<!-- Test dependencies -->

View File

@ -0,0 +1,13 @@
# Required metadata
sonar.projectKey=java-sonar-runner-simple
sonar.projectName=Sonar OpenVidu Server
sonar.projectVersion=1.0
# Comma-separated paths to directories with sources (required)
sonar.sources=src
# Language
sonar.language=java
# Encoding of the source files
sonar.sourceEncoding=UTF-8

View File

@ -3,39 +3,36 @@
"project": {
"name": "openvidu-server-frontend"
},
"apps": [
{
"root": "src",
"outDir": "dist",
"assets": [
"assets",
"favicon.ico"
],
"index": "index.html",
"main": "main.ts",
"polyfills": "polyfills.ts",
"test": "test.ts",
"tsconfig": "tsconfig.app.json",
"testTsconfig": "tsconfig.spec.json",
"prefix": "app",
"styles": [
"styles.css"
],
"scripts": [],
"environmentSource": "environments/environment.ts",
"environments": {
"dev": "environments/environment.ts",
"prod": "environments/environment.prod.ts"
}
"apps": [{
"root": "src",
"outDir": "dist",
"assets": [
"assets",
"favicon.ico"
],
"index": "index.html",
"main": "main.ts",
"polyfills": "polyfills.ts",
"test": "test.ts",
"tsconfig": "tsconfig.app.json",
"testTsconfig": "tsconfig.spec.json",
"prefix": "app",
"styles": [
"styles.css"
],
"scripts": [],
"environmentSource": "environments/environment.ts",
"environments": {
"dev": "environments/environment.ts",
"prod": "environments/environment.prod.ts"
}
],
}],
"e2e": {
"protractor": {
"config": "./protractor.conf.js"
}
},
"lint": [
{
"lint": [{
"project": "src/tsconfig.app.json"
},
{

View File

@ -14,7 +14,7 @@
"@angular/router": "5.0.5",
"core-js": "2.5.1",
"hammerjs": "2.0.8",
"openvidu-browser": "1.7.0",
"openvidu-browser": "1.8.0",
"rxjs": "5.5.3",
"zone.js": "0.8.18"
},

View File

@ -123,7 +123,13 @@ export class DashboardComponent implements OnInit, OnDestroy {
audioActive: true,
videoActive: true,
quality: 'MEDIUM'
});
},
e => {
if (!!e) {
console.error(e);
}
}
);
publisherRemote.on('accessAllowed', () => {
this.msgChain.push('Camera access allowed');

View File

@ -1,10 +1,871 @@
.bounds {
background-color: black;
height: 100%;
overflow: hidden;
cursor: none !important;
position: absolute;
left: 0;
right: 0;
top: 0;
bottom: 0;
}
video {
-o-object-fit: cover;
object-fit: cover;
display: block;
position: absolute;
width: 100%;
height: 100%;
color: #ffffff;
margin: 0;
padding: 0;
border: 0;
font-size: 100%;
font-family: Arial, Helvetica, sans-serif;
}
/*!
* Copyright (c) 2017 TokBox, Inc.
* Released under the MIT license
* http://opensource.org/licenses/MIT
*/
.custom-class {
min-height: 0px !important;
}
/**
* OT Base styles
*/
/* Root OT object, this is where our CSS reset happens */
.OT_root,
.OT_root * {
color: #ffffff;
margin: 0;
padding: 0;
border: 0;
font-size: 100%;
font-family: Arial, Helvetica, sans-serif;
vertical-align: baseline;
}
.OT_dialog-centering {
display: table;
width: 100%;
height: 100%;
}
.OT_dialog-centering-child {
display: table-cell;
vertical-align: middle;
}
.OT_dialog {
position: relative;
-webkit-box-sizing: border-box;
box-sizing: border-box;
max-width: 576px;
margin-right: auto;
margin-left: auto;
padding: 36px;
text-align: center; /* centers all the inline content */
background-color: #363636;
color: #fff;
-webkit-box-shadow: 2px 4px 6px #999;
box-shadow: 2px 4px 6px #999;
font-family: 'Didact Gothic', sans-serif;
font-size: 13px;
line-height: 1.4;
}
.OT_dialog * {
font-family: inherit;
-webkit-box-sizing: inherit;
box-sizing: inherit;
}
.OT_closeButton {
color: #999999;
cursor: pointer;
font-size: 32px;
line-height: 36px;
position: absolute;
right: 18px;
top: 0;
}
.OT_dialog-messages {
text-align: center;
}
.OT_dialog-messages-main {
margin-bottom: 36px;
line-height: 36px;
font-weight: 300;
font-size: 24px;
}
.OT_dialog-messages-minor {
margin-bottom: 18px;
font-size: 13px;
line-height: 18px;
color: #A4A4A4;
}
.OT_dialog-messages-minor strong {
color: #ffffff;
}
.OT_dialog-actions-card {
display: inline-block;
}
.OT_dialog-button-title {
margin-bottom: 18px;
line-height: 18px;
font-weight: 300;
text-align: center;
font-size: 14px;
color: #999999;
}
.OT_dialog-button-title label {
color: #999999;
}
.OT_dialog-button-title a,
.OT_dialog-button-title a:link,
.OT_dialog-button-title a:active {
color: #02A1DE;
}
.OT_dialog-button-title strong {
color: #ffffff;
font-weight: 100;
display: block;
}
.OT_dialog-button {
display: inline-block;
margin-bottom: 18px;
padding: 0 1em;
background-color: #1CA3DC;
text-align: center;
cursor: pointer;
}
.OT_dialog-button:disabled {
cursor: not-allowed;
opacity: 0.5;
}
.OT_dialog-button-large {
line-height: 36px;
padding-top: 9px;
padding-bottom: 9px;
font-weight: 100;
font-size: 24px;
}
.OT_dialog-button-small {
line-height: 18px;
padding-top: 9px;
padding-bottom: 9px;
background-color: #444444;
color: #999999;
font-size: 16px;
}
.OT_dialog-progress-bar {
display: inline-block; /* prevents margin collapse */
width: 100%;
margin-top: 5px;
margin-bottom: 41px;
border: 1px solid #4E4E4E;
height: 8px;
}
.OT_dialog-progress-bar-fill {
height: 100%;
background-color: #29A4DA;
}
.OT_dialog-plugin-upgrading .OT_dialog-plugin-upgrade-percentage {
line-height: 54px;
font-size: 48px;
font-weight: 100;
}
/* Helpers */
.OT_centered {
position: fixed;
left: 50%;
top: 50%;
margin: 0;
}
.OT_dialog-hidden {
display: none;
}
.OT_dialog-button-block {
display: block;
}
.OT_dialog-no-natural-margin {
margin-bottom: 0;
}
/* Publisher and Subscriber styles */
.OT_publisher, .OT_subscriber {
position: relative;
min-width: 48px;
min-height: 48px;
}
.OT_publisher .OT_video-element,
.OT_subscriber .OT_video-element {
display: block;
position: absolute;
width: 100%;
height: 100%;
-webkit-transform-origin: 0 0;
transform-origin: 0 0;
}
/* Styles that are applied when the video element should be mirrored */
.OT_publisher.OT_mirrored .OT_video-element {
-webkit-transform: scale(-1, 1);
transform: scale(-1, 1);
-webkit-transform-origin: 50% 50%;
transform-origin: 50% 50%;
}
.OT_subscriber_error {
background-color: #000;
color: #fff;
text-align: center;
}
.OT_subscriber_error > p {
padding: 20px;
}
/* The publisher/subscriber name/mute background */
.OT_publisher .OT_bar,
.OT_subscriber .OT_bar,
.OT_publisher .OT_name,
.OT_subscriber .OT_name,
.OT_publisher .OT_archiving,
.OT_subscriber .OT_archiving,
.OT_publisher .OT_archiving-status,
.OT_subscriber .OT_archiving-status,
.OT_publisher .OT_archiving-light-box,
.OT_subscriber .OT_archiving-light-box {
-webkit-box-sizing: border-box;
-ms-box-sizing: border-box;
box-sizing: border-box;
top: 0;
left: 0;
right: 0;
display: block;
height: 34px;
position: absolute;
}
.OT_publisher .OT_bar,
.OT_subscriber .OT_bar {
background: rgba(0, 0, 0, 0.4);
}
.OT_publisher .OT_edge-bar-item,
.OT_subscriber .OT_edge-bar-item {
z-index: 1; /* required to get audio level meter underneath */
}
/* The publisher/subscriber name panel/archiving status bar */
.OT_publisher .OT_name,
.OT_subscriber .OT_name {
background-color: transparent;
color: #ffffff;
font-size: 15px;
line-height: 34px;
font-weight: normal;
padding: 0 4px 0 36px;
}
.OT_publisher .OT_archiving-status,
.OT_subscriber .OT_archiving-status {
background: rgba(0, 0, 0, 0.4);
top: auto;
bottom: 0;
left: 34px;
padding: 0 4px;
color: rgba(255, 255, 255, 0.8);
font-size: 15px;
line-height: 34px;
font-weight: normal;
}
.OT_micro .OT_archiving-status,
.OT_micro:hover .OT_archiving-status,
.OT_mini .OT_archiving-status,
.OT_mini:hover .OT_archiving-status {
display: none;
}
.OT_publisher .OT_archiving-light-box,
.OT_subscriber .OT_archiving-light-box {
background: rgba(0, 0, 0, 0.4);
top: auto;
bottom: 0;
right: auto;
width: 34px;
height: 34px;
}
.OT_archiving-light {
width: 7px;
height: 7px;
border-radius: 30px;
position: absolute;
top: 14px;
left: 14px;
background-color: #575757;
-webkit-box-shadow: 0 0 5px 1px #575757;
box-shadow: 0 0 5px 1px #575757;
}
.OT_archiving-light.OT_active {
background-color: #970d13;
animation: OT_pulse 1.3s ease-in;
-webkit-animation: OT_pulse 1.3s ease-in;
-moz-animation: OT_pulse 1.3s ease-in;
-webkit-animation: OT_pulse 1.3s ease-in;
animation-iteration-count: infinite;
-webkit-animation-iteration-count: infinite;
-moz-animation-iteration-count: infinite;
-webkit-animation-iteration-count: infinite;
}
@-webkit-keyframes OT_pulse {
0% {
-webkit-box-shadow: 0 0 0px 0px #c70019;
box-shadow: 0 0 0px 0px #c70019;
}
30% {
-webkit-box-shadow: 0 0 5px 1px #c70019;
box-shadow: 0 0 5px 1px #c70019;
}
50% {
-webkit-box-shadow: 0 0 5px 1px #c70019;
box-shadow: 0 0 5px 1px #c70019;
}
80% {
-webkit-box-shadow: 0 0 0px 0px #c70019;
box-shadow: 0 0 0px 0px #c70019;
}
100% {
-webkit-box-shadow: 0 0 0px 0px #c70019;
box-shadow: 0 0 0px 0px #c70019;
}
}
@-webkit-keyframes OT_pulse {
0% {
-webkit-box-shadow: 0 0 0px 0px #c70019;
box-shadow: 0 0 0px 0px #c70019;
}
30% {
-webkit-box-shadow: 0 0 5px 1px #c70019;
box-shadow: 0 0 5px 1px #c70019;
}
50% {
-webkit-box-shadow: 0 0 5px 1px #c70019;
box-shadow: 0 0 5px 1px #c70019;
}
80% {
-webkit-box-shadow: 0 0 0px 0px #c70019;
box-shadow: 0 0 0px 0px #c70019;
}
100% {
-webkit-box-shadow: 0 0 0px 0px #c70019;
box-shadow: 0 0 0px 0px #c70019;
}
}
.OT_mini .OT_bar,
.OT_bar.OT_mode-mini,
.OT_bar.OT_mode-mini-auto {
bottom: 0;
height: auto;
}
.OT_mini .OT_name.OT_mode-off,
.OT_mini .OT_name.OT_mode-on,
.OT_mini .OT_name.OT_mode-auto,
.OT_mini:hover .OT_name.OT_mode-auto {
display: none;
}
.OT_publisher .OT_name,
.OT_subscriber .OT_name {
left: 10px;
right: 37px;
height: 34px;
padding-left: 0;
}
.OT_publisher .OT_mute,
.OT_subscriber .OT_mute {
border: none;
cursor: pointer;
display: block;
position: absolute;
text-align: center;
text-indent: -9999em;
background-color: transparent;
background-repeat: no-repeat;
}
.OT_publisher .OT_mute,
.OT_subscriber .OT_mute {
right: 0;
top: 0;
border-left: 1px solid rgba(255, 255, 255, 0.2);
height: 36px;
width: 37px;
}
.OT_mini .OT_mute,
.OT_publisher.OT_mini .OT_mute.OT_mode-auto.OT_mode-on-hold,
.OT_subscriber.OT_mini .OT_mute.OT_mode-auto.OT_mode-on-hold {
top: 50%;
left: 50%;
right: auto;
margin-top: -18px;
margin-left: -18.5px;
border-left: none;
}
.OT_publisher .OT_mute {
background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABMAAAAcCAMAAAC02HQrAAAA1VBMVEUAAAD3+Pj3+Pj3+Pj3+Pj3+Pj3+Pj3+Pj3+Pj3+Pn3+Pj3+Pj3+Pj3+Pj3+Pj3+Pj3+Pj3+Pj3+Pj3+Pj3+Pj3+Pj3+Pj3+Pj3+Pj3+Pj3+Pj3+Pj3+Pj3+Pj3+Pj3+Pj3+Pj3+Pj3+Pj3+Pj3+Pj3+Pj3+Pj3+Pj3+Pj3+Pj3+Pj3+Pj3+Pj3+Pj3+Pj3+Pj3+Pj3+Pn3+Pj3+Pj3+Pj3+Pj3+Pj3+Pj3+Pj3+Pj3+Pj3+Pj3+Pj3+Pj3+Pj3+Pj3+Pj3+Pj3+Pj39/j3+Pj3+Pn4+Pk/JRMlAAAAQ3RSTlMABAUHCQoLDhAQERwdHiAjLjAxOD9ASFBRVl1mbnZ6fH2LjI+QkaWqrrC1uLzAwcXJycrL1NXj5Ofo6u3w9fr7/P3+d4M3+QAAAQBJREFUGBlVwYdCglAABdCLlr5Unijm3hMUtBzlBLSr//9JgUToOQgVJgceJgU8aHgMeA38K50ZOpcQmTPwcyXn+JM8M3JJIqQypiIkeXelTyIkGZPwKS1NMia1lgKTVkaE3oQQGYsmHNqSMWnTgUFbMiZtGlD2dpaxrL1XgM0i4ZK8MeAmFhsAs29MGZniawagS63oMOQUNXYB5D0D1RMDpyoMLw/fiE2og/V+PVDR5AiBl0/2Uwik+vx4xV3a5G5Ye68Nd1czjUjZckm6VhmPciRzeCZICjwTJAViQq+3e+St167rAoHK8sLYZVkBYPCZAZ/eGa+2R5LH7Wrc0YFf/O9J3yBDFaoAAAAASUVORK5CYII=);
background-position: 9px 5px;
}
.OT_publisher .OT_mute.OT_active {
background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABUAAAAdCAYAAABFRCf7AAADcElEQVRIiaWVXWhcRRTHf7NNd2aDtUKMIjTpg4ufFIuiUOmDEWm0Vi3VYhXRqIggQh4sWJFSig9+oOhTKSpIRUWMIBIr2kptoTbgU6ooxCiIjR+14kcJmf9sNceHnd3ebnc3Uv9wuXfOzPzmnDMz5zozGwdWAbc65w5RUJQ8cC2wDJgFJioh/MJCMrNxq2vOzK4HmIvRRemxKP0RJWt53o7S+d2Yzsx6gQ+AIUDAnUqpBLzXZd4RYFUlhB/bdZacc3PAOmAcCMC7wfvFwLNdoAPAyx09bXyYWRl4E7gDmAdGlNKFwLYu8GolhO9O87RJd64GbMrgEvB68P4osMWdXLtVV7czlooNpVRWSs8DO7NpR/B+3rBHsvetCgtCMTxwQCm9BbyQrc8F7/uBex3uRCeXO0PrUZ4NfKyUPgWeyj3bg/crDNsIRGwBaJQGorQ3Svdn2wHgc2BUKb0DPJHtjwfvbwRucc7tz+N+i9LFUdoXpfVN36I0CVwBTFI/q9e1LPxT8P4qYEdu70q12mYzWw1MYQzjeJF6zq+shHC4B7jklOBPP/TzSunh4P0DwKvAfb5c9krpe+CcwsEoZdbhEvBM9wxRAl5RShcA9wAngE3B+8tLpdLuwrhp4MNmK0pfRWkySr7NXS8+L5nZbWZWy/Vin1IaitJnUTqvwevJ71lgSSWEFKUfHG7Q2m/xqFJaGry/GXgfGPLl8mJgrXPur2JoUC8Qy3OpG+sAbGhEKT0ErAWOA6uBPWbW1wr9BOgFbgKezot0kAPYqJQA1gC/A9cA+82svzksSn1R+jNKX0SpnM/e1x3yqig92JhrZivM7FjO8bSZLSuCR/Ok16K0KMNHojQWpYko7Y7S1igN5PE3ROl4lNaZ2UVmNpPBU01orvZvZPCeKFXbBR+lEKVtUapFaSZKg9njqpl9aWYTrmXCImA7sCWb9lK/jj9TrwkrgA1AH3AQuKsSwkzbrLfxpgpsBtYDxf/R3xm2ExirhNCuHHZXTsmRwiat+S/zSt06eysVA/4pmGr/G3qm6ik28v29FKgCg8BS6pvS0KNRGgZ+Bb4FpsxsOkfUlMuwDcBWYOUZOHYM2AU8WQmhBifDv70O7PjX7KZ+4G7g3FM8zd6uBIaBy4AqxnIcZwFLCovPAhE4Sj38b4BDwEeVEFKD9S94Khjn486v3QAAAABJRU5ErkJggg==);
background-position: 9px 4px;
}
.OT_subscriber .OT_mute {
background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABcAAAATCAYAAAB7u5a2AAABx0lEQVQ4jaWUv48NURiGn3ONmCs32ZBd28ht1gqyZAkF21ylQkEiSp2ehpDlD1BoFGqqVdJohYKI7MaPxMoVNghCWMF+7ybLUewnOXfcMWO9yeQ857zne8+XmZOBGjJpr0kvTIomvTZpS526UCO4DUwD64FjwCFgqZnnR+oc8LfgzKQ73vGsr42ZtGjSQFV9o8KfBCacZwCaef4YmAf2rzjcpN3A2WSpm/AssKcqPDNpDBjs410CViXzTwk/A7b1C4wxDgOngAsZcAXY2buDfp/6S4F3lDS8DjgBzDWAjX/Y/e/QgYS/AhsKHa+OMQ6GEJ4Cj4BOAxgq6aCowyZtdf4OtAr+FHDO+R4wWnVbihr3cQnICt4boO38GWj9a/icjwOACt4m4K3zEPA+AxaAtTWCnwN3lzHkEL8V/OPAGud9wK2GF9XR1Wae/1zG2AI+pGYI4VUIoRtjHAc2A9cz4LRPevYCZ+i9/4sJt4GXJU10gaPAzdI2TTro/5Tfz8XEe2LSZGmxq/SDNvP8BnA5WRrx4BwYBe6vONx1EnjovGvBLAAd4Adwuyq8UiaNmDTvr+a8SQ9MuvbfwckBHZPe+QEfTdpep+4XZmPBHiHgz74AAAAASUVORK5CYII=);
background-position: 8px 7px;
}
.OT_subscriber .OT_mute.OT_active {
background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAUCAYAAACXtf2DAAACtklEQVQ4jZ2VSYiURxTHf+/T9Nc9iRrBuYySmIsXUU9iFMEFERRBvAjJLUQi5ioiHvSScfTmgqC4XAT1ZIgLuJHkICaaQAgKI2hAUBT30bjUq7bbv4eukXK029F3+eqtv/fqK6qQdEnSNUmT6CDB/bvgfjO4N9zj2RD8007xg1IABkwEzkma0qb4PGAPMBZYLtSD8eNwAEjqTlNI0gNJM4YU7w7ut4O7gvuhZFsR3C8NC5BBLiTIY0mzM8AvqbiC++pk+zLpE95XuwAws3vAQuBPYDRwWtL84P4tsDSLv5oaug4EYOawAMF9jMdoLxqNZcDvQA04UVYqL4G/svj7AF21mhJscrvCksYBFO7xc2AAGGg2mrdjvf4rcAyomNn+slLZmUEGBgsYdh945xZJmgvckDSrEJpK6ySBgV6q12O8ABwGPjGzfWWlsjdN9rpjoSfA+DYDXARGAksK4Is3XC1Ub4z1f4CDQGFmu6tleQSYk0U+p7WVeefLJc00s4fAeWB6Qeunvj0m2ugx9gO7kmlrtSxvBfcy6fXUZS6rgG/S+jLQUwCVNmMC9HqM14EtSe+rluWazN8YEv8IqKZ1E1qnaIDO0ucx3gX6kv6TpM3AM+D/IbGjgP60/gq4WQA33gMA2OQxPgHWJX1ttSwL4FAeZGYLgB2SasBs4A8L7qOBf9M0uXQB3a+TMYSmVctyDrA9mfcBK82smSdKWgCcAaa1bTm4fxbc/8uuCQX3RanAD5Ka6Wo5IGnE0HxJPZ03pQX5Org3MsD3AO5xXLPZXJ9BjkrqdFg6QjZkgG3Jtsw93pG0VFI9QU5K6voYQBHcTydAfwheBI9HgvvPAJIWS3qeIL9JGvUxkO7gfi1BrqTvwkG/pPmSnibIqTzXPgAyEVgBjAEu1qrVPbk/PVTHgb/NbPGg/RVIzOQqzSTBaQAAAABJRU5ErkJggg==);
background-position: 7px 7px;
}
/**
* Styles for display modes
*
* Note: It's important that these completely control the display and opacity
* attributes, no other selectors should atempt to change them.
*/
/* Default display mode transitions for various chrome elements */
.OT_publisher .OT_edge-bar-item,
.OT_subscriber .OT_edge-bar-item {
-webkit-transition-property: top, bottom, opacity;
transition-property: top, bottom, opacity;
-webkit-transition-duration: 0.5s;
transition-duration: 0.5s;
-webkit-transition-timing-function: ease-in;
transition-timing-function: ease-in;
}
.OT_publisher .OT_edge-bar-item.OT_mode-off,
.OT_subscriber .OT_edge-bar-item.OT_mode-off,
.OT_publisher .OT_edge-bar-item.OT_mode-auto,
.OT_subscriber .OT_edge-bar-item.OT_mode-auto,
.OT_publisher .OT_edge-bar-item.OT_mode-mini-auto,
.OT_subscriber .OT_edge-bar-item.OT_mode-mini-auto {
top: -25px;
opacity: 0;
}
.OT_publisher .OT_edge-bar-item.OT_mode-off,
.OT_subscriber .OT_edge-bar-item.OT_mode-off {
display: none;
}
.OT_mini .OT_mute.OT_mode-auto,
.OT_publisher .OT_mute.OT_mode-mini-auto,
.OT_subscriber .OT_mute.OT_mode-mini-auto {
top: 50%;
}
.OT_publisher .OT_edge-bar-item.OT_edge-bottom.OT_mode-off,
.OT_subscriber .OT_edge-bar-item.OT_edge-bottom.OT_mode-off,
.OT_publisher .OT_edge-bar-item.OT_edge-bottom.OT_mode-auto,
.OT_subscriber .OT_edge-bar-item.OT_edge-bottom.OT_mode-auto,
.OT_publisher .OT_edge-bar-item.OT_edge-bottom.OT_mode-mini-auto,
.OT_subscriber .OT_edge-bar-item.OT_edge-bottom.OT_mode-mini-auto {
top: auto;
bottom: -25px;
}
.OT_publisher .OT_edge-bar-item.OT_mode-on,
.OT_subscriber .OT_edge-bar-item.OT_mode-on,
.OT_publisher .OT_edge-bar-item.OT_mode-auto.OT_mode-on-hold,
.OT_subscriber .OT_edge-bar-item.OT_mode-auto.OT_mode-on-hold,
.OT_publisher:hover .OT_edge-bar-item.OT_mode-auto,
.OT_subscriber:hover .OT_edge-bar-item.OT_mode-auto,
.OT_publisher:hover .OT_edge-bar-item.OT_mode-mini-auto,
.OT_subscriber:hover .OT_edge-bar-item.OT_mode-mini-auto {
top: 0;
opacity: 1;
}
.OT_mini .OT_mute.OT_mode-on,
.OT_mini:hover .OT_mute.OT_mode-auto,
.OT_mute.OT_mode-mini,
.OT_root:hover .OT_mute.OT_mode-mini-auto {
top: 50%;
}
.OT_publisher .OT_edge-bar-item.OT_edge-bottom.OT_mode-on,
.OT_subscriber .OT_edge-bar-item.OT_edge-bottom.OT_mode-on,
.OT_publisher:hover .OT_edge-bar-item.OT_edge-bottom.OT_mode-auto,
.OT_subscriber:hover .OT_edge-bar-item.OT_edge-bottom.OT_mode-auto {
top: auto;
bottom: 0;
opacity: 1;
}
/* Contains the video element, used to fix video letter-boxing */
.OT_widget-container {
width: 100%;
height: 100%;
position: absolute;
background-color: #000000;
overflow: hidden;
}
/* Load animation */
.OT_root .OT_video-loading {
position: absolute;
z-index: 1;
width: 100%;
height: 100%;
display: none;
background-color: rgba(0, 0, 0, .75);
}
.OT_root .OT_video-loading .OT_video-loading-spinner {
background: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9Ii0yMCAtMjAgMjQwIDI0MCI+PGRlZnM+PGxpbmVhckdyYWRpZW50IGlkPSJhIiB4Mj0iMCIgeTI9IjEiPjxzdG9wIG9mZnNldD0iMCIgc3RvcC1jb2xvcj0iI2ZmZiIgc3RvcC1vcGFjaXR5PSIwIi8+PHN0b3Agb2Zmc2V0PSIxIiBzdG9wLWNvbG9yPSIjZmZmIiBzdG9wLW9wYWNpdHk9IjAiLz48L2xpbmVhckdyYWRpZW50PjxsaW5lYXJHcmFkaWVudCBpZD0iYiIgeDE9IjEiIHgyPSIwIiB5Mj0iMSI+PHN0b3Agb2Zmc2V0PSIwIiBzdG9wLWNvbG9yPSIjZmZmIiBzdG9wLW9wYWNpdHk9IjAiLz48c3RvcCBvZmZzZXQ9IjEiIHN0b3AtY29sb3I9IiNmZmYiIHN0b3Atb3BhY2l0eT0iLjA4Ii8+PC9saW5lYXJHcmFkaWVudD48bGluZWFyR3JhZGllbnQgaWQ9ImMiIHgxPSIxIiB4Mj0iMCIgeTE9IjEiPjxzdG9wIG9mZnNldD0iMCIgc3RvcC1jb2xvcj0iI2ZmZiIgc3RvcC1vcGFjaXR5PSIuMDgiLz48c3RvcCBvZmZzZXQ9IjEiIHN0b3AtY29sb3I9IiNmZmYiIHN0b3Atb3BhY2l0eT0iLjE2Ii8+PC9saW5lYXJHcmFkaWVudD48bGluZWFyR3JhZGllbnQgaWQ9ImQiIHgyPSIwIiB5MT0iMSI+PHN0b3Agb2Zmc2V0PSIwIiBzdG9wLWNvbG9yPSIjZmZmIiBzdG9wLW9wYWNpdHk9Ii4xNiIvPjxzdG9wIG9mZnNldD0iMSIgc3RvcC1jb2xvcj0iI2ZmZiIgc3RvcC1vcGFjaXR5PSIuMzMiLz48L2xpbmVhckdyYWRpZW50PjxsaW5lYXJHcmFkaWVudCBpZD0iZSIgeDI9IjEiIHkxPSIxIj48c3RvcCBvZmZzZXQ9IjAiIHN0b3AtY29sb3I9IiNmZmYiIHN0b3Atb3BhY2l0eT0iLjMzIi8+PHN0b3Agb2Zmc2V0PSIxIiBzdG9wLWNvbG9yPSIjZmZmIiBzdG9wLW9wYWNpdHk9Ii42NiIvPjwvbGluZWFyR3JhZGllbnQ+PGxpbmVhckdyYWRpZW50IGlkPSJmIiB4Mj0iMSIgeTI9IjEiPjxzdG9wIG9mZnNldD0iMCIgc3RvcC1jb2xvcj0iI2ZmZiIgc3RvcC1vcGFjaXR5PSIuNjYiLz48c3RvcCBvZmZzZXQ9IjEiIHN0b3AtY29sb3I9IiNmZmYiLz48L2xpbmVhckdyYWRpZW50PjxtYXNrIGlkPSJnIj48ZyBmaWxsPSJub25lIiBzdHJva2Utd2lkdGg9IjQwIj48cGF0aCBzdHJva2U9InVybCgjYSkiIGQ9Ik04Ni42LTUwYTEwMCAxMDAgMCAwIDEgMCAxMDAiIHRyYW5zZm9ybT0idHJhbnNsYXRlKDEwMCAxMDApIi8+PHBhdGggc3Ryb2tlPSJ1cmwoI2IpIiBkPSJNODYuNiA1MEExMDAgMTAwIDAgMCAxIDAgMTAwIiB0cmFuc2Zvcm09InRyYW5zbGF0ZSgxMDAgMTAwKSIvPjxwYXRoIHN0cm9rZT0idXJsKCNjKSIgZD0iTTAgMTAwYTEwMCAxMDAgMCAwIDEtODYuNi01MCIgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoMTAwIDEwMCkiLz48cGF0aCBzdHJva2U9InVybCgjZCkiIGQ9Ik0tODYuNiA1MGExMDAgMTAwIDAgMCAxIDAtMTAwIiB0cmFuc2Zvcm09InRyYW5zbGF0ZSgxMDAgMTAwKSIvPjxwYXRoIHN0cm9rZT0idXJsKCNlKSIgZD0iTS04Ni42LTUwQTEwMCAxMDAgMCAwIDEgMC0xMDAiIHRyYW5zZm9ybT0idHJhbnNsYXRlKDEwMCAxMDApIi8+PHBhdGggc3Ryb2tlPSJ1cmwoI2YpIiBkPSJNMC0xMDBhMTAwIDEwMCAwIDAgMSA4Ni42IDUwIiB0cmFuc2Zvcm09InRyYW5zbGF0ZSgxMDAgMTAwKSIvPjwvZz48L21hc2s+PC9kZWZzPjxyZWN0IHdpZHRoPSIxMDAlIiBoZWlnaHQ9IjEwMCUiIHg9Ii0yMCIgeT0iLTIwIiBtYXNrPSJ1cmwoI2cpIiBmaWxsPSIjZmZmIi8+PC9zdmc+) no-repeat;
position: absolute;
width: 32px;
height: 32px;
left: 50%;
top: 50%;
margin-left: -16px;
margin-top: -16px;
-webkit-animation: OT_spin 2s linear infinite;
animation: OT_spin 2s linear infinite;
}
@-webkit-keyframes OT_spin {
100% {
-webkit-transform: rotate(360deg);
}
}
@keyframes OT_spin {
100% {
-webkit-transform: rotate(360deg);
transform: rotate(360deg);
}
}
.OT_publisher.OT_loading .OT_video-loading,
.OT_subscriber.OT_loading .OT_video-loading {
display: block;
}
.OT_video-centering {
display: table;
width: 100%;
height: 100%;
}
.OT_video-container {
display: table-cell;
vertical-align: middle;
}
.OT_video-poster {
position: absolute;
z-index: 1;
width: 100%;
height: 100%;
display: none;
opacity: .25;
background-repeat: no-repeat;
background-image: url(data:image/svg+xml;base64,PHN2ZyB2aWV3Qm94PSIwIDAgNDcxIDQ2NCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48bGluZWFyR3JhZGllbnQgaWQ9ImEiIHgyPSIwIiB5Mj0iMSI+PHN0b3Agb2Zmc2V0PSI2Ni42NiUiIHN0b3AtY29sb3I9IiNmZmYiLz48c3RvcCBvZmZzZXQ9IjEwMCUiIHN0b3AtY29sb3I9IiNmZmYiIHN0b3Atb3BhY2l0eT0iMCIvPjwvbGluZWFyR3JhZGllbnQ+PHBhdGggZmlsbD0idXJsKCNhKSIgZD0iTTc5IDMwOGMxNC4yNS02LjUgNTQuMjUtMTkuNzUgNzEtMjkgOS0zLjI1IDI1LTIxIDI1LTIxczMuNzUtMTMgMy0yMmMtMS43NS02Ljc1LTE1LTQzLTE1LTQzLTIuNSAzLTQuNzQxIDMuMjU5LTcgMS0zLjI1LTcuNS0yMC41LTQ0LjUtMTYtNTcgMS4yNS03LjUgMTAtNiAxMC02LTExLjI1LTMzLjc1LTgtNjctOC02N3MuMDczLTcuMzQ2IDYtMTVjLTMuNDguNjM3LTkgNC05IDQgMi41NjMtMTEuNzI3IDE1LTIxIDE1LTIxIC4xNDgtLjMxMi0xLjMyMS0xLjQ1NC0xMCAxIDEuNS0yLjc4IDE2LjY3NS04LjY1NCAzMC0xMSAzLjc4Ny05LjM2MSAxMi43ODItMTcuMzk4IDIyLTIyLTIuMzY1IDMuMTMzLTMgNi0zIDZzMTUuNjQ3LTguMDg4IDQxLTZjLTE5Ljc1IDItMjQgNi0yNCA2czc0LjUtMTAuNzUgMTA0IDM3YzcuNSA5LjUgMjQuNzUgNTUuNzUgMTAgODkgMy43NS0xLjUgNC41LTQuNSA5IDEgLjI1IDE0Ljc1LTExLjUgNjMtMTkgNjItMi43NSAxLTQtMy00LTMtMTAuNzUgMjkuNS0xNCAzOC0xNCAzOC0yIDQuMjUtMy43NSAxOC41LTEgMjIgMS4yNSA0LjUgMjMgMjMgMjMgMjNsMTI3IDUzYzM3IDM1IDIzIDEzNSAyMyAxMzVMMCA0NjRzLTMtOTYuNzUgMTQtMTIwYzUuMjUtNi4yNSAyMS43NS0xOS43NSA2NS0zNnoiLz48L3N2Zz4=);
background-size: auto 76%;
}
.OT_fit-mode-cover .OT_video-element {
-o-object-fit: cover;
object-fit: cover;
}
/* Workaround for iOS freezing issue when cropping videos */
/* https://bugs.webkit.org/show_bug.cgi?id=176439 */
@media only screen
and (orientation: portrait) {
.OT_subscriber.OT_ForceContain.OT_fit-mode-cover .OT_video-element {
-o-object-fit: contain !important;
object-fit: contain !important;
}
}
.OT_fit-mode-contain .OT_video-element {
-o-object-fit: contain;
object-fit: contain;
}
.OT_fit-mode-cover .OT_video-poster {
background-position: center bottom;
}
.OT_fit-mode-contain .OT_video-poster {
background-position: center;
}
.OT_audio-level-meter {
position: absolute;
width: 25%;
max-width: 224px;
min-width: 21px;
top: 0;
right: 0;
overflow: hidden;
}
.OT_audio-level-meter:before {
/* makes the height of the container equals its width */
content: '';
display: block;
padding-top: 100%;
}
.OT_audio-level-meter__bar {
position: absolute;
width: 192%; /* meter value can overflow of 8% */
height: 192%;
top: -96% /* half of the size */;
right: -96%;
border-radius: 50%;
background-color: rgba(0, 0, 0, .8);
}
.OT_audio-level-meter__audio-only-img {
position: absolute;
top: 22%;
right: 15%;
width: 40%;
opacity: .7;
background: url(data:image/svg+xml;base64,PHN2ZyB2aWV3Qm94PSIwIDAgNzkgODYiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PGcgZmlsbD0iI2ZmZiI+PHBhdGggZD0iTTkuNzU3IDQwLjkyNGMzLjczOC01LjE5MSAxMi43MTEtNC4zMDggMTIuNzExLTQuMzA4IDIuMjIzIDMuMDE0IDUuMTI2IDI0LjU4NiAzLjYyNCAyOC43MTgtMS40MDEgMS4zMDEtMTEuNjExIDEuNjI5LTEzLjM4LTEuNDM2LTEuMjI2LTguODA0LTIuOTU1LTIyLjk3NS0yLjk1NS0yMi45NzV6bTU4Ljc4NSAwYy0zLjczNy01LjE5MS0xMi43MTEtNC4zMDgtMTIuNzExLTQuMzA4LTIuMjIzIDMuMDE0LTUuMTI2IDI0LjU4Ni0zLjYyNCAyOC43MTggMS40MDEgMS4zMDEgMTEuNjExIDEuNjI5IDEzLjM4LTEuNDM2IDEuMjI2LTguODA0IDIuOTU1LTIyLjk3NSAyLjk1NS0yMi45NzV6Ii8+PHBhdGggZD0iTTY4LjY0NyA1OC42Yy43MjktNC43NTMgMi4zOC05LjU2MSAyLjM4LTE0LjgwNCAwLTIxLjQxMi0xNC4xMTUtMzguNzctMzEuNTI4LTM4Ljc3LTE3LjQxMiAwLTMxLjUyNyAxNy4zNTgtMzEuNTI3IDM4Ljc3IDAgNC41NDEuNTE1IDguOTM2IDEuODAyIDEyLjk1IDEuNjk4IDUuMjk1LTUuNTQyIDYuOTkxLTYuNjE2IDIuMDczQzIuNDEgNTUuMzk0IDAgNTEuNzg3IDAgNDguMTAzIDAgMjEuNTM2IDE3LjY4NSAwIDM5LjUgMCA2MS4zMTYgMCA3OSAyMS41MzYgNzkgNDguMTAzYzAgLjcxOC0yLjg5OSA5LjY5My0zLjI5MiAxMS40MDgtLjc1NCAzLjI5My03Ljc1MSAzLjU4OS03LjA2MS0uOTEyeiIvPjxwYXRoIGQ9Ik01LjA4NCA1MS4zODVjLS44MDQtMy43ODIuNTY5LTcuMzM1IDMuMTM0LTcuOTIxIDIuNjM2LS42MDMgNS40ODUgMi4xNSA2LjI4OSA2LjEzMi43OTcgMy45NDgtLjc1MiA3LjQ1Ny0zLjM4OCA3Ljg1OS0yLjU2Ni4zOTEtNS4yMzctMi4zMTgtNi4wMzQtNi4wN3ptNjguODM0IDBjLjgwNC0zLjc4Mi0uNTY4LTcuMzM1LTMuMTMzLTcuOTIxLTIuNjM2LS42MDMtNS40ODUgMi4xNS02LjI4OSA2LjEzMi0uNzk3IDMuOTQ4Ljc1MiA3LjQ1NyAzLjM4OSA3Ljg1OSAyLjU2NS4zOTEgNS4yMzctMi4zMTggNi4wMzQtNi4wN3ptLTIuMDM4IDguMjg4Yy0uOTI2IDE5LjY1OS0xNS4xMTIgMjQuNzU5LTI1Ljg1OSAyMC40NzUtNS40MDUtLjYwNi0zLjAzNCAxLjI2Mi0zLjAzNCAxLjI2MiAxMy42NjEgMy41NjIgMjYuMTY4IDMuNDk3IDMxLjI3My0yMC41NDktLjU4NS00LjUxMS0yLjM3OS0xLjE4Ny0yLjM3OS0xLjE4N3oiLz48cGF0aCBkPSJNNDEuNjYyIDc4LjQyMmw3LjU1My41NWMxLjE5Mi4xMDcgMi4xMiAxLjE1MyAyLjA3MiAyLjMzNWwtLjEwOSAyLjczOGMtLjA0NyAxLjE4Mi0xLjA1MSAyLjA1NC0yLjI0MyAxLjk0NmwtNy41NTMtLjU1Yy0xLjE5MS0uMTA3LTIuMTE5LTEuMTUzLTIuMDcyLTIuMzM1bC4xMDktMi43MzdjLjA0Ny0xLjE4MiAxLjA1Mi0yLjA1NCAyLjI0My0xLjk0N3oiLz48L2c+PC9zdmc+) no-repeat center;
}
.OT_audio-level-meter__audio-only-img:before {
/* makes the height of the container equals its width */
content: '';
display: block;
padding-top: 100%;
}
.OT_audio-level-meter__value {
position: absolute;
border-radius: 50%;
background-image: radial-gradient(circle, rgba(151, 206, 0, 1) 0%, rgba(151, 206, 0, 0) 100%);
}
.OT_audio-level-meter.OT_mode-off {
display: none;
}
.OT_audio-level-meter.OT_mode-on,
.OT_audio-only .OT_audio-level-meter.OT_mode-auto {
display: block;
}
.OT_audio-only.OT_publisher .OT_video-element,
.OT_audio-only.OT_subscriber .OT_video-element {
display: none;
}
.OT_video-disabled-indicator {
opacity: 1;
border: none;
display: none;
position: absolute;
background-color: transparent;
background-repeat: no-repeat;
background-position: bottom right;
pointer-events: none;
top: 0;
left: 0;
bottom: 3px;
right: 3px;
}
.OT_video-disabled {
background-image: url(data:image/png;base64,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);
background-size: 33px auto;
}
.OT_video-disabled-warning {
background-image: url(data:image/png;base64,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);
background-size: 33px auto;
}
.OT_video-disabled-indicator.OT_active {
display: block;
}
.OT_audio-blocked-indicator {
opacity: 1;
border: none;
display: none;
position: absolute;
background-color: transparent;
background-repeat: no-repeat;
background-position: center;
pointer-events: none;
top: 0;
left: 0;
bottom: 0;
right: 0;
}
.OT_audio-blocked {
background-image: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiB3aWR0aD0iMTUwIiBoZWlnaHQ9IjkwIj48ZGVmcz48cGF0aCBkPSJNNjcgMTJMNi40NDggNzIuNTUyIDAgMzFWMThMMjYgMGw0MSAxMnptMyA3bDYgNDctMjkgMTgtMzUuNTAyLTYuNDk4TDcwIDE5eiIgaWQ9ImEiLz48L2RlZnM+PHJlY3Qgd2lkdGg9IjE1MCIgaGVpZ2h0PSI5MCIgcng9IjM1IiByeT0iNDUiIG9wYWNpdHk9Ii41Ii8+PGcgZmlsbD0ibm9uZSIgZmlsbC1ydWxlPSJldmVub2RkIj48ZyB0cmFuc2Zvcm09InRyYW5zbGF0ZSgzNikiPjxtYXNrIGlkPSJiIiBmaWxsPSIjZmZmIj48dXNlIHhsaW5rOmhyZWY9IiNhIi8+PC9tYXNrPjxwYXRoIGQ9Ik0zOS4yNDkgNTEuMzEyYy42OTcgMTAuMzcgMi43ODUgMTcuODk3IDUuMjUxIDE3Ljg5NyAzLjAzOCAwIDUuNS0xMS40MTcgNS41LTI1LjVzLTIuNDYyLTI1LjUtNS41LTI1LjVjLTIuNTEgMC00LjYyOCA3Ljc5Ny01LjI4NyAxOC40NTNBOC45ODkgOC45ODkgMCAwIDEgNDMgNDRhOC45ODggOC45ODggMCAwIDEtMy43NTEgNy4zMTJ6TTIwLjk4NSAzMi4yMjRsMTUuNzQ2LTE2Ljg3N2E3LjM4NSA3LjM4NSAwIDAgMSAxMC4zNzQtLjQyQzUxLjcwMiAxOS4xMTQgNTQgMjkuMjA4IDU0IDQ1LjIwOGMwIDE0LjUyNy0yLjM0MyAyMy44OC03LjAzIDI4LjA1OGE3LjI4IDcuMjggMCAwIDEtMTAuMTY4LS40NjhMMjAuNDA1IDU1LjIyNEgxMmE1IDUgMCAwIDEtNS01di0xM2E1IDUgMCAwIDEgNS01aDguOTg1eiIgZmlsbD0iI0ZGRiIgbWFzaz0idXJsKCNiKSIvPjwvZz48cGF0aCBkPSJNMTA2LjUgMTMuNUw0NC45OTggNzUuMDAyIiBzdHJva2U9IiNGRkYiIHN0cm9rZS13aWR0aD0iMiIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIi8+PC9nPjwvc3ZnPg==);
background-size: 90px auto;
}
.OT_container-audio-blocked {
cursor: pointer;
}
.OT_container-audio-blocked.OT_mini .OT_edge-bar-item {
display: none;
}
.OT_container-audio-blocked .OT_mute {
display: none;
}
.OT_audio-blocked-indicator.OT_active {
display: block;
}
.OT_video-unsupported {
opacity: 1;
border: none;
display: none;
position: absolute;
background-color: transparent;
background-repeat: no-repeat;
background-position: center;
background-image: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iOTciIGhlaWdodD0iOTAiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiPjxkZWZzPjxwYXRoIGQ9Ik03MCAxMkw5LjQ0OCA3Mi41NTIgMCA2MmwzLTQ0TDI5IDBsNDEgMTJ6bTggMmwxIDUyLTI5IDE4LTM1LjUwMi02LjQ5OEw3OCAxNHoiIGlkPSJhIi8+PC9kZWZzPjxnIGZpbGw9Im5vbmUiIGZpbGwtcnVsZT0iZXZlbm9kZCI+PGcgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoOCAzKSI+PG1hc2sgaWQ9ImIiIGZpbGw9IiNmZmYiPjx1c2UgeGxpbms6aHJlZj0iI2EiLz48L21hc2s+PHBhdGggZD0iTTkuMTEgMjAuOTY4SDQ4LjFhNSA1IDAgMCAxIDUgNVY1OC4xOGE1IDUgMCAwIDEtNSA1SDkuMTFhNSA1IDAgMCAxLTUtNVYyNS45N2E1IDUgMCAwIDEgNS01em00Ny4wOCAxMy4zOTRjMC0uMzQ1IDUuNDcyLTMuMTU5IDE2LjQxNS04LjQ0M2EzIDMgMCAwIDEgNC4zMDQgMi43MDJ2MjYuODM1YTMgMyAwIDAgMS00LjMwNSAyLjcwMWMtMTAuOTQyLTUuMjg2LTE2LjQxMy04LjEtMTYuNDEzLTguNDQ2VjM0LjM2MnoiIGZpbGw9IiNGRkYiIG1hc2s9InVybCgjYikiLz48L2c+PHBhdGggZD0iTTgxLjUgMTYuNUwxOS45OTggNzguMDAyIiBzdHJva2U9IiNGRkYiIHN0cm9rZS13aWR0aD0iMiIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIi8+PC9nPjwvc3ZnPg==);
background-size: 58px auto;
pointer-events: none;
top: 0;
left: 0;
bottom: 0;
right: 0;
margin-top: -30px;
}
.OT_video-unsupported-bar {
display: none;
position: absolute;
width: 192%; /* copy the size of the audio meter bar for symmetry */
height: 192%;
top: -96% /* half of the size */;
left: -96%;
border-radius: 50%;
background-color: rgba(0, 0, 0, .8);
}
.OT_video-unsupported-img {
display: none;
position: absolute;
top: 11%;
left: 15%;
width: 70%;
opacity: .7;
background-image: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iOTciIGhlaWdodD0iOTAiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiPjxkZWZzPjxwYXRoIGQ9Ik03MCAxMkw5LjQ0OCA3Mi41NTIgMCA2MmwzLTQ0TDI5IDBsNDEgMTJ6bTggMmwxIDUyLTI5IDE4LTM1LjUwMi02LjQ5OEw3OCAxNHoiIGlkPSJhIi8+PC9kZWZzPjxnIGZpbGw9Im5vbmUiIGZpbGwtcnVsZT0iZXZlbm9kZCI+PGcgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoOCAzKSI+PG1hc2sgaWQ9ImIiIGZpbGw9IiNmZmYiPjx1c2UgeGxpbms6aHJlZj0iI2EiLz48L21hc2s+PHBhdGggZD0iTTkuMTEgMjAuOTY4SDQ4LjFhNSA1IDAgMCAxIDUgNVY1OC4xOGE1IDUgMCAwIDEtNSA1SDkuMTFhNSA1IDAgMCAxLTUtNVYyNS45N2E1IDUgMCAwIDEgNS01em00Ny4wOCAxMy4zOTRjMC0uMzQ1IDUuNDcyLTMuMTU5IDE2LjQxNS04LjQ0M2EzIDMgMCAwIDEgNC4zMDQgMi43MDJ2MjYuODM1YTMgMyAwIDAgMS00LjMwNSAyLjcwMWMtMTAuOTQyLTUuMjg2LTE2LjQxMy04LjEtMTYuNDEzLTguNDQ2VjM0LjM2MnoiIGZpbGw9IiNGRkYiIG1hc2s9InVybCgjYikiLz48L2c+PHBhdGggZD0iTTgxLjUgMTYuNUwxOS45OTggNzguMDAyIiBzdHJva2U9IiNGRkYiIHN0cm9rZS13aWR0aD0iMiIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIi8+PC9nPjwvc3ZnPg==);
background-repeat: no-repeat;
background-position: center;
background-size: 100% auto;
}
.OT_video-unsupported-img:before {
/* makes the height of the container 93% of its width (90/97 px) */
content: '';
display: block;
padding-top: 93%;
}
.OT_video-unsupported-text {
display: -webkit-box;
display: -ms-flexbox;
display: flex;
-webkit-box-pack: center;
-ms-flex-pack: center;
justify-content: center;
-webkit-box-align: center;
-ms-flex-align: center;
align-items: center;
text-align: center;
height: 100%;
margin-top: 40px;
}

View File

@ -1,8 +1,7 @@
<div class="bounds">
<div *ngFor="let streams of remoteStreams" class="content" fxLayout="row" fxFlexFill [style.height]="100 / remoteStreams.length + '%'"
[style.min-height]="100 / remoteStreams.length + '%'">
<div *ngFor="let s of streams" [fxFlex]="100 / streams">
<video [id]="'native-video-' + s.streamId" autoplay="true" [srcObject]="s.getMediaStream()"></video>
<div id="layout" class="bounds">
<div *ngFor="let s of streams" class="OT_root OT_publisher custom-class">
<div class="OT_widget-container">
<video [id]="'native-video-' + s.streamId" autoplay="true" [srcObject]="s.getMediaStream()" (playing)="onVideoPlaying($event)"></video>
</div>
</div>
</div>

View File

@ -1,22 +1,28 @@
import { Component, OnInit, OnDestroy, HostListener } from '@angular/core';
import { Component, OnInit, OnDestroy, HostListener, ViewEncapsulation, ApplicationRef } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { OpenVidu, Session, Stream } from 'openvidu-browser';
import { OpenVidu, Session, Stream, Subscriber } from 'openvidu-browser';
import { OpenViduLayout } from '../openvidu-layout';
@Component({
selector: 'app-layout-best-fit',
templateUrl: './layout-best-fit.component.html',
styleUrls: ['./layout-best-fit.component.css']
styleUrls: ['./layout-best-fit.component.css'],
encapsulation: ViewEncapsulation.None
})
export class LayoutBestFitComponent implements OnInit, OnDestroy {
openviduLayout: OpenViduLayout;
sessionId: string;
secret: string;
session: Session;
numberOfVideos = 0;
remoteStreams = [];
streams = [];
constructor(private route: ActivatedRoute) {
layout: any;
resizeTimeout;
constructor(private route: ActivatedRoute, private appRef: ApplicationRef) {
this.route.params.subscribe(params => {
this.sessionId = params.sessionId;
this.secret = params.secret;
@ -28,6 +34,14 @@ export class LayoutBestFitComponent implements OnInit, OnDestroy {
this.leaveSession();
}
@HostListener('window:resize', ['$event'])
sizeChange(event) {
clearTimeout(this.resizeTimeout);
this.resizeTimeout = setTimeout(() => {
this.openviduLayout.updateLayout();
}, 20);
}
ngOnDestroy() {
this.leaveSession();
}
@ -39,15 +53,14 @@ export class LayoutBestFitComponent implements OnInit, OnDestroy {
this.session = OV.initSession(fullSessionId);
this.session.on('streamCreated', (event) => {
this.numberOfVideos++;
const subscriber: Subscriber = this.session.subscribe(event.stream, '');
this.addRemoteStream(event.stream);
this.session.subscribe(event.stream, '');
});
this.session.on('streamDestroyed', (event) => {
this.numberOfVideos--;
event.preventDefault();
this.deleteRemoteStream(event.stream);
this.openviduLayout.updateLayout();
});
this.session.connect(null, (error) => {
@ -55,79 +68,46 @@ export class LayoutBestFitComponent implements OnInit, OnDestroy {
console.error(error);
}
});
this.openviduLayout = new OpenViduLayout();
this.openviduLayout.initLayoutContainer(document.getElementById('layout'), {
maxRatio: 3 / 2, // The narrowest ratio that will be used (default 2x3)
minRatio: 9 / 16, // The widest ratio that will be used (default 16x9)
fixedRatio: false, /* If this is true then the aspect ratio of the video is maintained
and minRatio and maxRatio are ignored (default false) */
bigClass: 'OV_big', // The class to add to elements that should be sized bigger
bigPercentage: 0.8, // The maximum percentage of space the big ones should take up
bigFixedRatio: false, // fixedRatio for the big ones
bigMaxRatio: 3 / 2, // The narrowest ratio to use for the big elements (default 2x3)
bigMinRatio: 9 / 16, // The widest ratio to use for the big elements (default 16x9)
bigFirst: true, // Whether to place the big one in the top left (true) or bottom right
animate: true // Whether you want to animate the transitions
});
}
private addRemoteStream(stream: Stream): void {
switch (true) {
case (this.numberOfVideos <= 2):
if (this.remoteStreams[0] == null) { this.remoteStreams[0] = []; }
this.remoteStreams[0].push(stream);
break;
case (this.numberOfVideos <= 4):
if (this.remoteStreams[1] == null) { this.remoteStreams[1] = []; }
this.remoteStreams[1].push(stream);
break;
case (this.numberOfVideos <= 5):
this.remoteStreams[0].push(stream);
break;
case (this.numberOfVideos <= 6):
this.remoteStreams[1].push(stream);
break;
default:
if (this.remoteStreams[2] == null) { this.remoteStreams[2] = []; }
this.remoteStreams[2].push(stream);
break;
}
this.streams.push(stream);
this.appRef.tick();
}
private deleteRemoteStream(stream: Stream): void {
for (let i = 0; i < this.remoteStreams.length; i++) {
const index = this.remoteStreams[i].indexOf(stream, 0);
if (index > -1) {
this.remoteStreams[i].splice(index, 1);
this.reArrangeVideos();
break;
}
const index = this.streams.indexOf(stream, 0);
if (index > -1) {
this.streams.splice(index, 1);
}
}
private reArrangeVideos(): void {
switch (true) {
case (this.numberOfVideos === 1):
if (this.remoteStreams[0].length === 0) {
this.remoteStreams[0].push(this.remoteStreams[1].pop());
}
break;
case (this.numberOfVideos === 2):
if (this.remoteStreams[0].length === 1) {
this.remoteStreams[0].push(this.remoteStreams[1].pop());
}
break;
case (this.numberOfVideos === 3):
if (this.remoteStreams[0].length === 1) {
this.remoteStreams[0].push(this.remoteStreams[1].pop());
}
break;
case (this.numberOfVideos === 4):
if (this.remoteStreams[0].length === 3) {
this.remoteStreams[1].unshift(this.remoteStreams[0].pop());
}
break;
case (this.numberOfVideos === 5):
if (this.remoteStreams[0].length === 2) {
this.remoteStreams[0].push(this.remoteStreams[1].shift());
}
break;
}
this.remoteStreams = this.remoteStreams.filter((array) => { return array.length > 0 });
this.appRef.tick();
}
leaveSession() {
if (this.session) { this.session.disconnect(); };
this.remoteStreams = [];
this.numberOfVideos = 0;
this.streams = [];
this.session = null;
}
onVideoPlaying(event) {
const video: HTMLVideoElement = event.target;
video.parentElement.parentElement.classList.remove('custom-class');
this.openviduLayout.updateLayout();
}
}

View File

@ -0,0 +1,357 @@
declare var $: any;
export interface OpenViduLayoutOptions {
maxRatio: number;
minRatio: number;
fixedRatio: boolean;
animate: any;
bigClass: string;
bigPercentage: any;
bigFixedRatio: any;
bigMaxRatio: any;
bigMinRatio: any;
bigFirst: any;
}
export class OpenViduLayout {
private layoutContainer: HTMLElement;
private opts: OpenViduLayoutOptions;
private fixAspectRatio(elem: HTMLVideoElement, width: number) {
const sub: HTMLVideoElement = <HTMLVideoElement>elem.querySelector('.OT_root');
if (sub) {
// If this is the parent of a subscriber or publisher then we need
// to force the mutation observer on the publisher or subscriber to
// trigger to get it to fix it's layout
const oldWidth = sub.style.width;
sub.style.width = width + 'px';
// sub.style.height = height + 'px';
sub.style.width = oldWidth || '';
}
}
private positionElement(elem: HTMLVideoElement, x: number, y: number, width: number, height: number, animate: any) {
const targetPosition = {
left: x + 'px',
top: y + 'px',
width: width + 'px',
height: height + 'px'
};
this.fixAspectRatio(elem, width);
if (animate && $) {
$(elem).stop();
$(elem).animate(targetPosition, animate.duration || 200, animate.easing || 'swing',
() => {
this.fixAspectRatio(elem, width);
if (animate.complete) { animate.complete.call(this); }
});
} else {
$(elem).css(targetPosition);
}
this.fixAspectRatio(elem, width);
}
private getVideoRatio(elem: HTMLVideoElement) {
if (!elem) {
return 3 / 4;
}
const video: HTMLVideoElement = <HTMLVideoElement>elem.querySelector('video');
if (video && video.videoHeight && video.videoWidth) {
return video.videoHeight / video.videoWidth;
} else if (elem.videoHeight && elem.videoWidth) {
return elem.videoHeight / elem.videoWidth;
}
return 3 / 4;
}
private getCSSNumber(elem: HTMLElement, prop: string) {
const cssStr = $(elem).css(prop);
return cssStr ? parseInt(cssStr, 10) : 0;
}
// Really cheap UUID function
private cheapUUID() {
return (Math.random() * 100000000).toFixed(0);
}
private getHeight(elem: HTMLElement) {
const heightStr = $(elem).css('height');
return heightStr ? parseInt(heightStr, 10) : 0;
}
private getWidth(elem: HTMLElement) {
const widthStr = $(elem).css('width');
return widthStr ? parseInt(widthStr, 10) : 0;
}
private getBestDimensions(minR: number, maxR: number, count: number, WIDTH: number, HEIGHT: number, targetHeight: number) {
let maxArea,
targetCols,
targetRows,
targetWidth,
tWidth,
tHeight,
tRatio;
// Iterate through every possible combination of rows and columns
// and see which one has the least amount of whitespace
for (let i = 1; i <= count; i++) {
const colsAux = i;
const rowsAux = Math.ceil(count / colsAux);
// Try taking up the whole height and width
tHeight = Math.floor(HEIGHT / rowsAux);
tWidth = Math.floor(WIDTH / colsAux);
tRatio = tHeight / tWidth;
if (tRatio > maxR) {
// We went over decrease the height
tRatio = maxR;
tHeight = tWidth * tRatio;
} else if (tRatio < minR) {
// We went under decrease the width
tRatio = minR;
tWidth = tHeight / tRatio;
}
const area = (tWidth * tHeight) * count;
// If this width and height takes up the most space then we're going with that
if (maxArea === undefined || (area > maxArea)) {
maxArea = area;
targetHeight = tHeight;
targetWidth = tWidth;
targetCols = colsAux;
targetRows = rowsAux;
}
}
return {
maxArea: maxArea,
targetCols: targetCols,
targetRows: targetRows,
targetHeight: targetHeight,
targetWidth: targetWidth,
ratio: targetHeight / targetWidth
};
};
private arrange(children: HTMLVideoElement[], WIDTH: number, HEIGHT: number, offsetLeft: number, offsetTop: number, fixedRatio: boolean,
minRatio: number, maxRatio: number, animate: any) {
let targetHeight;
const count = children.length;
let dimensions;
if (!fixedRatio) {
dimensions = this.getBestDimensions(minRatio, maxRatio, count, WIDTH, HEIGHT, targetHeight);
} else {
// Use the ratio of the first video element we find to approximate
const ratio = this.getVideoRatio(children.length > 0 ? children[0] : null);
dimensions = this.getBestDimensions(ratio, ratio, count, WIDTH, HEIGHT, targetHeight);
}
// Loop through each stream in the container and place it inside
let x = 0,
y = 0;
const rows = [];
let row;
// Iterate through the children and create an array with a new item for each row
// and calculate the width of each row so that we know if we go over the size and need
// to adjust
for (let i = 0; i < children.length; i++) {
if (i % dimensions.targetCols === 0) {
// This is a new row
row = {
children: [],
width: 0,
height: 0
};
rows.push(row);
}
const elem: HTMLVideoElement = children[i];
row.children.push(elem);
let targetWidth = dimensions.targetWidth;
targetHeight = dimensions.targetHeight;
// If we're using a fixedRatio then we need to set the correct ratio for this element
if (fixedRatio) {
targetWidth = targetHeight / this.getVideoRatio(elem);
}
row.width += targetWidth;
row.height = targetHeight;
}
// Calculate total row height adjusting if we go too wide
let totalRowHeight = 0;
let remainingShortRows = 0;
for (let i = 0; i < rows.length; i++) {
row = rows[i];
if (row.width > WIDTH) {
// Went over on the width, need to adjust the height proportionally
row.height = Math.floor(row.height * (WIDTH / row.width));
row.width = WIDTH;
} else if (row.width < WIDTH) {
remainingShortRows += 1;
}
totalRowHeight += row.height;
}
if (totalRowHeight < HEIGHT && remainingShortRows > 0) {
// We can grow some of the rows, we're not taking up the whole height
let remainingHeightDiff = HEIGHT - totalRowHeight;
totalRowHeight = 0;
for (let i = 0; i < rows.length; i++) {
row = rows[i];
if (row.width < WIDTH) {
// Evenly distribute the extra height between the short rows
let extraHeight = remainingHeightDiff / remainingShortRows;
if ((extraHeight / row.height) > ((WIDTH - row.width) / row.width)) {
// We can't go that big or we'll go too wide
extraHeight = Math.floor(((WIDTH - row.width) / row.width) * row.height);
}
row.width += Math.floor((extraHeight / row.height) * row.width);
row.height += extraHeight;
remainingHeightDiff -= extraHeight;
remainingShortRows -= 1;
}
totalRowHeight += row.height;
}
}
// vertical centering
y = ((HEIGHT - (totalRowHeight)) / 2);
// Iterate through each row and place each child
for (let i = 0; i < rows.length; i++) {
row = rows[i];
// center the row
const rowMarginLeft = ((WIDTH - row.width) / 2);
x = rowMarginLeft;
for (let j = 0; j < row.children.length; j++) {
const elem: HTMLVideoElement = row.children[j];
let targetWidth = dimensions.targetWidth;
targetHeight = row.height;
// If we're using a fixedRatio then we need to set the correct ratio for this element
if (fixedRatio) {
targetWidth = Math.floor(targetHeight / this.getVideoRatio(elem));
}
elem.style.position = 'absolute';
// $(elem).css('position', 'absolute');
const actualWidth = targetWidth - this.getCSSNumber(elem, 'paddingLeft') -
this.getCSSNumber(elem, 'paddingRight') -
this.getCSSNumber(elem, 'marginLeft') -
this.getCSSNumber(elem, 'marginRight') -
this.getCSSNumber(elem, 'borderLeft') -
this.getCSSNumber(elem, 'borderRight');
const actualHeight = targetHeight - this.getCSSNumber(elem, 'paddingTop') -
this.getCSSNumber(elem, 'paddingBottom') -
this.getCSSNumber(elem, 'marginTop') -
this.getCSSNumber(elem, 'marginBottom') -
this.getCSSNumber(elem, 'borderTop') -
this.getCSSNumber(elem, 'borderBottom');
this.positionElement(elem, x + offsetLeft, y + offsetTop, actualWidth, actualHeight, animate);
x += targetWidth;
}
y += targetHeight;
}
}
private filterDisplayNone(element: HTMLElement) {
return element.style.display !== 'none';
}
updateLayout() {
if (this.layoutContainer.style.display === 'none') {
return;
}
let id = this.layoutContainer.id;
if (!id) {
id = 'OT_' + this.cheapUUID();
this.layoutContainer.id = id;
}
const HEIGHT = this.getHeight(this.layoutContainer) -
this.getCSSNumber(this.layoutContainer, 'borderTop') -
this.getCSSNumber(this.layoutContainer, 'borderBottom');
const WIDTH = this.getWidth(this.layoutContainer) -
this.getCSSNumber(this.layoutContainer, 'borderLeft') -
this.getCSSNumber(this.layoutContainer, 'borderRight');
const availableRatio = HEIGHT / WIDTH;
let offsetLeft = 0;
let offsetTop = 0;
let bigOffsetTop = 0;
let bigOffsetLeft = 0;
const bigOnes = Array.prototype.filter.call(
this.layoutContainer.querySelectorAll('#' + id + '>.' + this.opts.bigClass),
this.filterDisplayNone);
const smallOnes = Array.prototype.filter.call(
this.layoutContainer.querySelectorAll('#' + id + '>*:not(.' + this.opts.bigClass + ')'),
this.filterDisplayNone);
if (bigOnes.length > 0 && smallOnes.length > 0) {
let bigWidth, bigHeight;
if (availableRatio > this.getVideoRatio(bigOnes[0])) {
// We are tall, going to take up the whole width and arrange small
// guys at the bottom
bigWidth = WIDTH;
bigHeight = Math.floor(HEIGHT * this.opts.bigPercentage);
offsetTop = bigHeight;
bigOffsetTop = HEIGHT - offsetTop;
} else {
// We are wide, going to take up the whole height and arrange the small
// guys on the right
bigHeight = HEIGHT;
bigWidth = Math.floor(WIDTH * this.opts.bigPercentage);
offsetLeft = bigWidth;
bigOffsetLeft = WIDTH - offsetLeft;
}
if (this.opts.bigFirst) {
this.arrange(bigOnes, bigWidth, bigHeight, 0, 0, this.opts.bigFixedRatio, this.opts.bigMinRatio,
this.opts.bigMaxRatio, this.opts.animate);
this.arrange(smallOnes, WIDTH - offsetLeft, HEIGHT - offsetTop, offsetLeft, offsetTop,
this.opts.fixedRatio, this.opts.minRatio, this.opts.maxRatio, this.opts.animate);
} else {
this.arrange(smallOnes, WIDTH - offsetLeft, HEIGHT - offsetTop, 0, 0, this.opts.fixedRatio,
this.opts.minRatio, this.opts.maxRatio, this.opts.animate);
this.arrange(bigOnes, bigWidth, bigHeight, bigOffsetLeft, bigOffsetTop,
this.opts.bigFixedRatio, this.opts.bigMinRatio, this.opts.bigMaxRatio, this.opts.animate);
}
} else if (bigOnes.length > 0 && smallOnes.length === 0) {
this.
// We only have one bigOne just center it
arrange(bigOnes, WIDTH, HEIGHT, 0, 0, this.opts.bigFixedRatio, this.opts.bigMinRatio,
this.opts.bigMaxRatio, this.opts.animate);
} else {
this.arrange(smallOnes, WIDTH - offsetLeft, HEIGHT - offsetTop, offsetLeft, offsetTop,
this.opts.fixedRatio, this.opts.minRatio, this.opts.maxRatio, this.opts.animate);
}
}
initLayoutContainer(container, opts) {
this.opts = {
maxRatio: (opts.maxRatio != null) ? opts.maxRatio : 3 / 2,
minRatio: (opts.minRatio != null) ? opts.minRatio : 9 / 16,
fixedRatio: (opts.fixedRatio != null) ? opts.fixedRatio : false,
animate: (opts.animate != null) ? opts.animate : false,
bigClass: (opts.bigClass != null) ? opts.bigClass : 'OT_big',
bigPercentage: (opts.bigPercentage != null) ? opts.bigPercentage : 0.8,
bigFixedRatio: (opts.bigFixedRatio != null) ? opts.bigFixedRatio : false,
bigMaxRatio: (opts.bigMaxRatio != null) ? opts.bigMaxRatio : 3 / 2,
bigMinRatio: (opts.bigMinRatio != null) ? opts.bigMinRatio : 9 / 16,
bigFirst: (opts.bigFirst != null) ? opts.bigFirst : true
};
this.layoutContainer = typeof (container) === 'string' ? $(container) : container;
}
setLayoutOptions(options: OpenViduLayoutOptions) {
this.opts = options;
}
}

View File

@ -9,7 +9,7 @@
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="icon" type="image/x-icon" href="favicon.ico">
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
<script src="https://code.jquery.com/jquery-3.2.1.slim.min.js" integrity="sha256-k2WSCIexGzOj3Euiig+TlR8gA0EmPjuc79OEeY5L45g="
<script src="https://code.jquery.com/jquery-3.3.1.min.js" integrity="sha256-FgpCb/KJQlLNfOu91ta32o/NMZxltwRo8QtmkMRdAu8="
crossorigin="anonymous"></script>
</head>

View File

@ -16,7 +16,7 @@ li {
list-style: none;
}
video {
#mirrored-video video {
width: 100%;
}

View File

@ -48,7 +48,7 @@ import io.openvidu.server.kurento.KurentoClientProvider;
import io.openvidu.server.kurento.core.KurentoSessionEventsHandler;
import io.openvidu.server.kurento.core.KurentoSessionManager;
import io.openvidu.server.kurento.kms.FixedOneKmsManager;
import io.openvidu.server.recording.RecordingService;
import io.openvidu.server.recording.ComposedRecordingService;
import io.openvidu.server.rest.NgrokRestController;
import io.openvidu.server.rpc.RpcHandler;
import io.openvidu.server.rpc.RpcNotificationService;
@ -150,6 +150,7 @@ public class OpenViduServer implements JsonRpcConfigurer {
NEW_LINE;
System.out.println(str);
OpenViduServer.publicUrl = ngrok.getNgrokServerUrl().replaceFirst("https://", "wss://");
openviduConf.setFinalUrl(ngrok.getNgrokServerUrl());
} catch (Exception e) {
System.err.println("Ngrok URL was configured, but there was an error connecting to ngrok: "
@ -161,6 +162,7 @@ public class OpenViduServer implements JsonRpcConfigurer {
case "docker":
try {
OpenViduServer.publicUrl = "wss://" + getContainerIp() + ":" + openviduConf.getServerPort();
openviduConf.setFinalUrl("https://" + getContainerIp() + ":" + openviduConf.getServerPort());
} catch (Exception e) {
System.err.println("Docker container IP was configured, but there was an error obtaining IP: "
+ e.getClass().getName() + " " + e.getMessage());
@ -171,19 +173,27 @@ public class OpenViduServer implements JsonRpcConfigurer {
case "local":
break;
case "docker-local":
break;
default:
URL url = new URL(publicUrl);
int port = url.getPort();
type = "custom";
OpenViduServer.publicUrl = publicUrl.replaceFirst("https://", "wss://");
if (publicUrl.startsWith("https://")) {
OpenViduServer.publicUrl = publicUrl.replace("https://", "wss://");
} else if (publicUrl.startsWith("http://")) {
OpenViduServer.publicUrl = publicUrl.replace("http://", "wss://");
}
openviduConf.setFinalUrl(url.toString());
if (!OpenViduServer.publicUrl.startsWith("wss://")) {
OpenViduServer.publicUrl = "wss://" + OpenViduServer.publicUrl;
}
if (OpenViduServer.publicUrl.endsWith("/")) {
OpenViduServer.publicUrl = OpenViduServer.publicUrl.substring(0, OpenViduServer.publicUrl.length() - 1);
}
if (port == -1) {
OpenViduServer.publicUrl += ":" + openviduConf.getServerPort();
}
@ -194,22 +204,35 @@ public class OpenViduServer implements JsonRpcConfigurer {
if (OpenViduServer.publicUrl == null) {
type = "local";
OpenViduServer.publicUrl = "wss://localhost:" + openviduConf.getServerPort();
openviduConf.setFinalUrl("https://localhost:" + openviduConf.getServerPort());
}
if (OpenViduServer.publicUrl.endsWith("/")) {
OpenViduServer.publicUrl = OpenViduServer.publicUrl.substring(0, OpenViduServer.publicUrl.length() - 1);
}
boolean recordingModuleEnabled = openviduConf.isRecordingModuleEnabled();
if (recordingModuleEnabled) {
RecordingService recordingService = context.getBean(RecordingService.class);
System.out.println("Recording module required: Downloading openvidu/openvidu-recording Docker image (800 MB aprox)");
ComposedRecordingService recordingService = context.getBean(ComposedRecordingService.class);
recordingService.setRecordingVersion(openviduConf.getOpenViduRecordingVersion());
System.out.println("Recording module required: Downloading openvidu/openvidu-recording:"
+ openviduConf.getOpenViduRecordingVersion() + " Docker image (800 MB aprox)");
boolean imageExists = false;
try {
imageExists = recordingService.recordingImageExistsLocally();
} catch (ProcessingException exception) {
log.error("Exception connecting to Docker daemon: you need Docker installed in this machine to enable OpenVidu recorder service");
throw new RuntimeException("Exception connecting to Docker daemon: you need Docker installed in this machine to enable OpenVidu recorder service");
}
if (imageExists) {
try {
imageExists = recordingService.recordingImageExistsLocally();
} catch (ProcessingException exception) {
String message = "Exception connecting to Docker daemon: ";
if ("docker-local".equals(openviduConf.getOpenViduPublicUrl())) {
message += "make sure you include flag \"-v /var/run/docker.sock:/var/run/docker.sock\" in \"docker run\" command";
} else {
message += "you need Docker installed in this machine to enable OpenVidu recorder service";
}
log.error(message);
throw new RuntimeException(message);
}
if (imageExists) {
System.out.println("Docker image already exists locally");
} else {
Thread t = new Thread(() -> {
@ -233,7 +256,7 @@ public class OpenViduServer implements JsonRpcConfigurer {
}
}
System.out.println("OpenVidu Server using " + type + " URL: " + OpenViduServer.publicUrl);
System.out.println("OpenVidu Server using " + type + " URL: [" + OpenViduServer.publicUrl + "]");
}
private static String getContainerIp() throws IOException, InterruptedException {

View File

@ -14,6 +14,7 @@ import io.openvidu.server.core.Participant;
/**
* CDR logger to register all information of each WebRTC connection:
* Enabled by property 'openvidu.cdr=true'
*
* - Participant unique identifier
* - Session unique identifier

View File

@ -7,7 +7,7 @@ import org.springframework.stereotype.Component;
public class OpenviduConfig {
@Value("${openvidu.publicurl}")
private String openviduPublicUrl; // local, ngrok, docker, [FINAL_URL]
private String openviduPublicUrl; // local, docker-local, ngrok, docker, [FINAL_URL]
@Value("${server.port}")
private String serverPort;
@ -27,6 +27,11 @@ public class OpenviduConfig {
@Value("${openvidu.recording.free-access}")
boolean openviduRecordingFreeAccess;
@Value("${openvidu.recording.version}")
String openviduRecordingVersion;
private String finalUrl;
public String getOpenViduPublicUrl() {
return this.openviduPublicUrl;
}
@ -59,4 +64,20 @@ public class OpenviduConfig {
return this.openviduRecordingFreeAccess;
}
public void setOpenViduRecordingPath(String recordingPath) {
this.openviduRecordingPath = recordingPath;
}
public String getFinalUrl() {
return finalUrl;
}
public void setFinalUrl(String finalUrl) {
this.finalUrl = finalUrl.endsWith("/") ? (finalUrl) : (finalUrl + "/");
}
public String getOpenViduRecordingVersion() {
return this.openviduRecordingVersion;
}
}

View File

@ -21,6 +21,11 @@ public class SecurityConfig extends WebSecurityConfigurerAdapter {
.authorizeRequests()
.antMatchers(HttpMethod.POST, "/api/sessions").authenticated()
.antMatchers(HttpMethod.POST, "/api/tokens").authenticated()
.antMatchers(HttpMethod.POST, "/api/recordings/start").authenticated()
.antMatchers(HttpMethod.POST, "/api/recordings/stop").authenticated()
.antMatchers(HttpMethod.GET, "/api/recordings").authenticated()
.antMatchers(HttpMethod.GET, "/api/recordings/**").authenticated()
.antMatchers(HttpMethod.DELETE, "/api/recordings/**").authenticated()
.antMatchers("/").authenticated();
if (openviduConf.getOpenViduRecordingFreeAccess()) {

View File

@ -0,0 +1,18 @@
package io.openvidu.server.config;
import org.springframework.boot.context.embedded.ConfigurableEmbeddedServletContainer;
import org.springframework.boot.context.embedded.EmbeddedServletContainerCustomizer;
import org.springframework.boot.context.embedded.MimeMappings;
import org.springframework.stereotype.Component;
@Component
public class ServletCustomizer implements EmbeddedServletContainerCustomizer {
@Override
public void customize(ConfigurableEmbeddedServletContainer container) {
MimeMappings mappings = new MimeMappings(MimeMappings.DEFAULT);
mappings.add("mp4", "video/mp4");
container.setMimeMappings(mappings);
}
}

View File

@ -24,4 +24,6 @@ public interface Session {
Participant getParticipantByPublicId(String participantPublicId);
int getActivePublishers();
}

View File

@ -1,7 +1,5 @@
package io.openvidu.server.core;
import java.math.BigInteger;
import java.security.SecureRandom;
import java.util.HashSet;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
@ -10,6 +8,7 @@ import java.util.stream.Collectors;
import javax.annotation.PreDestroy;
import org.apache.commons.lang3.RandomStringUtils;
import org.kurento.jsonrpc.message.Request;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@ -60,6 +59,15 @@ public abstract class SessionManager {
*/
public void evictParticipant(String participantPrivateId) throws OpenViduException {
}
/**
* Returns a Session given its id
*
* @return Session
*/
public Session getSession(String sessionId) {
return sessions.get(sessionId);
}
/**
* Returns all currently active (opened) sessions.
@ -141,7 +149,7 @@ public abstract class SessionManager {
public String newSessionId(SessionProperties sessionProperties) {
String sessionId = OpenViduServer.publicUrl;
sessionId += "/" + new BigInteger(130, new SecureRandom()).toString(32);
sessionId += "/" + RandomStringUtils.randomAlphanumeric(16).toLowerCase();
this.sessionidTokenTokenobj.put(sessionId, new ConcurrentHashMap<>());
this.sessionidParticipantpublicidParticipant.put(sessionId, new ConcurrentHashMap<>());
@ -155,7 +163,7 @@ public abstract class SessionManager {
if (this.sessionidParticipantpublicidParticipant.get(sessionId) != null
&& this.sessionidTokenTokenobj.get(sessionId) != null) {
if (isMetadataFormatCorrect(serverMetadata)) {
String token = new BigInteger(130, new SecureRandom()).toString(32);
String token = RandomStringUtils.randomAlphanumeric(16).toLowerCase();
this.sessionidTokenTokenobj.get(sessionId).put(token, new Token(token, role, serverMetadata));
showTokens();
return token;
@ -226,12 +234,12 @@ public abstract class SessionManager {
public Participant newParticipant(String sessionId, String participantPrivatetId, Token token,
String clientMetadata) {
if (this.sessionidParticipantpublicidParticipant.get(sessionId) != null) {
String participantPublicId = new BigInteger(130, new SecureRandom()).toString(32);
String participantPublicId = RandomStringUtils.randomAlphanumeric(16).toLowerCase();
ConcurrentHashMap<String, Participant> participantpublicidParticipant = this.sessionidParticipantpublicidParticipant
.get(sessionId);
while (participantpublicidParticipant.containsKey(participantPublicId)) {
// Avoid random 'participantpublicid' collisions
participantPublicId = new BigInteger(130, new SecureRandom()).toString(32);
participantPublicId = RandomStringUtils.randomAlphanumeric(16).toLowerCase();
}
Participant p = new Participant(participantPrivatetId, participantPublicId, token, clientMetadata);
this.sessionidParticipantpublicidParticipant.get(sessionId).put(participantPublicId, p);

View File

@ -219,7 +219,8 @@ public class KurentoSession implements Session {
other.cancelReceivingMedia(participant.getParticipantPublicId());
}
}
@Override
public int getActivePublishers() {
return activePublishers.get();
}

View File

@ -18,6 +18,7 @@ import com.google.gson.JsonSyntaxException;
import io.openvidu.client.OpenViduException;
import io.openvidu.client.OpenViduException.Code;
import io.openvidu.client.internal.ProtocolElements;
import io.openvidu.java.client.ArchiveLayout;
import io.openvidu.java.client.ArchiveMode;
import io.openvidu.java.client.MediaMode;
import io.openvidu.java.client.SessionProperties;
@ -26,7 +27,7 @@ import io.openvidu.server.kurento.KurentoClientProvider;
import io.openvidu.server.kurento.KurentoClientSessionInfo;
import io.openvidu.server.kurento.OpenViduKurentoClientSessionInfo;
import io.openvidu.server.kurento.endpoint.SdpType;
import io.openvidu.server.recording.RecordingService;
import io.openvidu.server.recording.ComposedRecordingService;
import io.openvidu.server.rpc.RpcHandler;
import io.openvidu.server.config.OpenviduConfig;
import io.openvidu.server.core.MediaOptions;
@ -44,7 +45,7 @@ public class KurentoSessionManager extends SessionManager {
private KurentoSessionEventsHandler sessionHandler;
@Autowired
private RecordingService recordingService;
private ComposedRecordingService recordingService;
@Autowired
OpenviduConfig openviduConfig;
@ -62,7 +63,7 @@ public class KurentoSessionManager extends SessionManager {
SessionProperties properties = sessionProperties.get(sessionId);
if (properties == null && this.isInsecureParticipant(participant.getParticipantPrivateId())) {
properties = new SessionProperties.Builder().mediaMode(MediaMode.ROUTED)
.archiveMode(ArchiveMode.ALWAYS).build();
.archiveMode(ArchiveMode.ALWAYS).archiveLayout(ArchiveLayout.BEST_FIT).build();
}
createSession(kcSessionInfo, properties);
}
@ -94,7 +95,7 @@ public class KurentoSessionManager extends SessionManager {
@Override
public void leaveRoom(Participant participant, Integer transactionId) {
log.debug("Request [LEAVE_ROOM] ({})", participant.getParticipantPublicId());
KurentoParticipant kParticipant = (KurentoParticipant) participant;
KurentoSession session = kParticipant.getSession();
String sessionId = session.getSessionId();
@ -147,16 +148,15 @@ public class KurentoSessionManager extends SessionManager {
showTokens();
log.warn("Session '{}' removed and closed", sessionId);
}
if (
remainingParticipants.size() == 1 &&
openviduConfig.isRecordingModuleEnabled() &&
MediaMode.ROUTED.equals(session.getSessionProperties().mediaMode()) &&
ProtocolElements.RECORDER_PARTICIPANT_ID_PUBLICID.equals(remainingParticipants.iterator().next().getParticipantPublicId())
) {
log.info("Last participant left. Stopping recording for session {}", sessionId);
evictParticipant(session.getParticipantByPublicId("RECORDER").getParticipantPrivateId());
recordingService.stopRecording(session);
}
if (remainingParticipants.size() == 1 && openviduConfig.isRecordingModuleEnabled()
&& MediaMode.ROUTED.equals(session.getSessionProperties().mediaMode())
&& ArchiveMode.ALWAYS.equals(session.getSessionProperties().archiveMode())
&& ProtocolElements.RECORDER_PARTICIPANT_ID_PUBLICID
.equals(remainingParticipants.iterator().next().getParticipantPublicId())) {
log.info("Last participant left. Stopping recording for session {}", sessionId);
evictParticipant(session.getParticipantByPublicId("RECORDER").getParticipantPrivateId());
recordingService.stopRecording(session);
}
sessionHandler.onParticipantLeft(participant, sessionId, remainingParticipants, transactionId, null);
}

View File

@ -0,0 +1,407 @@
package io.openvidu.server.recording;
import java.io.File;
import java.io.FileReader;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
import javax.ws.rs.ProcessingException;
import org.apache.commons.io.FilenameUtils;
import org.json.simple.JSONObject;
import org.json.simple.parser.JSONParser;
import org.json.simple.parser.ParseException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Service;
import com.github.dockerjava.api.DockerClient;
import com.github.dockerjava.api.command.CreateContainerCmd;
import com.github.dockerjava.api.command.CreateContainerResponse;
import com.github.dockerjava.api.command.ExecCreateCmdResponse;
import com.github.dockerjava.api.exception.ConflictException;
import com.github.dockerjava.api.exception.DockerClientException;
import com.github.dockerjava.api.exception.InternalServerErrorException;
import com.github.dockerjava.api.exception.NotFoundException;
import com.github.dockerjava.api.model.Bind;
import com.github.dockerjava.api.model.Volume;
import com.github.dockerjava.core.DefaultDockerClientConfig;
import com.github.dockerjava.core.DockerClientBuilder;
import com.github.dockerjava.core.DockerClientConfig;
import com.github.dockerjava.core.command.ExecStartResultCallback;
import com.github.dockerjava.core.command.PullImageResultCallback;
import io.openvidu.client.OpenViduException;
import io.openvidu.client.OpenViduException.Code;
import io.openvidu.server.CommandExecutor;
import io.openvidu.server.OpenViduServer;
import io.openvidu.server.config.OpenviduConfig;
import io.openvidu.server.core.Session;
@Service
public class ComposedRecordingService {
private Logger log = LoggerFactory.getLogger(ComposedRecordingService.class);
@Autowired
OpenviduConfig openviduConfig;
private Map<String, String> containers = new ConcurrentHashMap<>();
private Map<String, String> sessionsContainers = new ConcurrentHashMap<>();
private Map<String, Recording> startingRecordings = new ConcurrentHashMap<>();
private Map<String, Recording> startedRecordings = new ConcurrentHashMap<>();
private Map<String, Recording> sessionsRecordings = new ConcurrentHashMap<>();
private final String IMAGE_NAME = "openvidu/openvidu-recording";
private String IMAGE_TAG;
private final String RECORDING_ENTITY_FILE = ".recording.";
private DockerClient dockerClient;
public ComposedRecordingService() {
DockerClientConfig config = DefaultDockerClientConfig.createDefaultConfigBuilder().build();
this.dockerClient = DockerClientBuilder.getInstance(config).build();
}
public Recording startRecording(Session session) {
List<String> envs = new ArrayList<>();
String shortSessionId = session.getSessionId().substring(session.getSessionId().lastIndexOf('/') + 1,
session.getSessionId().length());
String videoId = this.getFreeRecordingId(session.getSessionId(), shortSessionId);
String secret = openviduConfig.getOpenViduSecret();
Recording recording = new Recording(session.getSessionId(), videoId, videoId);
this.sessionsRecordings.put(session.getSessionId(), recording);
this.startingRecordings.put(recording.getId(), recording);
String uid = null;
try {
uid = System.getenv("MY_UID");
if (uid == null) {
uid = CommandExecutor.execCommand("/bin/sh", "-c", "id -u " + System.getProperty("user.name"));
}
} catch (IOException | InterruptedException e) {
e.printStackTrace();
}
String location = OpenViduServer.publicUrl.replaceFirst("wss://", "");
String layoutUrl = session.getSessionProperties().archiveLayout().name().toLowerCase().replaceAll("_", "-");
envs.add("URL=https://OPENVIDUAPP:" + secret + "@" + location + "/#/layout-" + layoutUrl + "/" + shortSessionId
+ "/" + secret);
envs.add("RESOLUTION=1920x1080");
envs.add("FRAMERATE=30");
envs.add("VIDEO_NAME=" + videoId);
envs.add("VIDEO_FORMAT=mp4");
envs.add("USER_ID=" + uid);
envs.add("RECORDING_JSON=" + recording.toJson().toJSONString());
log.info(recording.toJson().toJSONString());
log.debug("Recorder connecting to url {}",
"https://OPENVIDUAPP:" + secret + "@localhost:8443/#/layout-best-fit/" + shortSessionId + "/" + secret);
String containerId = this.runRecordingContainer(envs, "recording_" + videoId);
this.waitForVideoFileNotEmpty(videoId);
this.sessionsContainers.put(session.getSessionId(), containerId);
recording.setStatus(Recording.Status.started);
this.startedRecordings.put(recording.getId(), recording);
this.startingRecordings.remove(recording.getId());
return recording;
}
public Recording stopRecording(Session session) {
Recording recording = this.sessionsRecordings.remove(session.getSessionId());
String containerId = this.sessionsContainers.remove(session.getSessionId());
this.startedRecordings.remove(recording.getId());
// Gracefully stop ffmpeg process
ExecCreateCmdResponse execCreateCmdResponse = dockerClient.execCreateCmd(containerId).withAttachStdout(true)
.withAttachStderr(true).withCmd("bash", "-c", "echo 'q' > stop").exec();
try {
dockerClient.execStartCmd(execCreateCmdResponse.getId()).exec(new ExecStartResultCallback())
.awaitCompletion();
} catch (InterruptedException e) {
e.printStackTrace();
}
// Wait for the container to be gracefully self-stopped
CountDownLatch latch = new CountDownLatch(1);
WaitForContainerStoppedCallback callback = new WaitForContainerStoppedCallback(latch);
dockerClient.waitContainerCmd(containerId).exec(callback);
boolean stopped = false;
try {
stopped = latch.await(60, TimeUnit.SECONDS);
} catch (InterruptedException e) {
recording.setStatus(Recording.Status.failed);
failRecordingCompletion(containerId, new OpenViduException(Code.RECORDING_COMPLETION_ERROR_CODE,
"The recording completion process has been unexpectedly interrupted"));
}
if (!stopped) {
recording.setStatus(Recording.Status.failed);
failRecordingCompletion(containerId, new OpenViduException(Code.RECORDING_COMPLETION_ERROR_CODE,
"The recording completion process couldn't finish in 60 seconds"));
}
// Remove container
this.removeDockerContainer(containerId);
// Update recording attributes reading from video report file
try {
RecordingInfoUtils infoUtils = new RecordingInfoUtils(
this.openviduConfig.getOpenViduRecordingPath() + recording.getName() + ".info");
if (openviduConfig.getOpenViduRecordingFreeAccess()) {
recording.setStatus(Recording.Status.available);
} else {
recording.setStatus(Recording.Status.stopped);
}
recording.setDuration(infoUtils.getDurationInSeconds());
recording.setSize(infoUtils.getSizeInBytes());
recording.setHasAudio(infoUtils.hasAudio());
recording.setHasVideo(infoUtils.hasVideo());
if (openviduConfig.getOpenViduRecordingFreeAccess()) {
recording.setUrl(this.openviduConfig.getFinalUrl() + "recordings/" + recording.getName() + ".mp4");
}
} catch (IOException | ParseException e) {
throw new OpenViduException(Code.RECORDING_REPORT_ERROR_CODE,
"There was an error generating the metadata report file for the recording");
}
return recording;
}
public boolean recordingImageExistsLocally() {
boolean imageExists = false;
try {
dockerClient.inspectImageCmd(IMAGE_NAME + ":" + IMAGE_TAG).exec();
imageExists = true;
} catch (NotFoundException nfe) {
imageExists = false;
} catch (ProcessingException e) {
throw e;
}
return imageExists;
}
public void downloadRecordingImage() {
try {
dockerClient.pullImageCmd(IMAGE_NAME + ":" + IMAGE_TAG).exec(new PullImageResultCallback()).awaitSuccess();
} catch (NotFoundException | InternalServerErrorException e) {
if (imageExistsLocally(IMAGE_NAME + ":" + IMAGE_TAG)) {
log.info("Docker image '{}' exists locally", IMAGE_NAME + ":" + IMAGE_TAG);
} else {
throw e;
}
} catch (DockerClientException e) {
log.info("Error on Pulling '{}' image. Probably because the user has stopped the execution", IMAGE_NAME + ":" + IMAGE_TAG);
throw e;
}
}
public boolean sessionIsBeingRecorded(String sessionId) {
return (this.sessionsRecordings.get(sessionId) != null);
}
public Recording getStartedRecording(String recordingId) {
return this.startedRecordings.get(recordingId);
}
public Recording getStartingRecording(String recordingId) {
return this.startingRecordings.get(recordingId);
}
private String runRecordingContainer(List<String> envs, String containerName) {
Volume volume1 = new Volume("/recordings");
CreateContainerCmd cmd = dockerClient.createContainerCmd(IMAGE_NAME + ":" + IMAGE_TAG).withName(containerName).withEnv(envs)
.withNetworkMode("host").withVolumes(volume1)
.withBinds(new Bind(openviduConfig.getOpenViduRecordingPath(), volume1));
CreateContainerResponse container = null;
try {
container = cmd.exec();
dockerClient.startContainerCmd(container.getId()).exec();
containers.put(container.getId(), containerName);
log.info("Container ID: {}", container.getId());
return container.getId();
} catch (ConflictException e) {
log.error(
"The container name {} is already in use. Probably caused by a session with unique publisher re-publishing a stream",
containerName);
return null;
}
}
private void removeDockerContainer(String containerId) {
dockerClient.removeContainerCmd(containerId).exec();
containers.remove(containerId);
}
private void stopDockerContainer(String containerId) {
dockerClient.stopContainerCmd(containerId).exec();
}
private boolean imageExistsLocally(String imageName) {
boolean imageExists = false;
try {
dockerClient.inspectImageCmd(imageName).exec();
imageExists = true;
} catch (NotFoundException nfe) {
imageExists = false;
}
return imageExists;
}
public Collection<Recording> getAllRecordings() {
return this.getRecordingEntitiesFromHost();
}
public Collection<Recording> getStartingRecordings() {
return this.startingRecordings.values();
}
public Collection<Recording> getStartedRecordings() {
return this.startedRecordings.values();
}
public Collection<Recording> getFinishedRecordings() {
return this.getRecordingEntitiesFromHost().stream()
.filter(recording -> (recording.getStatus().equals(Recording.Status.stopped)
|| recording.getStatus().equals(Recording.Status.available)))
.collect(Collectors.toSet());
}
private Set<String> getRecordingIdsFromHost() {
File folder = new File(this.openviduConfig.getOpenViduRecordingPath());
File[] files = folder.listFiles();
Set<String> fileNamesNoExtension = new HashSet<>();
for (int i = 0; i < files.length; i++) {
if (files[i].isFile() && !files[i].getName().startsWith(RECORDING_ENTITY_FILE)) {
fileNamesNoExtension.add(FilenameUtils.removeExtension(files[i].getName()));
}
}
return fileNamesNoExtension;
}
private Set<Recording> getRecordingEntitiesFromHost() {
File folder = new File(this.openviduConfig.getOpenViduRecordingPath());
File[] files = folder.listFiles();
Set<Recording> recordingEntities = new HashSet<>();
for (int i = 0; i < files.length; i++) {
Recording recording = this.getRecordingFromFile(files[i]);
if (recording != null) {
if (openviduConfig.getOpenViduRecordingFreeAccess()) {
if (Recording.Status.stopped.equals(recording.getStatus())) {
recording.setStatus(Recording.Status.available);
recording.setUrl(
this.openviduConfig.getFinalUrl() + "recordings/" + recording.getName() + ".mp4");
}
}
recordingEntities.add(recording);
}
}
return recordingEntities;
}
public HttpStatus deleteRecordingFromHost(String recordingId) {
if (this.startedRecordings.containsKey(recordingId) || this.startingRecordings.containsKey(recordingId)) {
// Cannot delete an active recording
return HttpStatus.CONFLICT;
}
File folder = new File(this.openviduConfig.getOpenViduRecordingPath());
File[] files = folder.listFiles();
int numFilesDeleted = 0;
for (int i = 0; i < files.length; i++) {
if (files[i].isFile() && isFileFromRecording(files[i], recordingId)) {
files[i].delete();
numFilesDeleted++;
}
}
HttpStatus status;
if (numFilesDeleted == 3) {
status = HttpStatus.NO_CONTENT;
} else {
status = HttpStatus.NOT_FOUND;
}
return status;
}
private Recording getRecordingFromFile(File file) {
if (file.isFile() && file.getName().startsWith(RECORDING_ENTITY_FILE)) {
JSONParser parser = new JSONParser();
JSONObject json = null;
try {
json = (JSONObject) parser.parse(new FileReader(file));
} catch (IOException | ParseException e) {
return null;
}
return new Recording(json);
}
return null;
}
private boolean isFileFromRecording(File file, String recordingId) {
return (((recordingId + ".info").equals(file.getName())) || ((recordingId + ".mp4").equals(file.getName()))
|| ((".recording." + recordingId).equals(file.getName())));
}
private String getFreeRecordingId(String sessionId, String shortSessionId) {
Set<String> recordingIds = this.getRecordingIdsFromHost();
String recordingId = shortSessionId;
boolean isPresent = recordingIds.contains(recordingId);
int i = 1;
while (isPresent) {
recordingId = shortSessionId + "-" + i;
i++;
isPresent = recordingIds.contains(recordingId);
}
return recordingId;
}
private void waitForVideoFileNotEmpty(String recordingId) {
boolean isPresent = false;
while (!isPresent) {
try {
Thread.sleep(150);
File f = new File(this.openviduConfig.getOpenViduRecordingPath() + recordingId + ".mp4");
isPresent = ((f.isFile()) && (f.length() > 0));
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
private void failRecordingCompletion(String containerId, OpenViduException e) {
this.stopDockerContainer(containerId);
this.removeDockerContainer(containerId);
throw e;
}
public void setRecordingVersion(String version) {
this.IMAGE_TAG = version;
}
}

View File

@ -0,0 +1,145 @@
package io.openvidu.server.recording;
import org.json.simple.JSONObject;
public class Recording {
public enum Status {
starting, // The recording is starting (cannot be stopped)
started, // The recording has started and is going on
stopped, // The recording has finished OK
available, // The recording is available for downloading. This status is reached for all
// stopped recordings if property 'openvidu.recording.free-access' is true
failed; // The recording has failed
}
private Status status;
private String id;
private String name;
private String sessionId;
private long createdAt; // milliseconds (UNIX Epoch time)
private long size = 0; // bytes
private double duration = 0; // seconds
private String url;
private boolean hasAudio = true;
private boolean hasVideo = true;
public Recording(String sessionId, String id, String name) {
this.sessionId = sessionId;
this.createdAt = System.currentTimeMillis();
this.id = id;
this.name = id; // For now the name of the recording file is the same as its id
this.status = Status.started;
}
public Recording(JSONObject json) {
this.id = (String) json.get("id");
this.name = (String) json.get("name");
this.sessionId = (String) json.get("sessionId");
this.createdAt = (long) json.get("createdAt");
this.size = (long) json.get("size");
this.duration = (double) json.get("duration");
this.url = (String) json.get("url");
this.hasAudio = (boolean) json.get("hasAudio");
this.hasVideo = (boolean) json.get("hasVideo");
this.status = Status.valueOf((String) json.get("status"));
}
public Status getStatus() {
return status;
}
public void setStatus(Status status) {
this.status = status;
}
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getSessionId() {
return sessionId;
}
public void setSessionId(String sessionId) {
this.sessionId = sessionId;
}
public long getCreatedAt() {
return createdAt;
}
public void setCreatedAt(long createdAt) {
this.createdAt = createdAt;
}
public long getSize() {
return size;
}
public void setSize(long l) {
this.size = l;
}
public double getDuration() {
return duration;
}
public void setDuration(double duration) {
this.duration = duration;
}
public String getUrl() {
return url;
}
public void setUrl(String url) {
this.url = url;
}
public boolean hasAudio() {
return hasAudio;
}
public void setHasAudio(boolean hasAudio) {
this.hasAudio = hasAudio;
}
public boolean hasVideo() {
return hasVideo;
}
public void setHasVideo(boolean hasVideo) {
this.hasVideo = hasVideo;
}
@SuppressWarnings("unchecked")
public JSONObject toJson() {
JSONObject json = new JSONObject();
json.put("id", this.id);
json.put("name", this.name);
json.put("sessionId", this.sessionId);
json.put("createdAt", this.createdAt);
json.put("size", this.size);
json.put("duration", this.duration);
json.put("url", this.url);
json.put("hasAudio", this.hasAudio);
json.put("hasVideo", this.hasVideo);
json.put("status", this.status.toString());
return json;
}
}

View File

@ -0,0 +1,104 @@
package io.openvidu.server.recording;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.IOException;
import org.json.simple.JSONArray;
import org.json.simple.JSONObject;
import org.json.simple.parser.JSONParser;
import org.json.simple.parser.ParseException;
import io.openvidu.client.OpenViduException;
import io.openvidu.client.OpenViduException.Code;
public class RecordingInfoUtils {
private JSONParser parser;
private JSONObject json;
private JSONObject jsonFormat;
private JSONObject videoStream;
private JSONObject audioStream;
public RecordingInfoUtils(String fullVideoPath)
throws FileNotFoundException, IOException, ParseException, OpenViduException {
this.parser = new JSONParser();
this.json = (JSONObject) parser.parse(new FileReader(fullVideoPath));
if (json.isEmpty()) {
// Recording metadata from ffprobe is empty: video file is corrupted or empty
throw new OpenViduException(Code.RECORDING_FILE_EMPTY_ERROR, "The recording file is empty or corrupted");
}
this.jsonFormat = (JSONObject) json.get("format");
JSONArray streams = (JSONArray) json.get("streams");
for (int i = 0; i < streams.size(); i++) {
JSONObject stream = (JSONObject) streams.get(i);
if ("video".equals(stream.get("codec_type").toString())) {
this.videoStream = stream;
} else if ("audio".equals(stream.get("codec_type").toString())) {
this.audioStream = stream;
}
}
}
public double getDurationInSeconds() {
return Double.parseDouble(jsonFormat.get("duration").toString());
}
public int getSizeInBytes() {
return Integer.parseInt(jsonFormat.get("size").toString());
}
public int getNumberOfStreams() {
return Integer.parseInt(jsonFormat.get("nb_streams").toString());
}
public int getBitRate() {
return (Integer.parseInt(jsonFormat.get("bit_rate").toString()) / 1000);
}
public boolean hasVideo() {
return this.videoStream != null;
}
public boolean hasAudio() {
return this.audioStream != null;
}
public int videoWidth() {
return Integer.parseInt(videoStream.get("width").toString());
}
public int videoHeight() {
return Integer.parseInt(videoStream.get("height").toString());
}
public int getVideoFramerate() {
String frameRate = videoStream.get("r_frame_rate").toString();
String[] frameRateParts = frameRate.split("/");
return Integer.parseInt(frameRateParts[0]) / Integer.parseInt(frameRateParts[1]);
}
public String getVideoCodec() {
return videoStream.get("codec_name").toString();
}
public String getLongVideoCodec() {
return videoStream.get("codec_long_name").toString();
}
public String getAudioCodec() {
return audioStream.get("codec_name").toString();
}
public String getLongAudioCodec() {
return audioStream.get("codec_long_name").toString();
}
}

View File

@ -1,171 +0,0 @@
package io.openvidu.server.recording;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.ws.rs.ProcessingException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import com.github.dockerjava.api.DockerClient;
import com.github.dockerjava.api.command.CreateContainerCmd;
import com.github.dockerjava.api.command.CreateContainerResponse;
import com.github.dockerjava.api.command.ExecCreateCmdResponse;
import com.github.dockerjava.api.exception.ConflictException;
import com.github.dockerjava.api.exception.DockerClientException;
import com.github.dockerjava.api.exception.InternalServerErrorException;
import com.github.dockerjava.api.exception.NotFoundException;
import com.github.dockerjava.api.model.Bind;
import com.github.dockerjava.api.model.Volume;
import com.github.dockerjava.core.DefaultDockerClientConfig;
import com.github.dockerjava.core.DockerClientBuilder;
import com.github.dockerjava.core.DockerClientConfig;
import com.github.dockerjava.core.command.ExecStartResultCallback;
import com.github.dockerjava.core.command.PullImageResultCallback;
import io.openvidu.server.CommandExecutor;
import io.openvidu.server.config.OpenviduConfig;
import io.openvidu.server.core.Session;
@Service
public class RecordingService {
private Logger log = LoggerFactory.getLogger(RecordingService.class);
@Autowired
OpenviduConfig openviduConfig;
private static final Map<String, String> createdContainers = new HashMap<>();
private final String IMAGE_NAME = "openvidu/openvidu-recording";
private DockerClient dockerClient;
private Map<String, String> recordingSessions = new HashMap<>();;
public RecordingService() {
DockerClientConfig config = DefaultDockerClientConfig.createDefaultConfigBuilder().build();
this.dockerClient = DockerClientBuilder.getInstance(config).build();
}
public void startRecording(Session session) {
List<String> envs = new ArrayList<>();
String shortSessionId = session.getSessionId().substring(session.getSessionId().lastIndexOf('/') + 1,
session.getSessionId().length());
String secret = openviduConfig.getOpenViduSecret();
String uid = null;
try {
uid = System.getenv("MY_UID");
if (uid==null) {
uid = CommandExecutor.execCommand("/bin/sh", "-c", "id -u " + System.getProperty("user.name"));
}
} catch (IOException | InterruptedException e) {
e.printStackTrace();
}
envs.add("URL=https://OPENVIDUAPP:" + secret + "@localhost:8443/#/layout-best-fit/" + shortSessionId + "/"
+ secret);
envs.add("RESOLUTION=1920x1080");
envs.add("FRAMERATE=30");
envs.add("VIDEO_NAME=" + shortSessionId);
envs.add("VIDEO_FORMAT=mp4");
envs.add("USER_ID=" + uid);
System.out.println(
"https://OPENVIDUAPP:" + secret + "@localhost:8443/#/layout-best-fit/" + shortSessionId + "/" + secret);
String containerId = this.runRecordingContainer(envs, "recording" + shortSessionId);
this.recordingSessions.putIfAbsent(session.getSessionId(), containerId);
}
public void stopRecording(Session session) {
String containerId = this.recordingSessions.remove(session.getSessionId());
ExecCreateCmdResponse execCreateCmdResponse = dockerClient.execCreateCmd(containerId)
.withAttachStdout(true)
.withAttachStderr(true)
.withCmd("bash", "-c", "echo 'q' > stop")
.exec();
try {
dockerClient.execStartCmd(execCreateCmdResponse.getId()).exec(new ExecStartResultCallback()).awaitCompletion();
} catch (InterruptedException e) {
e.printStackTrace();
}
this.stopDockerContainer(containerId);
this.removeDockerContainer(containerId);
}
public boolean recordingImageExistsLocally() {
boolean imageExists = false;
try {
dockerClient.inspectImageCmd(IMAGE_NAME).exec();
imageExists = true;
} catch (NotFoundException nfe) {
imageExists = false;
} catch (ProcessingException e) {
throw e;
}
return imageExists;
}
public void downloadRecordingImage() {
try {
dockerClient.pullImageCmd(IMAGE_NAME).exec(new PullImageResultCallback()).awaitSuccess();
} catch (NotFoundException | InternalServerErrorException e) {
if (imageExistsLocally(IMAGE_NAME)) {
log.info("Docker image '{}' exists locally", IMAGE_NAME);
} else {
throw e;
}
} catch (DockerClientException e) {
log.info("Error on Pulling '{}' image. Probably because the user has stopped the execution",
IMAGE_NAME);
throw e;
}
}
private String runRecordingContainer(List<String> envs, String containerName) {
Volume volume1 = new Volume("/recordings");
CreateContainerCmd cmd = dockerClient.createContainerCmd(IMAGE_NAME).withName(containerName).withEnv(envs)
.withNetworkMode("host").withVolumes(volume1)
.withBinds(new Bind(openviduConfig.getOpenViduRecordingPath(), volume1));
CreateContainerResponse container = null;
try {
container = cmd.exec();
dockerClient.startContainerCmd(container.getId()).exec();
createdContainers.put(container.getId(), containerName);
log.info("Container ID: {}", container.getId());
return container.getId();
} catch (ConflictException e) {
log.error(
"The container name {} is already in use. Probably caused by a session with unique publisher re-publishing a stream",
containerName);
return null;
}
}
private void removeDockerContainer(String containerId) {
dockerClient.removeContainerCmd(containerId).exec();
createdContainers.remove(containerId);
}
private void stopDockerContainer(String containerId) {
dockerClient.stopContainerCmd(containerId).exec();
}
private boolean imageExistsLocally(String imageName) {
boolean imageExists = false;
try {
dockerClient.inspectImageCmd(imageName).exec();
imageExists = true;
} catch (NotFoundException nfe) {
imageExists = false;
}
return imageExists;
}
}

View File

@ -19,6 +19,8 @@ public class RecordingsHttpHandler extends WebMvcConfigurerAdapter {
String recordingsPath = openviduConfig.getOpenViduRecordingPath();
recordingsPath = recordingsPath.endsWith("/") ? recordingsPath : recordingsPath + "/";
openviduConfig.setOpenViduRecordingPath(recordingsPath);
registry.addResourceHandler("/recordings/**").addResourceLocations("file:" + recordingsPath);
}

View File

@ -0,0 +1,43 @@
package io.openvidu.server.recording;
import java.io.Closeable;
import java.io.IOException;
import java.util.concurrent.CountDownLatch;
import com.github.dockerjava.api.async.ResultCallback;
import com.github.dockerjava.api.model.WaitResponse;
public class WaitForContainerStoppedCallback implements ResultCallback<WaitResponse> {
CountDownLatch latch;
public WaitForContainerStoppedCallback(CountDownLatch latch) {
this.latch = latch;
}
@Override
public void close() throws IOException {
// TODO Auto-generated method stub
}
@Override
public void onComplete() {
latch.countDown();
}
@Override
public void onError(Throwable arg0) {
// TODO Auto-generated method stub
}
@Override
public void onStart(Closeable arg0) {
// TODO Auto-generated method stub
}
@Override
public void onNext(WaitResponse arg0) {
// TODO Auto-generated method stub
}
}

View File

@ -18,14 +18,18 @@ package io.openvidu.server.rest;
import static org.kurento.commons.PropertiesManager.getProperty;
import java.util.Collection;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.Set;
import org.json.simple.JSONArray;
import org.json.simple.JSONObject;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
@ -37,7 +41,10 @@ import io.openvidu.java.client.ArchiveMode;
import io.openvidu.java.client.MediaMode;
import io.openvidu.java.client.SessionProperties;
import io.openvidu.server.core.ParticipantRole;
import io.openvidu.server.core.Session;
import io.openvidu.server.core.SessionManager;
import io.openvidu.server.recording.Recording;
import io.openvidu.server.recording.ComposedRecordingService;
/**
*
@ -54,6 +61,9 @@ public class SessionRestController {
@Autowired
private SessionManager sessionManager;
@Autowired
private ComposedRecordingService recordingService;
@RequestMapping(value = "/sessions", method = RequestMethod.GET)
public Set<String> getAllSessions() {
return sessionManager.getSessions();
@ -72,7 +82,7 @@ public class SessionRestController {
@SuppressWarnings("unchecked")
@RequestMapping(value = "/sessions", method = RequestMethod.POST)
public ResponseEntity<JSONObject> getSessionId(@RequestBody(required = false) Map<?, ?> params) {
SessionProperties.Builder builder = new SessionProperties.Builder();
if (params != null) {
String archiveModeString = (String) params.get("archiveMode");
@ -95,7 +105,7 @@ public class SessionRestController {
} catch (IllegalArgumentException e) {
return this.generateErrorResponse("ArchiveMode " + params.get("archiveMode") + " | " + "ArchiveLayout "
+ params.get("archiveLayout") + " | " + "MediaMode " + params.get("mediaMode")
+ " are not defined", "/api/tokens");
+ " are not defined", "/api/tokens", HttpStatus.BAD_REQUEST);
}
}
@ -104,7 +114,7 @@ public class SessionRestController {
String sessionId = sessionManager.newSessionId(sessionProperties);
JSONObject responseJson = new JSONObject();
responseJson.put("id", sessionId);
return new ResponseEntity<JSONObject>(responseJson, HttpStatus.OK);
return new ResponseEntity<>(responseJson, HttpStatus.OK);
}
@SuppressWarnings("unchecked")
@ -123,25 +133,122 @@ public class SessionRestController {
responseJson.put("role", role.toString());
responseJson.put("data", metadata);
responseJson.put("token", token);
return new ResponseEntity<JSONObject>(responseJson, HttpStatus.OK);
return new ResponseEntity<>(responseJson, HttpStatus.OK);
} catch (IllegalArgumentException e) {
return this.generateErrorResponse("Role " + params.get("role") + " is not defined", "/api/tokens");
return this.generateErrorResponse("Role " + params.get("role") + " is not defined", "/api/tokens",
HttpStatus.BAD_REQUEST);
} catch (OpenViduException e) {
return this.generateErrorResponse(
"Metadata [" + params.get("data") + "] unexpected format. Max length allowed is 1000 chars",
"/api/tokens");
"/api/tokens", HttpStatus.BAD_REQUEST);
}
}
@RequestMapping(value = "/recordings/start", method = RequestMethod.POST)
public ResponseEntity<JSONObject> startRecordingSession(@RequestBody Map<?, ?> params) {
String sessionId = (String) params.get("session");
if (sessionId == null) {
// "session" parameter not found
return new ResponseEntity<JSONObject>(HttpStatus.BAD_REQUEST);
}
Session session = sessionManager.getSession(sessionId);
if (session == null) {
// Session does not exist
return new ResponseEntity<JSONObject>(HttpStatus.NOT_FOUND);
}
if (session.getParticipants().isEmpty()) {
// Session has no participants
return new ResponseEntity<JSONObject>(HttpStatus.BAD_REQUEST);
}
if (!(session.getSessionProperties().mediaMode().equals(MediaMode.ROUTED))
|| this.recordingService.sessionIsBeingRecorded(session.getSessionId())) {
// Session is not in ROUTED MediMode or it is already being recorded
return new ResponseEntity<JSONObject>(HttpStatus.CONFLICT);
}
Recording startedRecording = this.recordingService.startRecording(session);
return new ResponseEntity<>(startedRecording.toJson(), HttpStatus.OK);
}
@RequestMapping(value = "/recordings/stop/{recordingId}", method = RequestMethod.POST)
public ResponseEntity<JSONObject> stopRecordingSession(@PathVariable("recordingId") String recordingId) {
if (recordingId == null) {
// "recordingId" parameter not found
return new ResponseEntity<JSONObject>(HttpStatus.BAD_REQUEST);
}
Recording recording = recordingService.getStartedRecording(recordingId);
if (recording == null) {
if (recordingService.getStartingRecording(recordingId) != null) {
// Recording is still starting
return new ResponseEntity<JSONObject>(HttpStatus.NOT_ACCEPTABLE);
}
// Recording does not exist
return new ResponseEntity<JSONObject>(HttpStatus.NOT_FOUND);
}
if (!this.recordingService.sessionIsBeingRecorded(recording.getSessionId())) {
// Session is not being recorded
return new ResponseEntity<JSONObject>(HttpStatus.CONFLICT);
}
Recording stoppedRecording = this.recordingService
.stopRecording(sessionManager.getSession(recording.getSessionId()));
return new ResponseEntity<>(stoppedRecording.toJson(), HttpStatus.OK);
}
@RequestMapping(value = "/recordings/{recordingId}", method = RequestMethod.GET)
public ResponseEntity<JSONObject> getRecording(@PathVariable("recordingId") String recordingId) {
try {
Recording recording = this.recordingService.getAllRecordings().stream()
.filter(rec -> rec.getId().equals(recordingId)).findFirst().get();
if (Recording.Status.started.equals(recording.getStatus())
&& recordingService.getStartingRecording(recording.getId()) != null) {
recording.setStatus(Recording.Status.starting);
}
return new ResponseEntity<>(recording.toJson(), HttpStatus.OK);
} catch (NoSuchElementException e) {
return new ResponseEntity<>(HttpStatus.NOT_FOUND);
}
}
@SuppressWarnings("unchecked")
private ResponseEntity<JSONObject> generateErrorResponse(String errorMessage, String path) {
@RequestMapping(value = "/recordings", method = RequestMethod.GET)
public ResponseEntity<JSONObject> getAllRecordings() {
Collection<Recording> recordings = this.recordingService.getAllRecordings();
JSONObject json = new JSONObject();
JSONArray jsonArray = new JSONArray();
recordings.forEach(rec -> {
if (Recording.Status.started.equals(rec.getStatus())
&& recordingService.getStartingRecording(rec.getId()) != null) {
rec.setStatus(Recording.Status.starting);
}
jsonArray.add(rec.toJson());
});
json.put("count", recordings.size());
json.put("items", jsonArray);
return new ResponseEntity<>(json, HttpStatus.OK);
}
@RequestMapping(value = "/recordings/{recordingId}", method = RequestMethod.DELETE)
public ResponseEntity<JSONObject> deleteRecording(@PathVariable("recordingId") String recordingId) {
return new ResponseEntity<>(this.recordingService.deleteRecordingFromHost(recordingId));
}
@SuppressWarnings("unchecked")
private ResponseEntity<JSONObject> generateErrorResponse(String errorMessage, String path, HttpStatus status) {
JSONObject responseJson = new JSONObject();
responseJson.put("timestamp", System.currentTimeMillis());
responseJson.put("status", HttpStatus.BAD_REQUEST.value());
responseJson.put("error", HttpStatus.BAD_REQUEST.getReasonPhrase());
responseJson.put("status", status.value());
responseJson.put("error", status.getReasonPhrase());
responseJson.put("message", errorMessage);
responseJson.put("path", path);
return new ResponseEntity<JSONObject>(responseJson, HttpStatus.BAD_REQUEST);
return new ResponseEntity<JSONObject>(responseJson, status);
}
}

View File

@ -16,12 +16,12 @@
},
{
"name": "openvidu.cdr",
"type": "java.lang.String",
"type": "java.lang.Boolean",
"description": "Whether to enable Call Detail Record or not"
},
{
"name": "openvidu.recording",
"type": "java.lang.String",
"type": "java.lang.Boolean",
"description": "Whether to start OpenVidu Server with recording module service available or not (a Docker image will be downloaded during the first execution). Apart from setting this param to true, it is also necessary to explicitly configure sessions to be recorded"
},
{
@ -31,7 +31,12 @@
},
{
"name": "openvidu.recording.free-access",
"type": "java.lang.String",
"type": "java.lang.Boolean",
"description": "'true' to allow free access to the video files specified in 'openviu.recording.path'. 'false' to only allow access to authenticated users"
},
{
"name": "openvidu.recording.version",
"type": "java.lang.String",
"description": "Tag for openvidu/openvidu-recording Docker image"
}
]}

View File

@ -5,6 +5,10 @@ server.ssl.enabled: false
server.address: 0.0.0.0
kms.uris=[\"ws://localhost:8888/kurento\"]
openvidu.secret: YOUR_SECRET
openvidu.publicurl: ngrok
openvidu.publicurl: ngrok
openvidu.cdr: false
openvidu.recording: false
openvidu.recording.path: /opt/openvidu/recordings
openvidu.recording.free-access: false
openvidu.recording.version: 1.8.0

View File

@ -1,11 +1,11 @@
server.port: 8443
server.address: 0.0.0.0
server.ssl.enabled: true
openvidu.recording.version: 1.8.0
server.port: 8443
server.ssl.key-store: classpath:cbx_cert.jks
server.ssl.key-store-password: C0uncilbox@2016
server.ssl.key-store-type: JKS
server.ssl.key-alias: cbx_cert
kms.uris=[\"ws://localhost:8888/kurento\"]
openvidu.secret: YOUR_SECRET
openvidu.publicurl: local

View File

@ -1,28 +1,42 @@
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<timestamp key="myTimestamp" timeReference="contextBirth" datePattern="HH-mm-ss"/>
<timestamp key="myTimestamp" timeReference="contextBirth"
datePattern="HH-mm-ss" />
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<layout class="ch.qos.logback.classic.PatternLayout">
<Pattern>[%p] %d [%.12t] %c \(%M\) - %msg%n</Pattern>
</layout>
</appender>
<appender name="CDR"
class="ch.qos.logback.core.rolling.RollingFileAppender">
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<FileNamePattern>log/CDR.%d{yyyy-MM-dd}_${myTimestamp}.log</FileNamePattern>
<MaxHistory>30</MaxHistory>
<TotalSizeCap>20GB</TotalSizeCap>
</rollingPolicy>
<layout>
<pattern>%m\n</pattern>
</layout>
</appender>
<logger name="io.openvidu.server.cdr.CallDetailRecord">
<level value="INFO" />
<appender-ref ref="CDR" />
</logger>
<root>
<level value="INFO" />
<appender-ref ref="STDOUT" />
</root>
<if condition='property("spring.profiles.active").contains("ngrok")'>
<then>
<property scope="context" resource="application-ngrok.properties" />
</then>
<else>
<property scope="context" resource="application.properties" />
</else>
</if>
<if condition='property("openvidu.cdr").contains("true")'>
<then>
<appender name="CDR"
class="ch.qos.logback.core.rolling.RollingFileAppender">
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<FileNamePattern>log/CDR.%d{yyyy-MM-dd}_${myTimestamp}.log
</FileNamePattern>
<MaxHistory>30</MaxHistory>
<TotalSizeCap>20GB</TotalSizeCap>
</rollingPolicy>
<layout>
<pattern>%m\n</pattern>
</layout>
</appender>
<logger name="io.openvidu.server.cdr.CallDetailRecord">
<level value="INFO" />
<appender-ref ref="CDR" />
</logger>
</then>
</if>
</configuration>

View File

@ -9,7 +9,7 @@
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="icon" type="image/x-icon" href="favicon.ico">
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
<script src="https://code.jquery.com/jquery-3.2.1.slim.min.js" integrity="sha256-k2WSCIexGzOj3Euiig+TlR8gA0EmPjuc79OEeY5L45g="
<script src="https://code.jquery.com/jquery-3.3.1.min.js" integrity="sha256-FgpCb/KJQlLNfOu91ta32o/NMZxltwRo8QtmkMRdAu8="
crossorigin="anonymous"></script>
</head>

View File

@ -61,55 +61,6 @@
/******/ return module.exports;
/******/ }
/******/
/******/ // This file contains only the entry chunk.
/******/ // The chunk loading function for additional chunks
/******/ __webpack_require__.e = function requireEnsure(chunkId) {
/******/ var installedChunkData = installedChunks[chunkId];
/******/ if(installedChunkData === 0) {
/******/ return new Promise(function(resolve) { resolve(); });
/******/ }
/******/
/******/ // a Promise means "currently loading".
/******/ if(installedChunkData) {
/******/ return installedChunkData[2];
/******/ }
/******/
/******/ // setup Promise in chunk cache
/******/ var promise = new Promise(function(resolve, reject) {
/******/ installedChunkData = installedChunks[chunkId] = [resolve, reject];
/******/ });
/******/ installedChunkData[2] = promise;
/******/
/******/ // start chunk loading
/******/ var head = document.getElementsByTagName('head')[0];
/******/ var script = document.createElement('script');
/******/ script.type = 'text/javascript';
/******/ script.charset = 'utf-8';
/******/ script.async = true;
/******/ script.timeout = 120000;
/******/
/******/ if (__webpack_require__.nc) {
/******/ script.setAttribute("nonce", __webpack_require__.nc);
/******/ }
/******/ script.src = __webpack_require__.p + "" + chunkId + ".chunk.js";
/******/ var timeout = setTimeout(onScriptComplete, 120000);
/******/ script.onerror = script.onload = onScriptComplete;
/******/ function onScriptComplete() {
/******/ // avoid mem leaks in IE.
/******/ script.onerror = script.onload = null;
/******/ clearTimeout(timeout);
/******/ var chunk = installedChunks[chunkId];
/******/ if(chunk !== 0) {
/******/ if(chunk) {
/******/ chunk[1](new Error('Loading chunk ' + chunkId + ' failed.'));
/******/ }
/******/ installedChunks[chunkId] = undefined;
/******/ }
/******/ };
/******/ head.appendChild(script);
/******/
/******/ return promise;
/******/ };
/******/
/******/ // expose the modules object (__webpack_modules__)
/******/ __webpack_require__.m = modules;

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

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