mirror of https://github.com/OpenVidu/openvidu.git
openvidu-server: custom recording layout
parent
13b4bf0f3a
commit
43894c215b
|
@ -32,6 +32,9 @@ public class OpenviduConfig {
|
||||||
@Value("${openvidu.recording.notification}")
|
@Value("${openvidu.recording.notification}")
|
||||||
String openviduRecordingNotification;
|
String openviduRecordingNotification;
|
||||||
|
|
||||||
|
@Value("${openvidu.recording.custom-layout}")
|
||||||
|
String openviduRecordingCustomLayout;
|
||||||
|
|
||||||
@Value("${openvidu.recording.version}")
|
@Value("${openvidu.recording.version}")
|
||||||
String openviduRecordingVersion;
|
String openviduRecordingVersion;
|
||||||
|
|
||||||
|
@ -68,12 +71,20 @@ public class OpenviduConfig {
|
||||||
return this.openviduRecordingPath;
|
return this.openviduRecordingPath;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void setOpenViduRecordingPath(String recordingPath) {
|
||||||
|
this.openviduRecordingPath = recordingPath;
|
||||||
|
}
|
||||||
|
|
||||||
public boolean getOpenViduRecordingPublicAccess() {
|
public boolean getOpenViduRecordingPublicAccess() {
|
||||||
return this.openviduRecordingPublicAccess;
|
return this.openviduRecordingPublicAccess;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public String getOpenviduRecordingCustomLayout() {
|
||||||
|
return this.openviduRecordingCustomLayout;
|
||||||
|
}
|
||||||
|
|
||||||
public void setOpenViduRecordingPath(String recordingPath) {
|
public void setOpenViduRecordingCustomLayout(String recordingCustomLayout) {
|
||||||
this.openviduRecordingPath = recordingPath;
|
this.openviduRecordingCustomLayout = recordingCustomLayout;
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getFinalUrl() {
|
public String getFinalUrl() {
|
||||||
|
|
|
@ -11,38 +11,40 @@ import org.springframework.security.config.http.SessionCreationPolicy;
|
||||||
|
|
||||||
@Configuration
|
@Configuration
|
||||||
public class SecurityConfig extends WebSecurityConfigurerAdapter {
|
public class SecurityConfig extends WebSecurityConfigurerAdapter {
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
OpenviduConfig openviduConf;
|
OpenviduConfig openviduConf;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void configure(HttpSecurity http) throws Exception {
|
protected void configure(HttpSecurity http) throws Exception {
|
||||||
|
|
||||||
|
// Security for API REST
|
||||||
ExpressionUrlAuthorizationConfigurer<HttpSecurity>.ExpressionInterceptUrlRegistry conf = 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(HttpMethod.POST, "/api/recordings/start").authenticated()
|
||||||
.antMatchers(HttpMethod.POST, "/api/recordings/start").authenticated()
|
.antMatchers(HttpMethod.POST, "/api/recordings/stop").authenticated()
|
||||||
.antMatchers(HttpMethod.POST, "/api/recordings/stop").authenticated()
|
.antMatchers(HttpMethod.GET, "/api/recordings").authenticated()
|
||||||
.antMatchers(HttpMethod.GET, "/api/recordings").authenticated()
|
.antMatchers(HttpMethod.GET, "/api/recordings/**").authenticated()
|
||||||
.antMatchers(HttpMethod.GET, "/api/recordings/**").authenticated()
|
.antMatchers(HttpMethod.DELETE, "/api/recordings/**").authenticated()
|
||||||
.antMatchers(HttpMethod.DELETE, "/api/recordings/**").authenticated()
|
.antMatchers(HttpMethod.GET, "/config/**").authenticated().antMatchers("/").authenticated();
|
||||||
.antMatchers(HttpMethod.GET, "/config/**").authenticated()
|
|
||||||
.antMatchers("/").authenticated();
|
// Security for layouts
|
||||||
|
conf.antMatchers("/layouts/*").authenticated();
|
||||||
if (openviduConf.getOpenViduRecordingPublicAccess()) {
|
|
||||||
conf = conf.antMatchers("/recordings/*").permitAll();
|
// Security for recorded videos
|
||||||
} else {
|
if (openviduConf.getOpenViduRecordingPublicAccess()) {
|
||||||
conf = conf.antMatchers("/recordings/*").authenticated();
|
conf = conf.antMatchers("/recordings/*").permitAll();
|
||||||
}
|
} else {
|
||||||
|
conf = conf.antMatchers("/recordings/*").authenticated();
|
||||||
conf.and().sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
|
}
|
||||||
.and().httpBasic();
|
|
||||||
|
conf.and().sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and().httpBasic();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
|
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
|
||||||
auth.inMemoryAuthentication()
|
auth.inMemoryAuthentication().withUser("OPENVIDUAPP").password(openviduConf.getOpenViduSecret()).roles("ADMIN");
|
||||||
.withUser("OPENVIDUAPP").password(openviduConf.getOpenViduSecret()).roles("ADMIN");
|
}
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
|
@ -237,8 +237,10 @@ public class KurentoSessionManager extends SessionManager {
|
||||||
&& session.getActivePublishers() == 0) {
|
&& session.getActivePublishers() == 0) {
|
||||||
// Insecure session recording
|
// Insecure session recording
|
||||||
new Thread(() -> {
|
new Thread(() -> {
|
||||||
recordingService.startRecording(session, new RecordingProperties.Builder().name("")
|
recordingService.startRecording(session,
|
||||||
.recordingLayout(session.getSessionProperties().defaultRecordingLayout()).build());
|
new RecordingProperties.Builder().name("")
|
||||||
|
.recordingLayout(session.getSessionProperties().defaultRecordingLayout())
|
||||||
|
.customLayout(session.getSessionProperties().defaultCustomLayout()).build());
|
||||||
}).start();
|
}).start();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -47,6 +47,7 @@ import com.github.dockerjava.core.command.PullImageResultCallback;
|
||||||
|
|
||||||
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.RecordingLayout;
|
||||||
import io.openvidu.java.client.RecordingProperties;
|
import io.openvidu.java.client.RecordingProperties;
|
||||||
import io.openvidu.server.CommandExecutor;
|
import io.openvidu.server.CommandExecutor;
|
||||||
import io.openvidu.server.OpenViduServer;
|
import io.openvidu.server.OpenViduServer;
|
||||||
|
@ -87,12 +88,11 @@ public class ComposedRecordingService {
|
||||||
String shortSessionId = session.getSessionId().substring(session.getSessionId().lastIndexOf('/') + 1,
|
String shortSessionId = session.getSessionId().substring(session.getSessionId().lastIndexOf('/') + 1,
|
||||||
session.getSessionId().length());
|
session.getSessionId().length());
|
||||||
String recordingId = this.getFreeRecordingId(session.getSessionId(), shortSessionId);
|
String recordingId = this.getFreeRecordingId(session.getSessionId(), shortSessionId);
|
||||||
String secret = openviduConfig.getOpenViduSecret();
|
|
||||||
|
|
||||||
if (properties.name() == null || properties.name().isEmpty()) {
|
if (properties.name() == null || properties.name().isEmpty()) {
|
||||||
// No name provided for the recording file
|
// No name provided for the recording file
|
||||||
properties = new RecordingProperties.Builder().name(recordingId)
|
properties = new RecordingProperties.Builder().name(recordingId)
|
||||||
.recordingLayout(properties.recordingLayout()).build();
|
.recordingLayout(properties.recordingLayout()).customLayout(properties.customLayout()).build();
|
||||||
}
|
}
|
||||||
|
|
||||||
Recording recording = new Recording(session.getSessionId(), recordingId, properties);
|
Recording recording = new Recording(session.getSessionId(), recordingId, properties);
|
||||||
|
@ -111,11 +111,9 @@ public class ComposedRecordingService {
|
||||||
e.printStackTrace();
|
e.printStackTrace();
|
||||||
}
|
}
|
||||||
|
|
||||||
String location = OpenViduServer.publicUrl.replaceFirst("wss://", "");
|
String layoutUrl = this.getLayoutUrl(recording, shortSessionId);
|
||||||
String layoutUrl = properties.recordingLayout().name().toLowerCase().replaceAll("_", "-");
|
|
||||||
|
|
||||||
envs.add("URL=https://OPENVIDUAPP:" + secret + "@" + location + "/#/layout-" + layoutUrl + "/" + shortSessionId
|
envs.add("URL=" + layoutUrl);
|
||||||
+ "/" + secret);
|
|
||||||
envs.add("RESOLUTION=1920x1080");
|
envs.add("RESOLUTION=1920x1080");
|
||||||
envs.add("FRAMERATE=30");
|
envs.add("FRAMERATE=30");
|
||||||
envs.add("VIDEO_ID=" + recordingId);
|
envs.add("VIDEO_ID=" + recordingId);
|
||||||
|
@ -125,8 +123,7 @@ public class ComposedRecordingService {
|
||||||
envs.add("RECORDING_JSON=" + recording.toJson().toJSONString());
|
envs.add("RECORDING_JSON=" + recording.toJson().toJSONString());
|
||||||
|
|
||||||
log.info(recording.toJson().toJSONString());
|
log.info(recording.toJson().toJSONString());
|
||||||
log.debug("Recorder connecting to url {}",
|
log.debug("Recorder connecting to url {}", layoutUrl);
|
||||||
"https://OPENVIDUAPP:" + secret + "@localhost:8443/#/layout-best-fit/" + shortSessionId + "/" + secret);
|
|
||||||
|
|
||||||
String containerId = this.runRecordingContainer(envs, "recording_" + recordingId);
|
String containerId = this.runRecordingContainer(envs, "recording_" + recordingId);
|
||||||
|
|
||||||
|
@ -456,6 +453,27 @@ public class ComposedRecordingService {
|
||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private String getLayoutUrl(Recording recording, String shortSessionId) {
|
||||||
|
String secret = openviduConfig.getOpenViduSecret();
|
||||||
|
String location = OpenViduServer.publicUrl.replaceFirst("wss://", "");
|
||||||
|
String layout, finalUrl;
|
||||||
|
|
||||||
|
if (RecordingLayout.CUSTOM.equals(recording.getLayout())) {
|
||||||
|
layout = recording.getCustomLayout();
|
||||||
|
layout = layout.startsWith("/") ? layout.substring(1) : layout;
|
||||||
|
layout = layout.endsWith("/") ? layout.substring(0, layout.length() - 1) : layout;
|
||||||
|
layout += "/index.html";
|
||||||
|
finalUrl = "https://OPENVIDUAPP:" + secret + "@" + location + "/layouts/custom/" + layout + "/?sessionId="
|
||||||
|
+ shortSessionId + "&secret=" + secret;
|
||||||
|
} else {
|
||||||
|
layout = recording.getLayout().name().toLowerCase().replaceAll("_", "-");
|
||||||
|
finalUrl = "https://OPENVIDUAPP:" + secret + "@" + location + "/#/layout-" + layout + "/" + shortSessionId
|
||||||
|
+ "/" + secret;
|
||||||
|
}
|
||||||
|
|
||||||
|
return finalUrl;
|
||||||
|
}
|
||||||
|
|
||||||
public void setRecordingVersion(String version) {
|
public void setRecordingVersion(String version) {
|
||||||
this.IMAGE_TAG = version;
|
this.IMAGE_TAG = version;
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,27 @@
|
||||||
|
package io.openvidu.server.recording;
|
||||||
|
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
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
|
||||||
|
public class LayoutsHttpHandler extends WebMvcConfigurerAdapter {
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
OpenviduConfig openviduConfig;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void addResourceHandlers(ResourceHandlerRegistry registry) {
|
||||||
|
|
||||||
|
String customLayoutsPath = openviduConfig.getOpenviduRecordingCustomLayout();
|
||||||
|
customLayoutsPath = customLayoutsPath.endsWith("/") ? customLayoutsPath : customLayoutsPath + "/";
|
||||||
|
|
||||||
|
openviduConfig.setOpenViduRecordingCustomLayout(customLayoutsPath);
|
||||||
|
|
||||||
|
registry.addResourceHandler("/layouts/custom/**").addResourceLocations("file:" + customLayoutsPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -66,21 +66,17 @@ public class Recording {
|
||||||
this.id = id;
|
this.id = id;
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getName() {
|
public String getName() {
|
||||||
return this.recordingProperties.name();
|
return this.recordingProperties.name();
|
||||||
}
|
}
|
||||||
|
|
||||||
public String setName() {
|
|
||||||
return this.recordingProperties.name();
|
|
||||||
}
|
|
||||||
|
|
||||||
public RecordingLayout getLayout() {
|
public RecordingLayout getLayout() {
|
||||||
return this.recordingProperties.recordingLayout();
|
return this.recordingProperties.recordingLayout();
|
||||||
}
|
}
|
||||||
|
|
||||||
public RecordingLayout setLayout() {
|
public String getCustomLayout() {
|
||||||
return this.recordingProperties.recordingLayout();
|
return this.recordingProperties.customLayout();
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getSessionId() {
|
public String getSessionId() {
|
||||||
return sessionId;
|
return sessionId;
|
||||||
|
|
|
@ -72,23 +72,34 @@ public class SessionRestController {
|
||||||
|
|
||||||
SessionProperties.Builder builder = new SessionProperties.Builder();
|
SessionProperties.Builder builder = new SessionProperties.Builder();
|
||||||
if (params != null) {
|
if (params != null) {
|
||||||
|
String mediaModeString = (String) params.get("mediaMode");
|
||||||
String recordingModeString = (String) params.get("recordingMode");
|
String recordingModeString = (String) params.get("recordingMode");
|
||||||
String defaultRecordingLayoutString = (String) params.get("defaultRecordingLayout");
|
String defaultRecordingLayoutString = (String) params.get("defaultRecordingLayout");
|
||||||
String mediaModeString = (String) params.get("mediaMode");
|
String defaultCustomLayout = (String) params.get("defaultCustomLayout");
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|
||||||
|
// Safe parameter retrieval. Default values if not defined
|
||||||
if (recordingModeString != null) {
|
if (recordingModeString != null) {
|
||||||
RecordingMode recordingMode = RecordingMode.valueOf(recordingModeString);
|
RecordingMode recordingMode = RecordingMode.valueOf(recordingModeString);
|
||||||
builder = builder.recordingMode(recordingMode);
|
builder = builder.recordingMode(recordingMode);
|
||||||
|
} else {
|
||||||
|
builder = builder.recordingMode(RecordingMode.MANUAL);
|
||||||
}
|
}
|
||||||
if (defaultRecordingLayoutString != null) {
|
if (defaultRecordingLayoutString != null) {
|
||||||
RecordingLayout defaultRecordingLayout = RecordingLayout.valueOf(defaultRecordingLayoutString);
|
RecordingLayout defaultRecordingLayout = RecordingLayout.valueOf(defaultRecordingLayoutString);
|
||||||
builder = builder.defaultRecordingLayout(defaultRecordingLayout);
|
builder = builder.defaultRecordingLayout(defaultRecordingLayout);
|
||||||
|
} else {
|
||||||
|
builder.defaultRecordingLayout(RecordingLayout.BEST_FIT);
|
||||||
}
|
}
|
||||||
if (mediaModeString != null) {
|
if (mediaModeString != null) {
|
||||||
MediaMode mediaMode = MediaMode.valueOf(mediaModeString);
|
MediaMode mediaMode = MediaMode.valueOf(mediaModeString);
|
||||||
builder = builder.mediaMode(mediaMode);
|
builder = builder.mediaMode(mediaMode);
|
||||||
|
} else {
|
||||||
|
builder = builder.mediaMode(MediaMode.ROUTED);
|
||||||
}
|
}
|
||||||
|
builder = builder.defaultCustomLayout((defaultCustomLayout != null) ? defaultCustomLayout : "");
|
||||||
|
|
||||||
} catch (IllegalArgumentException e) {
|
} catch (IllegalArgumentException e) {
|
||||||
return this.generateErrorResponse("RecordingMode " + params.get("recordingMode") + " | "
|
return this.generateErrorResponse("RecordingMode " + params.get("recordingMode") + " | "
|
||||||
+ "Default RecordingLayout " + params.get("defaultRecordingLayout") + " | " + "MediaMode "
|
+ "Default RecordingLayout " + params.get("defaultRecordingLayout") + " | " + "MediaMode "
|
||||||
|
@ -120,9 +131,7 @@ public class SessionRestController {
|
||||||
role = ParticipantRole.PUBLISHER;
|
role = ParticipantRole.PUBLISHER;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (metadata == null) {
|
metadata = (metadata != null) ? metadata : "";
|
||||||
metadata = "";
|
|
||||||
}
|
|
||||||
|
|
||||||
String token = sessionManager.newToken(sessionId, role, metadata);
|
String token = sessionManager.newToken(sessionId, role, metadata);
|
||||||
JSONObject responseJson = new JSONObject();
|
JSONObject responseJson = new JSONObject();
|
||||||
|
@ -149,6 +158,7 @@ public class SessionRestController {
|
||||||
String sessionId = (String) params.get("session");
|
String sessionId = (String) params.get("session");
|
||||||
String name = (String) params.get("name");
|
String name = (String) params.get("name");
|
||||||
String recordingLayoutString = (String) params.get("recordingLayout");
|
String recordingLayoutString = (String) params.get("recordingLayout");
|
||||||
|
String customLayout = (String) params.get("customLayout");
|
||||||
|
|
||||||
if (sessionId == null) {
|
if (sessionId == null) {
|
||||||
// "session" parameter not found
|
// "session" parameter not found
|
||||||
|
@ -180,9 +190,11 @@ public class SessionRestController {
|
||||||
} else {
|
} else {
|
||||||
recordingLayout = RecordingLayout.valueOf(recordingLayoutString);
|
recordingLayout = RecordingLayout.valueOf(recordingLayoutString);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
customLayout = (customLayout == null) ? session.getSessionProperties().defaultCustomLayout() : customLayout;
|
||||||
|
|
||||||
Recording startedRecording = this.recordingService.startRecording(session,
|
Recording startedRecording = this.recordingService.startRecording(session,
|
||||||
new RecordingProperties.Builder().name(name).recordingLayout(recordingLayout).build());
|
new RecordingProperties.Builder().name(name).recordingLayout(recordingLayout).customLayout(customLayout).build());
|
||||||
return new ResponseEntity<>(startedRecording.toJson(), HttpStatus.OK);
|
return new ResponseEntity<>(startedRecording.toJson(), HttpStatus.OK);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -39,6 +39,11 @@
|
||||||
"type": "java.lang.String",
|
"type": "java.lang.String",
|
||||||
"description": "Which users will receive a notfication (client events 'recordingStarted' and 'recordingStopped') when recording starts and stops: 'none', 'publisher_moderator', 'all'"
|
"description": "Which users will receive a notfication (client events 'recordingStarted' and 'recordingStopped') when recording starts and stops: 'none', 'publisher_moderator', 'all'"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "openvidu.recording.custom-layout",
|
||||||
|
"type": "java.lang.String",
|
||||||
|
"description": "Where should OpenVidu Server look for custom recording layouts"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "openvidu.recording.version",
|
"name": "openvidu.recording.version",
|
||||||
"type": "java.lang.String",
|
"type": "java.lang.String",
|
||||||
|
|
|
@ -17,4 +17,5 @@ openvidu.cdr: false
|
||||||
openvidu.recording: false
|
openvidu.recording: false
|
||||||
openvidu.recording.path: /opt/openvidu/recordings
|
openvidu.recording.path: /opt/openvidu/recordings
|
||||||
openvidu.recording.public-access: false
|
openvidu.recording.public-access: false
|
||||||
openvidu.recording.notification: publisher_moderator
|
openvidu.recording.notification: publisher_moderator
|
||||||
|
openvidu.recording.custom-layout: /opt/openvidu/custom-layout
|
|
@ -29,7 +29,7 @@ export class TestApirestComponent implements OnInit, OnDestroy {
|
||||||
recordingModes = ['ALWAYS', 'MANUAL'];
|
recordingModes = ['ALWAYS', 'MANUAL'];
|
||||||
selectedRecordingMode = 'MANUAL';
|
selectedRecordingMode = 'MANUAL';
|
||||||
|
|
||||||
defaultRecordingLayouts = ['BEST_FIT'];
|
defaultRecordingLayouts = ['BEST_FIT', 'CUSTOM'];
|
||||||
selectedDefaultRecordingLayout = 'BEST_FIT';
|
selectedDefaultRecordingLayout = 'BEST_FIT';
|
||||||
|
|
||||||
mediaModes = ['ROUTED'];
|
mediaModes = ['ROUTED'];
|
||||||
|
|
Loading…
Reference in New Issue