openvidu-server updated to recording

pull/30/head
pabloFuente 2018-01-29 15:26:31 +01:00
parent 3f80d2cbc4
commit 75afcf16e1
37 changed files with 1156 additions and 300 deletions

View File

@ -31,6 +31,7 @@ export class OpenViduInternal {
private localStream: Stream; private localStream: Stream;
private remoteStreams: Stream[] = []; private remoteStreams: Stream[] = [];
private secret: string; private secret: string;
private recorder: boolean = false;
/* NEW METHODS */ /* NEW METHODS */
initSession(sessionId) { initSession(sessionId) {
@ -170,6 +171,14 @@ export class OpenViduInternal {
this.secret = secret; this.secret = secret;
} }
getRecorder() {
return this.recorder;
}
setRecorder(recorder: boolean) {
this.recorder = recorder;
}
getOpenViduServerURL() { getOpenViduServerURL() {
return 'https://' + this.wsUri.split("wss://")[1].split("/room")[0]; return 'https://' + this.wsUri.split("wss://")[1].split("/room")[0];
} }

View File

@ -6,6 +6,7 @@ import { Publisher } from '../OpenVidu/Publisher';
import EventEmitter = require('wolfy87-eventemitter'); import EventEmitter = require('wolfy87-eventemitter');
const SECRET_PARAM = '?secret='; const SECRET_PARAM = '?secret=';
const RECORDER_PARAM = '&recorder=';
export interface SessionOptions { export interface SessionOptions {
sessionId: string; sessionId: string;
@ -46,18 +47,38 @@ export class SessionInternal {
} }
private processOpenViduUrl(url: string) { private processOpenViduUrl(url: string) {
this.openVidu.setSecret(this.getSecretFromUrl(url)); let secret = this.getSecretFromUrl(url);
let recorder = this.getRecorderFromUrl(url);
if (!(secret == null)) {
this.openVidu.setSecret(secret);
}
if (!(recorder == null)) {
this.openVidu.setRecorder(recorder);
}
this.openVidu.setWsUri(this.getFinalUrl(url)); this.openVidu.setWsUri(this.getFinalUrl(url));
} }
private getSecretFromUrl(url: string): string { private getSecretFromUrl(url: string): string {
let secret = ''; let secret = '';
if (url.indexOf(SECRET_PARAM) !== -1) { if (url.indexOf(SECRET_PARAM) !== -1) {
let endOfSecret = url.lastIndexOf(RECORDER_PARAM);
if (endOfSecret !== -1) {
secret = url.substring(url.lastIndexOf(SECRET_PARAM) + SECRET_PARAM.length, endOfSecret);
} else {
secret = url.substring(url.lastIndexOf(SECRET_PARAM) + SECRET_PARAM.length, url.length); secret = url.substring(url.lastIndexOf(SECRET_PARAM) + SECRET_PARAM.length, url.length);
} }
}
return secret; return secret;
} }
private getRecorderFromUrl(url: string): boolean {
let recorder = '';
if (url.indexOf(RECORDER_PARAM) !== -1) {
recorder = url.substring(url.lastIndexOf(RECORDER_PARAM) + RECORDER_PARAM.length, url.length);
}
return new Boolean(recorder).valueOf();;
}
private getUrlWithoutSecret(url: string): string { private getUrlWithoutSecret(url: string): string {
if (!url) { if (!url) {
console.error('sessionId is not defined'); console.error('sessionId is not defined');
@ -102,6 +123,7 @@ export class SessionInternal {
session: this.sessionId, session: this.sessionId,
metadata: this.options.metadata, metadata: this.options.metadata,
secret: this.openVidu.getSecret(), secret: this.openVidu.getSecret(),
recorder: this.openVidu.getRecorder(),
dataChannels: false dataChannels: false
} }

View File

@ -36,6 +36,7 @@ public class ProtocolElements {
public static final String JOINROOM_ROOM_PARAM = "session"; public static final String JOINROOM_ROOM_PARAM = "session";
public static final String JOINROOM_METADATA_PARAM = "metadata"; public static final String JOINROOM_METADATA_PARAM = "metadata";
public static final String JOINROOM_SECRET_PARAM = "secret"; public static final String JOINROOM_SECRET_PARAM = "secret";
public static final String JOINROOM_RECORDER_PARAM = "recorder";
public static final String JOINROOM_DATACHANNELS_PARAM = "dataChannels"; public static final String JOINROOM_DATACHANNELS_PARAM = "dataChannels";
public static final String JOINROOM_PEERID_PARAM = "id"; public static final String JOINROOM_PEERID_PARAM = "id";
@ -111,4 +112,6 @@ public class ProtocolElements {
public static final String ICECANDIDATE_SDPMLINEINDEX_PARAM = "sdpMLineIndex"; public static final String ICECANDIDATE_SDPMLINEINDEX_PARAM = "sdpMLineIndex";
public static final String CUSTOM_NOTIFICATION = "custonNotification"; public static final String CUSTOM_NOTIFICATION = "custonNotification";
public static final String RECORDER_PARTICIPANT_ID_PUBLICID = "RECORDER";
} }

View File

@ -8,6 +8,9 @@ RUN wget -q -O - https://dl-ssl.google.com/linux/linux_signing_key.pub | apt-key
apt-get update && apt-get install -y google-chrome-stable apt-get update && apt-get install -y google-chrome-stable
# Install media packages # Install media packages
RUN apt-get install -y software-properties-common
RUN add-apt-repository ppa:mc3man/xerus-media
RUN apt-get update
RUN apt-get install -y ffmpeg pulseaudio xvfb RUN apt-get install -y ffmpeg pulseaudio xvfb
# Clean # Clean

View File

@ -1,28 +1,47 @@
#!/bin/bash #!/bin/bash
adduser --uid $USER_ID --disabled-password --gecos "" myuser
URL="${URL:-https://www.youtube.com/watch?v=JMuzlEQz3uo}" URL="${URL:-https://www.youtube.com/watch?v=JMuzlEQz3uo}"
RESOLUTION="${RESOLUTION:-1920x1080}" RESOLUTION="${RESOLUTION:-1920x1080}"
FRAMERATE="${FRAMERATE:-30}" FRAMERATE="${FRAMERATE:-30}"
VIDEO_SIZE="$RESOLUTION" VIDEO_SIZE="$RESOLUTION"
ARRAY=(${VIDEO_SIZE//x/ }) ARRAY=(${VIDEO_SIZE//x/ })
VIDEO_NAME="${VIDEO_NAME:-video}" VIDEO_NAME="${VIDEO_NAME:-video}"
VIDEO_FORMAT="${VIDEO_FORMAT:-avi}" VIDEO_FORMAT="${VIDEO_FORMAT:-mp4}"
pulseaudio -D echo "----------------------------------------"
xvfb-run -s "-ac -screen 0 ${RESOLUTION}x16" google-chrome -no-sandbox -disable-infobars -window-size=${ARRAY[0]},${ARRAY[1]} -start-fullscreen -no-first-run $URL &> xvfb.log & echo "Recording URL -> $URL"
echo "----------------------------------------"
sleep 4 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
done
echo "First available display -> :$DISPLAY_NUM"
echo "----------------------------------------"
su myuser -c "pulseaudio -D"
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
touch stop touch stop
<./stop ffmpeg -y -video_size $RESOLUTION -framerate $FRAMERATE -f x11grab -i :99 -f pulse -ac 2 -i default -strict -2 /recordings/${VIDEO_NAME}.${VIDEO_FORMAT} 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}"
# TO START THE CONTAINER
# docker run --name=recording openvidu/openvidu-recording
# TO STOP THE RECORDING
# docker exec recording bash -c "echo 'q' > stop"
# TO GET THE VIDEO FILE
# docker cp recording:recordings/video.mp4 ./video.mp4

View File

@ -1,5 +1,6 @@
<?xml version='1.0' encoding='utf-8'?> <?xml version='1.0' encoding='utf-8'?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion> <modelVersion>4.0.0</modelVersion>
<parent> <parent>
@ -201,6 +202,16 @@
<artifactId>spring-boot-starter-logging</artifactId> <artifactId>spring-boot-starter-logging</artifactId>
<version>${version.spring-boot}</version> <version>${version.spring-boot}</version>
</dependency> </dependency>
<dependency>
<groupId>com.github.docker-java</groupId>
<artifactId>docker-java</artifactId>
<version>3.0.6</version>
</dependency>
<dependency>
<groupId>io.openvidu</groupId>
<artifactId>openvidu-java-client</artifactId>
<version>1.7.0</version>
</dependency>
<!-- Test dependencies --> <!-- Test dependencies -->

View File

@ -19,7 +19,7 @@
"zone.js": "0.8.18" "zone.js": "0.8.18"
}, },
"devDependencies": { "devDependencies": {
"@angular/cli": "1.5.5", "@angular/cli": "^1.6.0",
"@angular/compiler-cli": "5.0.5", "@angular/compiler-cli": "5.0.5",
"@types/jasmine": "2.5.38", "@types/jasmine": "2.5.38",
"@types/node": "~6.0.60", "@types/node": "~6.0.60",

View File

@ -8,44 +8,6 @@ import { InfoService } from 'app/services/info.service';
templateUrl: './app.component.html', templateUrl: './app.component.html',
styleUrls: ['./app.component.css'] styleUrls: ['./app.component.css']
}) })
export class AppComponent implements OnInit, OnDestroy { export class AppComponent {
websocket: WebSocket;
constructor(private infoService: InfoService) { }
ngOnInit() {
const protocol = location.protocol.includes('https') ? 'wss://' : 'ws://';
const port = (location.port) ? (':' + location.port) : '';
this.websocket = new WebSocket(protocol + location.hostname + port + '/info');
this.websocket.onopen = (event) => {
console.log('Info websocket connected');
};
this.websocket.onclose = (event) => {
console.log('Info websocket closed');
};
this.websocket.onerror = (event) => {
console.log('Info websocket error');
};
this.websocket.onmessage = (event) => {
console.log('Info websocket message');
console.log(event.data);
this.infoService.updateInfo(event.data);
};
}
ngOnDestroy() {
this.websocket.close();
}
@HostListener('window:beforeunload', ['$event'])
beforeUnloadHander(event) {
console.warn('Closing info websocket');
this.websocket.close();
}
} }

View File

@ -15,6 +15,7 @@ import { AppComponent } from './app.component';
import { DashboardComponent } from './components/dashboard/dashboard.component'; import { DashboardComponent } from './components/dashboard/dashboard.component';
import { SessionDetailsComponent } from './components/session-details/session-details.component'; import { SessionDetailsComponent } from './components/session-details/session-details.component';
import { CredentialsDialogComponent } from './components/dashboard/credentials-dialog.component'; import { CredentialsDialogComponent } from './components/dashboard/credentials-dialog.component';
import { LayoutBestFitComponent } from './components/layouts/layout-best-fit/layout-best-fit.component';
@NgModule({ @NgModule({
@ -23,6 +24,7 @@ import { CredentialsDialogComponent } from './components/dashboard/credentials-d
DashboardComponent, DashboardComponent,
SessionDetailsComponent, SessionDetailsComponent,
CredentialsDialogComponent, CredentialsDialogComponent,
LayoutBestFitComponent,
], ],
imports: [ imports: [
BrowserModule, BrowserModule,

View File

@ -3,16 +3,25 @@ import { Routes, RouterModule } from '@angular/router';
import { DashboardComponent } from 'app/components/dashboard/dashboard.component'; import { DashboardComponent } from 'app/components/dashboard/dashboard.component';
import { SessionDetailsComponent } from 'app/components/session-details/session-details.component'; import { SessionDetailsComponent } from 'app/components/session-details/session-details.component';
import { LayoutBestFitComponent } from 'app/components/layouts/layout-best-fit/layout-best-fit.component';
const appRoutes: Routes = [ const appRoutes: Routes = [
{ {
path: '', path: '',
component: DashboardComponent component: DashboardComponent,
pathMatch: 'full'
}, },
{ {
path: 'session/:id', path: 'session/:sessionId',
component: SessionDetailsComponent component: SessionDetailsComponent,
pathMatch: 'full'
},
{
path: 'layout-best-fit/:sessionId/:secret',
component: LayoutBestFitComponent,
pathMatch: 'full'
} }
]; ];
export const routing: ModuleWithProviders = RouterModule.forRoot(appRoutes); export const routing: ModuleWithProviders = RouterModule.forRoot(appRoutes, { useHash: true });

View File

@ -16,6 +16,8 @@ declare const $;
}) })
export class DashboardComponent implements OnInit, OnDestroy { export class DashboardComponent implements OnInit, OnDestroy {
websocket: WebSocket;
@ViewChild('scrollMe') private myScrollContainer: ElementRef; @ViewChild('scrollMe') private myScrollContainer: ElementRef;
lockScroll = false; lockScroll = false;
@ -41,21 +43,43 @@ export class DashboardComponent implements OnInit, OnDestroy {
ngOnInit() { ngOnInit() {
const protocol = location.protocol.includes('https') ? 'wss://' : 'ws://';
const port = (location.port) ? (':' + location.port) : '';
this.websocket = new WebSocket(protocol + location.hostname + port + '/info');
this.websocket.onopen = (event) => {
console.log('Info websocket connected');
};
this.websocket.onclose = (event) => {
console.log('Info websocket closed');
};
this.websocket.onerror = (event) => {
console.log('Info websocket error');
};
this.websocket.onmessage = (event) => {
console.log('Info websocket message');
console.log(event.data);
this.infoService.updateInfo(event.data);
};
} }
@HostListener('window:beforeunload') @HostListener('window:beforeunload')
beforeunloadHandler() { beforeunloadHandler() {
// On window closed leave test session // On window closed leave test session and close info websocket
if (this.session) { if (this.session) {
this.endTestVideo(); this.endTestVideo();
} }
this.websocket.close();
} }
ngOnDestroy() { ngOnDestroy() {
// On component destroyed leave test session // On component destroyed leave test session and close info websocket
if (this.session) { if (this.session) {
this.endTestVideo(); this.endTestVideo();
} }
this.websocket.close();
} }
toggleTestVideo() { toggleTestVideo() {

View File

@ -0,0 +1,10 @@
.bounds {
background-color: black;
height: 100%;
overflow: hidden;
cursor: none !important;
}
video {
height: 100%;
}

View File

@ -0,0 +1,8 @@
<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>
</div>
</div>

View File

@ -0,0 +1,25 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { LayoutBestFitComponent } from './layout-best-fit.component';
describe('SessionDetailsComponent', () => {
let component: LayoutBestFitComponent;
let fixture: ComponentFixture<LayoutBestFitComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ LayoutBestFitComponent ]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(LayoutBestFitComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,133 @@
import { Component, OnInit, OnDestroy, HostListener } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { OpenVidu, Session, Stream } from 'openvidu-browser';
@Component({
selector: 'app-layout-best-fit',
templateUrl: './layout-best-fit.component.html',
styleUrls: ['./layout-best-fit.component.css']
})
export class LayoutBestFitComponent implements OnInit, OnDestroy {
sessionId: string;
secret: string;
session: Session;
numberOfVideos = 0;
remoteStreams = [];
constructor(private route: ActivatedRoute) {
this.route.params.subscribe(params => {
this.sessionId = params.sessionId;
this.secret = params.secret;
});
}
@HostListener('window:beforeunload')
beforeunloadHandler() {
this.leaveSession();
}
ngOnDestroy() {
this.leaveSession();
}
ngOnInit() {
const OV = new OpenVidu();
const fullSessionId = 'wss://' + location.hostname + ':8443/' + this.sessionId + '?secret=' + this.secret + '&recorder=true';
this.session = OV.initSession(fullSessionId);
this.session.on('streamCreated', (event) => {
this.numberOfVideos++;
this.addRemoteStream(event.stream);
this.session.subscribe(event.stream, '');
});
this.session.on('streamDestroyed', (event) => {
this.numberOfVideos--;
event.preventDefault();
this.deleteRemoteStream(event.stream);
});
this.session.connect(null, (error) => {
if (error) {
console.error(error);
}
});
}
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;
}
}
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;
}
}
}
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 });
}
leaveSession() {
if (this.session) { this.session.disconnect(); };
this.remoteStreams = [];
this.numberOfVideos = 0;
this.session = null;
}
}

View File

@ -46,6 +46,7 @@ import io.openvidu.server.kurento.KurentoClientProvider;
import io.openvidu.server.kurento.core.KurentoSessionEventsHandler; import io.openvidu.server.kurento.core.KurentoSessionEventsHandler;
import io.openvidu.server.kurento.core.KurentoSessionManager; import io.openvidu.server.kurento.core.KurentoSessionManager;
import io.openvidu.server.kurento.kms.FixedOneKmsManager; import io.openvidu.server.kurento.kms.FixedOneKmsManager;
import io.openvidu.server.recording.RecordingService;
import io.openvidu.server.rest.NgrokRestController; import io.openvidu.server.rest.NgrokRestController;
import io.openvidu.server.rpc.RpcHandler; import io.openvidu.server.rpc.RpcHandler;
import io.openvidu.server.rpc.RpcNotificationService; import io.openvidu.server.rpc.RpcNotificationService;
@ -193,6 +194,35 @@ public class OpenViduServer implements JsonRpcConfigurer {
OpenViduServer.publicUrl = "wss://localhost:" + openviduConf.getServerPort(); OpenViduServer.publicUrl = "wss://localhost:" + openviduConf.getServerPort();
} }
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)");
if (recordingService.recordingImageExistsLocally()) {
System.out.println("Docker image already exists locally");
} else {
Thread t = new Thread(() -> {
boolean keep = true;
System.out.print("Downloading ");
while (keep) {
System.out.print(".");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
keep = false;
System.out.println("\nDownload complete");
}
}
});
t.start();
recordingService.downloadRecordingImage();
t.interrupt();
t.join();
System.out.println("Docker image available");
}
}
System.out.println("OpenVidu Server using " + type + " URL: " + OpenViduServer.publicUrl); System.out.println("OpenVidu Server using " + type + " URL: " + OpenViduServer.publicUrl);
} }

