Migrated openvidu-recording-java to Livekit

This commit is contained in:
Carlos Santos 2023-11-20 13:48:59 +01:00
parent 8d46598f48
commit 3145698c70
13 changed files with 476 additions and 15345 deletions

View File

@ -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>

View File

@ -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;
}
}

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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 {

View File

@ -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);
}
}

View File

@ -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;
}
}

View File

@ -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>

View File

@ -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