diff --git a/openvidu-test-e2e/src/test/java/io/openvidu/test/e2e/OpenViduTestAppE2eTest.java b/openvidu-test-e2e/src/test/java/io/openvidu/test/e2e/OpenViduTestAppE2eTest.java index 1a377aa3..49d18686 100644 --- a/openvidu-test-e2e/src/test/java/io/openvidu/test/e2e/OpenViduTestAppE2eTest.java +++ b/openvidu-test-e2e/src/test/java/io/openvidu/test/e2e/OpenViduTestAppE2eTest.java @@ -22,10 +22,15 @@ import static org.openqa.selenium.OutputType.BASE64; import java.awt.Color; import java.awt.image.BufferedImage; +import java.io.BufferedInputStream; +import java.io.BufferedOutputStream; import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; import java.io.IOException; import java.nio.file.Path; import java.nio.file.Paths; +import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; import java.util.Iterator; @@ -35,6 +40,8 @@ import java.util.Queue; import java.util.concurrent.ConcurrentLinkedQueue; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; +import java.util.zip.ZipEntry; +import java.util.zip.ZipInputStream; import javax.imageio.ImageIO; @@ -170,9 +177,7 @@ public class OpenViduTestAppE2eTest { @AfterEach void dispose() { - if (user != null) { - user.dispose(); - } + user.dispose(); } @Test @@ -974,9 +979,9 @@ public class OpenViduTestAppE2eTest { void remoteComposedRecordTest() throws Exception { setupBrowser("chrome"); - log.info("Remote record"); + log.info("Remote composed record"); - final String sessionName = "RECORDED_SESSION"; + final String sessionName = "COMPOSED_RECORDED_SESSION"; user.getDriver().findElement(By.id("add-user-btn")).click(); user.getDriver().findElement(By.id("session-name-input-0")).clear(); @@ -1073,9 +1078,9 @@ public class OpenViduTestAppE2eTest { File file2 = new File(recordingsPath + sessionName + "/" + ".recording." + sessionName); File file3 = new File(recordingsPath + sessionName + "/" + sessionName + ".jpg"); - Assert.assertTrue(file1.exists() || file1.length() > 0); - Assert.assertTrue(file2.exists() || file2.length() > 0); - Assert.assertTrue(file3.exists() || file3.length() > 0); + Assert.assertTrue(file1.exists() && file1.length() > 0); + Assert.assertTrue(file2.exists() && file2.length() > 0); + Assert.assertTrue(file3.exists() && file3.length() > 0); Assert.assertTrue( this.recordedFileFine(file1, new OpenVidu(OPENVIDU_URL, OPENVIDU_SECRET).getRecording(sessionName))); @@ -1109,7 +1114,117 @@ public class OpenViduTestAppE2eTest { @Test @DisplayName("Remote individual record") void remoteIndividualRecordTest() throws Exception { + setupBrowser("chrome"); + log.info("Remote individual record"); + + final String sessionName = "TestSession"; + final String recordingName = "CUSTOM_NAME"; + + user.getDriver().findElement(By.id("add-user-btn")).click(); + user.getDriver().findElement(By.id("session-name-input-0")).clear(); + user.getDriver().findElement(By.id("session-name-input-0")).sendKeys(sessionName); + + user.getDriver().findElement(By.id("auto-join-checkbox")).click(); + user.getDriver().findElement(By.id("one2one-btn")).click(); + + user.getEventManager().waitUntilEventReaches("connectionCreated", 4); + user.getEventManager().waitUntilEventReaches("accessAllowed", 2); + user.getEventManager().waitUntilEventReaches("streamCreated", 4); + user.getEventManager().waitUntilEventReaches("streamPlaying", 4); + + Assert.assertTrue(user.getEventManager().assertMediaTracks(user.getDriver().findElements(By.tagName("video")), + true, true)); + + user.getDriver().findElement(By.id("session-api-btn-0")).click(); + Thread.sleep(1000); + user.getDriver().findElement(By.id("rec-properties-btn")).click(); + Thread.sleep(500); + + // Set recording name + user.getDriver().findElement(By.id("recording-name-field")).sendKeys(recordingName); + // Set OutputMode to INDIVIDUAL + user.getDriver().findElement(By.id("rec-outputmode-select")).click(); + Thread.sleep(500); + user.getDriver().findElement(By.id("option-INDIVIDUAL")).click(); + Thread.sleep(500); + + user.getDriver().findElement(By.id("start-recording-btn")).click(); + + user.getWaiter().until(ExpectedConditions.attributeToBe(By.id("api-response-text-area"), "value", + "Recording started [" + sessionName + "]")); + + user.getEventManager().waitUntilEventReaches("recordingStarted", 2); + + Thread.sleep(8000); + + user.getDriver().findElement(By.id("recording-id-field")).clear(); + user.getDriver().findElement(By.id("recording-id-field")).sendKeys(sessionName); + + // Try to start an ongoing recording + user.getDriver().findElement(By.id("start-recording-btn")).click(); + user.getWaiter() + .until(ExpectedConditions.attributeToBe(By.id("api-response-text-area"), "value", "Error [409]")); + + // Try to get an existing recording + user.getDriver().findElement(By.id("get-recording-btn")).click(); + user.getWaiter().until(ExpectedConditions.attributeToBe(By.id("api-response-text-area"), "value", + "Recording got [" + sessionName + "]")); + + // Try to delete an ongoing recording + user.getDriver().findElement(By.id("delete-recording-btn")).click(); + user.getWaiter() + .until(ExpectedConditions.attributeToBe(By.id("api-response-text-area"), "value", "Error [409]")); + + // List existing recordings (one) + user.getDriver().findElement(By.id("list-recording-btn")).click(); + user.getWaiter().until(ExpectedConditions.attributeToBe(By.id("api-response-text-area"), "value", + "Recording list [" + sessionName + "]")); + + // Stop ongoing recording + user.getDriver().findElement(By.id("stop-recording-btn")).click(); + user.getWaiter().until(ExpectedConditions.attributeToBe(By.id("api-response-text-area"), "value", + "Recording stopped [" + sessionName + "]")); + + user.getEventManager().waitUntilEventReaches("recordingStopped", 2); + + String recordingsPath = "/opt/openvidu/recordings/"; + + // Should be only 2 files: zip and metadata + File folder = new File(recordingsPath + sessionName); + Assert.assertEquals(folder.listFiles().length, 2); + + File file1 = new File(recordingsPath + sessionName + "/" + recordingName + ".zip"); + File file2 = new File(recordingsPath + sessionName + "/" + ".recording." + sessionName); + + Assert.assertTrue(file1.exists() && file1.length() > 0); + Assert.assertTrue(file2.exists() && file2.length() > 0); + + unzipAndCheckFile(recordingsPath + sessionName + "/", recordingName + ".zip", + new OpenVidu(OPENVIDU_URL, OPENVIDU_SECRET).getRecording(sessionName)); + + // Try to get the stopped recording + user.getDriver().findElement(By.id("get-recording-btn")).click(); + user.getWaiter().until(ExpectedConditions.attributeToBe(By.id("api-response-text-area"), "value", + "Recording got [" + sessionName + "]")); + + // Try to list the stopped recording + user.getDriver().findElement(By.id("list-recording-btn")).click(); + user.getWaiter().until(ExpectedConditions.attributeToBe(By.id("api-response-text-area"), "value", + "Recording list [" + sessionName + "]")); + + // Delete the recording + user.getDriver().findElement(By.id("delete-recording-btn")).click(); + user.getWaiter() + .until(ExpectedConditions.attributeToBe(By.id("api-response-text-area"), "value", "Recording deleted")); + + Assert.assertFalse(file1.exists()); + Assert.assertFalse(file2.exists()); + + user.getDriver().findElement(By.id("close-dialog-btn")).click(); + Thread.sleep(1000); + + gracefullyLeaveParticipants(2); } @Test @@ -1553,6 +1668,7 @@ public class OpenViduTestAppE2eTest { // Get a frame at 75% duration frame = FrameGrab.getFrameAtSec(file, (double) (recording.getDuration() * 0.75)); Map colorMap = this.averageColor(AWTUtil.toBufferedImage(frame)); + log.info("Recording map color: {}", colorMap.toString()); isFine = this.checkVideoAverageRgbGreen(colorMap); } catch (IOException | JCodecException e) { @@ -1602,4 +1718,45 @@ public class OpenViduTestAppE2eTest { return colorMap; } + private void unzipAndCheckFile(String path, String fileName, Recording recording) { + final int BUFFER = 2048; + final List recordingFiles = new ArrayList<>(); + try { + BufferedOutputStream dest = null; + FileInputStream fis = new FileInputStream(path + fileName); + ZipInputStream zis = new ZipInputStream(new BufferedInputStream(fis)); + ZipEntry entry; + while ((entry = zis.getNextEntry()) != null) { + log.info("Extracting: " + entry); + if (entry.getName().endsWith(".webm")) { + recordingFiles.add(entry.getName()); + } + int count; + byte data[] = new byte[BUFFER]; + FileOutputStream fos = new FileOutputStream(path + entry.getName()); + dest = new BufferedOutputStream(fos, BUFFER); + while ((count = zis.read(data, 0, BUFFER)) != -1) { + dest.write(data, 0, count); + } + dest.flush(); + dest.close(); + } + zis.close(); + } catch (Exception e) { + e.printStackTrace(); + } + long totalFileSize = 0; + for (String videoFileName : recordingFiles) { + File videoFile = new File(path + videoFileName); + totalFileSize += videoFile.length(); + Assert.assertTrue(videoFile.exists() && videoFile.length() > 0); + videoFile.delete(); + } + Assert.assertEquals(recording.getSize() / 1000, totalFileSize / 1000); + + File jsonSyncFile = new File(path + recording.getSessionId() + ".json"); + Assert.assertTrue(jsonSyncFile.exists() && jsonSyncFile.length() > 0); + jsonSyncFile.delete(); + } + } diff --git a/openvidu-testapp/src/app/components/dialogs/session-api-dialog/session-api-dialog.component.css b/openvidu-testapp/src/app/components/dialogs/session-api-dialog/session-api-dialog.component.css new file mode 100644 index 00000000..9323798a --- /dev/null +++ b/openvidu-testapp/src/app/components/dialogs/session-api-dialog/session-api-dialog.component.css @@ -0,0 +1,55 @@ +#response-text-area { + width: 100%; + color: #808080; +} + +#response-text-area textarea { + resize: none; +} + +mat-dialog-content button, +mat-divider { + margin-bottom: 5px; +} + +mat-dialog-content button { + height: 30px; + line-height: 30px; + padding-left: 12px; + padding-right: 12px; + display: inline-flex; +} + +.label { + display: block; + font-size: 12px; + color: rgba(0, 0, 0, 0.54); + font-weight: 400; + margin-bottom: 5px; + margin-top: 13px +} + +.inner-text-input { + margin-left: 16px; +} + +#rec-properties-btn { + padding-left: 0; + padding-right: 0; + display: inline-block; +} + +#rec-properties-div { + margin-bottom: 10px; + padding: 5px; + border: 1px solid #00000026; + border-radius: 3px; +} + +#rec-properties-div mat-form-field { + max-width: 150px; +} + +#rec-properties-div mat-checkbox { + margin-left: 16px; +} diff --git a/openvidu-testapp/src/app/components/dialogs/session-api-dialog/session-api-dialog.component.html b/openvidu-testapp/src/app/components/dialogs/session-api-dialog/session-api-dialog.component.html new file mode 100644 index 00000000..3f0510f7 --- /dev/null +++ b/openvidu-testapp/src/app/components/dialogs/session-api-dialog/session-api-dialog.component.html @@ -0,0 +1,73 @@ +
+

API REST

+ + +
+ + + +
+ + + +
+ + + +
+ + + + +
+
+ + + + + + + {{ enumerator }} + + + +
+
+ + + + {{ enumerator }} + + + + + + +
+
+ Has audio + Has video +
+
+ + + +
+ + + +
+ + + +
+ + + +
diff --git a/openvidu-testapp/src/app/components/dialogs/session-api-dialog/session-api-dialog.component.ts b/openvidu-testapp/src/app/components/dialogs/session-api-dialog/session-api-dialog.component.ts index bfe50b06..f1acab0e 100644 --- a/openvidu-testapp/src/app/components/dialogs/session-api-dialog/session-api-dialog.component.ts +++ b/openvidu-testapp/src/app/components/dialogs/session-api-dialog/session-api-dialog.component.ts @@ -1,50 +1,12 @@ import { Component, Inject } from '@angular/core'; import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material'; -import { OpenVidu as OpenViduAPI, Session as SessionAPI } from 'openvidu-node-client'; +import { OpenVidu as OpenViduAPI, Session as SessionAPI, Recording, RecordingProperties, RecordingLayout } from 'openvidu-node-client'; @Component({ selector: 'app-session-api-dialog', - template: ` -
-

API REST

- - - - - - - - - - - - - - - - - - - - - - - - - - - -
- `, - styles: [ - '#response-text-area { width: 100%; color: #808080; }', - '#response-text-area textarea { resize: none; }', - 'mat-dialog-content button, mat-divider { margin-bottom: 5px; }', - 'mat-dialog-content button { height: 30px; line-height: 30px; padding-left: 12px; padding-right: 12px; display: inline-flex;}', - '.label { display: block; font-size: 12px; color: rgba(0, 0, 0, 0.54); font-weight: 400; margin-bottom: 5px; margin-top: 13px}', - '.inner-text-input { margin-left: 16px; }' - ] + templateUrl: './session-api-dialog.component.html', + styleUrls: ['./session-api-dialog.component.css'] }) export class SessionApiDialogComponent { @@ -55,11 +17,19 @@ export class SessionApiDialogComponent { resourceId: string; response: string; + recordingProperties: RecordingProperties; + recMode = Recording.OutputMode; + recLayouts = RecordingLayout; + customLayout = ''; + recPropertiesIcon = 'add_circle'; + showRecProperties = false; + constructor(public dialogRef: MatDialogRef, @Inject(MAT_DIALOG_DATA) public data) { this.OV = data.openVidu; this.session = data.session; this.sessionId = data.sessionId; + this.recordingProperties = data.recordingProperties; } closeSession() { @@ -80,7 +50,7 @@ export class SessionApiDialogComponent { startRecording() { console.log('Starting recording'); - this.OV.startRecording(this.sessionId) + this.OV.startRecording(this.sessionId, this.recordingProperties) .then(recording => { this.response = 'Recording started [' + recording.id + ']'; }) @@ -195,4 +165,13 @@ export class SessionApiDialogComponent { }); } + enumToArray(enumerator: any) { + return Object.keys(enumerator); + } + + toggleRecProperties() { + this.showRecProperties = !this.showRecProperties; + this.recPropertiesIcon = this.showRecProperties ? 'remove_circle' : 'add_circle'; + } + } diff --git a/openvidu-testapp/src/app/components/openvidu-instance/openvidu-instance.component.ts b/openvidu-testapp/src/app/components/openvidu-instance/openvidu-instance.component.ts index e30e789b..3a7fa04c 100644 --- a/openvidu-testapp/src/app/components/openvidu-instance/openvidu-instance.component.ts +++ b/openvidu-testapp/src/app/components/openvidu-instance/openvidu-instance.component.ts @@ -16,7 +16,9 @@ import { RecordingMode, RecordingLayout, TokenOptions, - OpenViduRole + OpenViduRole, + RecordingProperties, + Recording } from 'openvidu-node-client'; import { MatDialog, MAT_CHECKBOX_CLICK_ACTION } from '@angular/material'; import { ExtensionDialogComponent } from '../dialogs/extension-dialog/extension-dialog.component'; @@ -71,6 +73,9 @@ export class OpenviduInstanceComponent implements OnInit, OnChanges, OnDestroy { subscribeToRemote = false; optionsVideo = 'video'; + // Recording options + recordingProperties: RecordingProperties; + // OpenVidu Browser objects OV: OpenVidu; session: Session; @@ -441,7 +446,7 @@ export class OpenviduInstanceComponent implements OnInit, OnChanges, OnDestroy { this.publisher = this.OV.initPublisher( undefined, this.publisherProperties, - (err) => { + err => { if (err) { console.warn(err); this.openviduError = err; @@ -528,7 +533,7 @@ export class OpenviduInstanceComponent implements OnInit, OnChanges, OnDestroy { width: '450px' }); - dialogRef.afterClosed().subscribe((result) => { + dialogRef.afterClosed().subscribe(result => { if (!!result) { this.sessionProperties = result.sessionProperties; if (!!this.sessionProperties.customSessionId) { @@ -546,11 +551,20 @@ export class OpenviduInstanceComponent implements OnInit, OnChanges, OnDestroy { openSessionApiDialog() { const dialogRef = this.dialog.open(SessionApiDialogComponent, { data: { - openVidu: this.OV_NodeClient ? this.OV_NodeClient : new OpenViduAPI(this.openviduUrl, this.openviduSecret), + openVidu: !!this.OV_NodeClient ? this.OV_NodeClient : new OpenViduAPI(this.openviduUrl, this.openviduSecret), session: this.sessionAPI, - sessionId: !!this.session ? this.session.sessionId : this.sessionName + sessionId: !!this.session ? this.session.sessionId : this.sessionName, + recordingProperties: !!this.recordingProperties ? this.recordingProperties : + { + name: '', + outputMode: Recording.OutputMode.COMPOSED, + recordingLayout: this.sessionProperties.defaultRecordingLayout, + customLayout: this.sessionProperties.defaultCustomLayout, + hasAudio: true, + hasVideo: true + } }, - width: '280px', + width: '425px', disableClose: true }); @@ -558,6 +572,7 @@ export class OpenviduInstanceComponent implements OnInit, OnChanges, OnDestroy { if (!result.session) { delete this.sessionAPI; } + this.recordingProperties = result.recordingProperties; document.getElementById('session-api-btn-' + this.index).classList.remove('cdk-program-focused'); }); } @@ -588,7 +603,7 @@ export class OpenviduInstanceComponent implements OnInit, OnChanges, OnDestroy { disableClose: true }); - dialogRef.afterClosed().subscribe((result) => { + dialogRef.afterClosed().subscribe(result => { if (!!this.session && JSON.stringify(this.sessionEvents) !== JSON.stringify(oldValues)) { this.updateSessionEvents(oldValues, false);