mirror of https://github.com/OpenVidu/openvidu.git
commit
517edd0ac6
|
@ -16,7 +16,13 @@ nbactions.xml
|
|||
*bower_components/
|
||||
.externalToolBuilders
|
||||
*bin/
|
||||
/.vscode
|
||||
|
||||
*/.vscode/*
|
||||
*/.sts4-cache/*
|
||||
*/.project
|
||||
*/.classpath
|
||||
*/.settings/*
|
||||
.idea/
|
||||
log/
|
||||
\.vscode/
|
||||
*.iml
|
||||
|
||||
|
|
|
@ -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
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -4,3 +4,4 @@ export * from './Publisher';
|
|||
export * from './Subscriber';
|
||||
export * from '../OpenViduInternal/Stream';
|
||||
export * from '../OpenViduInternal/Connection';
|
||||
export * from '../OpenViduInternal/LocalRecorder';
|
|
@ -21,7 +21,8 @@
|
|||
"outDir": "../../lib",
|
||||
"emitBOM": false,
|
||||
"preserveConstEnums": true,
|
||||
"sourceMap": true
|
||||
"sourceMap": true,
|
||||
"lib": ["dom","es5","es2015.promise","scripthost"]
|
||||
},
|
||||
//"buildOnSave": true,
|
||||
"compileOnSave": true
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
|
@ -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'
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -2,4 +2,5 @@ export * from './OpenViduInternal';
|
|||
export * from './Connection';
|
||||
export * from './SessionInternal';
|
||||
export * from './Stream';
|
||||
export * from './LocalRecorder';
|
||||
export * from './OpenViduError';
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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>
|
|
@ -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>
|
|
@ -1,4 +0,0 @@
|
|||
eclipse.preferences.version=1
|
||||
encoding//src/main/java=UTF-8
|
||||
encoding//src/test/java=UTF-8
|
||||
encoding/<project>=UTF-8
|
|
@ -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
|
|
@ -1,4 +0,0 @@
|
|||
activeProfiles=
|
||||
eclipse.preferences.version=1
|
||||
resolveWorkspaceProjects=true
|
||||
version=1
|
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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()));
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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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"}
|
|
@ -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);
|
||||
|
|
|
@ -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"}
|
|
@ -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';
|
||||
|
|
|
@ -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
|
|
@ -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"}
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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'
|
||||
|
|
|
@ -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 + "')");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
|
@ -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);
|
||||
|
|
|
@ -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';
|
|
@ -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>
|
|
@ -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>
|
|
@ -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
|
|
@ -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
|
|
@ -1,4 +0,0 @@
|
|||
activeProfiles=
|
||||
eclipse.preferences.version=1
|
||||
resolveWorkspaceProjects=true
|
||||
version=1
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 /
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 -->
|
||||
|
|
|
@ -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
|
|
@ -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"
|
||||
},
|
||||
{
|
||||
|
|
|
@ -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"
|
||||
},
|
||||
|
|
|
@ -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');
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
|
@ -16,7 +16,7 @@ li {
|
|||
list-style: none;
|
||||
}
|
||||
|
||||
video {
|
||||
#mirrored-video video {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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()) {
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
|
@ -24,4 +24,6 @@ public interface Session {
|
|||
|
||||
Participant getParticipantByPublicId(String participantPublicId);
|
||||
|
||||
int getActivePublishers();
|
||||
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -219,7 +219,8 @@ public class KurentoSession implements Session {
|
|||
other.cancelReceivingMedia(participant.getParticipantPublicId());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public int getActivePublishers() {
|
||||
return activePublishers.get();
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
]}
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -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>
|
|
@ -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>
|
||||
|
|
|
@ -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
Loading…
Reference in New Issue