From 3145698c707291814a4175f921b5c24638f5e933 Mon Sep 17 00:00:00 2001 From: Carlos Santos <4a.santos@gmail.com> Date: Mon, 20 Nov 2023 13:48:59 +0100 Subject: [PATCH] Migrated openvidu-recording-java to Livekit --- openvidu-recording-java/pom.xml | 65 +- .../recording/java/MyRestController.java | 517 +- .../application-container.properties | 7 +- .../src/main/resources/application.properties | 6 +- .../src/main/resources/static/app.js | 599 +- .../static/images/subscriber-msg.jpg | Bin 82734 -> 0 bytes .../src/main/resources/static/index.html | 62 +- .../static/openvidu-browser-2.27.0.js | 14128 ---------------- .../src/main/resources/static/style.css | 23 +- .../openvidu/recording/java/test/AppTest.java | 19 - .../recording/java/test/AppTestE2e.java | 301 - .../src/test/resources/logback.xml | 14 - .../src/test/resources/run-composite-test.sh | 80 - 13 files changed, 476 insertions(+), 15345 deletions(-) delete mode 100644 openvidu-recording-java/src/main/resources/static/images/subscriber-msg.jpg delete mode 100644 openvidu-recording-java/src/main/resources/static/openvidu-browser-2.27.0.js delete mode 100644 openvidu-recording-java/src/test/java/io/openvidu/recording/java/test/AppTest.java delete mode 100644 openvidu-recording-java/src/test/java/io/openvidu/recording/java/test/AppTestE2e.java delete mode 100644 openvidu-recording-java/src/test/resources/logback.xml delete mode 100755 openvidu-recording-java/src/test/resources/run-composite-test.sh 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 1666fb4099a9236147861e734e61869e37267767..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 82734 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 pgR(+GNPdoX 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 G?f(Kus+KSS 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 @@ - +