openvidu-test-e2e: add reliable and lossy data packet tests

pull/894/head
pabloFuente 2026-04-21 19:04:51 +02:00
parent 05209f1d97
commit 7907fc40e5
8 changed files with 164 additions and 68 deletions

View File

@ -24,6 +24,7 @@ import java.time.Duration;
import java.util.AbstractMap;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Map.Entry;
import java.util.concurrent.Callable;
@ -164,10 +165,26 @@ public class OpenViduTestAppE2eTest extends AbstractOpenViduTestappE2eTest {
}
@Test
@DisplayName("Signal")
void signalTest() throws Exception {
@DisplayName("Signal Reliable DataChannel")
void signalReliableTest() throws Exception {
OpenViduTestappUser user = setupBrowserAndConnectToOpenViduTestapp("chrome");
log.info("Signal");
log.info("Signal Reliable DataChannel");
signalReliableLossyAux(user, true);
}
@Test
@DisplayName("Signal Lossy DataChannel")
void signalLossyTest() throws Exception {
OpenViduTestappUser user = setupBrowserAndConnectToOpenViduTestapp("chrome");
log.info("Signal Lossy DataChannel");
signalReliableLossyAux(user, false);
}
private void signalReliableLossyAux(OpenViduTestappUser user, boolean reliable) throws Exception {
final int expectedKind = reliable ? 0 : 1; // DataPacket_Kind: RELIABLE=0, LOSSY=1
final String expectedKindStr = reliable ? "RELIABLE" : "LOSSY";
final String btnClass = reliable ? ".message-reliable-btn" : ".message-lossy-btn";
for (int i = 0; i < 2; i++) {
WebElement addUserBtn = user.getDriver().findElement(By.id("add-user-btn"));
addUserBtn.click();
@ -181,129 +198,156 @@ public class OpenViduTestAppE2eTest extends AbstractOpenViduTestappE2eTest {
user.getEventManager().waitUntilEventReaches("participantConnected", "RoomEvent", 1);
user.getEventManager().waitUntilEventReaches("participantActive", "RoomEvent", 1);
// Broadcast signal
Collection<Entry<String, String>> assertions = new ArrayList<>();
List<Integer> kindAssertions = Collections.synchronizedList(new ArrayList<>());
// Broadcast from TestParticipant0
final CountDownLatch signalEventLatch1 = new CountDownLatch(2);
final CountDownLatch broadcastLatch0 = new CountDownLatch(2);
user.getEventManager().on(1, "dataReceived", "RoomEvent", json -> {
assertions.add(new AbstractMap.SimpleEntry<>("Message from TestParticipant0 to all room",
assertions.add(new AbstractMap.SimpleEntry<>(
"Message from TestParticipant0 to all room (kind: " + expectedKindStr + ")",
json.getAsJsonObject().get("eventDescription").getAsString()));
signalEventLatch1.countDown();
kindAssertions.add(json.getAsJsonObject().get("eventContent").getAsJsonObject().get("kind").getAsInt());
broadcastLatch0.countDown();
});
user.getEventManager().on(1, "dataReceived", "ParticipantEvent", json -> {
assertions.add(new AbstractMap.SimpleEntry<>("Message from TestParticipant0 to all room",
assertions.add(new AbstractMap.SimpleEntry<>(
"Message from TestParticipant0 to all room (kind: " + expectedKindStr + ")",
json.getAsJsonObject().get("eventDescription").getAsString()));
signalEventLatch1.countDown();
kindAssertions.add(json.getAsJsonObject().get("eventContent").getAsJsonObject().get("kind").getAsInt());
broadcastLatch0.countDown();
});
user.getDriver().findElement(By.cssSelector("#openvidu-instance-0 .message-btn")).click();
user.getDriver().findElement(By.cssSelector("#openvidu-instance-0 " + btnClass)).click();
user.getEventManager().waitUntilEventReaches(1, "dataReceived", "RoomEvent", 1);
user.getEventManager().waitUntilEventReaches(1, "dataReceived", "ParticipantEvent", 1);
// Do not trigger own signals
Assertions.assertEquals(0, user.getEventManager().getNumEvents(0, "dataReceived-RoomEvent").get());
Assertions.assertEquals(0, user.getEventManager().getNumEvents(0, "dataReceived-ParticipantEvent").get());
if (!signalEventLatch1.await(3, TimeUnit.SECONDS)) {
Assertions.fail("Timeout waiting for signal event content check");
if (!broadcastLatch0.await(3, TimeUnit.SECONDS)) {
Assertions.fail("Timeout waiting for broadcast signal event from TestParticipant0");
}
assertions.forEach(assertion -> Assertions.assertEquals(assertion.getKey(), assertion.getValue()));
kindAssertions.forEach(
kind -> Assertions.assertEquals(expectedKind, kind, "Expected DataPacket_Kind " + expectedKind));
user.getEventManager().off(1, "dataReceived", "RoomEvent");
user.getEventManager().off(1, "dataReceived", "ParticipantEvent");
assertions.clear();
kindAssertions.clear();
user.getEventManager().clearAllCurrentEvents();
// Broadcast from TestParticipant1
final CountDownLatch signalEventLatch2 = new CountDownLatch(2);
final CountDownLatch broadcastLatch1 = new CountDownLatch(2);
user.getEventManager().on(0, "dataReceived", "RoomEvent", json -> {
assertions.add(new AbstractMap.SimpleEntry<>("Message from TestParticipant1 to all room",
assertions.add(new AbstractMap.SimpleEntry<>(
"Message from TestParticipant1 to all room (kind: " + expectedKindStr + ")",
json.getAsJsonObject().get("eventDescription").getAsString()));
signalEventLatch2.countDown();
kindAssertions.add(json.getAsJsonObject().get("eventContent").getAsJsonObject().get("kind").getAsInt());
broadcastLatch1.countDown();
});
user.getEventManager().on(0, "dataReceived", "ParticipantEvent", json -> {
assertions.add(new AbstractMap.SimpleEntry<>("Message from TestParticipant1 to all room",
assertions.add(new AbstractMap.SimpleEntry<>(
"Message from TestParticipant1 to all room (kind: " + expectedKindStr + ")",
json.getAsJsonObject().get("eventDescription").getAsString()));
signalEventLatch2.countDown();
kindAssertions.add(json.getAsJsonObject().get("eventContent").getAsJsonObject().get("kind").getAsInt());
broadcastLatch1.countDown();
});
user.getDriver().findElement(By.cssSelector("#openvidu-instance-1 .message-btn")).click();
user.getDriver().findElement(By.cssSelector("#openvidu-instance-1 " + btnClass)).click();
user.getEventManager().waitUntilEventReaches(0, "dataReceived", "RoomEvent", 1);
user.getEventManager().waitUntilEventReaches(0, "dataReceived", "ParticipantEvent", 1);
// Do not trigger own signals
Assertions.assertEquals(1, user.getEventManager().getNumEvents(0, "dataReceived-RoomEvent").get());
Assertions.assertEquals(1, user.getEventManager().getNumEvents(0, "dataReceived-ParticipantEvent").get());
if (!signalEventLatch2.await(3, TimeUnit.SECONDS)) {
Assertions.fail("Timeout waiting for signal event content check");
if (!broadcastLatch1.await(3, TimeUnit.SECONDS)) {
Assertions.fail("Timeout waiting for broadcast signal event from TestParticipant1");
}
assertions.forEach(assertion -> Assertions.assertEquals(assertion.getKey(), assertion.getValue()));
kindAssertions.forEach(
kind -> Assertions.assertEquals(expectedKind, kind, "Expected DataPacket_Kind " + expectedKind));
user.getEventManager().off(0, "dataReceived", "RoomEvent");
user.getEventManager().off(0, "dataReceived", "ParticipantEvent");
assertions.clear();
kindAssertions.clear();
user.getEventManager().clearAllCurrentEvents();
// Signal specific participant
// Signal from TestParticipant0 to TestParticipant1
final CountDownLatch signalEventLatch3 = new CountDownLatch(2);
final CountDownLatch directLatch0 = new CountDownLatch(2);
user.getEventManager().on(1, "dataReceived", "RoomEvent", json -> {
assertions.add(new AbstractMap.SimpleEntry<>("Message from TestParticipant0 to TestParticipant1",
assertions.add(new AbstractMap.SimpleEntry<>(
"Message from TestParticipant0 to TestParticipant1 (kind: " + expectedKindStr + ")",
json.getAsJsonObject().get("eventDescription").getAsString()));
signalEventLatch3.countDown();
kindAssertions.add(json.getAsJsonObject().get("eventContent").getAsJsonObject().get("kind").getAsInt());
directLatch0.countDown();
});
user.getEventManager().on(1, "dataReceived", "ParticipantEvent", json -> {
assertions.add(new AbstractMap.SimpleEntry<>("Message from TestParticipant0 to TestParticipant1",
assertions.add(new AbstractMap.SimpleEntry<>(
"Message from TestParticipant0 to TestParticipant1 (kind: " + expectedKindStr + ")",
json.getAsJsonObject().get("eventDescription").getAsString()));
signalEventLatch3.countDown();
kindAssertions.add(json.getAsJsonObject().get("eventContent").getAsJsonObject().get("kind").getAsInt());
directLatch0.countDown();
});
user.getDriver()
.findElement(By.cssSelector("#openvidu-instance-0 app-participant.remote-participant .message-btn"))
.findElement(
By.cssSelector("#openvidu-instance-0 app-participant.remote-participant " + btnClass))
.click();
user.getEventManager().waitUntilEventReaches(1, "dataReceived", "RoomEvent", 1);
user.getEventManager().waitUntilEventReaches(1, "dataReceived", "ParticipantEvent", 1);
// Do not trigger own signals
Assertions.assertEquals(0, user.getEventManager().getNumEvents(0, "dataReceived-RoomEvent").get());
Assertions.assertEquals(0, user.getEventManager().getNumEvents(0, "dataReceived-ParticipantEvent").get());
if (!signalEventLatch3.await(3, TimeUnit.SECONDS)) {
Assertions.fail("Timeout waiting for signal event content check");
if (!directLatch0.await(3, TimeUnit.SECONDS)) {
Assertions.fail("Timeout waiting for direct signal event from TestParticipant0");
}
assertions.forEach(assertion -> Assertions.assertEquals(assertion.getKey(), assertion.getValue()));
kindAssertions.forEach(
kind -> Assertions.assertEquals(expectedKind, kind, "Expected DataPacket_Kind " + expectedKind));
user.getEventManager().off(1, "dataReceived", "RoomEvent");
user.getEventManager().off(1, "dataReceived", "ParticipantEvent");
assertions.clear();
kindAssertions.clear();
user.getEventManager().clearAllCurrentEvents();
// Signal from TestParticipant1 to TestParticipant0
final CountDownLatch signalEventLatch4 = new CountDownLatch(2);
final CountDownLatch directLatch1 = new CountDownLatch(2);
user.getEventManager().on(0, "dataReceived", "RoomEvent", json -> {
assertions.add(new AbstractMap.SimpleEntry<>("Message from TestParticipant1 to TestParticipant0",
assertions.add(new AbstractMap.SimpleEntry<>(
"Message from TestParticipant1 to TestParticipant0 (kind: " + expectedKindStr + ")",
json.getAsJsonObject().get("eventDescription").getAsString()));
signalEventLatch4.countDown();
kindAssertions.add(json.getAsJsonObject().get("eventContent").getAsJsonObject().get("kind").getAsInt());
directLatch1.countDown();
});
user.getEventManager().on(0, "dataReceived", "ParticipantEvent", json -> {
assertions.add(new AbstractMap.SimpleEntry<>("Message from TestParticipant1 to TestParticipant0",
assertions.add(new AbstractMap.SimpleEntry<>(
"Message from TestParticipant1 to TestParticipant0 (kind: " + expectedKindStr + ")",
json.getAsJsonObject().get("eventDescription").getAsString()));
signalEventLatch4.countDown();
kindAssertions.add(json.getAsJsonObject().get("eventContent").getAsJsonObject().get("kind").getAsInt());
directLatch1.countDown();
});
user.getDriver()
.findElement(By.cssSelector("#openvidu-instance-1 app-participant.remote-participant .message-btn"))
.findElement(
By.cssSelector("#openvidu-instance-1 app-participant.remote-participant " + btnClass))
.click();
user.getEventManager().waitUntilEventReaches(0, "dataReceived", "RoomEvent", 1);
user.getEventManager().waitUntilEventReaches(0, "dataReceived", "ParticipantEvent", 1);
// Do not trigger own signals
Assertions.assertEquals(0, user.getEventManager().getNumEvents(1, "dataReceived-RoomEvent").get());
Assertions.assertEquals(0, user.getEventManager().getNumEvents(1, "dataReceived-ParticipantEvent").get());
if (!signalEventLatch4.await(3, TimeUnit.SECONDS)) {
Assertions.fail("Timeout waiting for signal event content check");
if (!directLatch1.await(3, TimeUnit.SECONDS)) {
Assertions.fail("Timeout waiting for direct signal event from TestParticipant1");
}
assertions.forEach(assertion -> Assertions.assertEquals(assertion.getKey(), assertion.getValue()));
kindAssertions.forEach(
kind -> Assertions.assertEquals(expectedKind, kind, "Expected DataPacket_Kind " + expectedKind));
gracefullyLeaveParticipants(user, 2);
}
@ -2819,7 +2863,8 @@ public class OpenViduTestAppE2eTest extends AbstractOpenViduTestappE2eTest {
Thread.sleep(retryIntervalMillis);
} catch (InterruptedException e) {
// Print screenshot
String screenshot = "data:image/png;base64," + ((TakesScreenshot) user.getDriver()).getScreenshotAs(BASE64);
String screenshot = "data:image/png;base64,"
+ ((TakesScreenshot) user.getDriver()).getScreenshotAs(BASE64);
System.out.println("INTERRUPTED EXCEPTION WHILE WAITING FOR ELEMENT TO BE CLICKABLE: " + cssSelector);
System.out.println(screenshot);
Thread.currentThread().interrupt();
@ -2836,10 +2881,10 @@ public class OpenViduTestAppE2eTest extends AbstractOpenViduTestappE2eTest {
+ "' to be clickable without backdrop interference after " + timeoutMillis + "ms");
}
public boolean assertAllElementsHaveTracks(OpenViduTestappUser user, String selector, boolean hasAudio, boolean hasVideo) {
public boolean assertAllElementsHaveTracks(OpenViduTestappUser user, String selector, boolean hasAudio,
boolean hasVideo) {
org.openqa.selenium.JavascriptExecutor js = (org.openqa.selenium.JavascriptExecutor) user.getDriver();
String script =
"var elements = document.querySelectorAll(arguments[0]);" +
String script = "var elements = document.querySelectorAll(arguments[0]);" +
"for (var i = 0; i < elements.length; i++) {" +
" var el = elements[i];" +
" if (!el.srcObject) return false;" +
@ -2852,9 +2897,12 @@ public class OpenViduTestAppE2eTest extends AbstractOpenViduTestappE2eTest {
return (Boolean) js.executeScript(script, selector, hasAudio, hasVideo);
}
public void changeElementSize(OpenViduTestappUser user, org.openqa.selenium.WebElement element, int width, int height) {
public void changeElementSize(OpenViduTestappUser user, org.openqa.selenium.WebElement element, int width,
int height) {
org.openqa.selenium.JavascriptExecutor js = (org.openqa.selenium.JavascriptExecutor) user.getDriver();
js.executeScript("arguments[0].style.width = '" + width + "px'; arguments[0].style.height = '" + height + "px';", element);
js.executeScript(
"arguments[0].style.width = '" + width + "px'; arguments[0].style.height = '" + height + "px';",
element);
}
}

View File

@ -38,6 +38,7 @@ mat-card.room-card {
cursor: pointer;
padding: 0;
margin-right: 4px;
font-size: 22px;
}
.room-actions button:hover {

View File

@ -58,8 +58,14 @@
<button class="peer-info-btn" (click)="openInfoDialog()" title="PCTransports info">
<mat-icon aria-label="PCTransports info button">info</mat-icon>
</button>
<button class="message-btn" (click)="sendData()" title="Broadcast message to room">
<mat-icon aria-label="Send message button">chat</mat-icon>
<button class="message-reliable-btn" (click)="sendDataReliable()" title="Broadcast reliable message to room">
<mat-icon aria-label="Send reliable message button">chat_bubble</mat-icon>
</button>
<button class="message-lossy-btn" (click)="sendDataLossy()" title="Broadcast lossy message to room">
<mat-icon aria-label="Send lossy message button">chat_dashed</mat-icon>
</button>
<button class="message-lossy-burst-btn" (click)="sendDataLossyBurst()" title="Broadcast 500 lossy messages in burst">
<mat-icon aria-label="Send lossy burst button">fast_forward</mat-icon>
</button>
<button class="disconnect-btn" (click)="disconnectRoom()" title="Disconnect room">
<mat-icon aria-label="Disconnect button">clear</mat-icon>
@ -89,7 +95,9 @@
[index]="index"></app-participant>
@for (participant of room.remoteParticipants | keyvalue; track participant) {
<app-participant class="remote-participant"
[participant]="participant.value" [room]="room" [index]="index" (sendDataToOneParticipant)="sendData($event)"></app-participant>
[participant]="participant.value" [room]="room" [index]="index"
(sendReliableDataToOneParticipant)="sendDataReliable($event)"
(sendLossyDataToOneParticipant)="sendDataLossy($event)"></app-participant>
}
</div>
</mat-card-content>

View File

@ -770,7 +770,8 @@ export class OpenviduInstanceComponent {
kind?: DataPacket_Kind,
topic?: string
) => {
const decodedPayload = this.decoder.decode(payload);
let decodedPayload = this.decoder.decode(payload);
decodedPayload += ` (kind: ${DataPacket_Kind[kind!]})`;
this.updateEventList(
RoomEvent.DataReceived,
{ payload: decodedPayload, participant, kind, topic },
@ -1195,14 +1196,14 @@ export class OpenviduInstanceComponent {
});
}
sendData(destinationIdentity?: string) {
sendData(destinationIdentity?: string, reliable: boolean = true) {
let strData = `Message from ${this.room?.localParticipant.identity}`;
strData += destinationIdentity
? ` to ${destinationIdentity}`
: ' to all room';
const data = new TextEncoder().encode(strData);
let options: DataPublishOptions = {
reliable: true,
reliable,
};
if (destinationIdentity) {
options.destinationIdentities = [destinationIdentity];
@ -1210,6 +1211,22 @@ export class OpenviduInstanceComponent {
this.room?.localParticipant.publishData(data, options);
}
sendDataReliable(destinationIdentity?: string) {
this.sendData(destinationIdentity, true);
}
sendDataLossy(destinationIdentity?: string) {
this.sendData(destinationIdentity, false);
}
sendDataLossyBurst(count: number = 500) {
for (let i = 0; i < count; i++) {
const strData = `Lossy burst ${i + 1}/${count} from ${this.room?.localParticipant.identity}`;
const data = new TextEncoder().encode(strData);
this.room?.localParticipant.publishData(data, { reliable: false });
}
}
openInfoDialog() {
const updateFunction = async (): Promise<string> => {
const pub: PCTransport = this.getPublisherPC()!;

View File

@ -46,8 +46,11 @@
</button>
}
@if (!participant.isLocal) {
<button class="message-btn" (click)="sendData()" title="Send message to this participant">
<mat-icon aria-label="Send message button">chat</mat-icon>
<button class="message-reliable-btn" (click)="sendDataReliable()" title="Send reliable message to this participant">
<mat-icon aria-label="Send reliable message button">chat_bubble</mat-icon>
</button>
<button class="message-lossy-btn" (click)="sendDataLossy()" title="Send lossy message to this participant">
<mat-icon aria-label="Send lossy message button">chat_dashed</mat-icon>
</button>
}
</div>

View File

@ -57,7 +57,10 @@ export class ParticipantComponent {
index: number;
@Output()
sendDataToOneParticipant = new EventEmitter<string>();
sendReliableDataToOneParticipant = new EventEmitter<string>();
@Output()
sendLossyDataToOneParticipant = new EventEmitter<string>();
localParticipant: LocalParticipant | undefined;
@ -340,7 +343,8 @@ export class ParticipantComponent {
.on(
ParticipantEvent.DataReceived,
(payload: Uint8Array, kind: DataPacket_Kind) => {
const decodedPayload = this.decoder.decode(payload);
let decodedPayload = this.decoder.decode(payload);
decodedPayload += ` (kind: ${DataPacket_Kind[kind]})`;
this.updateEventList(
ParticipantEvent.DataReceived,
'ParticipantEvent',
@ -468,7 +472,11 @@ export class ParticipantComponent {
this.testFeedService.pushNewEvent({ user: this.index, event });
}
sendData() {
this.sendDataToOneParticipant.emit(this.participant.identity);
sendDataReliable() {
this.sendReliableDataToOneParticipant.emit(this.participant.identity);
}
sendDataLossy() {
this.sendLossyDataToOneParticipant.emit(this.participant.identity);
}
}

View File

@ -9,7 +9,7 @@
<link rel="icon" type="image/x-icon" href="favicon.ico">
<link rel="preconnect" href="https://fonts.gstatic.com">
<link href="https://fonts.googleapis.com/css2?family=Roboto:wght@300;400;500&display=swap" rel="stylesheet">
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Material+Symbols+Sharp:opsz,wght,FILL,GRAD@24,500,1,-25"/>
</head>
<body class="mat-typography">

View File

@ -1,4 +1,9 @@
/* You can add global styles to this file, and also import other style files */
.mat-icon {
font-family: "Material Symbols Sharp" !important;
}
html,
body {
height: 100%;
@ -8,8 +13,14 @@ body {
}
body {
font-family: Roboto, Helvetica Neue Light, Helvetica Neue, Helvetica, Arial,
Lucida Grande, sans-serif;
font-family:
Roboto,
Helvetica Neue Light,
Helvetica Neue,
Helvetica,
Arial,
Lucida Grande,
sans-serif;
}
a {