Migrated openvidu-recording-java to Livekit
This commit is contained in:
parent
8d46598f48
commit
3145698c70
@ -19,14 +19,10 @@
|
||||
|
||||
<properties>
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
<java.version>1.8</java.version>
|
||||
<java.version>11</java.version>
|
||||
<start-class>io.openvidu.recording.java.App</start-class>
|
||||
<docker.image.prefix>openvidu</docker.image.prefix>
|
||||
|
||||
<!-- Test dependencies versions -->
|
||||
<junit.jupiter.version>5.0.3</junit.jupiter.version>
|
||||
<junit.platform.version>1.3.2</junit.platform.version>
|
||||
<selenium-jupiter.version>3.1.0</selenium-jupiter.version>
|
||||
</properties>
|
||||
|
||||
<build>
|
||||
@ -39,6 +35,11 @@
|
||||
</build>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.jetbrains.kotlin</groupId>
|
||||
<artifactId>kotlin-stdlib</artifactId>
|
||||
<version>1.5.21</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
@ -49,55 +50,19 @@
|
||||
<artifactId>spring-boot-devtools</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.openvidu</groupId>
|
||||
<artifactId>openvidu-java-client</artifactId>
|
||||
<version>2.27.0</version>
|
||||
<groupId>io.livekit</groupId>
|
||||
<artifactId>livekit-server</artifactId>
|
||||
<version>0.5.7</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.junit.jupiter</groupId>
|
||||
<artifactId>junit-jupiter-api</artifactId>
|
||||
<scope>test</scope>
|
||||
<groupId>com.squareup.okhttp3</groupId>
|
||||
<artifactId>okhttp</artifactId>
|
||||
<version>4.12.0</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.github.bonigarcia</groupId>
|
||||
<artifactId>selenium-jupiter</artifactId>
|
||||
<version>${selenium-jupiter.version}</version>
|
||||
<scope>test</scope>
|
||||
<groupId>org.json</groupId>
|
||||
<artifactId>json</artifactId>
|
||||
<version>20231013</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.github.bonigarcia</groupId>
|
||||
<artifactId>webdrivermanager</artifactId>
|
||||
<version>2.2.0</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.junit.platform</groupId>
|
||||
<artifactId>junit-platform-runner</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.junit.jupiter</groupId>
|
||||
<artifactId>junit-jupiter-engine</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>ws.schild</groupId>
|
||||
<artifactId>jave-all-deps</artifactId>
|
||||
<version>2.4.6</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>ws.schild</groupId>
|
||||
<artifactId>jave-core</artifactId>
|
||||
<version>2.4.6</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.openvidu</groupId>
|
||||
<artifactId>openvidu-test-browsers</artifactId>
|
||||
<version>1.0.0</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
</project>
|
||||
|
||||
@ -1,402 +1,251 @@
|
||||
package io.openvidu.recording.java;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.StandardCopyOption;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
import javax.annotation.PostConstruct;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.util.ResourceUtils;
|
||||
import org.springframework.web.bind.annotation.CrossOrigin;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMethod;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.JsonArray;
|
||||
import com.google.gson.JsonObject;
|
||||
import org.json.JSONObject;
|
||||
|
||||
import io.openvidu.java.client.ConnectionProperties;
|
||||
import io.openvidu.java.client.ConnectionType;
|
||||
import io.openvidu.java.client.OpenVidu;
|
||||
import io.openvidu.java.client.OpenViduHttpException;
|
||||
import io.openvidu.java.client.OpenViduJavaClientException;
|
||||
import io.openvidu.java.client.OpenViduRole;
|
||||
import io.openvidu.java.client.Recording;
|
||||
import io.openvidu.java.client.RecordingProperties;
|
||||
import io.openvidu.java.client.Session;
|
||||
import io.livekit.server.*;
|
||||
import livekit.LivekitEgress;
|
||||
import livekit.LivekitEgress.EgressInfo;
|
||||
|
||||
import livekit.LivekitEgress.EncodedFileOutput;
|
||||
import livekit.LivekitEgress.EncodedFileType;
|
||||
import livekit.LivekitEgress.EncodedFileOutput.Builder;
|
||||
|
||||
@CrossOrigin(origins = "*")
|
||||
@RestController
|
||||
@RequestMapping("/recording-java/api")
|
||||
public class MyRestController {
|
||||
|
||||
@Value("${LIVEKIT_URL}")
|
||||
private String LIVEKIT_URL;
|
||||
|
||||
@Value("${LIVEKIT_API_KEY}")
|
||||
private String LIVEKIT_API_KEY;
|
||||
|
||||
@Value("${LIVEKIT_API_SECRET}")
|
||||
private String LIVEKIT_API_SECRET;
|
||||
|
||||
@Value("${RECORDINGS_PATH}")
|
||||
private String RECORDINGS_PATH;
|
||||
|
||||
// OpenVidu object as entrypoint of the SDK
|
||||
private OpenVidu openVidu;
|
||||
private EgressServiceClient egressClient;
|
||||
|
||||
// Collection to pair session names and OpenVidu Session objects
|
||||
private Map<String, Session> mapSessions = new ConcurrentHashMap<>();
|
||||
// Collection to pair session names and tokens (the inner Map pairs tokens and
|
||||
// role associated)
|
||||
private Map<String, Map<String, OpenViduRole>> mapSessionNamesTokens = new ConcurrentHashMap<>();
|
||||
// Collection to pair session names and recording objects
|
||||
private Map<String, Boolean> sessionRecordings = new ConcurrentHashMap<>();
|
||||
@PostConstruct()
|
||||
public void initialize() {
|
||||
|
||||
// URL where our OpenVidu server is listening
|
||||
private String OPENVIDU_URL;
|
||||
// Secret shared with our OpenVidu server
|
||||
private String SECRET;
|
||||
|
||||
public MyRestController(@Value("${openvidu.secret}") String secret, @Value("${openvidu.url}") String openviduUrl) {
|
||||
this.SECRET = secret;
|
||||
this.OPENVIDU_URL = openviduUrl;
|
||||
this.openVidu = new OpenVidu(OPENVIDU_URL, SECRET);
|
||||
String livekitUrlHostname = LIVEKIT_URL.replaceFirst("^ws:", "http:").replaceFirst("^wss:", "https:");
|
||||
this.egressClient = EgressServiceClient.create(livekitUrlHostname, LIVEKIT_API_KEY, LIVEKIT_API_SECRET, true);
|
||||
}
|
||||
|
||||
/*******************/
|
||||
/*** Session API ***/
|
||||
/*******************/
|
||||
|
||||
@RequestMapping(value = "/get-token", method = RequestMethod.POST)
|
||||
public ResponseEntity<JsonObject> getToken(@RequestBody Map<String, Object> sessionNameParam) {
|
||||
/**
|
||||
* @param params The JSON object with roomName and participantName
|
||||
* @return The JWT token
|
||||
*/
|
||||
@PostMapping("/token")
|
||||
public ResponseEntity<?> getToken(@RequestBody(required = true) Map<String, String> params) {
|
||||
String roomName = params.get("roomName");
|
||||
String participantName = params.get("participantName");
|
||||
JSONObject response = new JSONObject();
|
||||
|
||||
System.out.println("Getting sessionId and token | {sessionName}=" + sessionNameParam);
|
||||
|
||||
// The video-call to connect ("TUTORIAL")
|
||||
String sessionName = (String) sessionNameParam.get("sessionName");
|
||||
|
||||
// Role associated to this user
|
||||
OpenViduRole role = OpenViduRole.PUBLISHER;
|
||||
|
||||
// Build connectionProperties object with the serverData and the role
|
||||
ConnectionProperties connectionProperties = new ConnectionProperties.Builder().type(ConnectionType.WEBRTC)
|
||||
.role(role).data("user_data").build();
|
||||
|
||||
JsonObject responseJson = new JsonObject();
|
||||
|
||||
if (this.mapSessions.get(sessionName) != null) {
|
||||
// Session already exists
|
||||
System.out.println("Existing session " + sessionName);
|
||||
try {
|
||||
|
||||
// Generate a new token with the recently created connectionProperties
|
||||
String token = this.mapSessions.get(sessionName).createConnection(connectionProperties).getToken();
|
||||
|
||||
// Update our collection storing the new token
|
||||
this.mapSessionNamesTokens.get(sessionName).put(token, role);
|
||||
|
||||
// Prepare the response with the token
|
||||
responseJson.addProperty("0", token);
|
||||
|
||||
// Return the response to the client
|
||||
return new ResponseEntity<>(responseJson, HttpStatus.OK);
|
||||
|
||||
} catch (OpenViduJavaClientException e1) {
|
||||
// If internal error generate an error message and return it to client
|
||||
return getErrorResponse(e1);
|
||||
} catch (OpenViduHttpException e2) {
|
||||
if (404 == e2.getStatus()) {
|
||||
// Invalid sessionId (user left unexpectedly). Session object is not valid
|
||||
// anymore. Clean collections and continue as new session
|
||||
this.mapSessions.remove(sessionName);
|
||||
this.mapSessionNamesTokens.remove(sessionName);
|
||||
}
|
||||
}
|
||||
if (roomName == null || participantName == null) {
|
||||
return new ResponseEntity<>(HttpStatus.BAD_REQUEST);
|
||||
}
|
||||
|
||||
// New session
|
||||
System.out.println("New session " + sessionName);
|
||||
try {
|
||||
// By default, tokens expire 6 hours after generation.
|
||||
// You may override this by using token.setTtl(long millis).
|
||||
AccessToken token = new AccessToken(LIVEKIT_API_KEY, LIVEKIT_API_SECRET);
|
||||
token.setName(participantName);
|
||||
token.setIdentity(participantName);
|
||||
|
||||
// Create a new OpenVidu Session
|
||||
Session session = this.openVidu.createSession();
|
||||
// Generate a new token with the recently created connectionProperties
|
||||
String token = session.createConnection(connectionProperties).getToken();
|
||||
JSONObject metadata = new JSONObject();
|
||||
metadata.put("livekitUrl", LIVEKIT_URL);
|
||||
// add metadata to the token, which will be available in the participant's
|
||||
// metadata
|
||||
token.setMetadata(metadata.toString());
|
||||
token.addGrants(new RoomJoin(true), new RoomName(roomName));
|
||||
|
||||
// Store the session and the token in our collections
|
||||
this.mapSessions.put(sessionName, session);
|
||||
this.mapSessionNamesTokens.put(sessionName, new ConcurrentHashMap<>());
|
||||
this.mapSessionNamesTokens.get(sessionName).put(token, role);
|
||||
|
||||
// Prepare the response with the sessionId and the token
|
||||
responseJson.addProperty("0", token);
|
||||
|
||||
// Return the response to the client
|
||||
return new ResponseEntity<>(responseJson, HttpStatus.OK);
|
||||
|
||||
} catch (Exception e) {
|
||||
// If error generate an error message and return it to client
|
||||
return getErrorResponse(e);
|
||||
}
|
||||
}
|
||||
|
||||
@RequestMapping(value = "/remove-user", method = RequestMethod.POST)
|
||||
public ResponseEntity<JsonObject> removeUser(@RequestBody Map<String, Object> sessionNameToken) throws Exception {
|
||||
|
||||
System.out.println("Removing user | {sessionName, token}=" + sessionNameToken);
|
||||
|
||||
// Retrieve the params from BODY
|
||||
String sessionName = (String) sessionNameToken.get("sessionName");
|
||||
String token = (String) sessionNameToken.get("token");
|
||||
|
||||
// If the session exists
|
||||
if (this.mapSessions.get(sessionName) != null && this.mapSessionNamesTokens.get(sessionName) != null) {
|
||||
|
||||
// If the token exists
|
||||
if (this.mapSessionNamesTokens.get(sessionName).remove(token) != null) {
|
||||
// User left the session
|
||||
if (this.mapSessionNamesTokens.get(sessionName).isEmpty()) {
|
||||
// Last user left: session must be removed
|
||||
this.mapSessions.remove(sessionName);
|
||||
}
|
||||
return new ResponseEntity<>(HttpStatus.OK);
|
||||
} else {
|
||||
// The TOKEN wasn't valid
|
||||
System.out.println("Problems in the app server: the TOKEN wasn't valid");
|
||||
return new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR);
|
||||
}
|
||||
|
||||
} else {
|
||||
// The SESSION does not exist
|
||||
System.out.println("Problems in the app server: the SESSION does not exist");
|
||||
return new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR);
|
||||
}
|
||||
}
|
||||
|
||||
@RequestMapping(value = "/close-session", method = RequestMethod.DELETE)
|
||||
public ResponseEntity<JsonObject> closeSession(@RequestBody Map<String, Object> sessionName) throws Exception {
|
||||
|
||||
System.out.println("Closing session | {sessionName}=" + sessionName);
|
||||
|
||||
// Retrieve the param from BODY
|
||||
String session = (String) sessionName.get("sessionName");
|
||||
|
||||
// If the session exists
|
||||
if (this.mapSessions.get(session) != null && this.mapSessionNamesTokens.get(session) != null) {
|
||||
Session s = this.mapSessions.get(session);
|
||||
s.close();
|
||||
this.mapSessions.remove(session);
|
||||
this.mapSessionNamesTokens.remove(session);
|
||||
this.sessionRecordings.remove(s.getSessionId());
|
||||
return new ResponseEntity<>(HttpStatus.OK);
|
||||
} else {
|
||||
// The SESSION does not exist
|
||||
System.out.println("Problems in the app server: the SESSION does not exist");
|
||||
return new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR);
|
||||
}
|
||||
}
|
||||
|
||||
@RequestMapping(value = "/fetch-info", method = RequestMethod.POST)
|
||||
public ResponseEntity<JsonObject> fetchInfo(@RequestBody Map<String, Object> sessionName) {
|
||||
try {
|
||||
System.out.println("Fetching session info | {sessionName}=" + sessionName);
|
||||
|
||||
// Retrieve the param from BODY
|
||||
String session = (String) sessionName.get("sessionName");
|
||||
|
||||
// If the session exists
|
||||
if (this.mapSessions.get(session) != null && this.mapSessionNamesTokens.get(session) != null) {
|
||||
Session s = this.mapSessions.get(session);
|
||||
boolean changed = s.fetch();
|
||||
System.out.println("Any change: " + changed);
|
||||
return new ResponseEntity<>(this.sessionToJson(s), HttpStatus.OK);
|
||||
} else {
|
||||
// The SESSION does not exist
|
||||
System.out.println("Problems in the app server: the SESSION does not exist");
|
||||
return new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR);
|
||||
}
|
||||
} catch (OpenViduJavaClientException | OpenViduHttpException e) {
|
||||
e.printStackTrace();
|
||||
return getErrorResponse(e);
|
||||
}
|
||||
}
|
||||
|
||||
@RequestMapping(value = "/fetch-all", method = RequestMethod.GET)
|
||||
public ResponseEntity<?> fetchAll() {
|
||||
try {
|
||||
System.out.println("Fetching all session info");
|
||||
boolean changed = this.openVidu.fetch();
|
||||
System.out.println("Any change: " + changed);
|
||||
JsonArray jsonArray = new JsonArray();
|
||||
for (Session s : this.openVidu.getActiveSessions()) {
|
||||
jsonArray.add(this.sessionToJson(s));
|
||||
}
|
||||
return new ResponseEntity<>(jsonArray, HttpStatus.OK);
|
||||
} catch (OpenViduJavaClientException | OpenViduHttpException e) {
|
||||
e.printStackTrace();
|
||||
return getErrorResponse(e);
|
||||
}
|
||||
}
|
||||
|
||||
@RequestMapping(value = "/force-disconnect", method = RequestMethod.DELETE)
|
||||
public ResponseEntity<JsonObject> forceDisconnect(@RequestBody Map<String, Object> params) {
|
||||
try {
|
||||
// Retrieve the param from BODY
|
||||
String session = (String) params.get("sessionName");
|
||||
String connectionId = (String) params.get("connectionId");
|
||||
|
||||
// If the session exists
|
||||
if (this.mapSessions.get(session) != null && this.mapSessionNamesTokens.get(session) != null) {
|
||||
Session s = this.mapSessions.get(session);
|
||||
s.forceDisconnect(connectionId);
|
||||
return new ResponseEntity<>(HttpStatus.OK);
|
||||
} else {
|
||||
// The SESSION does not exist
|
||||
return new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR);
|
||||
}
|
||||
} catch (OpenViduJavaClientException | OpenViduHttpException e) {
|
||||
e.printStackTrace();
|
||||
return getErrorResponse(e);
|
||||
}
|
||||
}
|
||||
|
||||
@RequestMapping(value = "/force-unpublish", method = RequestMethod.DELETE)
|
||||
public ResponseEntity<JsonObject> forceUnpublish(@RequestBody Map<String, Object> params) {
|
||||
try {
|
||||
// Retrieve the param from BODY
|
||||
String session = (String) params.get("sessionName");
|
||||
String streamId = (String) params.get("streamId");
|
||||
|
||||
// If the session exists
|
||||
if (this.mapSessions.get(session) != null && this.mapSessionNamesTokens.get(session) != null) {
|
||||
Session s = this.mapSessions.get(session);
|
||||
s.forceUnpublish(streamId);
|
||||
return new ResponseEntity<>(HttpStatus.OK);
|
||||
} else {
|
||||
// The SESSION does not exist
|
||||
return new ResponseEntity<>(HttpStatus.NOT_FOUND);
|
||||
}
|
||||
} catch (OpenViduJavaClientException | OpenViduHttpException e) {
|
||||
e.printStackTrace();
|
||||
return getErrorResponse(e);
|
||||
}
|
||||
response.put("token", token.toJwt());
|
||||
return new ResponseEntity<>(response.toMap(), HttpStatus.OK);
|
||||
}
|
||||
|
||||
/*******************/
|
||||
/** Recording API **/
|
||||
/*******************/
|
||||
|
||||
@RequestMapping(value = "/recording/start", method = RequestMethod.POST)
|
||||
@RequestMapping(value = "/recordings/start", method = RequestMethod.POST)
|
||||
public ResponseEntity<?> startRecording(@RequestBody Map<String, Object> params) {
|
||||
String sessionId = (String) params.get("session");
|
||||
Recording.OutputMode outputMode = Recording.OutputMode.valueOf((String) params.get("outputMode"));
|
||||
boolean hasAudio = (boolean) params.get("hasAudio");
|
||||
boolean hasVideo = (boolean) params.get("hasVideo");
|
||||
|
||||
RecordingProperties properties = new RecordingProperties.Builder().outputMode(outputMode).hasAudio(hasAudio)
|
||||
.hasVideo(hasVideo).build();
|
||||
|
||||
System.out.println("Starting recording for session " + sessionId + " with properties {outputMode=" + outputMode
|
||||
+ ", hasAudio=" + hasAudio + ", hasVideo=" + hasVideo + "}");
|
||||
|
||||
try {
|
||||
Recording recording = this.openVidu.startRecording(sessionId, properties);
|
||||
this.sessionRecordings.put(sessionId, true);
|
||||
return new ResponseEntity<>(recording, HttpStatus.OK);
|
||||
} catch (OpenViduJavaClientException | OpenViduHttpException e) {
|
||||
return new ResponseEntity<>(e.getMessage(), HttpStatus.BAD_REQUEST);
|
||||
String roomName = (String) params.get("roomName");
|
||||
String outputMode = (String) params.get("outputMode");
|
||||
Boolean videoOnly = (Boolean) params.get("videoOnly");
|
||||
Boolean audioOnly = (Boolean) params.get("audioOnly");
|
||||
String audioTrackId = (String) params.get("audioTrackId");
|
||||
String videoTrackId = (String) params.get("videoTrackId");
|
||||
|
||||
Builder outputBuilder = LivekitEgress.EncodedFileOutput.newBuilder()
|
||||
.setFileType(EncodedFileType.DEFAULT_FILETYPE)
|
||||
.setFilepath("/recordings/" + roomName + "-" + new Date().getTime())
|
||||
.setDisableManifest(true);
|
||||
|
||||
EncodedFileOutput output = outputBuilder.build();
|
||||
|
||||
System.out.println("Starting recording " + roomName);
|
||||
|
||||
LivekitEgress.EgressInfo egressInfo;
|
||||
|
||||
if ("COMPOSED".equals(outputMode)) {
|
||||
|
||||
System.out.println("Starting COMPOSED recording " + roomName);
|
||||
egressInfo = this.egressClient
|
||||
.startRoomCompositeEgress(roomName, output, "grid", null, null, audioOnly, videoOnly)
|
||||
.execute().body();
|
||||
} else if ("INDIVIDUAL".equals(outputMode)) {
|
||||
System.out.println("Starting INDIVIDUAL recording " + roomName);
|
||||
egressInfo = this.egressClient.startTrackCompositeEgress(roomName, output, audioTrackId, videoTrackId)
|
||||
.execute().body();
|
||||
} else {
|
||||
return ResponseEntity.badRequest().body("outputMode is required");
|
||||
}
|
||||
|
||||
return ResponseEntity.ok().body(generateEgressInfoResponse(egressInfo));
|
||||
|
||||
} catch (Exception e) {
|
||||
System.out.println("Error starting recording " + e.getMessage());
|
||||
return ResponseEntity.badRequest().body("Error starting recording");
|
||||
}
|
||||
}
|
||||
|
||||
@RequestMapping(value = "/recording/stop", method = RequestMethod.POST)
|
||||
@RequestMapping(value = "/recordings/stop", method = RequestMethod.POST)
|
||||
public ResponseEntity<?> stopRecording(@RequestBody Map<String, Object> params) {
|
||||
String recordingId = (String) params.get("recording");
|
||||
|
||||
String recordingId = (String) params.get("recordingId");
|
||||
|
||||
if (recordingId == null) {
|
||||
return ResponseEntity.badRequest().body("recordingId is required");
|
||||
}
|
||||
|
||||
System.out.println("Stoping recording | {recordingId}=" + recordingId);
|
||||
|
||||
try {
|
||||
Recording recording = this.openVidu.stopRecording(recordingId);
|
||||
this.sessionRecordings.remove(recording.getSessionId());
|
||||
return new ResponseEntity<>(recording, HttpStatus.OK);
|
||||
} catch (OpenViduJavaClientException | OpenViduHttpException e) {
|
||||
return new ResponseEntity<>(e.getMessage(), HttpStatus.BAD_REQUEST);
|
||||
LivekitEgress.EgressInfo egressInfo = this.egressClient.stopEgress(recordingId).execute().body();
|
||||
return ResponseEntity.ok().body(generateEgressInfoResponse(egressInfo));
|
||||
} catch (Exception e) {
|
||||
System.out.println("Error stoping recording " + e.getMessage());
|
||||
return ResponseEntity.badRequest().body("Error stoping recording");
|
||||
}
|
||||
}
|
||||
|
||||
@RequestMapping(value = "/recording/delete", method = RequestMethod.DELETE)
|
||||
public ResponseEntity<?> deleteRecording(@RequestBody Map<String, Object> params) {
|
||||
String recordingId = (String) params.get("recording");
|
||||
|
||||
System.out.println("Deleting recording | {recordingId}=" + recordingId);
|
||||
@RequestMapping(value = "/recordings", method = RequestMethod.DELETE)
|
||||
public ResponseEntity<?> deleteRecordings() {
|
||||
|
||||
try {
|
||||
this.openVidu.deleteRecording(recordingId);
|
||||
return new ResponseEntity<>(HttpStatus.OK);
|
||||
} catch (OpenViduJavaClientException | OpenViduHttpException e) {
|
||||
return new ResponseEntity<>(e.getMessage(), HttpStatus.BAD_REQUEST);
|
||||
File recordingsDir = ResourceUtils.getFile("classpath:static");
|
||||
deleteFiles(new File(RECORDINGS_PATH));
|
||||
deleteFiles(new File(recordingsDir.getAbsolutePath()));
|
||||
JSONObject response = new JSONObject();
|
||||
response.put("message", "All recordings deleted");
|
||||
|
||||
return ResponseEntity.ok().body(response.toMap());
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("Error deleting recordings");
|
||||
}
|
||||
}
|
||||
|
||||
@RequestMapping(value = "/recording/get/{recordingId}", method = RequestMethod.GET)
|
||||
public ResponseEntity<?> getRecording(@PathVariable(value = "recordingId") String recordingId) {
|
||||
|
||||
System.out.println("Getting recording | {recordingId}=" + recordingId);
|
||||
|
||||
try {
|
||||
Recording recording = this.openVidu.getRecording(recordingId);
|
||||
return new ResponseEntity<>(recording, HttpStatus.OK);
|
||||
} catch (OpenViduJavaClientException | OpenViduHttpException e) {
|
||||
return new ResponseEntity<>(e.getMessage(), HttpStatus.BAD_REQUEST);
|
||||
}
|
||||
}
|
||||
|
||||
@RequestMapping(value = "/recording/list", method = RequestMethod.GET)
|
||||
@RequestMapping(value = "/recordings/list", method = RequestMethod.GET)
|
||||
public ResponseEntity<?> listRecordings() {
|
||||
|
||||
System.out.println("Listing recordings");
|
||||
|
||||
try {
|
||||
List<Recording> recordings = this.openVidu.listRecordings();
|
||||
List<JSONObject> recordings = new ArrayList<>();
|
||||
|
||||
return new ResponseEntity<>(recordings, HttpStatus.OK);
|
||||
} catch (OpenViduJavaClientException | OpenViduHttpException e) {
|
||||
return new ResponseEntity<>(e.getMessage(), HttpStatus.BAD_REQUEST);
|
||||
try {
|
||||
File recordingsDir = ResourceUtils.getFile("classpath:static");
|
||||
Files.walk(Path.of(RECORDINGS_PATH)).forEach(filePath -> {
|
||||
JSONObject recordingsMap = new JSONObject();
|
||||
|
||||
if (Files.isRegularFile(filePath)) {
|
||||
String fileName = filePath.getFileName().toString();
|
||||
String destinationPath = recordingsDir.getAbsolutePath() + File.separator + fileName;
|
||||
|
||||
try {
|
||||
Files.copy(filePath, Path.of(destinationPath), StandardCopyOption.REPLACE_EXISTING);
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
|
||||
recordingsMap.put("name", fileName);
|
||||
recordingsMap.put("path", "/" + fileName);
|
||||
|
||||
recordings.add(recordingsMap);
|
||||
}
|
||||
});
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
return new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR);
|
||||
}
|
||||
|
||||
JSONObject response = new JSONObject();
|
||||
response.put("recordings", recordings);
|
||||
return new ResponseEntity<>(response.toMap(), HttpStatus.OK);
|
||||
}
|
||||
|
||||
private void deleteFiles(File directory) {
|
||||
if (directory.exists() && directory.isDirectory()) {
|
||||
File[] files = directory.listFiles();
|
||||
if (files != null) {
|
||||
for (File file : files) {
|
||||
if (file.isDirectory()) {
|
||||
deleteFiles(file);
|
||||
} else {
|
||||
if (file.delete()) {
|
||||
System.out.println("Deleted file: " + file.getAbsolutePath());
|
||||
} else {
|
||||
System.out.println("Failed to delete file: " + file.getAbsolutePath());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private ResponseEntity<JsonObject> getErrorResponse(Exception e) {
|
||||
JsonObject json = new JsonObject();
|
||||
json.addProperty("cause", e.getCause().toString());
|
||||
json.addProperty("error", e.getMessage());
|
||||
json.addProperty("exception", e.getClass().getCanonicalName());
|
||||
return new ResponseEntity<>(json, HttpStatus.INTERNAL_SERVER_ERROR);
|
||||
}
|
||||
private Map<String, Object> generateEgressInfoResponse(EgressInfo egressInfo) {
|
||||
JSONObject info = new JSONObject();
|
||||
JSONObject response = new JSONObject();
|
||||
|
||||
info.put("egressId", egressInfo.getEgressId());
|
||||
info.put("roomName", egressInfo.getRoomName());
|
||||
info.put("status", egressInfo.getStatus().toString());
|
||||
|
||||
response.put("info", info);
|
||||
return response.toMap();
|
||||
|
||||
protected JsonObject sessionToJson(Session session) {
|
||||
Gson gson = new Gson();
|
||||
JsonObject json = new JsonObject();
|
||||
json.addProperty("sessionId", session.getSessionId());
|
||||
json.addProperty("customSessionId", session.getProperties().customSessionId());
|
||||
json.addProperty("recording", session.isBeingRecorded());
|
||||
json.addProperty("mediaMode", session.getProperties().mediaMode().name());
|
||||
json.addProperty("recordingMode", session.getProperties().recordingMode().name());
|
||||
json.add("defaultRecordingProperties",
|
||||
gson.toJsonTree(session.getProperties().defaultRecordingProperties()).getAsJsonObject());
|
||||
JsonObject connections = new JsonObject();
|
||||
connections.addProperty("numberOfElements", session.getConnections().size());
|
||||
JsonArray jsonArrayConnections = new JsonArray();
|
||||
session.getConnections().forEach(con -> {
|
||||
JsonObject c = new JsonObject();
|
||||
c.addProperty("connectionId", con.getConnectionId());
|
||||
c.addProperty("role", con.getRole().name());
|
||||
c.addProperty("token", con.getToken());
|
||||
c.addProperty("clientData", con.getClientData());
|
||||
c.addProperty("serverData", con.getServerData());
|
||||
JsonArray pubs = new JsonArray();
|
||||
con.getPublishers().forEach(p -> {
|
||||
pubs.add(gson.toJsonTree(p).getAsJsonObject());
|
||||
});
|
||||
JsonArray subs = new JsonArray();
|
||||
con.getSubscribers().forEach(s -> {
|
||||
subs.add(s);
|
||||
});
|
||||
c.add("publishers", pubs);
|
||||
c.add("subscribers", subs);
|
||||
jsonArrayConnections.add(c);
|
||||
});
|
||||
connections.add("content", jsonArrayConnections);
|
||||
json.add("connections", connections);
|
||||
return json;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -3,6 +3,7 @@ spring.profiles.active=container
|
||||
server.port: 3000
|
||||
server.ssl.enabled: false
|
||||
|
||||
openvidu.url: http://localhost:5000/
|
||||
openvidu.secret: MY_SECRET
|
||||
openvidu.publicurl: ngrok
|
||||
|
||||
LIVEKIT_URL: ws://localhost:7880/
|
||||
LIVEKIT_API_KEY: http://localhost:4443/
|
||||
LIVEKIT_API_SECRET: MY_SECRET
|
||||
@ -6,5 +6,7 @@ server.ssl.key-store-type: JKS
|
||||
server.ssl.key-alias: openvidu-selfsigned
|
||||
spring.http.converters.preferred-json-mapper=gson
|
||||
|
||||
openvidu.url: http://localhost:4443/
|
||||
openvidu.secret: MY_SECRET
|
||||
LIVEKIT_URL: ws://localhost:7880/
|
||||
LIVEKIT_API_KEY: key1
|
||||
LIVEKIT_API_SECRET: abcdefghijklmnopqrstuvwxyz123456
|
||||
RECORDINGS_PATH: ../../openvidu-lk/local-deployment/cluster/recordings
|
||||
@ -1,415 +1,289 @@
|
||||
var OV;
|
||||
var session;
|
||||
|
||||
var sessionName;
|
||||
var LivekitClient = window.LivekitClient;
|
||||
var room;
|
||||
var myRoomName;
|
||||
var token;
|
||||
var nickname;
|
||||
var numVideos = 0;
|
||||
|
||||
var localVideoPublication;
|
||||
var localAudioPublication;
|
||||
|
||||
/* OPENVIDU METHODS */
|
||||
|
||||
function joinSession() {
|
||||
|
||||
function joinRoom() {
|
||||
// --- 0) Change the button ---
|
||||
|
||||
document.getElementById("join-btn").disabled = true;
|
||||
document.getElementById("join-btn").innerHTML = "Joining...";
|
||||
|
||||
getToken(function () {
|
||||
document.getElementById('join-btn').disabled = true;
|
||||
document.getElementById('join-btn').innerHTML = 'Joining...';
|
||||
const myParticipantName = `Participant${Math.floor(Math.random() * 100)}`;
|
||||
const myRoomName = $('#roomName').val();
|
||||
|
||||
// --- 1) Get an OpenVidu object ---
|
||||
room = new LivekitClient.Room();
|
||||
|
||||
OV = new OpenVidu();
|
||||
|
||||
// --- 2) Init a session ---
|
||||
|
||||
session = OV.initSession();
|
||||
|
||||
// --- 3) Specify the actions when events take place in the session ---
|
||||
|
||||
session.on('connectionCreated', event => {
|
||||
pushEvent(event);
|
||||
});
|
||||
|
||||
session.on('connectionDestroyed', event => {
|
||||
pushEvent(event);
|
||||
});
|
||||
|
||||
// On every new Stream received...
|
||||
session.on('streamCreated', event => {
|
||||
pushEvent(event);
|
||||
|
||||
// Subscribe to the Stream to receive it
|
||||
// HTML video will be appended to element with 'video-container' id
|
||||
var subscriber = session.subscribe(event.stream, 'video-container');
|
||||
|
||||
// When the HTML video has been appended to DOM...
|
||||
subscriber.on('videoElementCreated', event => {
|
||||
pushEvent(event);
|
||||
// Add a new HTML element for the user's name and nickname over its video
|
||||
room.on(
|
||||
LivekitClient.RoomEvent.TrackSubscribed,
|
||||
(track, publication, participant) => {
|
||||
const element = track.attach();
|
||||
element.id = track.sid;
|
||||
document.getElementById('video-container').appendChild(element);
|
||||
if (track.kind === 'video') {
|
||||
var audioTrackId;
|
||||
var videoTrackId;
|
||||
participant.getTracks().forEach((track) => {
|
||||
if (track.kind === 'audio') {
|
||||
audioTrackId = track.trackInfo.sid;
|
||||
} else if (track.kind === 'video') {
|
||||
videoTrackId = track.trackInfo.sid;
|
||||
}
|
||||
});
|
||||
addIndividualRecordingButton(element.id, videoTrackId, audioTrackId);
|
||||
updateNumVideos(1);
|
||||
});
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// When the HTML video has been appended to DOM...
|
||||
subscriber.on('videoElementDestroyed', event => {
|
||||
pushEvent(event);
|
||||
// Add a new HTML element for the user's name and nickname over its video
|
||||
// On every new Track destroyed...
|
||||
room.on(
|
||||
LivekitClient.RoomEvent.TrackUnsubscribed,
|
||||
(track, publication, participant) => {
|
||||
track.detach();
|
||||
document.getElementById(track.sid)?.remove();
|
||||
if (track.kind === 'video') {
|
||||
// removeUserData(participant);
|
||||
updateNumVideos(-1);
|
||||
});
|
||||
|
||||
// When the subscriber stream has started playing media...
|
||||
subscriber.on('streamPlaying', event => {
|
||||
pushEvent(event);
|
||||
});
|
||||
});
|
||||
|
||||
session.on('streamDestroyed', event => {
|
||||
pushEvent(event);
|
||||
});
|
||||
|
||||
session.on('sessionDisconnected', event => {
|
||||
pushEvent(event);
|
||||
if (event.reason !== 'disconnect') {
|
||||
removeUser();
|
||||
}
|
||||
if (event.reason !== 'sessionClosedByServer') {
|
||||
session = null;
|
||||
numVideos = 0;
|
||||
$('#join').show();
|
||||
$('#session').hide();
|
||||
}
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
session.on('recordingStarted', event => {
|
||||
pushEvent(event);
|
||||
});
|
||||
room.on(LivekitClient.RoomEvent.RecordingStatusChanged, (isRecording) => {
|
||||
console.log('Recording status changed: ' + status);
|
||||
if (!isRecording) {
|
||||
listRecordings();
|
||||
}
|
||||
});
|
||||
|
||||
session.on('recordingStopped', event => {
|
||||
pushEvent(event);
|
||||
});
|
||||
getToken(myRoomName, myParticipantName).then(async (token) => {
|
||||
const livekitUrl = getLivekitUrlFromMetadata(token);
|
||||
|
||||
// On every asynchronous exception...
|
||||
session.on('exception', (exception) => {
|
||||
console.warn(exception);
|
||||
});
|
||||
try {
|
||||
await room.connect(livekitUrl, token);
|
||||
|
||||
// --- 4) Connect to the session passing the retrieved token and some more data from
|
||||
// the client (in this case a JSON with the nickname chosen by the user) ---
|
||||
var participantName = $('#user').val();
|
||||
$('#room-title').text(myRoomName);
|
||||
$('#join').hide();
|
||||
$('#room').show();
|
||||
|
||||
session.connect(token)
|
||||
.then(() => {
|
||||
const [audioPublication, videoPublication] = await Promise.all([
|
||||
room.localParticipant.setMicrophoneEnabled(true),
|
||||
room.localParticipant.setCameraEnabled(true),
|
||||
]);
|
||||
localVideoPublication = videoPublication;
|
||||
localAudioPublication = audioPublication;
|
||||
|
||||
// --- 5) Set page layout for active call ---
|
||||
|
||||
$('#session-title').text(sessionName);
|
||||
$('#join').hide();
|
||||
$('#session').show();
|
||||
|
||||
// --- 6) Get your own camera stream ---
|
||||
|
||||
var publisher = OV.initPublisher('video-container', {
|
||||
audioSource: undefined, // The source of audio. If undefined default microphone
|
||||
videoSource: undefined, // The source of video. If undefined default webcam
|
||||
publishAudio: true, // Whether you want to start publishing with your audio unmuted or not
|
||||
publishVideo: true, // Whether you want to start publishing with your video enabled or not
|
||||
resolution: '640x480', // The resolution of your video
|
||||
frameRate: 30, // The frame rate of your video
|
||||
insertMode: 'APPEND', // How the video is inserted in the target element 'video-container'
|
||||
mirror: false // Whether to mirror your local video or not
|
||||
});
|
||||
|
||||
// --- 7) Specify the actions when events take place in our publisher ---
|
||||
|
||||
// When the publisher stream has started playing media...
|
||||
publisher.on('accessAllowed', event => {
|
||||
pushEvent({
|
||||
type: 'accessAllowed'
|
||||
});
|
||||
});
|
||||
|
||||
publisher.on('accessDenied', event => {
|
||||
pushEvent(event);
|
||||
});
|
||||
|
||||
publisher.on('accessDialogOpened', event => {
|
||||
pushEvent({
|
||||
type: 'accessDialogOpened'
|
||||
});
|
||||
});
|
||||
|
||||
publisher.on('accessDialogClosed', event => {
|
||||
pushEvent({
|
||||
type: 'accessDialogClosed'
|
||||
});
|
||||
});
|
||||
|
||||
// When the publisher stream has started playing media...
|
||||
publisher.on('streamCreated', event => {
|
||||
pushEvent(event);
|
||||
});
|
||||
|
||||
// When our HTML video has been added to DOM...
|
||||
publisher.on('videoElementCreated', event => {
|
||||
pushEvent(event);
|
||||
updateNumVideos(1);
|
||||
$(event.element).prop('muted', true); // Mute local video
|
||||
});
|
||||
|
||||
// When the HTML video has been appended to DOM...
|
||||
publisher.on('videoElementDestroyed', event => {
|
||||
pushEvent(event);
|
||||
// Add a new HTML element for the user's name and nickname over its video
|
||||
updateNumVideos(-1);
|
||||
});
|
||||
|
||||
// When the publisher stream has started playing media...
|
||||
publisher.on('streamPlaying', event => {
|
||||
pushEvent(event);
|
||||
});
|
||||
|
||||
// --- 8) Publish your stream ---
|
||||
|
||||
session.publish(publisher);
|
||||
|
||||
})
|
||||
.catch(error => {
|
||||
console.warn('There was an error connecting to the session:', error.code, error.message);
|
||||
enableBtn();
|
||||
});
|
||||
console.log('Connected to room ' + myRoomName);
|
||||
const element = videoPublication.track.attach();
|
||||
element.id = videoPublication.track.sid;
|
||||
document.getElementById('video-container').appendChild(element);
|
||||
addIndividualRecordingButton(
|
||||
element.id,
|
||||
videoPublication.track.sid,
|
||||
audioPublication.track.sid
|
||||
);
|
||||
updateNumVideos(1);
|
||||
} catch (error) {
|
||||
console.warn(
|
||||
'There was an error connecting to the room:',
|
||||
error.code,
|
||||
error.message
|
||||
);
|
||||
enableBtn();
|
||||
}
|
||||
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
function leaveSession() {
|
||||
function leaveRoom() {
|
||||
room.disconnect();
|
||||
room = null;
|
||||
|
||||
$('#video-container').empty();
|
||||
numVideos = 0;
|
||||
|
||||
$('#join').show();
|
||||
$('#room').hide();
|
||||
|
||||
// --- 9) Leave the session by calling 'disconnect' method over the Session object ---
|
||||
session.disconnect();
|
||||
enableBtn();
|
||||
|
||||
}
|
||||
|
||||
/* OPENVIDU METHODS */
|
||||
|
||||
function enableBtn (){
|
||||
document.getElementById("join-btn").disabled = false;
|
||||
document.getElementById("join-btn").innerHTML = "Join!";
|
||||
function enableBtn() {
|
||||
document.getElementById('join-btn').disabled = false;
|
||||
document.getElementById('join-btn').innerHTML = 'Join!';
|
||||
}
|
||||
|
||||
/* APPLICATION REST METHODS */
|
||||
|
||||
function getToken(callback) {
|
||||
sessionName = $("#sessionName").val(); // Video-call chosen by the user
|
||||
|
||||
httpRequest(
|
||||
'POST',
|
||||
'recording-java/api/get-token', {
|
||||
sessionName: sessionName
|
||||
},
|
||||
'Request of TOKEN gone WRONG:',
|
||||
res => {
|
||||
token = res[0]; // Get token from response
|
||||
console.warn('Request of TOKEN gone WELL (TOKEN:' + token + ')');
|
||||
callback(token); // Continue the join operation
|
||||
}
|
||||
);
|
||||
function getToken(roomName, participantName) {
|
||||
return new Promise((resolve, reject) => {
|
||||
// Video-call chosen by the user
|
||||
httpRequest(
|
||||
'POST',
|
||||
'token',
|
||||
{ roomName, participantName },
|
||||
'Error generating token',
|
||||
(response) => resolve(response.token)
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
function removeUser() {
|
||||
httpRequest(
|
||||
'POST',
|
||||
'recording-java/api/remove-user', {
|
||||
sessionName: sessionName,
|
||||
token: token
|
||||
},
|
||||
'User couldn\'t be removed from session',
|
||||
res => {
|
||||
console.warn("You have been removed from session " + sessionName);
|
||||
}
|
||||
);
|
||||
}
|
||||
async function httpRequest(method, url, body, errorMsg, successCallback) {
|
||||
try {
|
||||
const response = await fetch(url, {
|
||||
method,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: method === 'GET' ? undefined : JSON.stringify(body),
|
||||
});
|
||||
|
||||
function closeSession() {
|
||||
httpRequest(
|
||||
'DELETE',
|
||||
'recording-java/api/close-session', {
|
||||
sessionName: sessionName
|
||||
},
|
||||
'Session couldn\'t be closed',
|
||||
res => {
|
||||
console.warn("Session " + sessionName + " has been closed");
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
function fetchInfo() {
|
||||
httpRequest(
|
||||
'POST',
|
||||
'recording-java/api/fetch-info', {
|
||||
sessionName: sessionName
|
||||
},
|
||||
'Session couldn\'t be fetched',
|
||||
res => {
|
||||
console.warn("Session info has been fetched");
|
||||
$('#textarea-http').text(JSON.stringify(res, null, "\t"));
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
function fetchAll() {
|
||||
httpRequest(
|
||||
'GET',
|
||||
'recording-java/api/fetch-all', {},
|
||||
'All session info couldn\'t be fetched',
|
||||
res => {
|
||||
console.warn("All session info has been fetched");
|
||||
$('#textarea-http').text(JSON.stringify(res, null, "\t"));
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
function forceDisconnect() {
|
||||
httpRequest(
|
||||
'DELETE',
|
||||
'recording-java/api/force-disconnect', {
|
||||
sessionName: sessionName,
|
||||
connectionId: document.getElementById('forceValue').value
|
||||
},
|
||||
'Connection couldn\'t be closed',
|
||||
res => {
|
||||
console.warn("Connection has been closed");
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
function forceUnpublish() {
|
||||
httpRequest(
|
||||
'DELETE',
|
||||
'recording-java/api/force-unpublish', {
|
||||
sessionName: sessionName,
|
||||
streamId: document.getElementById('forceValue').value
|
||||
},
|
||||
'Stream couldn\'t be closed',
|
||||
res => {
|
||||
console.warn("Stream has been closed");
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
function httpRequest(method, url, body, errorMsg, callback) {
|
||||
$('#textarea-http').text('');
|
||||
var http = new XMLHttpRequest();
|
||||
http.open(method, url, true);
|
||||
http.setRequestHeader('Content-type', 'application/json');
|
||||
http.addEventListener('readystatechange', processRequest, false);
|
||||
http.send(JSON.stringify(body));
|
||||
|
||||
function processRequest() {
|
||||
if (http.readyState == 4) {
|
||||
if (http.status == 200) {
|
||||
try {
|
||||
callback(JSON.parse(http.responseText));
|
||||
} catch (e) {
|
||||
callback(e);
|
||||
}
|
||||
} else {
|
||||
console.warn(errorMsg + ' (' + http.status + ')');
|
||||
console.warn(http.responseText);
|
||||
$('#textarea-http').text(errorMsg + ": HTTP " + http.status + " (" + http.responseText + ")");
|
||||
}
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
successCallback(data);
|
||||
} else {
|
||||
console.warn(errorMsg);
|
||||
console.warn('Error: ' + response.statusText);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
}
|
||||
|
||||
function startRecording() {
|
||||
var outputMode = $('input[name=outputMode]:checked').val();
|
||||
function startComposedRecording() {
|
||||
var hasAudio = $('#has-audio-checkbox').prop('checked');
|
||||
var hasVideo = $('#has-video-checkbox').prop('checked');
|
||||
|
||||
httpRequest(
|
||||
'POST',
|
||||
'recording-java/api/recording/start', {
|
||||
session: session.sessionId,
|
||||
outputMode: outputMode,
|
||||
hasAudio: hasAudio,
|
||||
hasVideo: hasVideo
|
||||
'recordings/start',
|
||||
{
|
||||
roomName: room.roomInfo.name,
|
||||
outputMode: 'COMPOSED',
|
||||
videoOnly: hasVideo && !hasAudio,
|
||||
audioOnly: hasAudio && !hasVideo,
|
||||
},
|
||||
'Start recording WRONG',
|
||||
res => {
|
||||
(res) => {
|
||||
console.log(res);
|
||||
document.getElementById('forceRecordingId').value = res.id;
|
||||
checkBtnsRecordings();
|
||||
$('#textarea-http').text(JSON.stringify(res, null, "\t"));
|
||||
$('#textarea-http').text(JSON.stringify(res, null, '\t'));
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
function stopRecording() {
|
||||
var forceRecordingId = document.getElementById('forceRecordingId').value;
|
||||
function startIndividualRecording(videoTrackId, audioTrackId) {
|
||||
return new Promise((resolve, reject) => {
|
||||
httpRequest(
|
||||
'POST',
|
||||
'recordings/start',
|
||||
{
|
||||
roomName: room.roomInfo.name,
|
||||
outputMode: 'INDIVIDUAL',
|
||||
audioTrackId,
|
||||
videoTrackId,
|
||||
},
|
||||
'Start recording WRONG',
|
||||
(res) => {
|
||||
console.log(res);
|
||||
$('#textarea-http').text(JSON.stringify(res.info, null, '\t'));
|
||||
resolve(res);
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
function stopRecording(id) {
|
||||
var forceRecordingId = id ? id : $('#forceRecordingId').val();
|
||||
httpRequest(
|
||||
'POST',
|
||||
'recording-java/api/recording/stop', {
|
||||
recording: forceRecordingId
|
||||
'recordings/stop',
|
||||
{
|
||||
recordingId: forceRecordingId,
|
||||
},
|
||||
'Stop recording WRONG',
|
||||
res => {
|
||||
(res) => {
|
||||
console.log(res);
|
||||
$('#textarea-http').text(JSON.stringify(res, null, "\t"));
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
function deleteRecording() {
|
||||
var forceRecordingId = document.getElementById('forceRecordingId').value;
|
||||
httpRequest(
|
||||
'DELETE',
|
||||
'recording-java/api/recording/delete', {
|
||||
recording: forceRecordingId
|
||||
},
|
||||
'Delete recording WRONG',
|
||||
res => {
|
||||
console.log("DELETE ok");
|
||||
$('#textarea-http').text("DELETE ok");
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
function getRecording() {
|
||||
var forceRecordingId = document.getElementById('forceRecordingId').value;
|
||||
httpRequest(
|
||||
'GET',
|
||||
'recording-java/api/recording/get/' + forceRecordingId, {},
|
||||
'Get recording WRONG',
|
||||
res => {
|
||||
console.log(res);
|
||||
$('#textarea-http').text(JSON.stringify(res, null, "\t"));
|
||||
$('#forceRecordingId').val('');
|
||||
$('#textarea-http').text(JSON.stringify(res.info, null, '\t'));
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
function listRecordings() {
|
||||
httpRequest(
|
||||
'GET',
|
||||
'recording-java/api/recording/list', {},
|
||||
'List recordings WRONG',
|
||||
res => {
|
||||
console.log(res);
|
||||
$('#textarea-http').text(JSON.stringify(res, null, "\t"));
|
||||
httpRequest('GET', 'recordings/list', {}, 'List recordings WRONG', (res) => {
|
||||
console.log(res);
|
||||
$('#recording-list').empty();
|
||||
if (res.recordings && res.recordings.length > 0) {
|
||||
res.recordings.forEach((recording) => {
|
||||
var li = document.createElement('li');
|
||||
var a = document.createElement('a');
|
||||
a.href = recording.path;
|
||||
a.target = '_blank';
|
||||
a.appendChild(document.createTextNode(recording.name));
|
||||
li.appendChild(a);
|
||||
$('#recording-list').append(li);
|
||||
});
|
||||
$('#delete-recordings-btn').prop('disabled', res.recordings.length === 0);
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
function deleteRecordings() {
|
||||
httpRequest('DELETE', 'recordings', {}, 'Delete recordings WRONG', (res) => {
|
||||
console.log(res);
|
||||
$('#recording-list').empty();
|
||||
$('#delete-recordings-btn').prop('disabled', true);
|
||||
$('#textarea-http').text(JSON.stringify(res, null, '\t'));
|
||||
});
|
||||
}
|
||||
|
||||
/* APPLICATION REST METHODS */
|
||||
|
||||
|
||||
|
||||
/* APPLICATION BROWSER METHODS */
|
||||
|
||||
events = '';
|
||||
|
||||
window.onbeforeunload = function () { // Gracefully leave session
|
||||
if (session) {
|
||||
window.onbeforeunload = function () {
|
||||
// Gracefully leave room
|
||||
if (room) {
|
||||
removeUser();
|
||||
leaveSession();
|
||||
leaveRoom();
|
||||
}
|
||||
};
|
||||
|
||||
function getLivekitUrlFromMetadata(token) {
|
||||
if (!token) throw new Error('Trying to get metadata from an empty token');
|
||||
try {
|
||||
const base64Url = token.split('.')[1];
|
||||
const base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/');
|
||||
const jsonPayload = decodeURIComponent(
|
||||
window
|
||||
.atob(base64)
|
||||
.split('')
|
||||
.map((c) => {
|
||||
return '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2);
|
||||
})
|
||||
.join('')
|
||||
);
|
||||
|
||||
const payload = JSON.parse(jsonPayload);
|
||||
if (!payload?.metadata) throw new Error('Token does not contain metadata');
|
||||
const metadata = JSON.parse(payload.metadata);
|
||||
return metadata.livekitUrl;
|
||||
} catch (error) {
|
||||
throw new Error('Error decoding and parsing token: ' + error);
|
||||
}
|
||||
}
|
||||
|
||||
@ -432,40 +306,43 @@ function updateNumVideos(i) {
|
||||
}
|
||||
}
|
||||
|
||||
function checkBtnsForce() {
|
||||
if (document.getElementById("forceValue").value === "") {
|
||||
document.getElementById('buttonForceUnpublish').disabled = true;
|
||||
document.getElementById('buttonForceDisconnect').disabled = true;
|
||||
} else {
|
||||
document.getElementById('buttonForceUnpublish').disabled = false;
|
||||
document.getElementById('buttonForceDisconnect').disabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
function checkBtnsRecordings() {
|
||||
if (document.getElementById("forceRecordingId").value === "") {
|
||||
document.getElementById('buttonGetRecording').disabled = true;
|
||||
if (document.getElementById('forceRecordingId').value === '') {
|
||||
document.getElementById('buttonStopRecording').disabled = true;
|
||||
document.getElementById('buttonDeleteRecording').disabled = true;
|
||||
} else {
|
||||
document.getElementById('buttonGetRecording').disabled = false;
|
||||
document.getElementById('buttonStopRecording').disabled = false;
|
||||
document.getElementById('buttonDeleteRecording').disabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
function pushEvent(event) {
|
||||
events += (!events ? '' : '\n') + event.type;
|
||||
$('#textarea-events').text(events);
|
||||
function addIndividualRecordingButton(elementId, videoTrackId, audioTrackId) {
|
||||
const div = document.createElement('div');
|
||||
|
||||
var button = document.createElement('button');
|
||||
// button.id = elementId + '-button';
|
||||
button.className = 'recording-track-button btn btn-sm';
|
||||
|
||||
button.innerHTML = 'Record Track';
|
||||
button.style = 'position: absolute; left: 0; z-index: 1000;';
|
||||
|
||||
button.onclick = async () => {
|
||||
if (button.innerHTML === 'Record Track') {
|
||||
button.innerHTML = 'Stop Recording';
|
||||
button.className = 'recording-track-button btn btn-sm btn-danger';
|
||||
var res = await startIndividualRecording(videoTrackId, audioTrackId);
|
||||
button.id = res.info.egressId;
|
||||
} else {
|
||||
button.innerHTML = 'Record Track';
|
||||
button.className = 'recording-track-button btn btn-sm';
|
||||
stopRecording(button.id);
|
||||
}
|
||||
};
|
||||
div.appendChild(button);
|
||||
var element = document.getElementById(elementId);
|
||||
element.parentNode.insertBefore(div, element.nextSibling);
|
||||
}
|
||||
|
||||
function clearHttpTextarea() {
|
||||
$('#textarea-http').text('');
|
||||
}
|
||||
|
||||
function clearEventsTextarea() {
|
||||
$('#textarea-events').text('');
|
||||
events = '';
|
||||
}
|
||||
|
||||
/* APPLICATION BROWSER METHODS */
|
||||
/* APPLICATION BROWSER METHODS */
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 81 KiB |
@ -1,7 +1,7 @@
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<title>openvidu-recording-java</title>
|
||||
<title>openvidu-recording-node</title>
|
||||
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" charset="utf-8">
|
||||
<link rel="shortcut icon" href="images/favicon.ico" type="image/x-icon">
|
||||
@ -17,7 +17,7 @@
|
||||
<!-- Bootstrap -->
|
||||
|
||||
<link rel="styleSheet" href="style.css" type="text/css" media="screen">
|
||||
<script src="openvidu-browser-2.27.0.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/livekit-client/dist/livekit-client.umd.min.js"></script>
|
||||
<script src="app.js"></script>
|
||||
<script>
|
||||
$(document).ready(function () {
|
||||
@ -35,11 +35,11 @@
|
||||
<div class="navbar-header">
|
||||
<a class="navbar-brand" href="/">
|
||||
<img class="demo-logo" src="images/openvidu_vert_white_bg_trans_cropped.png" /> Recording Java</a>
|
||||
<a class="navbar-brand nav-icon" href="https://github.com/OpenVidu/openvidu-tutorials/tree/master/openvidu-recording-java"
|
||||
<a class="navbar-brand nav-icon" href="https://github.com/OpenVidu/openvidu-livekit-tutorials/tree/master/openvidu-recording-node"
|
||||
title="GitHub Repository" target="_blank">
|
||||
<i class="fa fa-github" aria-hidden="true"></i>
|
||||
</a>
|
||||
<a class="navbar-brand nav-icon" href="http://www.docs.openvidu.io/en/stable/tutorials/openvidu-recording-java/" title="Documentation"
|
||||
<a class="navbar-brand nav-icon" href="#" title="Documentation"
|
||||
target="_blank">
|
||||
<i class="fa fa-book" aria-hidden="true"></i>
|
||||
</a>
|
||||
@ -53,48 +53,30 @@
|
||||
<img src="images/openvidu_grey_bg_transp_cropped.png" />
|
||||
</div>
|
||||
<div id="join-dialog" class="jumbotron">
|
||||
<h1>Join a video session</h1>
|
||||
<h1>Join a video room</h1>
|
||||
<form class="form-group" onsubmit="return false">
|
||||
<p>
|
||||
<label>Session</label>
|
||||
<input class="form-control" type="text" id="sessionName" value="SessionA" required>
|
||||
<label>Room</label>
|
||||
<input class="form-control" type="text" id="roomName" value="RoomA" required>
|
||||
</p>
|
||||
<p class="text-center">
|
||||
<button class="btn btn-lg btn-success" id="join-btn" onclick="joinSession()">Join!</button>
|
||||
<button class="btn btn-lg btn-success" id="join-btn" onclick="joinRoom()">Join!</button>
|
||||
</p>
|
||||
</form>
|
||||
<hr>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="session" style="display: none">
|
||||
<div id="session-header">
|
||||
<h1 id="session-title"></h1>
|
||||
<input class="btn btn-sm btn-danger" type="button" id="buttonCloseSession" onmouseup="closeSession()" value="Close session">
|
||||
<input class="btn btn-sm btn-danger" type="button" id="buttonLeaveSession" onmouseup="removeUser(); leaveSession()"
|
||||
value="Leave session">
|
||||
<div class="vertical-separator-top"></div>
|
||||
<input class="form-control" id="forceValue" type="text" onkeyup="checkBtnsForce()">
|
||||
<input class="btn btn-sm" type="button" id="buttonForceUnpublish" onmouseup="forceUnpublish()" value="Force unpublish"
|
||||
disabled>
|
||||
<input class="btn btn-sm" type="button" id="buttonForceDisconnect" onmouseup="forceDisconnect()" value="Force disconnect"
|
||||
disabled>
|
||||
<div class="vertical-separator-top"></div>
|
||||
<input class="btn btn-sm" type="button" id="buttonFetchInfo" onmouseup="fetchInfo()" value="Fetch info">
|
||||
<input class="btn btn-sm" type="button" id="buttonFetchAll" onmouseup="fetchAll()" value="Fetch all">
|
||||
<div id="room" style="display: none">
|
||||
<div id="room-header">
|
||||
<h1 id="room-title"></h1>
|
||||
<input class="btn btn-sm btn-danger" type="button" id="buttonLeaveRoom" onmouseup="leaveRoom()"
|
||||
value="Leave room">
|
||||
</div>
|
||||
<div id="video-container" class="col-md-12"></div>
|
||||
<div id="recording-btns">
|
||||
<div class="btns">
|
||||
<input class="btn btn-md" type="button" id="buttonStartRecording" onmouseup="startRecording()" value="Start recording">
|
||||
<form>
|
||||
<label class="radio-inline">
|
||||
<input type="radio" name="outputMode" value="COMPOSED" id="radio-composed" checked>COMPOSED
|
||||
</label>
|
||||
<label class="radio-inline">
|
||||
<input type="radio" name="outputMode" value="INDIVIDUAL" id="radio-individual">INDIVIDUAL
|
||||
</label>
|
||||
</form>
|
||||
<input class="btn btn-md" type="button" id="buttonStartRecording" onmouseup="startComposedRecording()" value="Start Composed recording">
|
||||
<form>
|
||||
<label class="checkbox-inline">
|
||||
<input type="checkbox" id="has-audio-checkbox" checked>Has audio
|
||||
@ -107,11 +89,7 @@
|
||||
<div class="btns">
|
||||
<input class="btn btn-md" type="button" id="buttonListRecording" onmouseup="listRecordings()" value="List recordings">
|
||||
<div class="vertical-separator-bottom"></div>
|
||||
<input class="btn btn-md" type="button" id="buttonGetRecording" onmouseup="getRecording()" value="Get recording"
|
||||
disabled>
|
||||
<input class="btn btn-md" type="button" id="buttonStopRecording" onmouseup="stopRecording()" value="Stop recording"
|
||||
disabled>
|
||||
<input class="btn btn-md" type="button" id="buttonDeleteRecording" onmouseup="deleteRecording()" value="Delete recording"
|
||||
<input class="btn btn-md btn-danger" type="button" id="buttonStopRecording" onmouseup="stopRecording()" value="Stop recording"
|
||||
disabled>
|
||||
<input class="form-control" id="forceRecordingId" type="text" onkeyup="checkBtnsRecordings()">
|
||||
</div>
|
||||
@ -120,10 +98,10 @@
|
||||
<span>HTTP responses</span>
|
||||
<textarea id="textarea-http" readonly="true" class="form-control" name="textarea-http"></textarea>
|
||||
</div>
|
||||
<div class="textarea-container" id="textarea-events-container">
|
||||
<button type="button" class="btn btn-outline-secondary" id="clear-events-btn" onclick="clearEventsTextarea()">Clear</button>
|
||||
<span>OpenVidu events</span>
|
||||
<textarea id="textarea-events" readonly="true" class="form-control" name="textarea-events"></textarea>
|
||||
<div class="textarea-container" id="recordings-list-container">
|
||||
<button type="button" class="btn btn-md btn-danger" id="delete-recordings-btn" onclick="deleteRecordings()" disabled>Delete All</button>
|
||||
<span>Recordings list</span>
|
||||
<ul id="recording-list"></ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -131,7 +109,7 @@
|
||||
|
||||
<footer class="footer">
|
||||
<div class="container">
|
||||
<div class="text-muted">OpenVidu © 2022</div>
|
||||
<div class="text-muted">OpenVidu © 2023</div>
|
||||
<a href="http://www.openvidu.io/" target="_blank">
|
||||
<img class="openvidu-logo" src="images/openvidu_globe_bg_transp_cropped.png" />
|
||||
</a>
|
||||
|
||||
File diff suppressed because one or more lines are too long
@ -222,27 +222,27 @@ a:hover .demo-logo {
|
||||
margin-top: 100px;
|
||||
}
|
||||
|
||||
#session-header {
|
||||
#room-header {
|
||||
margin-bottom: 20px;
|
||||
height: 8%;
|
||||
margin-top: 70px;
|
||||
}
|
||||
|
||||
#session-header form {
|
||||
#room-header form {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
#session-header input.btn {
|
||||
#room-header input.btn {
|
||||
float: right;
|
||||
margin-top: 20px;
|
||||
margin-left: 5px;
|
||||
}
|
||||
|
||||
#session-title {
|
||||
#room-title {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
#session-header .form-control {
|
||||
#room-header .form-control {
|
||||
width: initial;
|
||||
float: right;
|
||||
margin: 18px 0px 0px 5px;
|
||||
@ -300,12 +300,12 @@ video {
|
||||
object-fit: scale-down;
|
||||
}
|
||||
|
||||
#session {
|
||||
#room {
|
||||
height: 100%;
|
||||
padding-bottom: 80px;
|
||||
}
|
||||
|
||||
#session img {
|
||||
#room img {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
display: inline-block;
|
||||
@ -313,7 +313,7 @@ video {
|
||||
vertical-align: baseline;
|
||||
}
|
||||
|
||||
#session #video-container img {
|
||||
#room #video-container img {
|
||||
position: relative;
|
||||
float: left;
|
||||
width: 50%;
|
||||
@ -387,11 +387,12 @@ table i {
|
||||
}
|
||||
|
||||
#textarea-http-container {
|
||||
width: 69%;
|
||||
width: 59%;
|
||||
}
|
||||
|
||||
#textarea-events-container {
|
||||
width: 29%;
|
||||
#recordings-list-container {
|
||||
width: 39%;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.textarea-container button {
|
||||
|
||||
@ -1,19 +0,0 @@
|
||||
package io.openvidu.recording.java.test;
|
||||
|
||||
import org.junit.platform.runner.JUnitPlatform;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.junit.Assert;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
/**
|
||||
* Unit test for openvidu-recording-java
|
||||
*/
|
||||
@RunWith(JUnitPlatform.class)
|
||||
public class AppTest {
|
||||
|
||||
@Test
|
||||
void testApp() {
|
||||
Assert.assertTrue(true);
|
||||
}
|
||||
|
||||
}
|
||||
@ -1,301 +0,0 @@
|
||||
package io.openvidu.recording.java.test;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Map.Entry;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import org.apache.commons.io.FileUtils;
|
||||
import org.apache.commons.lang.StringUtils;
|
||||
import org.junit.Assert;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeAll;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.junit.platform.runner.JUnitPlatform;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.openqa.selenium.By;
|
||||
import org.openqa.selenium.Dimension;
|
||||
import org.openqa.selenium.WebDriver;
|
||||
import org.openqa.selenium.support.ui.ExpectedCondition;
|
||||
import org.openqa.selenium.support.ui.ExpectedConditions;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import io.github.bonigarcia.seljup.SeleniumExtension;
|
||||
import io.github.bonigarcia.wdm.WebDriverManager;
|
||||
import io.openvidu.java.client.OpenVidu;
|
||||
import io.openvidu.java.client.OpenViduHttpException;
|
||||
import io.openvidu.java.client.OpenViduJavaClientException;
|
||||
import io.openvidu.java.client.Recording;
|
||||
import io.openvidu.java.client.Recording.OutputMode;
|
||||
import io.openvidu.test.browsers.BrowserUser;
|
||||
import io.openvidu.test.browsers.ChromeUser;
|
||||
import ws.schild.jave.EncoderException;
|
||||
import ws.schild.jave.MultimediaInfo;
|
||||
import ws.schild.jave.MultimediaObject;
|
||||
|
||||
/**
|
||||
* E2E tests for openvidu-java-recording app
|
||||
*
|
||||
* mvn -Dtest=AppTestE2e -DAPP_URL=https://localhost:5000/
|
||||
* -DOPENVIDU_URL=https://localhost:4443/ -DOPENVIDU_SECRET=MY_SECRET
|
||||
* -DNUMBER_OF_ATTEMPTS=30 -DRECORDING_DURATION=5 -DDURATION_THRESHOLD=5 test
|
||||
*
|
||||
* @author Pablo Fuente (pablofuenteperez@gmail.com)
|
||||
*/
|
||||
@DisplayName("E2E tests for openvidu-java-recording")
|
||||
@ExtendWith(SeleniumExtension.class)
|
||||
@RunWith(JUnitPlatform.class)
|
||||
public class AppTestE2e {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(AppTestE2e.class);
|
||||
|
||||
static String OPENVIDU_SECRET = "MY_SECRET";
|
||||
static String OPENVIDU_URL = "https://localhost:4443/";
|
||||
static String APP_URL = "https://localhost:5000/";
|
||||
static int NUMBER_OF_ATTEMPTS = 10;
|
||||
static int RECORDING_DURATION = 5; // seconds
|
||||
static double DURATION_THRESHOLD = 10.0; // seconds
|
||||
|
||||
static String RECORDING_PATH = "/opt/openvidu/recordings/";
|
||||
|
||||
private BrowserUser user;
|
||||
private static OpenVidu OV;
|
||||
|
||||
boolean deleteRecordings = true;
|
||||
|
||||
@BeforeAll()
|
||||
static void setupAll() {
|
||||
|
||||
WebDriverManager.chromedriver().setup();
|
||||
|
||||
String appUrl = System.getProperty("APP_URL");
|
||||
if (appUrl != null) {
|
||||
APP_URL = appUrl;
|
||||
}
|
||||
log.info("Using URL {} to connect to openvidu-recording-java app", APP_URL);
|
||||
|
||||
String openviduUrl = System.getProperty("OPENVIDU_URL");
|
||||
if (openviduUrl != null) {
|
||||
OPENVIDU_URL = openviduUrl;
|
||||
}
|
||||
log.info("Using URL {} to connect to openvidu-server", OPENVIDU_URL);
|
||||
|
||||
String openvidusecret = System.getProperty("OPENVIDU_SECRET");
|
||||
if (openvidusecret != null) {
|
||||
OPENVIDU_SECRET = openvidusecret;
|
||||
}
|
||||
log.info("Using secret {} to connect to openvidu-server", OPENVIDU_SECRET);
|
||||
|
||||
String numberOfAttempts = System.getProperty("NUMBER_OF_ATTEMPTS");
|
||||
if (numberOfAttempts != null) {
|
||||
NUMBER_OF_ATTEMPTS = Integer.parseInt(numberOfAttempts);
|
||||
}
|
||||
log.info("Number of attempts: {}", NUMBER_OF_ATTEMPTS);
|
||||
|
||||
String recordingDuration = System.getProperty("RECORDING_DURATION");
|
||||
if (recordingDuration != null) {
|
||||
RECORDING_DURATION = Integer.parseInt(recordingDuration);
|
||||
}
|
||||
log.info("Recording duration: {} s", RECORDING_DURATION);
|
||||
|
||||
String durationThreshold = System.getProperty("DURATION_THRESHOLD");
|
||||
if (durationThreshold != null) {
|
||||
DURATION_THRESHOLD = Double.parseDouble(durationThreshold);
|
||||
}
|
||||
log.info("Duration threshold: {} s", DURATION_THRESHOLD);
|
||||
|
||||
String recordingPath = System.getProperty("RECORDING_PATH");
|
||||
if (recordingPath != null) {
|
||||
recordingPath = recordingPath.endsWith("/") ? recordingPath : recordingPath + "/";
|
||||
RECORDING_PATH = recordingPath;
|
||||
}
|
||||
log.info("Using recording path {} to search for recordings", RECORDING_PATH);
|
||||
|
||||
try {
|
||||
log.info("Cleaning folder {}", RECORDING_PATH);
|
||||
FileUtils.cleanDirectory(new File(RECORDING_PATH));
|
||||
} catch (IOException e) {
|
||||
log.error(e.getMessage());
|
||||
}
|
||||
OV = new OpenVidu(OPENVIDU_URL, OPENVIDU_SECRET);
|
||||
}
|
||||
|
||||
@AfterEach()
|
||||
void dispose() {
|
||||
try {
|
||||
OV.fetch();
|
||||
} catch (OpenViduJavaClientException | OpenViduHttpException e1) {
|
||||
log.error("Error fetching sessions: {}", e1.getMessage());
|
||||
}
|
||||
OV.getActiveSessions().forEach(session -> {
|
||||
try {
|
||||
session.close();
|
||||
log.info("Session {} successfully closed", session.getSessionId());
|
||||
} catch (OpenViduJavaClientException | OpenViduHttpException e2) {
|
||||
log.error("Error closing session: {}", e2.getMessage());
|
||||
}
|
||||
});
|
||||
if (deleteRecordings) {
|
||||
try {
|
||||
OV.listRecordings().forEach(recording -> {
|
||||
if (recording.getStatus().equals(Recording.Status.started)) {
|
||||
try {
|
||||
OV.stopRecording(recording.getId());
|
||||
log.info("Recording {} successfully stopped", recording.getId());
|
||||
} catch (OpenViduJavaClientException | OpenViduHttpException e) {
|
||||
log.error("Error stopping recording {}: {}", recording.getId(), e.getMessage());
|
||||
}
|
||||
}
|
||||
try {
|
||||
OV.deleteRecording(recording.getId());
|
||||
log.info("Recording {} successfully deleted", recording.getId());
|
||||
} catch (OpenViduJavaClientException | OpenViduHttpException e1) {
|
||||
log.error("Error deleting recording {}: {}", recording.getId(), e1.getMessage());
|
||||
}
|
||||
});
|
||||
} catch (OpenViduJavaClientException | OpenViduHttpException e2) {
|
||||
log.error("Error listing recordings: {}", e2.getMessage());
|
||||
}
|
||||
}
|
||||
this.user.dispose();
|
||||
}
|
||||
|
||||
void setupBrowser() {
|
||||
user = new ChromeUser("TestUser", 20, false);
|
||||
user.getDriver().manage().timeouts().setScriptTimeout(20, TimeUnit.SECONDS);
|
||||
user.getDriver().manage().window().setSize(new Dimension(1920, 1080));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Composed recording test")
|
||||
void composedRecordingTest() throws Exception {
|
||||
|
||||
boolean durationDifferenceAcceptable = true;
|
||||
int i = 0;
|
||||
|
||||
double realTimeDuration = 0;
|
||||
double entityDuration = 0;
|
||||
String videoFile = "";
|
||||
|
||||
setupBrowser();
|
||||
user.getDriver().get(APP_URL);
|
||||
|
||||
user.getDriver().findElement(By.id("join-btn")).click();
|
||||
waitUntilEvents("connectionCreated", "videoElementCreated", "accessAllowed", "streamCreated", "streamPlaying");
|
||||
|
||||
user.getDriver().findElement(By.id("has-video-checkbox")).click();
|
||||
|
||||
while (durationDifferenceAcceptable && (i < NUMBER_OF_ATTEMPTS)) {
|
||||
|
||||
log.info("----------");
|
||||
log.info("Attempt {}", i + 1);
|
||||
log.info("----------");
|
||||
|
||||
user.getDriver().findElement(By.id("buttonStartRecording")).click();
|
||||
|
||||
waitUntilEvents("recordingStarted");
|
||||
|
||||
Thread.sleep(RECORDING_DURATION * 1000);
|
||||
|
||||
user.getDriver().findElement(By.id("buttonStopRecording")).click();
|
||||
waitUntilEvents("recordingStopped");
|
||||
|
||||
Recording rec = OV.listRecordings().get(0);
|
||||
|
||||
String extension = rec.getOutputMode().equals(OutputMode.COMPOSED) && rec.hasVideo() ? ".mp4" : ".webm";
|
||||
|
||||
videoFile = RECORDING_PATH + rec.getId() + "/" + rec.getName() + extension;
|
||||
realTimeDuration = getRealTimeDuration(videoFile);
|
||||
entityDuration = rec.getDuration();
|
||||
|
||||
double differenceInDurations = (double) Math.abs(realTimeDuration - entityDuration);
|
||||
|
||||
log.info("Real media file duration: {} s", realTimeDuration);
|
||||
log.info("Entity file duration: {} s", entityDuration);
|
||||
log.info("Difference between durations: {} s", differenceInDurations);
|
||||
|
||||
durationDifferenceAcceptable = differenceInDurations < DURATION_THRESHOLD;
|
||||
i++;
|
||||
|
||||
if (durationDifferenceAcceptable) {
|
||||
// Delete acceptable recording
|
||||
try {
|
||||
OV.deleteRecording(rec.getId());
|
||||
log.info("Recording {} was acceptable and is succesfully deleted", rec.getId());
|
||||
} catch (OpenViduJavaClientException | OpenViduHttpException e) {
|
||||
log.error("Error deleteing acceptable recording {}: {}", rec.getId(), e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
if (i == NUMBER_OF_ATTEMPTS) {
|
||||
log.info("Media file recorded with Composite has not exceeded the duration threshold ({} s) in {} attempts",
|
||||
DURATION_THRESHOLD, NUMBER_OF_ATTEMPTS);
|
||||
} else {
|
||||
log.error(
|
||||
"Real video duration recorded with Composite ({} s) exceeds threshold of {} s compared to entity duration ({} s), in file {}",
|
||||
realTimeDuration, DURATION_THRESHOLD, entityDuration, videoFile);
|
||||
deleteRecordings = false;
|
||||
Assert.fail("Real video duration recorded with Composite (" + realTimeDuration + " s) exceeds threshold of "
|
||||
+ DURATION_THRESHOLD + " s compared to entity duration (" + entityDuration + " s), in file "
|
||||
+ videoFile);
|
||||
}
|
||||
}
|
||||
|
||||
private void waitUntilEvents(String... events) {
|
||||
user.getWaiter().until(eventsToBe(events));
|
||||
user.getDriver().findElement(By.id("clear-events-btn")).click();
|
||||
user.getWaiter().until(ExpectedConditions.textToBePresentInElementLocated(By.id("textarea-events"), ""));
|
||||
}
|
||||
|
||||
private ExpectedCondition<Boolean> eventsToBe(String... events) {
|
||||
final Map<String, Integer> expectedEvents = new HashMap<>();
|
||||
for (String event : events) {
|
||||
Integer currentNumber = expectedEvents.get(event);
|
||||
if (currentNumber == null) {
|
||||
expectedEvents.put(event, 1);
|
||||
} else {
|
||||
expectedEvents.put(event, currentNumber++);
|
||||
}
|
||||
}
|
||||
return new ExpectedCondition<Boolean>() {
|
||||
@Override
|
||||
public Boolean apply(WebDriver driver) {
|
||||
boolean eventsCorrect = true;
|
||||
String events = driver.findElement(By.id("textarea-events")).getText();
|
||||
|
||||
for (Entry<String, Integer> entry : expectedEvents.entrySet()) {
|
||||
eventsCorrect = eventsCorrect
|
||||
&& StringUtils.countMatches(events, entry.getKey()) == entry.getValue();
|
||||
if (!eventsCorrect) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
return eventsCorrect;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return " OpenVidu events " + expectedEvents.toString();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private double getRealTimeDuration(String pathToVideoFile) {
|
||||
long time = 0;
|
||||
File source = new File(pathToVideoFile);
|
||||
try {
|
||||
MultimediaObject media = new MultimediaObject(source);
|
||||
MultimediaInfo info = media.getInfo();
|
||||
time = info.getDuration();
|
||||
} catch (EncoderException e) {
|
||||
log.error("Error getting MultimediaInfo from file {}: {}", pathToVideoFile, e.getMessage());
|
||||
}
|
||||
return (double) time / 1000;
|
||||
}
|
||||
|
||||
}
|
||||
@ -1,14 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<configuration>
|
||||
<timestamp key="myTimestamp" timeReference="contextBirth"
|
||||
datePattern="HH-mm-ss" />
|
||||
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
|
||||
<layout class="ch.qos.logback.classic.PatternLayout">
|
||||
<Pattern>[%p] %d [%.12t] %c \(%M\) - %msg%n</Pattern>
|
||||
</layout>
|
||||
</appender>
|
||||
<root>
|
||||
<level value="INFO" />
|
||||
<appender-ref ref="STDOUT" />
|
||||
</root>
|
||||
</configuration>
|
||||
@ -1,80 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
##################################################################################################
|
||||
# This script automatically builds and launches e2e tests for openvidu-recording-java application
|
||||
#
|
||||
# Prerequisites:
|
||||
# - Have maven installed in the host machine (sudo apt-get install maven)
|
||||
# - Have KMS installed in the host machine (https://docs.openvidu.io/en/stable/deployment/deploying-ubuntu/#1-install-kms)
|
||||
# - Run this command to configure KMS with current user (sudo sed -i "s/DAEMON_USER=\"kurento\"/DAEMON_USER=\"${USER}\"/g" /etc/default/kurento-media-server)
|
||||
# - Store and run this script inside a folder with write and execute permissions for the current user
|
||||
##################################################################################################
|
||||
|
||||
|
||||
CURRENT_PATH=$PWD
|
||||
|
||||
|
||||
### Check for write permissions in current path with current user
|
||||
if [ ! -w $CURRENT_PATH ]; then
|
||||
echo "User does not have write permissions in this path"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
|
||||
### Check that no openvidu-server or openvidu-recording-java are already running
|
||||
if nc -z localhost 4443; then
|
||||
echo "ERROR launching openvidu-server. Port 4443 is already occupied"
|
||||
echo "You may kill all openvidu processes before running the script with this command: $ sudo kill -9 \$(ps aux | grep openvidu-recording-java | awk '{print \$2}')"
|
||||
exit 1
|
||||
fi
|
||||
if nc -z localhost 5000; then
|
||||
echo "ERROR launching openvidu-recording-java. Port 5000 is already occupied"
|
||||
echo "You may kill all openvidu processes before running the script with this command: $ sudo kill -9 \$(ps aux | grep openvidu-recording-java | awk '{print \$2}')"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
|
||||
### Delete repo folders if they exist
|
||||
rm -rf openvidu
|
||||
rm -rf openvidu-tutorials
|
||||
|
||||
|
||||
### Init KMS as local user
|
||||
sudo service kurento-media-server restart
|
||||
|
||||
|
||||
### Clone projects
|
||||
git clone https://github.com/OpenVidu/openvidu-tutorials.git || exit 1
|
||||
git clone https://github.com/OpenVidu/openvidu.git || exit 1
|
||||
|
||||
|
||||
### Launch openvidu-server in the background
|
||||
cd $CURRENT_PATH/openvidu
|
||||
mvn -DskipTests=true clean -DskipTests=true compile -DskipTests=true install || exit 1
|
||||
cd $CURRENT_PATH/openvidu/openvidu-server
|
||||
mvn package -Dopenvidu.recording=true -Dopenvidu.recording.path=$CURRENT_PATH/recordings exec:java &> $CURRENT_PATH/openvidu-server.log &
|
||||
|
||||
|
||||
### Launch openvidu-recording-java app in the background
|
||||
cd $CURRENT_PATH/openvidu-tutorials/openvidu-recording-java
|
||||
mvn package -DskipTests=true exec:java &> $CURRENT_PATH/openvidu-recording-java.log &
|
||||
|
||||
|
||||
### Wait for both processes
|
||||
echo "Waiting openvidu-recording-java app to launch on 5000"
|
||||
while ! nc -z localhost 5000; do
|
||||
sleep 1
|
||||
echo "Waiting..."
|
||||
done
|
||||
echo "openvidu-recording-java app ready"
|
||||
|
||||
echo "Waiting openvidu-server to launch on 4443"
|
||||
while ! nc -z localhost 4443; do
|
||||
sleep 1
|
||||
echo "Waiting..."
|
||||
done
|
||||
echo "openvidu-server ready"
|
||||
|
||||
|
||||
### Launch e2e test in the foreground
|
||||
mvn -Dtest=AppTestE2e -DAPP_URL=https://localhost:5000/ -DOPENVIDU_URL=https://localhost:4443/ -DOPENVIDU_SECRET=MY_SECRET -DNUMBER_OF_ATTEMPTS=30 -DRECORDING_DURATION=5 -DDURATION_THRESHOLD=5 -DRECORDING_PATH=$CURRENT_PATH/recordings test
|
||||
Loading…
x
Reference in New Issue
Block a user