View File

@ -18,6 +18,15 @@ public class OpenviduConfig {
@Value("${openvidu.cdr}") @Value("${openvidu.cdr}")
private boolean openviduCdr; private boolean openviduCdr;
@Value("${openvidu.recording}")
private boolean openviduRecording;
@Value("${openvidu.recording.path}")
private String openviduRecordingPath;
@Value("${openvidu.recording.free-access}")
boolean openviduRecordingFreeAccess;
public String getOpenViduPublicUrl() { public String getOpenViduPublicUrl() {
return this.openviduPublicUrl; return this.openviduPublicUrl;
} }
@ -38,4 +47,16 @@ public class OpenviduConfig {
return this.openviduCdr; return this.openviduCdr;
} }
public boolean isRecordingModuleEnabled() {
return this.openviduRecording;
}
public String getOpenViduRecordingPath() {
return this.openviduRecordingPath;
}
public boolean getOpenViduRecordingFreeAccess() {
return this.openviduRecordingFreeAccess;
}
} }

View File

@ -6,6 +6,7 @@ import org.springframework.http.HttpMethod;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.annotation.web.configurers.ExpressionUrlAuthorizationConfigurer;
import org.springframework.security.config.http.SessionCreationPolicy; import org.springframework.security.config.http.SessionCreationPolicy;
@Configuration @Configuration
@ -16,12 +17,19 @@ public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override @Override
protected void configure(HttpSecurity http) throws Exception { protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable() ExpressionUrlAuthorizationConfigurer<HttpSecurity>.ExpressionInterceptUrlRegistry conf = http.csrf().disable()
.authorizeRequests() .authorizeRequests()
.antMatchers(HttpMethod.POST, "/api/sessions").authenticated() .antMatchers(HttpMethod.POST, "/api/sessions").authenticated()
.antMatchers(HttpMethod.POST, "/api/tokens").authenticated() .antMatchers(HttpMethod.POST, "/api/tokens").authenticated()
.antMatchers("/").authenticated() .antMatchers("/").authenticated();
.and().sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
if (openviduConf.getOpenViduRecordingFreeAccess()) {
conf = conf.antMatchers("/recordings/*").anonymous();
} else {
conf = conf.antMatchers("/recordings/*").authenticated();
}
conf.and().sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and().httpBasic(); .and().httpBasic();
} }

View File

