diff --git a/openvidu-recording-java/pom.xml b/openvidu-recording-java/pom.xml
index dec5e5ac..8954aea9 100644
--- a/openvidu-recording-java/pom.xml
+++ b/openvidu-recording-java/pom.xml
@@ -19,14 +19,10 @@
UTF-8
- 1.8
+ 11
io.openvidu.recording.java.App
openvidu
-
- 5.0.3
- 1.3.2
- 3.1.0
@@ -39,6 +35,11 @@
+
+ org.jetbrains.kotlin
+ kotlin-stdlib
+ 1.5.21
+
org.springframework.boot
@@ -49,55 +50,19 @@
spring-boot-devtools
- io.openvidu
- openvidu-java-client
- 2.27.0
+ io.livekit
+ livekit-server
+ 0.5.7
- org.junit.jupiter
- junit-jupiter-api
- test
+ com.squareup.okhttp3
+ okhttp
+ 4.12.0
- io.github.bonigarcia
- selenium-jupiter
- ${selenium-jupiter.version}
- test
+ org.json
+ json
+ 20231013
-
- io.github.bonigarcia
- webdrivermanager
- 2.2.0
- test
-
-
- org.junit.platform
- junit-platform-runner
- test
-
-
- org.junit.jupiter
- junit-jupiter-engine
- test
-
-
- ws.schild
- jave-all-deps
- 2.4.6
- test
-
-
- ws.schild
- jave-core
- 2.4.6
- test
-
-
- io.openvidu
- openvidu-test-browsers
- 1.0.0
- test
-
-
diff --git a/openvidu-recording-java/src/main/java/io/openvidu/recording/java/MyRestController.java b/openvidu-recording-java/src/main/java/io/openvidu/recording/java/MyRestController.java
index 8e1d4280..c5549a49 100644
--- a/openvidu-recording-java/src/main/java/io/openvidu/recording/java/MyRestController.java
+++ b/openvidu-recording-java/src/main/java/io/openvidu/recording/java/MyRestController.java
@@ -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 mapSessions = new ConcurrentHashMap<>();
- // Collection to pair session names and tokens (the inner Map pairs tokens and
- // role associated)
- private Map> mapSessionNamesTokens = new ConcurrentHashMap<>();
- // Collection to pair session names and recording objects
- private Map 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 getToken(@RequestBody Map sessionNameParam) {
+ /**
+ * @param params The JSON object with roomName and participantName
+ * @return The JWT token
+ */
+ @PostMapping("/token")
+ public ResponseEntity> getToken(@RequestBody(required = true) Map 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 removeUser(@RequestBody Map 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 closeSession(@RequestBody Map 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 fetchInfo(@RequestBody Map 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 forceDisconnect(@RequestBody Map 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 forceUnpublish(@RequestBody Map 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 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 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 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 recordings = this.openVidu.listRecordings();
+ List 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 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 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;
}
}
diff --git a/openvidu-recording-java/src/main/resources/application-container.properties b/openvidu-recording-java/src/main/resources/application-container.properties
index 8a4b7c07..5ab5cc01 100644
--- a/openvidu-recording-java/src/main/resources/application-container.properties
+++ b/openvidu-recording-java/src/main/resources/application-container.properties
@@ -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
\ No newline at end of file
diff --git a/openvidu-recording-java/src/main/resources/application.properties b/openvidu-recording-java/src/main/resources/application.properties
index 61bef2ed..93773915 100644
--- a/openvidu-recording-java/src/main/resources/application.properties
+++ b/openvidu-recording-java/src/main/resources/application.properties
@@ -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
\ No newline at end of file
diff --git a/openvidu-recording-java/src/main/resources/static/app.js b/openvidu-recording-java/src/main/resources/static/app.js
index 9cc03476..ce00514f 100644
--- a/openvidu-recording-java/src/main/resources/static/app.js
+++ b/openvidu-recording-java/src/main/resources/static/app.js
@@ -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 */
\ No newline at end of file
+/* APPLICATION BROWSER METHODS */
diff --git a/openvidu-recording-java/src/main/resources/static/images/subscriber-msg.jpg b/openvidu-recording-java/src/main/resources/static/images/subscriber-msg.jpg
deleted file mode 100644
index 1666fb40..00000000
Binary files a/openvidu-recording-java/src/main/resources/static/images/subscriber-msg.jpg and /dev/null differ
diff --git a/openvidu-recording-java/src/main/resources/static/index.html b/openvidu-recording-java/src/main/resources/static/index.html
index afdcdf52..1ded9bdd 100644
--- a/openvidu-recording-java/src/main/resources/static/index.html
+++ b/openvidu-recording-java/src/main/resources/static/index.html
@@ -1,7 +1,7 @@
- openvidu-recording-java
+ openvidu-recording-node
@@ -17,7 +17,7 @@
-
+