@ -2,10 +2,14 @@ package io.openvidu.server.core;
import java.util.Set; import java.util.Set;
import io.openvidu.java.client.SessionProperties;
public interface Session { public interface Session {
String getSessionId(); String getSessionId();
SessionProperties getSessionProperties();
void join(Participant participant); void join(Participant participant);
void leave(String participantPrivateId); void leave(String participantPrivateId);

View File

@ -18,6 +18,8 @@ import com.google.gson.JsonObject;
import io.openvidu.client.OpenViduException; import io.openvidu.client.OpenViduException;
import io.openvidu.client.OpenViduException.Code; import io.openvidu.client.OpenViduException.Code;
import io.openvidu.client.internal.ProtocolElements;
import io.openvidu.java.client.SessionProperties;
import io.openvidu.server.OpenViduServer; import io.openvidu.server.OpenViduServer;
public abstract class SessionManager { public abstract class SessionManager {
@ -25,6 +27,7 @@ public abstract class SessionManager {
private static final Logger log = LoggerFactory.getLogger(SessionManager.class); private static final Logger log = LoggerFactory.getLogger(SessionManager.class);
protected ConcurrentMap<String, Session> sessions = new ConcurrentHashMap<>(); protected ConcurrentMap<String, Session> sessions = new ConcurrentHashMap<>();
protected ConcurrentMap<String, SessionProperties> sessionProperties = new ConcurrentHashMap<>();
protected ConcurrentMap<String, ConcurrentHashMap<String, Token>> sessionidTokenTokenobj = new ConcurrentHashMap<>(); protected ConcurrentMap<String, ConcurrentHashMap<String, Token>> sessionidTokenTokenobj = new ConcurrentHashMap<>();
protected ConcurrentMap<String, ConcurrentHashMap<String, Participant>> sessionidParticipantpublicidParticipant = new ConcurrentHashMap<>(); protected ConcurrentMap<String, ConcurrentHashMap<String, Participant>> sessionidParticipantpublicidParticipant = new ConcurrentHashMap<>();
protected ConcurrentMap<String, Boolean> insecureUsers = new ConcurrentHashMap<>(); protected ConcurrentMap<String, Boolean> insecureUsers = new ConcurrentHashMap<>();
@ -136,12 +139,13 @@ public abstract class SessionManager {
return null; return null;
} }
public String newSessionId() { public String newSessionId(SessionProperties sessionProperties) {
String sessionId = OpenViduServer.publicUrl; String sessionId = OpenViduServer.publicUrl;
sessionId += "/" + new BigInteger(130, new SecureRandom()).toString(32); sessionId += "/" + new BigInteger(130, new SecureRandom()).toString(32);
this.sessionidTokenTokenobj.put(sessionId, new ConcurrentHashMap<>()); this.sessionidTokenTokenobj.put(sessionId, new ConcurrentHashMap<>());
this.sessionidParticipantpublicidParticipant.put(sessionId, new ConcurrentHashMap<>()); this.sessionidParticipantpublicidParticipant.put(sessionId, new ConcurrentHashMap<>());
this.sessionProperties.put(sessionId, sessionProperties);
showTokens(); showTokens();
return sessionId; return sessionId;
@ -237,6 +241,18 @@ public abstract class SessionManager {
} }
} }
public Participant newRecorderParticipant(String sessionId, String participantPrivatetId, Token token,
String clientMetadata) {
if (this.sessionidParticipantpublicidParticipant.get(sessionId) != null) {
String participantPublicId = ProtocolElements.RECORDER_PARTICIPANT_ID_PUBLICID;
Participant p = new Participant(participantPrivatetId, participantPublicId, token, clientMetadata);
this.sessionidParticipantpublicidParticipant.get(sessionId).put(participantPublicId, p);
return p;
} else {
throw new OpenViduException(Code.ROOM_NOT_FOUND_ERROR_CODE, sessionId);
}
}
public Token consumeToken(String sessionId, String participantPrivateId, String token) { public Token consumeToken(String sessionId, String participantPrivateId, String token) {
if (this.sessionidTokenTokenobj.get(sessionId) != null) { if (this.sessionidTokenTokenobj.get(sessionId) != null) {
Token t = this.sessionidTokenTokenobj.get(sessionId).remove(token); Token t = this.sessionidTokenTokenobj.get(sessionId).remove(token);
@ -324,6 +340,7 @@ public abstract class SessionManager {
session.close(); session.close();
sessions.remove(sessionId); sessions.remove(sessionId);
sessionProperties.remove(sessionId);
sessionidParticipantpublicidParticipant.remove(sessionId); sessionidParticipantpublicidParticipant.remove(sessionId);
sessionidTokenTokenobj.remove(sessionId); sessionidTokenTokenobj.remove(sessionId);

View File

@ -19,6 +19,7 @@ import org.slf4j.LoggerFactory;
import io.openvidu.client.OpenViduException; import io.openvidu.client.OpenViduException;
import io.openvidu.client.OpenViduException.Code; import io.openvidu.client.OpenViduException.Code;
import io.openvidu.java.client.SessionProperties;
import io.openvidu.server.core.Participant; import io.openvidu.server.core.Participant;
import io.openvidu.server.core.Session; import io.openvidu.server.core.Session;
@ -32,6 +33,7 @@ public class KurentoSession implements Session {
private final ConcurrentMap<String, KurentoParticipant> participants = new ConcurrentHashMap<>(); private final ConcurrentMap<String, KurentoParticipant> participants = new ConcurrentHashMap<>();
private String sessionId; private String sessionId;
private SessionProperties sessionProperties;
private MediaPipeline pipeline; private MediaPipeline pipeline;
private CountDownLatch pipelineLatch = new CountDownLatch(1); private CountDownLatch pipelineLatch = new CountDownLatch(1);
@ -49,9 +51,10 @@ public class KurentoSession implements Session {
private volatile boolean pipelineReleased = false; private volatile boolean pipelineReleased = false;
private boolean destroyKurentoClient; private boolean destroyKurentoClient;
public KurentoSession(String sessionId, KurentoClient kurentoClient, KurentoSessionEventsHandler kurentoSessionHandler, public KurentoSession(String sessionId, SessionProperties sessionProperties, KurentoClient kurentoClient, KurentoSessionEventsHandler kurentoSessionHandler,
boolean destroyKurentoClient) { boolean destroyKurentoClient) {
this.sessionId = sessionId; this.sessionId = sessionId;
this.sessionProperties = sessionProperties;
this.kurentoClient = kurentoClient; this.kurentoClient = kurentoClient;
this.destroyKurentoClient = destroyKurentoClient; this.destroyKurentoClient = destroyKurentoClient;
this.kurentoSessionHandler = kurentoSessionHandler; this.kurentoSessionHandler = kurentoSessionHandler;
@ -63,6 +66,11 @@ public class KurentoSession implements Session {
return this.sessionId; return this.sessionId;
} }
@Override
public SessionProperties getSessionProperties() {
return this.sessionProperties;
}
@Override @Override
public void join(Participant participant) { public void join(Participant participant) {
checkClosed(); checkClosed();

View File

@ -18,12 +18,17 @@ import com.google.gson.JsonSyntaxException;
import io.openvidu.client.OpenViduException; import io.openvidu.client.OpenViduException;
import io.openvidu.client.OpenViduException.Code; import io.openvidu.client.OpenViduException.Code;
import io.openvidu.client.internal.ProtocolElements; import io.openvidu.client.internal.ProtocolElements;
import io.openvidu.java.client.ArchiveMode;
import io.openvidu.java.client.MediaMode;
import io.openvidu.java.client.SessionProperties;
import io.openvidu.server.core.SessionManager; import io.openvidu.server.core.SessionManager;
import io.openvidu.server.kurento.KurentoClientProvider; import io.openvidu.server.kurento.KurentoClientProvider;
import io.openvidu.server.kurento.KurentoClientSessionInfo; import io.openvidu.server.kurento.KurentoClientSessionInfo;
import io.openvidu.server.kurento.OpenViduKurentoClientSessionInfo; import io.openvidu.server.kurento.OpenViduKurentoClientSessionInfo;
import io.openvidu.server.kurento.endpoint.SdpType; import io.openvidu.server.kurento.endpoint.SdpType;
import io.openvidu.server.recording.RecordingService;
import io.openvidu.server.rpc.RpcHandler; import io.openvidu.server.rpc.RpcHandler;
import io.openvidu.server.config.OpenviduConfig;
import io.openvidu.server.core.MediaOptions; import io.openvidu.server.core.MediaOptions;
import io.openvidu.server.core.Participant; import io.openvidu.server.core.Participant;
import io.openvidu.server.core.Session; import io.openvidu.server.core.Session;
@ -38,6 +43,12 @@ public class KurentoSessionManager extends SessionManager {
@Autowired @Autowired
private KurentoSessionEventsHandler sessionHandler; private KurentoSessionEventsHandler sessionHandler;
@Autowired
private RecordingService recordingService;
@Autowired
OpenviduConfig openviduConfig;
@Override @Override
public synchronized void joinRoom(Participant participant, String sessionId, Integer transactionId) { public synchronized void joinRoom(Participant participant, String sessionId, Integer transactionId) {
Set<Participant> existingParticipants = null; Set<Participant> existingParticipants = null;
@ -48,7 +59,12 @@ public class KurentoSessionManager extends SessionManager {
KurentoSession session = (KurentoSession) sessions.get(sessionId); KurentoSession session = (KurentoSession) sessions.get(sessionId);
if (session == null && kcSessionInfo != null) { if (session == null && kcSessionInfo != null) {
createSession(kcSessionInfo); SessionProperties properties = sessionProperties.get(sessionId);
if (properties == null && this.isInsecureParticipant(participant.getParticipantPrivateId())) {
properties = new SessionProperties.Builder().mediaMode(MediaMode.ROUTED)
.archiveMode(ArchiveMode.ALWAYS).build();
}
createSession(kcSessionInfo, properties);
} }
session = (KurentoSession) sessions.get(sessionId); session = (KurentoSession) sessions.get(sessionId);
if (session == null) { if (session == null) {
@ -118,12 +134,13 @@ public class KurentoSessionManager extends SessionManager {
remainingParticipants = Collections.emptySet(); remainingParticipants = Collections.emptySet();
} }
if (remainingParticipants.isEmpty()) { if (remainingParticipants.isEmpty()) {
log.debug("No more participants in session '{}', removing it and closing it", sessionId); log.info("No more participants in session '{}', removing it and closing it", sessionId);
if (session.close()) { if (session.close()) {
sessionHandler.onSessionClosed(sessionId); sessionHandler.onSessionClosed(sessionId);
} }
sessions.remove(sessionId); sessions.remove(sessionId);
sessionProperties.remove(sessionId);
sessionidParticipantpublicidParticipant.remove(sessionId); sessionidParticipantpublicidParticipant.remove(sessionId);
sessionidTokenTokenobj.remove(sessionId); sessionidTokenTokenobj.remove(sessionId);
@ -131,7 +148,16 @@ public class KurentoSessionManager extends SessionManager {
log.warn("Session '{}' removed and closed", sessionId); 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);
}
sessionHandler.onParticipantLeft(participant, sessionId, remainingParticipants, transactionId, null); sessionHandler.onParticipantLeft(participant, sessionId, remainingParticipants, transactionId, null);
} }
@ -192,7 +218,15 @@ public class KurentoSessionManager extends SessionManager {
OpenViduException e = new OpenViduException(Code.MEDIA_SDP_ERROR_CODE, OpenViduException e = new OpenViduException(Code.MEDIA_SDP_ERROR_CODE,
"Error generating SDP response for publishing user " + participant.getParticipantPublicId()); "Error generating SDP response for publishing user " + participant.getParticipantPublicId());
log.error("PARTICIPANT {}: Error publishing media", participant.getParticipantPublicId(), e); log.error("PARTICIPANT {}: Error publishing media", participant.getParticipantPublicId(), e);
sessionHandler.onPublishMedia(participant, session.getSessionId(), mediaOptions, sdpAnswer, participants, transactionId, e); sessionHandler.onPublishMedia(participant, session.getSessionId(), mediaOptions, sdpAnswer, participants,
transactionId, e);
}
if (this.openviduConfig.isRecordingModuleEnabled()
&& MediaMode.ROUTED.equals(session.getSessionProperties().mediaMode())
&& ArchiveMode.ALWAYS.equals(session.getSessionProperties().archiveMode())
&& session.getActivePublishers() == 0) {
recordingService.startRecording(session);
} }
session.newPublisher(participant); session.newPublisher(participant);
@ -204,7 +238,8 @@ public class KurentoSessionManager extends SessionManager {
participants = kurentoParticipant.getSession().getParticipants(); participants = kurentoParticipant.getSession().getParticipants();
if (sdpAnswer != null) { if (sdpAnswer != null) {
sessionHandler.onPublishMedia(participant, session.getSessionId(), mediaOptions, sdpAnswer, participants, transactionId, null); sessionHandler.onPublishMedia(participant, session.getSessionId(), mediaOptions, sdpAnswer, participants,
transactionId, null);
} }
} }
@ -327,7 +362,6 @@ public class KurentoSessionManager extends SessionManager {
} }
} }
/** /**
* Creates a session if it doesn't already exist. The session's id will be * Creates a session if it doesn't already exist. The session's id will be
* indicated by the session info bean. * indicated by the session info bean.
@ -339,7 +373,8 @@ public class KurentoSessionManager extends SessionManager {
* @throws OpenViduException * @throws OpenViduException
* in case of error while creating the session * in case of error while creating the session
*/ */
public void createSession(KurentoClientSessionInfo kcSessionInfo) throws OpenViduException { public void createSession(KurentoClientSessionInfo kcSessionInfo, SessionProperties sessionProperties)
throws OpenViduException {
String sessionId = kcSessionInfo.getRoomName(); String sessionId = kcSessionInfo.getRoomName();
KurentoSession session = (KurentoSession) sessions.get(sessionId); KurentoSession session = (KurentoSession) sessions.get(sessionId);
if (session != null) { if (session != null) {
@ -347,7 +382,8 @@ public class KurentoSessionManager extends SessionManager {
"Session '" + sessionId + "' already exists"); "Session '" + sessionId + "' already exists");
} }
KurentoClient kurentoClient = kcProvider.getKurentoClient(kcSessionInfo); KurentoClient kurentoClient = kcProvider.getKurentoClient(kcSessionInfo);
session = new KurentoSession(sessionId, kurentoClient, sessionHandler, kcProvider.destroyWhenUnused()); session = new KurentoSession(sessionId, sessionProperties, kurentoClient, sessionHandler,
kcProvider.destroyWhenUnused());
KurentoSession oldSession = (KurentoSession) sessions.putIfAbsent(sessionId, session); KurentoSession oldSession = (KurentoSession) sessions.putIfAbsent(sessionId, session);
if (oldSession != null) { if (oldSession != null) {

View File

@ -0,0 +1,167 @@
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 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;
}
return imageExists;
}
public void downloadRecordingImage() {
try {
dockerClient.pullImageCmd(IMAGE_NAME).exec(new PullImageResultCallback()).awaitSuccess();
} catch (NotFoundException | InternalServerErrorException e) {
if (imageExistsLocally(IMAGE_NAME)) {
log.info("Docker image '{}' exists locally", IMAGE_NAME);
} else {
throw e;
}
} catch (DockerClientException e) {
log.info("Error on Pulling '{}' image. Probably because the user has stopped the execution",
IMAGE_NAME);
throw e;
}
}
private String runRecordingContainer(List<String> envs, String containerName) {
Volume volume1 = new Volume("/recordings");
CreateContainerCmd cmd = dockerClient.createContainerCmd(IMAGE_NAME).withName(containerName).withEnv(envs)
.withNetworkMode("host").withVolumes(volume1)
.withBinds(new Bind(openviduConfig.getOpenViduRecordingPath(), volume1));
CreateContainerResponse container = null;
try {
container = cmd.exec();
dockerClient.startContainerCmd(container.getId()).exec();
createdContainers.put(container.getId(), containerName);
log.info("Container ID: {}", container.getId());
return container.getId();
} catch (ConflictException e) {
log.error(
"The container name {} is already in use. Probably caused by a session with unique publisher re-publishing a stream",
containerName);
return null;
}
}
private void removeDockerContainer(String containerId) {
dockerClient.removeContainerCmd(containerId).exec();
createdContainers.remove(containerId);
}
private void stopDockerContainer(String containerId) {
dockerClient.stopContainerCmd(containerId).exec();
}
private boolean imageExistsLocally(String imageName) {
boolean imageExists = false;
try {
dockerClient.inspectImageCmd(imageName).exec();
imageExists = true;
} catch (NotFoundException nfe) {
imageExists = false;
}
return imageExists;
}
}

View File

@ -0,0 +1,27 @@
package io.openvidu.server.recording;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;
import io.openvidu.server.config.OpenviduConfig;
@Configuration
@ConditionalOnExpression("'${openvidu.recording}' == 'true'")
public class RecordingsHttpHandler extends WebMvcConfigurerAdapter {
@Autowired
OpenviduConfig openviduConfig;
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
String recordingsPath = openviduConfig.getOpenViduRecordingPath();
recordingsPath = recordingsPath.endsWith("/") ? recordingsPath : recordingsPath + "/";
registry.addResourceHandler("/recordings/**").addResourceLocations("file:" + recordingsPath);
}
}

View File

@ -32,6 +32,10 @@ import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestController;
import io.openvidu.client.OpenViduException; import io.openvidu.client.OpenViduException;
import io.openvidu.java.client.ArchiveLayout;
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.ParticipantRole;
import io.openvidu.server.core.SessionManager; import io.openvidu.server.core.SessionManager;
@ -67,8 +71,37 @@ public class SessionRestController {
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
@RequestMapping(value = "/sessions", method = RequestMethod.POST) @RequestMapping(value = "/sessions", method = RequestMethod.POST)
public ResponseEntity<JSONObject> getSessionId() { public ResponseEntity<JSONObject> getSessionId(@RequestBody(required = false) Map<?, ?> params) {
String sessionId = sessionManager.newSessionId();
SessionProperties.Builder builder = new SessionProperties.Builder();
if (params != null) {
String archiveModeString = (String) params.get("archiveMode");
String archiveLayoutString = (String) params.get("archiveLayout");
String mediaModeString = (String) params.get("mediaMode");
try {
if (archiveModeString != null) {
ArchiveMode archiveMode = ArchiveMode.valueOf(archiveModeString);
builder = builder.archiveMode(archiveMode);
}
if (archiveLayoutString != null) {
ArchiveLayout archiveLayout = ArchiveLayout.valueOf(archiveLayoutString);
builder = builder.archiveLayout(archiveLayout);
}
if (mediaModeString != null) {
MediaMode mediaMode = MediaMode.valueOf(mediaModeString);
builder = builder.mediaMode(mediaMode);
}
} catch (IllegalArgumentException e) {
return this.generateErrorResponse("ArchiveMode " + params.get("archiveMode") + " | " + "ArchiveLayout "
+ params.get("archiveLayout") + " | " + "MediaMode " + params.get("mediaMode")
+ " are not defined", "/api/tokens");
}
}
SessionProperties sessionProperties = builder.build();
String sessionId = sessionManager.newSessionId(sessionProperties);
JSONObject responseJson = new JSONObject(); JSONObject responseJson = new JSONObject();
responseJson.put("id", sessionId); responseJson.put("id", sessionId);
return new ResponseEntity<JSONObject>(responseJson, HttpStatus.OK); return new ResponseEntity<JSONObject>(responseJson, HttpStatus.OK);
@ -76,11 +109,13 @@ public class SessionRestController {
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
@RequestMapping(value = "/tokens", method = RequestMethod.POST) @RequestMapping(value = "/tokens", method = RequestMethod.POST)
public ResponseEntity<JSONObject> newToken(@RequestBody Map<?, ?> sessionIdRoleMetadata) { public ResponseEntity<JSONObject> newToken(@RequestBody Map<?, ?> params) {
try { try {
String sessionId = (String) sessionIdRoleMetadata.get("session");
ParticipantRole role = ParticipantRole.valueOf((String) sessionIdRoleMetadata.get("role")); String sessionId = (String) params.get("session");
String metadata = (String) sessionIdRoleMetadata.get("data"); ParticipantRole role = ParticipantRole.valueOf((String) params.get("role"));
String metadata = (String) params.get("data");
String token = sessionManager.newToken(sessionId, role, metadata); String token = sessionManager.newToken(sessionId, role, metadata);
JSONObject responseJson = new JSONObject(); JSONObject responseJson = new JSONObject();
responseJson.put("id", token); responseJson.put("id", token);
@ -89,12 +124,13 @@ public class SessionRestController {
responseJson.put("data", metadata); responseJson.put("data", metadata);
responseJson.put("token", token); responseJson.put("token", token);
return new ResponseEntity<JSONObject>(responseJson, HttpStatus.OK); return new ResponseEntity<JSONObject>(responseJson, HttpStatus.OK);
} catch (IllegalArgumentException e) { } catch (IllegalArgumentException e) {
return this.generateErrorResponse("Role " + sessionIdRoleMetadata.get("1") + " is not defined", return this.generateErrorResponse("Role " + params.get("role") + " is not defined", "/api/tokens");
"/api/tokens");
} catch (OpenViduException e) { } catch (OpenViduException e) {
return this.generateErrorResponse("Metadata [" + sessionIdRoleMetadata.get("2") return this.generateErrorResponse(
+ "] unexpected format. Max length allowed is 1000 chars", "/api/tokens"); "Metadata [" + params.get("data") + "] unexpected format. Max length allowed is 1000 chars",
"/api/tokens");
} }
} }

View File

@ -94,8 +94,21 @@ public class RpcHandler extends DefaultJsonRpcHandler<JsonObject> {
String secret = getStringParam(request, ProtocolElements.JOINROOM_SECRET_PARAM); String secret = getStringParam(request, ProtocolElements.JOINROOM_SECRET_PARAM);
String participantPrivatetId = rpcConnection.getParticipantPrivateId(); String participantPrivatetId = rpcConnection.getParticipantPrivateId();
boolean recorder = false;
try {
recorder = getBooleanParam(request, ProtocolElements.JOINROOM_RECORDER_PARAM);
} catch (RuntimeException e) {
// Nothing happens. 'recorder' param to false
}
boolean generateRecorderParticipant = false;
if (openviduConfig.isOpenViduSecret(secret)) { if (openviduConfig.isOpenViduSecret(secret)) {
sessionManager.newInsecureParticipant(participantPrivatetId); sessionManager.newInsecureParticipant(participantPrivatetId);
if (recorder) {
generateRecorderParticipant = true;
}
} }
if (sessionManager.isTokenValidInSession(token, sessionId, participantPrivatetId)) { if (sessionManager.isTokenValidInSession(token, sessionId, participantPrivatetId)) {
@ -105,8 +118,15 @@ public class RpcHandler extends DefaultJsonRpcHandler<JsonObject> {
if (sessionManager.isMetadataFormatCorrect(clientMetadata)) { if (sessionManager.isMetadataFormatCorrect(clientMetadata)) {
Token tokenObj = sessionManager.consumeToken(sessionId, participantPrivatetId, token); Token tokenObj = sessionManager.consumeToken(sessionId, participantPrivatetId, token);
Participant participant = sessionManager.newParticipant(sessionId, participantPrivatetId, tokenObj, Participant participant;
if (generateRecorderParticipant) {
participant = sessionManager.newRecorderParticipant(sessionId, participantPrivatetId, tokenObj,
clientMetadata); clientMetadata);
} else {
participant = sessionManager.newParticipant(sessionId, participantPrivatetId, tokenObj,
clientMetadata);
}
rpcConnection.setSessionId(sessionId); rpcConnection.setSessionId(sessionId);
sessionManager.joinRoom(participant, sessionId, request.getId()); sessionManager.joinRoom(participant, sessionId, request.getId());

View File

@ -0,0 +1,37 @@
{"properties": [
{
"name": "kms.uris",
"type": "java.lang.String",
"description": "KMS URL's to which OpenVidu Server will try to connect. They are tested in order until a valid one is found"
},
{
"name": "openvidu.secret",
"type": "java.lang.String",
"description": "Secret used to connect to OpenVidu Server. This value is required when using the REST API or any server client, as well as when connecting to openvidu-server dashboard"
},
{
"name": "openvidu.publicurl",
"type": "java.lang.String",
"description": "URL to connect clients to OpenVidu Server. This must be the full IP of your OpenVidu Server, including protocol, host and port (for example: https://my.openvidu.server.ip:8443). If no port argument is provided, 'server.port' param will be appended to it"
},
{
"name": "openvidu.cdr",
"type": "java.lang.String",
"description": "Whether to enable Call Detail Record or not"
},
{
"name": "openvidu.recording",
"type": "java.lang.String",
"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"
},
{
"name": "openvidu.recording.path",
"type": "java.lang.String",
"description": "Where to store the recorded video files"
},
{
"name": "openvidu.recording.free-access",
"type": "java.lang.String",
"description": "'true' to allow free access to the video files specified in 'openviu.recording.path'. 'false' to only allow access to authenticated users"
}
]}

View File

@ -7,7 +7,9 @@ server.ssl.key-store-type: JKS
server.ssl.key-alias: openvidu-selfsigned server.ssl.key-alias: openvidu-selfsigned
kms.uris=[\"ws://localhost:8888/kurento\"] kms.uris=[\"ws://localhost:8888/kurento\"]
openvidu.secret: MY_SECRET openvidu.secret: MY_SECRET
openvidu.publicurl: local openvidu.publicurl: local
openvidu.cdr: false openvidu.cdr: false
openvidu.recording: false
openvidu.recording.path: /opt/openvidu/recordings
openvidu.recording.free-access: false

File diff suppressed because one or more lines are too long

View File

@ -2764,6 +2764,7 @@ var RpcBuilder = __webpack_require__("../../../../../../../../../openvidu-browse
var OpenViduInternal = /** @class */ (function () { var OpenViduInternal = /** @class */ (function () {
function OpenViduInternal() { function OpenViduInternal() {
this.remoteStreams = []; this.remoteStreams = [];
this.recorder = false;
} }
/* NEW METHODS */ /* NEW METHODS */
OpenViduInternal.prototype.initSession = function (sessionId) { OpenViduInternal.prototype.initSession = function (sessionId) {
@ -2891,6 +2892,12 @@ var OpenViduInternal = /** @class */ (function () {
OpenViduInternal.prototype.setSecret = function (secret) { OpenViduInternal.prototype.setSecret = function (secret) {
this.secret = secret; this.secret = secret;
}; };
OpenViduInternal.prototype.getRecorder = function () {
return this.recorder;
};
OpenViduInternal.prototype.setRecorder = function (recorder) {
this.recorder = recorder;
};
OpenViduInternal.prototype.getOpenViduServerURL = function () { OpenViduInternal.prototype.getOpenViduServerURL = function () {
return 'https://' + this.wsUri.split("wss://")[1].split("/room")[0]; return 'https://' + this.wsUri.split("wss://")[1].split("/room")[0];
}; };
@ -3102,6 +3109,7 @@ exports.__esModule = true;
var Connection_1 = __webpack_require__("../../../../../../../../../openvidu-browser/lib/OpenViduInternal/Connection.js"); var Connection_1 = __webpack_require__("../../../../../../../../../openvidu-browser/lib/OpenViduInternal/Connection.js");
var EventEmitter = __webpack_require__("../../../../../../../../../openvidu-browser/node_modules/wolfy87-eventemitter/EventEmitter.js"); var EventEmitter = __webpack_require__("../../../../../../../../../openvidu-browser/node_modules/wolfy87-eventemitter/EventEmitter.js");
var SECRET_PARAM = '?secret='; var SECRET_PARAM = '?secret=';
var RECORDER_PARAM = '&recorder=';
var SessionInternal = /** @class */ (function () { var SessionInternal = /** @class */ (function () {
function SessionInternal(openVidu, sessionId) { function SessionInternal(openVidu, sessionId) {
this.openVidu = openVidu; this.openVidu = openVidu;
@ -3117,16 +3125,37 @@ var SessionInternal = /** @class */ (function () {
} }
} }
SessionInternal.prototype.processOpenViduUrl = function (url) { SessionInternal.prototype.processOpenViduUrl = function (url) {
this.openVidu.setSecret(this.getSecretFromUrl(url)); var secret = this.getSecretFromUrl(url);
var recorder = this.getRecorderFromUrl(url);
if (!(secret == null)) {
this.openVidu.setSecret(secret);
}
if (!(recorder == null)) {
this.openVidu.setRecorder(recorder);
}
this.openVidu.setWsUri(this.getFinalUrl(url)); this.openVidu.setWsUri(this.getFinalUrl(url));
}; };
SessionInternal.prototype.getSecretFromUrl = function (url) { SessionInternal.prototype.getSecretFromUrl = function (url) {
var secret = ''; var secret = '';
if (url.indexOf(SECRET_PARAM) !== -1) { if (url.indexOf(SECRET_PARAM) !== -1) {
var endOfSecret = url.lastIndexOf(RECORDER_PARAM);
if (endOfSecret !== -1) {
secret = url.substring(url.lastIndexOf(SECRET_PARAM) + SECRET_PARAM.length, endOfSecret);
}
else {
secret = url.substring(url.lastIndexOf(SECRET_PARAM) + SECRET_PARAM.length, url.length); secret = url.substring(url.lastIndexOf(SECRET_PARAM) + SECRET_PARAM.length, url.length);
} }
}
return secret; return secret;
}; };
SessionInternal.prototype.getRecorderFromUrl = function (url) {
var recorder = '';
if (url.indexOf(RECORDER_PARAM) !== -1) {
recorder = url.substring(url.lastIndexOf(RECORDER_PARAM) + RECORDER_PARAM.length, url.length);
}
return new Boolean(recorder).valueOf();
;
};
SessionInternal.prototype.getUrlWithoutSecret = function (url) { SessionInternal.prototype.getUrlWithoutSecret = function (url) {
if (!url) { if (!url) {
console.error('sessionId is not defined'); console.error('sessionId is not defined');
@ -3165,6 +3194,7 @@ var SessionInternal = /** @class */ (function () {
session: _this.sessionId, session: _this.sessionId,
metadata: _this.options.metadata, metadata: _this.options.metadata,
secret: _this.openVidu.getSecret(), secret: _this.openVidu.getSecret(),
recorder: _this.openVidu.getRecorder(),
dataChannels: false dataChannels: false
}; };
if (_this.localParticipant) { if (_this.localParticipant) {
@ -9819,9 +9849,9 @@ var __WEBPACK_AMD_DEFINE_RESULT__;/**
} else { } else {
// requirejs env (optional) // requirejs env (optional)
if ("function" === FUNC_TYPE && __webpack_require__("../../../../webpack/buildin/amd-options.js")) { if ("function" === FUNC_TYPE && __webpack_require__("../../../../webpack/buildin/amd-options.js")) {
!(__WEBPACK_AMD_DEFINE_RESULT__ = function () { !(__WEBPACK_AMD_DEFINE_RESULT__ = (function () {
return UAParser; return UAParser;
}.call(exports, __webpack_require__, exports, module), }).call(exports, __webpack_require__, exports, module),
__WEBPACK_AMD_DEFINE_RESULT__ !== undefined && (module.exports = __WEBPACK_AMD_DEFINE_RESULT__)); __WEBPACK_AMD_DEFINE_RESULT__ !== undefined && (module.exports = __WEBPACK_AMD_DEFINE_RESULT__));
} else if (window) { } else if (window) {
// browser env // browser env
@ -13071,9 +13101,9 @@ var __WEBPACK_AMD_DEFINE_RESULT__;/*!
// Expose the class either via AMD, CommonJS or the global object // Expose the class either via AMD, CommonJS or the global object
if (true) { if (true) {
!(__WEBPACK_AMD_DEFINE_RESULT__ = function () { !(__WEBPACK_AMD_DEFINE_RESULT__ = (function () {
return EventEmitter; return EventEmitter;
}.call(exports, __webpack_require__, exports, module), }).call(exports, __webpack_require__, exports, module),
__WEBPACK_AMD_DEFINE_RESULT__ !== undefined && (module.exports = __WEBPACK_AMD_DEFINE_RESULT__)); __WEBPACK_AMD_DEFINE_RESULT__ !== undefined && (module.exports = __WEBPACK_AMD_DEFINE_RESULT__));
} }
else if (typeof module === 'object' && module.exports){ else if (typeof module === 'object' && module.exports){
@ -13135,62 +13165,22 @@ module.exports = "<main>\n <router-outlet></router-outlet>\n</main>"
"use strict"; "use strict";
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "a", function() { return AppComponent; }); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "a", function() { return AppComponent; });
/* harmony import */ var __WEBPACK_IMPORTED_MODULE_0__angular_core__ = __webpack_require__("../../../core/esm5/core.js"); /* harmony import */ var __WEBPACK_IMPORTED_MODULE_0__angular_core__ = __webpack_require__("../../../core/esm5/core.js");
/* harmony import */ var __WEBPACK_IMPORTED_MODULE_1_app_services_info_service__ = __webpack_require__("../../../../../src/app/services/info.service.ts");
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) { var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d; var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc); if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r; else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
return c > 3 && r && Object.defineProperty(target, key, r), r; return c > 3 && r && Object.defineProperty(target, key, r), r;
}; };
var __metadata = (this && this.__metadata) || function (k, v) {
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
};
var AppComponent = (function () { var AppComponent = (function () {
function AppComponent(infoService) { function AppComponent() {
this.infoService = infoService;
} }
AppComponent.prototype.ngOnInit = function () {
var _this = this;
var protocol = location.protocol.includes('https') ? 'wss://' : 'ws://';
var port = (location.port) ? (':' + location.port) : '';
this.websocket = new WebSocket(protocol + location.hostname + port + '/info');
this.websocket.onopen = function (event) {
console.log('Info websocket connected');
};
this.websocket.onclose = function (event) {
console.log('Info websocket closed');
};
this.websocket.onerror = function (event) {
console.log('Info websocket error');
};
this.websocket.onmessage = function (event) {
console.log('Info websocket message');
console.log(event.data);
_this.infoService.updateInfo(event.data);
};
};
AppComponent.prototype.ngOnDestroy = function () {
this.websocket.close();
};
AppComponent.prototype.beforeUnloadHander = function (event) {
console.warn('Closing info websocket');
this.websocket.close();
};
__decorate([
Object(__WEBPACK_IMPORTED_MODULE_0__angular_core__["A" /* HostListener */])('window:beforeunload', ['$event']),
__metadata("design:type", Function),
__metadata("design:paramtypes", [Object]),
__metadata("design:returntype", void 0)
], AppComponent.prototype, "beforeUnloadHander", null);
AppComponent = __decorate([ AppComponent = __decorate([
Object(__WEBPACK_IMPORTED_MODULE_0__angular_core__["n" /* Component */])({ Object(__WEBPACK_IMPORTED_MODULE_0__angular_core__["n" /* Component */])({
selector: 'app-root', selector: 'app-root',
template: __webpack_require__("../../../../../src/app/app.component.html"), template: __webpack_require__("../../../../../src/app/app.component.html"),
styles: [__webpack_require__("../../../../../src/app/app.component.css")] styles: [__webpack_require__("../../../../../src/app/app.component.css")]
}), })
__metadata("design:paramtypes", [__WEBPACK_IMPORTED_MODULE_1_app_services_info_service__["a" /* InfoService */]])
], AppComponent); ], AppComponent);
return AppComponent; return AppComponent;
}()); }());
@ -13275,6 +13265,7 @@ var AppMaterialModule = (function () {
/* harmony import */ var __WEBPACK_IMPORTED_MODULE_10__components_dashboard_dashboard_component__ = __webpack_require__("../../../../../src/app/components/dashboard/dashboard.component.ts"); /* harmony import */ var __WEBPACK_IMPORTED_MODULE_10__components_dashboard_dashboard_component__ = __webpack_require__("../../../../../src/app/components/dashboard/dashboard.component.ts");
/* harmony import */ var __WEBPACK_IMPORTED_MODULE_11__components_session_details_session_details_component__ = __webpack_require__("../../../../../src/app/components/session-details/session-details.component.ts"); /* harmony import */ var __WEBPACK_IMPORTED_MODULE_11__components_session_details_session_details_component__ = __webpack_require__("../../../../../src/app/components/session-details/session-details.component.ts");
/* harmony import */ var __WEBPACK_IMPORTED_MODULE_12__components_dashboard_credentials_dialog_component__ = __webpack_require__("../../../../../src/app/components/dashboard/credentials-dialog.component.ts"); /* harmony import */ var __WEBPACK_IMPORTED_MODULE_12__components_dashboard_credentials_dialog_component__ = __webpack_require__("../../../../../src/app/components/dashboard/credentials-dialog.component.ts");
/* harmony import */ var __WEBPACK_IMPORTED_MODULE_13__components_layouts_layout_best_fit_layout_best_fit_component__ = __webpack_require__("../../../../../src/app/components/layouts/layout-best-fit/layout-best-fit.component.ts");
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) { var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d; var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc); if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
@ -13294,6 +13285,7 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key,
var AppModule = (function () { var AppModule = (function () {
function AppModule() { function AppModule() {
} }
@ -13304,6 +13296,7 @@ var AppModule = (function () {
__WEBPACK_IMPORTED_MODULE_10__components_dashboard_dashboard_component__["a" /* DashboardComponent */], __WEBPACK_IMPORTED_MODULE_10__components_dashboard_dashboard_component__["a" /* DashboardComponent */],
__WEBPACK_IMPORTED_MODULE_11__components_session_details_session_details_component__["a" /* SessionDetailsComponent */], __WEBPACK_IMPORTED_MODULE_11__components_session_details_session_details_component__["a" /* SessionDetailsComponent */],
__WEBPACK_IMPORTED_MODULE_12__components_dashboard_credentials_dialog_component__["a" /* CredentialsDialogComponent */], __WEBPACK_IMPORTED_MODULE_12__components_dashboard_credentials_dialog_component__["a" /* CredentialsDialogComponent */],
__WEBPACK_IMPORTED_MODULE_13__components_layouts_layout_best_fit_layout_best_fit_component__["a" /* LayoutBestFitComponent */],
], ],
imports: [ imports: [
__WEBPACK_IMPORTED_MODULE_0__angular_platform_browser__["a" /* BrowserModule */], __WEBPACK_IMPORTED_MODULE_0__angular_platform_browser__["a" /* BrowserModule */],
@ -13335,20 +13328,29 @@ var AppModule = (function () {
/* harmony import */ var __WEBPACK_IMPORTED_MODULE_0__angular_router__ = __webpack_require__("../../../router/esm5/router.js"); /* harmony import */ var __WEBPACK_IMPORTED_MODULE_0__angular_router__ = __webpack_require__("../../../router/esm5/router.js");
/* harmony import */ var __WEBPACK_IMPORTED_MODULE_1_app_components_dashboard_dashboard_component__ = __webpack_require__("../../../../../src/app/components/dashboard/dashboard.component.ts"); /* harmony import */ var __WEBPACK_IMPORTED_MODULE_1_app_components_dashboard_dashboard_component__ = __webpack_require__("../../../../../src/app/components/dashboard/dashboard.component.ts");
/* harmony import */ var __WEBPACK_IMPORTED_MODULE_2_app_components_session_details_session_details_component__ = __webpack_require__("../../../../../src/app/components/session-details/session-details.component.ts"); /* harmony import */ var __WEBPACK_IMPORTED_MODULE_2_app_components_session_details_session_details_component__ = __webpack_require__("../../../../../src/app/components/session-details/session-details.component.ts");
/* harmony import */ var __WEBPACK_IMPORTED_MODULE_3_app_components_layouts_layout_best_fit_layout_best_fit_component__ = __webpack_require__("../../../../../src/app/components/layouts/layout-best-fit/layout-best-fit.component.ts");
var appRoutes = [ var appRoutes = [
{ {
path: '', path: '',
component: __WEBPACK_IMPORTED_MODULE_1_app_components_dashboard_dashboard_component__["a" /* DashboardComponent */] component: __WEBPACK_IMPORTED_MODULE_1_app_components_dashboard_dashboard_component__["a" /* DashboardComponent */],
pathMatch: 'full'
}, },
{ {
path: 'session/:id', path: 'session/:sessionId',
component: __WEBPACK_IMPORTED_MODULE_2_app_components_session_details_session_details_component__["a" /* SessionDetailsComponent */] component: __WEBPACK_IMPORTED_MODULE_2_app_components_session_details_session_details_component__["a" /* SessionDetailsComponent */],
pathMatch: 'full'
},
{
path: 'layout-best-fit/:sessionId/:secret',
component: __WEBPACK_IMPORTED_MODULE_3_app_components_layouts_layout_best_fit_layout_best_fit_component__["a" /* LayoutBestFitComponent */],
pathMatch: 'full'
} }
]; ];
var routing = __WEBPACK_IMPORTED_MODULE_0__angular_router__["a" /* RouterModule */].forRoot(appRoutes); var routing = __WEBPACK_IMPORTED_MODULE_0__angular_router__["b" /* RouterModule */].forRoot(appRoutes, { useHash: true });
/***/ }), /***/ }),
@ -13398,7 +13400,7 @@ exports = module.exports = __webpack_require__("../../../../css-loader/lib/css-b
// module // module
exports.push([module.i, "#dashboard-div {\n padding: 20px;\n}\n\n#log {\n height: 90%;\n}\n\n#log-content {\n height: 90%;\n font-family: Consolas, 'Liberation Mono', Menlo, Courier, monospace;\n overflow-y: auto;\n overflow-x: hidden\n}\n\nul {\n margin: 0;\n}\n\n#test-btn {\n text-transform: uppercase;\n float: right;\n}\n\nmat-card-title button.blue {\n color: #ffffff;\n background-color: #0088aa;\n}\n\nmat-card-title button.yellow {\n color: rgba(0, 0, 0, 0.87);\n background-color: #ffcc00;\n}\n\nmat-spinner {\n position: absolute;\n top: 50%;\n left: 50%;\n -webkit-transform: translate(-50%, -50%);\n transform: translate(-50%, -50%);\n}\n\n#tick-div {\n width: 100px;\n height: 100px;\n z-index: 1;\n position: absolute;\n top: 50%;\n left: 50%;\n -webkit-transform: translate(-50%, -50%);\n transform: translate(-50%, -50%);\n}\n\n#tooltip-tick {\n position: absolute;\n width: 100%;\n height: 100%;\n z-index: 2;\n}\n\n.circ {\n opacity: 0;\n stroke-dasharray: 130;\n stroke-dashoffset: 130;\n transition: all 1s;\n}\n\n.tick {\n stroke-dasharray: 50;\n stroke-dashoffset: 50;\n transition: stroke-dashoffset 1s 0.5s ease-out;\n}\n\n.drawn+svg .path {\n opacity: 1;\n stroke-dashoffset: 0;\n}\n\n#mirrored-video {\n position: relative;\n}\n\n\n/* Pure CSS loader */\n\n#loader {\n width: 100px;\n height: 100px;\n z-index: 1;\n position: absolute;\n top: 50%;\n left: 50%;\n -webkit-transform: translate(-50%, -50%);\n transform: translate(-50%, -50%);\n}\n\n#loader * {\n box-sizing: border-box;\n}\n\n#loader ::after {\n box-sizing: border-box;\n}\n\n#loader ::before {\n box-sizing: border-box;\n}\n\n.loader-1 {\n height: 100px;\n width: 100px;\n -webkit-animation: loader-1-1 4.8s linear infinite;\n animation: loader-1-1 4.8s linear infinite;\n}\n\n@-webkit-keyframes loader-1-1 {\n 0% {\n -webkit-transform: rotate(0deg);\n }\n 100% {\n -webkit-transform: rotate(360deg);\n }\n}\n\n@keyframes loader-1-1 {\n 0% {\n -webkit-transform: rotate(0deg);\n transform: rotate(0deg);\n }\n 100% {\n -webkit-transform: rotate(360deg);\n transform: rotate(360deg);\n }\n}\n\n.loader-1 span {\n display: block;\n position: absolute;\n top: 0;\n left: 0;\n bottom: 0;\n right: 0;\n margin: auto;\n height: 100px;\n width: 100px;\n clip: rect(0, 100px, 100px, 50px);\n -webkit-animation: loader-1-2 1.2s linear infinite;\n animation: loader-1-2 1.2s linear infinite;\n}\n\n@-webkit-keyframes loader-1-2 {\n 0% {\n -webkit-transform: rotate(0deg);\n }\n 100% {\n -webkit-transform: rotate(220deg);\n }\n}\n\n@keyframes loader-1-2 {\n 0% {\n -webkit-transform: rotate(0deg);\n transform: rotate(0deg);\n }\n 100% {\n -webkit-transform: rotate(220deg);\n transform: rotate(220deg);\n }\n}\n\n.loader-1 span::after {\n content: \"\";\n position: absolute;\n top: 0;\n left: 0;\n bottom: 0;\n right: 0;\n margin: auto;\n height: 100px;\n width: 100px;\n clip: rect(0, 100px, 100px, 50px);\n border: 8px solid #4d4d4d;\n border-radius: 50%;\n -webkit-animation: loader-1-3 1.2s cubic-bezier(0.770, 0.000, 0.175, 1.000) infinite;\n animation: loader-1-3 1.2s cubic-bezier(0.770, 0.000, 0.175, 1.000) infinite;\n}\n\n@-webkit-keyframes loader-1-3 {\n 0% {\n -webkit-transform: rotate(-140deg);\n }\n 50% {\n -webkit-transform: rotate(-160deg);\n }\n 100% {\n -webkit-transform: rotate(140deg);\n }\n}\n\n@keyframes loader-1-3 {\n 0% {\n -webkit-transform: rotate(-140deg);\n transform: rotate(-140deg);\n }\n 50% {\n -webkit-transform: rotate(-160deg);\n transform: rotate(-160deg);\n }\n 100% {\n -webkit-transform: rotate(140deg);\n transform: rotate(140deg);\n }\n}", ""]); exports.push([module.i, "#dashboard-div {\n padding: 20px;\n}\n\n#log {\n height: 90%;\n}\n\n#log-content {\n height: 90%;\n font-family: Consolas, 'Liberation Mono', Menlo, Courier, monospace;\n overflow-y: auto;\n overflow-x: hidden\n}\n\nul {\n margin: 0;\n}\n\n#test-btn {\n text-transform: uppercase;\n float: right;\n}\n\nmat-card-title button.blue {\n color: #ffffff;\n background-color: #0088aa;\n}\n\nmat-card-title button.yellow {\n color: rgba(0, 0, 0, 0.87);\n background-color: #ffcc00;\n}\n\nmat-spinner {\n position: absolute;\n top: 50%;\n left: 50%;\n -webkit-transform: translate(-50%, -50%);\n transform: translate(-50%, -50%);\n}\n\n#tick-div {\n width: 100px;\n height: 100px;\n z-index: 1;\n position: absolute;\n top: 50%;\n left: 50%;\n -webkit-transform: translate(-50%, -50%);\n transform: translate(-50%, -50%);\n}\n\n#tooltip-tick {\n position: absolute;\n width: 100%;\n height: 100%;\n z-index: 2;\n}\n\n.circ {\n opacity: 0;\n stroke-dasharray: 130;\n stroke-dashoffset: 130;\n -webkit-transition: all 1s;\n transition: all 1s;\n}\n\n.tick {\n stroke-dasharray: 50;\n stroke-dashoffset: 50;\n -webkit-transition: stroke-dashoffset 1s 0.5s ease-out;\n transition: stroke-dashoffset 1s 0.5s ease-out;\n}\n\n.drawn+svg .path {\n opacity: 1;\n stroke-dashoffset: 0;\n}\n\n#mirrored-video {\n position: relative;\n}\n\n/* Pure CSS loader */\n\n#loader {\n width: 100px;\n height: 100px;\n z-index: 1;\n position: absolute;\n top: 50%;\n left: 50%;\n -webkit-transform: translate(-50%, -50%);\n transform: translate(-50%, -50%);\n}\n\n#loader * {\n -webkit-box-sizing: border-box;\n box-sizing: border-box;\n}\n\n#loader ::after {\n -webkit-box-sizing: border-box;\n box-sizing: border-box;\n}\n\n#loader ::before {\n -webkit-box-sizing: border-box;\n box-sizing: border-box;\n}\n\n.loader-1 {\n height: 100px;\n width: 100px;\n -webkit-animation: loader-1-1 4.8s linear infinite;\n animation: loader-1-1 4.8s linear infinite;\n}\n\n@-webkit-keyframes loader-1-1 {\n 0% {\n -webkit-transform: rotate(0deg);\n }\n 100% {\n -webkit-transform: rotate(360deg);\n }\n}\n\n@keyframes loader-1-1 {\n 0% {\n -webkit-transform: rotate(0deg);\n transform: rotate(0deg);\n }\n 100% {\n -webkit-transform: rotate(360deg);\n transform: rotate(360deg);\n }\n}\n\n.loader-1 span {\n display: block;\n position: absolute;\n top: 0;\n left: 0;\n bottom: 0;\n right: 0;\n margin: auto;\n height: 100px;\n width: 100px;\n clip: rect(0, 100px, 100px, 50px);\n -webkit-animation: loader-1-2 1.2s linear infinite;\n animation: loader-1-2 1.2s linear infinite;\n}\n\n@-webkit-keyframes loader-1-2 {\n 0% {\n -webkit-transform: rotate(0deg);\n }\n 100% {\n -webkit-transform: rotate(220deg);\n }\n}\n\n@keyframes loader-1-2 {\n 0% {\n -webkit-transform: rotate(0deg);\n transform: rotate(0deg);\n }\n 100% {\n -webkit-transform: rotate(220deg);\n transform: rotate(220deg);\n }\n}\n\n.loader-1 span::after {\n content: \"\";\n position: absolute;\n top: 0;\n left: 0;\n bottom: 0;\n right: 0;\n margin: auto;\n height: 100px;\n width: 100px;\n clip: rect(0, 100px, 100px, 50px);\n border: 8px solid #4d4d4d;\n border-radius: 50%;\n -webkit-animation: loader-1-3 1.2s cubic-bezier(0.770, 0.000, 0.175, 1.000) infinite;\n animation: loader-1-3 1.2s cubic-bezier(0.770, 0.000, 0.175, 1.000) infinite;\n}\n\n@-webkit-keyframes loader-1-3 {\n 0% {\n -webkit-transform: rotate(-140deg);\n }\n 50% {\n -webkit-transform: rotate(-160deg);\n }\n 100% {\n -webkit-transform: rotate(140deg);\n }\n}\n\n@keyframes loader-1-3 {\n 0% {\n -webkit-transform: rotate(-140deg);\n transform: rotate(-140deg);\n }\n 50% {\n -webkit-transform: rotate(-160deg);\n transform: rotate(-160deg);\n }\n 100% {\n -webkit-transform: rotate(140deg);\n transform: rotate(140deg);\n }\n}", ""]);
// exports // exports
@ -13459,18 +13461,38 @@ var DashboardComponent = (function () {
}); });
} }
DashboardComponent.prototype.ngOnInit = function () { DashboardComponent.prototype.ngOnInit = function () {
var _this = this;
var protocol = location.protocol.includes('https') ? 'wss://' : 'ws://';
var port = (location.port) ? (':' + location.port) : '';
this.websocket = new WebSocket(protocol + location.hostname + port + '/info');
this.websocket.onopen = function (event) {
console.log('Info websocket connected');
};
this.websocket.onclose = function (event) {
console.log('Info websocket closed');
};
this.websocket.onerror = function (event) {
console.log('Info websocket error');
};
this.websocket.onmessage = function (event) {
console.log('Info websocket message');
console.log(event.data);
_this.infoService.updateInfo(event.data);
};
}; };
DashboardComponent.prototype.beforeunloadHandler = function () { DashboardComponent.prototype.beforeunloadHandler = function () {
// On window closed leave test session // On window closed leave test session and close info websocket
if (this.session) { if (this.session) {
this.endTestVideo(); this.endTestVideo();
} }
this.websocket.close();
}; };
DashboardComponent.prototype.ngOnDestroy = function () { DashboardComponent.prototype.ngOnDestroy = function () {
// On component destroyed leave test session // On component destroyed leave test session and close info websocket
if (this.session) { if (this.session) {
this.endTestVideo(); this.endTestVideo();
} }
this.websocket.close();
}; };
DashboardComponent.prototype.toggleTestVideo = function () { DashboardComponent.prototype.toggleTestVideo = function () {
if (!this.session) { if (!this.session) {
@ -13589,6 +13611,188 @@ var DashboardComponent = (function () {
/***/ }),
/***/ "../../../../../src/app/components/layouts/layout-best-fit/layout-best-fit.component.css":
/***/ (function(module, exports, __webpack_require__) {
exports = module.exports = __webpack_require__("../../../../css-loader/lib/css-base.js")(false);
// imports
// module
exports.push([module.i, ".bounds {\n background-color: black;\n height: 100%;\n overflow: hidden;\n cursor: none !important;\n}\n\nvideo {\n height: 100%;\n}\n", ""]);
// exports
/*** EXPORTS FROM exports-loader ***/
module.exports = module.exports.toString();
/***/ }),
/***/ "../../../../../src/app/components/layouts/layout-best-fit/layout-best-fit.component.html":
/***/ (function(module, exports) {
module.exports = "<div class=\"bounds\">\n <div *ngFor=\"let streams of remoteStreams\" class=\"content\" fxLayout=\"row\" fxFlexFill [style.height]=\"100 / remoteStreams.length + '%'\"\n [style.min-height]=\"100 / remoteStreams.length + '%'\">\n <div *ngFor=\"let s of streams\" [fxFlex]=\"100 / streams\">\n <video [id]=\"'native-video-' + s.streamId\" autoplay=\"true\" [srcObject]=\"s.getMediaStream()\"></video>\n </div>\n </div>\n</div>\n"
/***/ }),
/***/ "../../../../../src/app/components/layouts/layout-best-fit/layout-best-fit.component.ts":
/***/ (function(module, __webpack_exports__, __webpack_require__) {
"use strict";
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "a", function() { return LayoutBestFitComponent; });
/* harmony import */ var __WEBPACK_IMPORTED_MODULE_0__angular_core__ = __webpack_require__("../../../core/esm5/core.js");
/* harmony import */ var __WEBPACK_IMPORTED_MODULE_1__angular_router__ = __webpack_require__("../../../router/esm5/router.js");
/* harmony import */ var __WEBPACK_IMPORTED_MODULE_2_openvidu_browser__ = __webpack_require__("../../../../../../../../../openvidu-browser/lib/OpenVidu/index.js");
/* harmony import */ var __WEBPACK_IMPORTED_MODULE_2_openvidu_browser___default = __webpack_require__.n(__WEBPACK_IMPORTED_MODULE_2_openvidu_browser__);
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
return c > 3 && r && Object.defineProperty(target, key, r), r;
};
var __metadata = (this && this.__metadata) || function (k, v) {
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
};
var LayoutBestFitComponent = (function () {
function LayoutBestFitComponent(route) {
var _this = this;
this.route = route;
this.numberOfVideos = 0;
this.remoteStreams = [];
this.route.params.subscribe(function (params) {
_this.sessionId = params.sessionId;
_this.secret = params.secret;
});
}
LayoutBestFitComponent.prototype.beforeunloadHandler = function () {
this.leaveSession();
};
LayoutBestFitComponent.prototype.ngOnDestroy = function () {
this.leaveSession();
};
LayoutBestFitComponent.prototype.ngOnInit = function () {
var _this = this;
var OV = new __WEBPACK_IMPORTED_MODULE_2_openvidu_browser__["OpenVidu"]();
var fullSessionId = 'wss://' + location.hostname + ':8443/' + this.sessionId + '?secret=' + this.secret + '&recorder=true';
this.session = OV.initSession(fullSessionId);
this.session.on('streamCreated', function (event) {
_this.numberOfVideos++;
_this.addRemoteStream(event.stream);
_this.session.subscribe(event.stream, '');
});
this.session.on('streamDestroyed', function (event) {
_this.numberOfVideos--;
event.preventDefault();
_this.deleteRemoteStream(event.stream);
});
this.session.connect(null, function (error) {
if (error) {
console.error(error);
}
});
};
LayoutBestFitComponent.prototype.addRemoteStream = function (stream) {
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;
}
};
LayoutBestFitComponent.prototype.deleteRemoteStream = function (stream) {
for (var i = 0; i < this.remoteStreams.length; i++) {
var index = this.remoteStreams[i].indexOf(stream, 0);
if (index > -1) {
this.remoteStreams[i].splice(index, 1);
this.reArrangeVideos();
break;
}
}
};
LayoutBestFitComponent.prototype.reArrangeVideos = function () {
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(function (array) { return array.length > 0; });
};
LayoutBestFitComponent.prototype.leaveSession = function () {
if (this.session) {
this.session.disconnect();
}
;
this.remoteStreams = [];
this.numberOfVideos = 0;
this.session = null;
};
__decorate([
Object(__WEBPACK_IMPORTED_MODULE_0__angular_core__["A" /* HostListener */])('window:beforeunload'),
__metadata("design:type", Function),
__metadata("design:paramtypes", []),
__metadata("design:returntype", void 0)
], LayoutBestFitComponent.prototype, "beforeunloadHandler", null);
LayoutBestFitComponent = __decorate([
Object(__WEBPACK_IMPORTED_MODULE_0__angular_core__["n" /* Component */])({
selector: 'app-layout-best-fit',
template: __webpack_require__("../../../../../src/app/components/layouts/layout-best-fit/layout-best-fit.component.html"),
styles: [__webpack_require__("../../../../../src/app/components/layouts/layout-best-fit/layout-best-fit.component.css")]
}),
__metadata("design:paramtypes", [__WEBPACK_IMPORTED_MODULE_1__angular_router__["a" /* ActivatedRoute */]])
], LayoutBestFitComponent);
return LayoutBestFitComponent;
}());
/***/ }), /***/ }),
/***/ "../../../../../src/app/components/session-details/session-details.component.css": /***/ "../../../../../src/app/components/session-details/session-details.component.css":

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

View File

@ -3028,9 +3028,9 @@ var freeGlobal = (typeof window !== 'undefined' ? window : (typeof self !== 'und
freeGlobal.Hammer = Hammer; freeGlobal.Hammer = Hammer;
if (true) { if (true) {
!(__WEBPACK_AMD_DEFINE_RESULT__ = function() { !(__WEBPACK_AMD_DEFINE_RESULT__ = (function() {
return Hammer; return Hammer;
}.call(exports, __webpack_require__, exports, module), }).call(exports, __webpack_require__, exports, module),
__WEBPACK_AMD_DEFINE_RESULT__ !== undefined && (module.exports = __WEBPACK_AMD_DEFINE_RESULT__)); __WEBPACK_AMD_DEFINE_RESULT__ !== undefined && (module.exports = __WEBPACK_AMD_DEFINE_RESULT__));
} else if (typeof module != 'undefined' && module.exports) { } else if (typeof module != 'undefined' && module.exports) {
module.exports = Hammer; module.exports = Hammer;
@ -20931,6 +20931,8 @@ function tryCatch(fn) {
/* unused harmony export __asyncDelegator */ /* unused harmony export __asyncDelegator */
/* unused harmony export __asyncValues */ /* unused harmony export __asyncValues */
/* unused harmony export __makeTemplateObject */ /* unused harmony export __makeTemplateObject */
/* unused harmony export __importStar */
/* unused harmony export __importDefault */
/*! ***************************************************************************** /*! *****************************************************************************
Copyright (c) Microsoft Corporation. All rights reserved. Copyright (c) Microsoft Corporation. All rights reserved.
Licensed under the Apache License, Version 2.0 (the "License"); you may not use Licensed under the Apache License, Version 2.0 (the "License"); you may not use
@ -21098,6 +21100,18 @@ function __makeTemplateObject(cooked, raw) {
return cooked; return cooked;
}; };
function __importStar(mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k in mod) if (Object.hasOwnProperty.call(mod, k)) result[k] = mod[k];
result.default = mod;
return result;
}
function __importDefault(mod) {
return (mod && mod.__esModule) ? mod : { default: mod };
}
/***/ }), /***/ }),
@ -153507,7 +153521,7 @@ var VERSION = new __WEBPACK_IMPORTED_MODULE_1__angular_core__["_11" /* Version *
/* unused harmony export ROUTES */ /* unused harmony export ROUTES */
/* unused harmony export ROUTER_CONFIGURATION */ /* unused harmony export ROUTER_CONFIGURATION */
/* unused harmony export ROUTER_INITIALIZER */ /* unused harmony export ROUTER_INITIALIZER */
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "a", function() { return RouterModule; }); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "b", function() { return RouterModule; });
/* unused harmony export provideRoutes */ /* unused harmony export provideRoutes */
/* unused harmony export ChildrenOutletContexts */ /* unused harmony export ChildrenOutletContexts */
/* unused harmony export OutletContext */ /* unused harmony export OutletContext */
@ -153515,7 +153529,7 @@ var VERSION = new __WEBPACK_IMPORTED_MODULE_1__angular_core__["_11" /* Version *
/* unused harmony export PreloadAllModules */ /* unused harmony export PreloadAllModules */
/* unused harmony export PreloadingStrategy */ /* unused harmony export PreloadingStrategy */
/* unused harmony export RouterPreloader */ /* unused harmony export RouterPreloader */
/* unused harmony export ActivatedRoute */ /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "a", function() { return ActivatedRoute; });
/* unused harmony export ActivatedRouteSnapshot */ /* unused harmony export ActivatedRouteSnapshot */
/* unused harmony export RouterState */ /* unused harmony export RouterState */
/* unused harmony export RouterStateSnapshot */ /* unused harmony export RouterStateSnapshot */

File diff suppressed because one or more lines are too long