mirror of https://github.com/OpenVidu/openvidu.git
openvidu-testapp & openvidu-test-e2e: add Data Track e2e test
parent
8b3bfc3b69
commit
66001d8f11
|
|
@ -381,6 +381,127 @@ public class OpenViduTestAppE2eTest extends AbstractOpenViduTestappE2eTest {
|
||||||
gracefullyLeaveParticipants(user, 2);
|
gracefullyLeaveParticipants(user, 2);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("Data Tracks publish, subscribe, send and receive")
|
||||||
|
void dataTracksTest() throws Exception {
|
||||||
|
|
||||||
|
OpenViduTestappUser user = setupBrowserAndConnectToOpenViduTestapp("chrome");
|
||||||
|
|
||||||
|
log.info("Data Tracks publish, subscribe, send and receive");
|
||||||
|
|
||||||
|
// Two participants, no audio/video publishing
|
||||||
|
for (int i = 0; i < 2; i++) {
|
||||||
|
WebElement addUserBtn = user.getDriver().findElement(By.id("add-user-btn"));
|
||||||
|
addUserBtn.click();
|
||||||
|
user.getDriver().findElement(By.cssSelector("#openvidu-instance-" + i + " .subscriber-checkbox")).click();
|
||||||
|
user.getDriver().findElement(By.cssSelector("#openvidu-instance-" + i + " .publisher-checkbox")).click();
|
||||||
|
}
|
||||||
|
user.getDriver().findElements(By.className("connect-btn")).forEach(el -> el.sendKeys(Keys.ENTER));
|
||||||
|
user.getEventManager().waitUntilEventReaches("signalConnected", "RoomEvent", 2);
|
||||||
|
user.getEventManager().waitUntilEventReaches("connected", "RoomEvent", 2);
|
||||||
|
user.getEventManager().waitUntilEventReaches("participantConnected", "RoomEvent", 1);
|
||||||
|
user.getEventManager().waitUntilEventReaches("participantActive", "RoomEvent", 1);
|
||||||
|
|
||||||
|
// Participant 0 publishes a data track
|
||||||
|
user.getDriver().findElement(By.cssSelector("#openvidu-instance-0 .add-data-track-btn")).click();
|
||||||
|
|
||||||
|
// Wait for localDataTrackPublished on participant 0 and dataTrackPublished on participant 1
|
||||||
|
user.getEventManager().waitUntilEventReaches(0, "localDataTrackPublished", "RoomEvent", 1);
|
||||||
|
user.getEventManager().waitUntilEventReaches(1, "dataTrackPublished", "RoomEvent", 1);
|
||||||
|
|
||||||
|
// Verify event descriptions
|
||||||
|
user.getDriver().findElements(By.cssSelector(".localDataTrackPublished-TestParticipant0 .event-content"))
|
||||||
|
.forEach(el -> Assertions.assertTrue(el.getText().contains("data_track_1"),
|
||||||
|
"Expected localDataTrackPublished to contain track name 'data_track_1'"));
|
||||||
|
user.getDriver().findElements(By.cssSelector(".dataTrackPublished-TestParticipant1 .event-content"))
|
||||||
|
.forEach(el -> Assertions.assertTrue(
|
||||||
|
el.getText().contains("TestParticipant0") && el.getText().contains("data_track_1"),
|
||||||
|
"Expected dataTrackPublished to contain publisher identity and track name"));
|
||||||
|
|
||||||
|
// Participant 0 does not receive its own dataTrackPublished
|
||||||
|
Assertions.assertEquals(0, user.getEventManager().getNumEvents(0, "dataTrackPublished-RoomEvent").get(),
|
||||||
|
"Publisher should not receive dataTrackPublished for its own data track");
|
||||||
|
|
||||||
|
// Participant 0 sends data frames
|
||||||
|
org.openqa.selenium.JavascriptExecutor js = (org.openqa.selenium.JavascriptExecutor) user.getDriver();
|
||||||
|
js.executeScript("document.querySelector('#openvidu-instance-0 .send-data-frame-btn').click()");
|
||||||
|
js.executeScript("document.querySelector('#openvidu-instance-0 .send-data-frame-btn').click()");
|
||||||
|
js.executeScript("document.querySelector('#openvidu-instance-0 .send-data-frame-btn').click()");
|
||||||
|
|
||||||
|
// Wait for frames to arrive at participant 1
|
||||||
|
Thread.sleep(1000);
|
||||||
|
Long frameCount = (Long) js.executeScript(
|
||||||
|
"var el = document.querySelector('#openvidu-instance-1 .data-track-frame-count');" +
|
||||||
|
"return el ? parseInt(el.textContent) : 0;");
|
||||||
|
Assertions.assertEquals(frameCount, 3, "Expected 3 data track frames received, got " + frameCount);
|
||||||
|
|
||||||
|
String lastPayload = (String) js.executeScript(
|
||||||
|
"var el = document.querySelector('#openvidu-instance-1 .data-track-last-payload');" +
|
||||||
|
"return el ? el.textContent : '';");
|
||||||
|
Assertions.assertTrue(lastPayload.contains("DataTrackFrame from TestParticipant0"),
|
||||||
|
"Expected last payload to contain 'DataTrackFrame from TestParticipant0', got: " + lastPayload);
|
||||||
|
|
||||||
|
// Participant 0 unpublishes the data track
|
||||||
|
js.executeScript("document.querySelector('#openvidu-instance-0 .unpublish-data-track-btn').click()");
|
||||||
|
|
||||||
|
// Wait for unpublish events
|
||||||
|
user.getEventManager().waitUntilEventReaches(0, "localDataTrackUnpublished", "RoomEvent", 1);
|
||||||
|
user.getEventManager().waitUntilEventReaches(1, "dataTrackUnpublished", "RoomEvent", 1);
|
||||||
|
|
||||||
|
// Participant 0 does not receive its own dataTrackUnpublished
|
||||||
|
Assertions.assertEquals(0, user.getEventManager().getNumEvents(0, "dataTrackUnpublished-RoomEvent").get(),
|
||||||
|
"Publisher should not receive dataTrackUnpublished for its own data track");
|
||||||
|
|
||||||
|
user.getEventManager().clearAllCurrentEvents();
|
||||||
|
|
||||||
|
// Now participant 1 publishes a data track with custom name (bidirectional test)
|
||||||
|
String customTrackName = "my_custom_data_track";
|
||||||
|
user.getDriver().findElement(By.cssSelector("#openvidu-instance-1 .options-data-track-btn")).click();
|
||||||
|
Thread.sleep(500);
|
||||||
|
WebElement trackNameInput = user.getDriver().findElement(By.id("dataTrack-name"));
|
||||||
|
trackNameInput.clear();
|
||||||
|
trackNameInput.sendKeys(customTrackName);
|
||||||
|
user.getDriver().findElement(By.id("close-dialog-btn")).click();
|
||||||
|
Thread.sleep(500);
|
||||||
|
user.getDriver().findElement(By.cssSelector("#openvidu-instance-1 .add-data-track-btn")).click();
|
||||||
|
|
||||||
|
user.getEventManager().waitUntilEventReaches(1, "localDataTrackPublished", "RoomEvent", 1);
|
||||||
|
user.getEventManager().waitUntilEventReaches(0, "dataTrackPublished", "RoomEvent", 1);
|
||||||
|
|
||||||
|
// Verify participant 0 sees the custom track name on the remote data track
|
||||||
|
Thread.sleep(500);
|
||||||
|
String remoteTrackName = (String) js.executeScript(
|
||||||
|
"var el = document.querySelector('#openvidu-instance-0 .data-track-name');" +
|
||||||
|
"return el ? el.textContent.trim() : '';");
|
||||||
|
Assertions.assertEquals(customTrackName, remoteTrackName,
|
||||||
|
"Remote data track name should be '" + customTrackName + "', got: " + remoteTrackName);
|
||||||
|
|
||||||
|
// Participant 1 sends a data frame
|
||||||
|
js.executeScript("document.querySelector('#openvidu-instance-1 .send-data-frame-btn').click()");
|
||||||
|
|
||||||
|
// Wait for frame to arrive at participant 0
|
||||||
|
Thread.sleep(1000);
|
||||||
|
|
||||||
|
Long frameCount0 = (Long) js.executeScript(
|
||||||
|
"var el = document.querySelector('#openvidu-instance-0 .data-track-frame-count');" +
|
||||||
|
"return el ? parseInt(el.textContent) : 0;");
|
||||||
|
Assertions.assertTrue(frameCount0 >= 1,
|
||||||
|
"Expected at least 1 data track frame received by participant 0, got " + frameCount0);
|
||||||
|
|
||||||
|
String lastPayload0 = (String) js.executeScript(
|
||||||
|
"var el = document.querySelector('#openvidu-instance-0 .data-track-last-payload');" +
|
||||||
|
"return el ? el.textContent : '';");
|
||||||
|
Assertions.assertTrue(lastPayload0.contains("DataTrackFrame from TestParticipant1"),
|
||||||
|
"Expected last payload to contain 'DataTrackFrame from TestParticipant1', got: " + lastPayload0);
|
||||||
|
|
||||||
|
// Participant 1 unpublishes
|
||||||
|
js.executeScript("document.querySelector('#openvidu-instance-1 .unpublish-data-track-btn').click()");
|
||||||
|
user.getEventManager().waitUntilEventReaches(1, "localDataTrackUnpublished", "RoomEvent", 1);
|
||||||
|
user.getEventManager().waitUntilEventReaches(0, "dataTrackUnpublished", "RoomEvent", 1);
|
||||||
|
|
||||||
|
gracefullyLeaveParticipants(user, 2);
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@DisplayName("One2One only audio")
|
@DisplayName("One2One only audio")
|
||||||
void oneToOneOnlyAudioSession() throws Exception {
|
void oneToOneOnlyAudioSession() throws Exception {
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,52 @@
|
||||||
|
.parent-div {
|
||||||
|
display: grid;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
background-color: #fcfcfc;
|
||||||
|
padding: 2px;
|
||||||
|
border-radius: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.data-track-name {
|
||||||
|
font-size: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bottom-div {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-around;
|
||||||
|
margin-bottom: -2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.data-btn {
|
||||||
|
border: none;
|
||||||
|
background: rgba(255, 255, 255, 0.75);
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 0;
|
||||||
|
height: 16px;
|
||||||
|
float: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.data-btn:hover {
|
||||||
|
color: #4d4d4d;
|
||||||
|
}
|
||||||
|
|
||||||
|
.data-btn mat-icon {
|
||||||
|
font-size: 14px;
|
||||||
|
width: 14px;
|
||||||
|
height: 14px;
|
||||||
|
line-height: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.data-track-info {
|
||||||
|
font-size: 10px;
|
||||||
|
padding: 2px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.data-track-frame-count {
|
||||||
|
font-weight: bold;
|
||||||
|
margin-right: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.data-track-last-payload {
|
||||||
|
color: #555;
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,25 @@
|
||||||
|
<div class="parent-div">
|
||||||
|
<span class="data-track-name">{{getTrackName()}}</span>
|
||||||
|
<div class="bottom-div">
|
||||||
|
@if (localParticipant && localDataTrack) {
|
||||||
|
<button (click)="sendDataFrame()" class="data-btn send-data-frame-btn" matTooltip="Send data frame"
|
||||||
|
matTooltipClass="custom-tooltip">
|
||||||
|
<mat-icon aria-label="Send data frame" class="mat-icon material-icons" role="img"
|
||||||
|
aria-hidden="true">send</mat-icon>
|
||||||
|
</button>
|
||||||
|
}
|
||||||
|
@if (localParticipant && localDataTrack) {
|
||||||
|
<button (click)="unpublishDataTrack()" class="data-btn unpublish-data-track-btn" matTooltip="Unpublish data track"
|
||||||
|
matTooltipClass="custom-tooltip">
|
||||||
|
<mat-icon aria-label="Unpublish data track" class="mat-icon material-icons" role="img"
|
||||||
|
aria-hidden="true">stop</mat-icon>
|
||||||
|
</button>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
@if (frameCount > 0) {
|
||||||
|
<div class="data-track-info">
|
||||||
|
<span class="data-track-frame-count">{{frameCount}}</span>
|
||||||
|
<span class="data-track-last-payload">{{lastPayload}}</span>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
@ -0,0 +1,98 @@
|
||||||
|
import { Component, EventEmitter, Input, Output, ChangeDetectorRef } from '@angular/core';
|
||||||
|
import { MatIconModule } from '@angular/material/icon';
|
||||||
|
import { MatTooltipModule } from '@angular/material/tooltip';
|
||||||
|
import {
|
||||||
|
LocalDataTrack,
|
||||||
|
LocalParticipant,
|
||||||
|
RemoteDataTrack,
|
||||||
|
} from 'livekit-client';
|
||||||
|
import {
|
||||||
|
TestAppEvent,
|
||||||
|
TestFeedService,
|
||||||
|
} from 'src/app/services/test-feed.service';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-data-track',
|
||||||
|
templateUrl: './data-track.component.html',
|
||||||
|
styleUrl: './data-track.component.css',
|
||||||
|
imports: [MatIconModule, MatTooltipModule],
|
||||||
|
})
|
||||||
|
export class DataTrackComponent {
|
||||||
|
@Input()
|
||||||
|
localParticipant: LocalParticipant | undefined;
|
||||||
|
|
||||||
|
@Input()
|
||||||
|
index: number;
|
||||||
|
|
||||||
|
@Input()
|
||||||
|
localDataTrack?: LocalDataTrack;
|
||||||
|
|
||||||
|
@Input()
|
||||||
|
remoteDataTrack?: RemoteDataTrack;
|
||||||
|
|
||||||
|
@Output()
|
||||||
|
newTrackEvent = new EventEmitter<TestAppEvent>();
|
||||||
|
|
||||||
|
@Output()
|
||||||
|
trackUnpublished = new EventEmitter<LocalDataTrack>();
|
||||||
|
|
||||||
|
// Received frame info
|
||||||
|
frameCount: number = 0;
|
||||||
|
lastPayload: string = '';
|
||||||
|
|
||||||
|
private decoder = new TextDecoder();
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private testFeedService: TestFeedService,
|
||||||
|
private cdr: ChangeDetectorRef
|
||||||
|
) {}
|
||||||
|
|
||||||
|
ngOnInit() {
|
||||||
|
if (this.remoteDataTrack) {
|
||||||
|
this.subscribeToRemoteDataTrack(this.remoteDataTrack);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sendDataFrame() {
|
||||||
|
if (this.localDataTrack) {
|
||||||
|
const payload = new TextEncoder().encode(
|
||||||
|
`DataTrackFrame from ${this.localParticipant?.identity}`
|
||||||
|
);
|
||||||
|
this.localDataTrack.tryPush({ payload });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async unpublishDataTrack() {
|
||||||
|
if (this.localDataTrack) {
|
||||||
|
const track = this.localDataTrack;
|
||||||
|
await track.unpublish();
|
||||||
|
this.trackUnpublished.emit(track);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async subscribeToRemoteDataTrack(track: RemoteDataTrack) {
|
||||||
|
const stream = track.subscribe();
|
||||||
|
const reader = stream.getReader();
|
||||||
|
try {
|
||||||
|
while (true) {
|
||||||
|
const { value, done } = await reader.read();
|
||||||
|
if (done) break;
|
||||||
|
this.frameCount++;
|
||||||
|
this.lastPayload = this.decoder.decode(value.payload);
|
||||||
|
this.cdr.detectChanges();
|
||||||
|
}
|
||||||
|
} catch (_) {
|
||||||
|
// Stream closed
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getTrackName(): string {
|
||||||
|
if (this.localDataTrack?.info) {
|
||||||
|
return this.localDataTrack.info.name;
|
||||||
|
}
|
||||||
|
if (this.remoteDataTrack) {
|
||||||
|
return this.remoteDataTrack.info.name;
|
||||||
|
}
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -233,6 +233,16 @@
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
@if (dataTrackName !== undefined) {
|
||||||
|
<mat-divider></mat-divider>
|
||||||
|
<div>
|
||||||
|
<label>DataTrackOptions</label><br>
|
||||||
|
<mat-form-field>
|
||||||
|
<mat-label>Track Name</mat-label>
|
||||||
|
<input matInput id="dataTrack-name" placeholder="data_track_1" [(ngModel)]="dataTrackName"/>
|
||||||
|
</mat-form-field>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
</mat-dialog-content>
|
</mat-dialog-content>
|
||||||
|
|
||||||
<mat-dialog-actions>
|
<mat-dialog-actions>
|
||||||
|
|
|
||||||
|
|
@ -38,6 +38,8 @@ export class OptionsDialogComponent {
|
||||||
allowDisablingVideo = true;
|
allowDisablingVideo = true;
|
||||||
allowDisablingScreen = true;
|
allowDisablingScreen = true;
|
||||||
|
|
||||||
|
dataTrackName?: string;
|
||||||
|
|
||||||
videoOption: true | false | 'custom';
|
videoOption: true | false | 'custom';
|
||||||
audioOption: true | false | 'custom';
|
audioOption: true | false | 'custom';
|
||||||
screenOption: true | false | 'custom';
|
screenOption: true | false | 'custom';
|
||||||
|
|
@ -62,6 +64,7 @@ export class OptionsDialogComponent {
|
||||||
allowDisablingAudio?: boolean;
|
allowDisablingAudio?: boolean;
|
||||||
allowDisablingVideo?: boolean;
|
allowDisablingVideo?: boolean;
|
||||||
allowDisablingScreen?: boolean;
|
allowDisablingScreen?: boolean;
|
||||||
|
dataTrackName?: string;
|
||||||
}>(MAT_DIALOG_DATA);
|
}>(MAT_DIALOG_DATA);
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
|
|
@ -114,6 +117,9 @@ export class OptionsDialogComponent {
|
||||||
if (this.data.allowDisablingScreen === false) {
|
if (this.data.allowDisablingScreen === false) {
|
||||||
this.allowDisablingScreen = false;
|
this.allowDisablingScreen = false;
|
||||||
}
|
}
|
||||||
|
if (this.data.dataTrackName !== undefined) {
|
||||||
|
this.dataTrackName = this.data.dataTrackName;
|
||||||
|
}
|
||||||
Room.getLocalDevices('videoinput').then((devices) => {
|
Room.getLocalDevices('videoinput').then((devices) => {
|
||||||
this.inputVideoDevices = devices;
|
this.inputVideoDevices = devices;
|
||||||
});
|
});
|
||||||
|
|
@ -152,6 +158,7 @@ export class OptionsDialogComponent {
|
||||||
createLocalTracksOptions: this.createLocalTracksOptions,
|
createLocalTracksOptions: this.createLocalTracksOptions,
|
||||||
screenShareCaptureOptions: this.screenShareCaptureOptions,
|
screenShareCaptureOptions: this.screenShareCaptureOptions,
|
||||||
trackPublishOptions: this.trackPublishOptions,
|
trackPublishOptions: this.trackPublishOptions,
|
||||||
|
dataTrackName: this.dataTrackName,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,7 @@ import {
|
||||||
DataPublishOptions,
|
DataPublishOptions,
|
||||||
DisconnectReason,
|
DisconnectReason,
|
||||||
LocalAudioTrack,
|
LocalAudioTrack,
|
||||||
|
LocalDataTrack,
|
||||||
LocalParticipant,
|
LocalParticipant,
|
||||||
LocalTrack,
|
LocalTrack,
|
||||||
LocalTrackPublication,
|
LocalTrackPublication,
|
||||||
|
|
@ -28,6 +29,7 @@ import {
|
||||||
MediaDeviceFailure,
|
MediaDeviceFailure,
|
||||||
Participant,
|
Participant,
|
||||||
RemoteAudioTrack,
|
RemoteAudioTrack,
|
||||||
|
RemoteDataTrack,
|
||||||
RemoteParticipant,
|
RemoteParticipant,
|
||||||
RemoteTrack,
|
RemoteTrack,
|
||||||
RemoteTrackPublication,
|
RemoteTrackPublication,
|
||||||
|
|
@ -143,7 +145,7 @@ export class OpenviduInstanceComponent {
|
||||||
this.participantName += this.index;
|
this.participantName += this.index;
|
||||||
if (this.roomConf.startSession) {
|
if (this.roomConf.startSession) {
|
||||||
const token = await this.roomApiService.createToken(
|
const token = await this.roomApiService.createToken(
|
||||||
{ roomJoin: true },
|
{ roomJoin: true, canPublishData: true },
|
||||||
this.participantName,
|
this.participantName,
|
||||||
this.roomName
|
this.roomName
|
||||||
);
|
);
|
||||||
|
|
@ -163,7 +165,7 @@ export class OpenviduInstanceComponent {
|
||||||
async createTokenAndConnectRoom() {
|
async createTokenAndConnectRoom() {
|
||||||
this.connectRoom(
|
this.connectRoom(
|
||||||
await this.roomApiService.createToken(
|
await this.roomApiService.createToken(
|
||||||
{ roomJoin: true },
|
{ roomJoin: true, canPublishData: true },
|
||||||
this.participantName,
|
this.participantName,
|
||||||
this.roomName
|
this.roomName
|
||||||
)
|
)
|
||||||
|
|
@ -1078,6 +1080,86 @@ export class OpenviduInstanceComponent {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
firstTime ||
|
||||||
|
this.roomEvents.get(RoomEvent.DataTrackPublished) !==
|
||||||
|
oldValues.get(RoomEvent.DataTrackPublished)
|
||||||
|
) {
|
||||||
|
this.room?.removeAllListeners(RoomEvent.DataTrackPublished);
|
||||||
|
if (this.roomEvents.get(RoomEvent.DataTrackPublished)) {
|
||||||
|
this.room!.on(
|
||||||
|
RoomEvent.DataTrackPublished,
|
||||||
|
(track: RemoteDataTrack) => {
|
||||||
|
this.updateEventList(
|
||||||
|
RoomEvent.DataTrackPublished,
|
||||||
|
{ name: track.info.name, sid: track.info.sid, publisherIdentity: track.publisherIdentity },
|
||||||
|
`${track.publisherIdentity} (${track.info.name})`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
firstTime ||
|
||||||
|
this.roomEvents.get(RoomEvent.DataTrackUnpublished) !==
|
||||||
|
oldValues.get(RoomEvent.DataTrackUnpublished)
|
||||||
|
) {
|
||||||
|
this.room?.removeAllListeners(RoomEvent.DataTrackUnpublished);
|
||||||
|
if (this.roomEvents.get(RoomEvent.DataTrackUnpublished)) {
|
||||||
|
this.room!.on(
|
||||||
|
RoomEvent.DataTrackUnpublished,
|
||||||
|
(sid: string) => {
|
||||||
|
this.updateEventList(
|
||||||
|
RoomEvent.DataTrackUnpublished,
|
||||||
|
{ sid },
|
||||||
|
`sid: ${sid}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
firstTime ||
|
||||||
|
this.roomEvents.get(RoomEvent.LocalDataTrackPublished) !==
|
||||||
|
oldValues.get(RoomEvent.LocalDataTrackPublished)
|
||||||
|
) {
|
||||||
|
this.room?.removeAllListeners(RoomEvent.LocalDataTrackPublished);
|
||||||
|
if (this.roomEvents.get(RoomEvent.LocalDataTrackPublished)) {
|
||||||
|
this.room!.on(
|
||||||
|
RoomEvent.LocalDataTrackPublished,
|
||||||
|
(track: LocalDataTrack) => {
|
||||||
|
this.updateEventList(
|
||||||
|
RoomEvent.LocalDataTrackPublished,
|
||||||
|
{ name: track.info?.name, sid: track.info?.sid },
|
||||||
|
`${track.info?.name}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
firstTime ||
|
||||||
|
this.roomEvents.get(RoomEvent.LocalDataTrackUnpublished) !==
|
||||||
|
oldValues.get(RoomEvent.LocalDataTrackUnpublished)
|
||||||
|
) {
|
||||||
|
this.room?.removeAllListeners(RoomEvent.LocalDataTrackUnpublished);
|
||||||
|
if (this.roomEvents.get(RoomEvent.LocalDataTrackUnpublished)) {
|
||||||
|
this.room!.on(
|
||||||
|
RoomEvent.LocalDataTrackUnpublished,
|
||||||
|
(sid: string) => {
|
||||||
|
this.updateEventList(
|
||||||
|
RoomEvent.LocalDataTrackUnpublished,
|
||||||
|
{ sid },
|
||||||
|
`sid: ${sid}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
updateEventList(
|
updateEventList(
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,18 @@
|
||||||
<p class="participant-identity" [ngClass]="{'local-participant-identity' : participant.isLocal}">
|
<p class="participant-identity" [ngClass]="{'local-participant-identity' : participant.isLocal}">
|
||||||
{{participant.identity}}</p>
|
{{participant.identity}}</p>
|
||||||
<div class="participant-buttons">
|
<div class="participant-buttons">
|
||||||
|
@if (participant.isLocal) {
|
||||||
|
<button class="add-data-track-btn" (click)="addDataTrack()" title="New data track"
|
||||||
|
matTooltip="New data track" matTooltipClass="custom-tooltip">
|
||||||
|
<mat-icon aria-label="New data track">stream</mat-icon>
|
||||||
|
</button>
|
||||||
|
}
|
||||||
|
@if (participant.isLocal) {
|
||||||
|
<button class="options-data-track-btn" (click)="openDataTrackOptionsDialog()"
|
||||||
|
title="Data track options" matTooltip="Data track options" matTooltipClass="custom-tooltip">
|
||||||
|
<mat-icon aria-label="Data track options">more_vert</mat-icon>
|
||||||
|
</button>
|
||||||
|
}
|
||||||
@if (participant.isLocal) {
|
@if (participant.isLocal) {
|
||||||
<button class="add-audio-btn" (click)="addAudioTrack()" title="New audio track"
|
<button class="add-audio-btn" (click)="addAudioTrack()" title="New audio track"
|
||||||
matTooltip="New audio track" matTooltipClass="custom-tooltip">
|
matTooltip="New audio track" matTooltipClass="custom-tooltip">
|
||||||
|
|
@ -71,6 +83,24 @@
|
||||||
}
|
}
|
||||||
</mat-accordion>
|
</mat-accordion>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="data-tracks-container">
|
||||||
|
@for (track of localDataTracks; track track.info?.sid) {
|
||||||
|
<app-data-track
|
||||||
|
[index]="index"
|
||||||
|
[localDataTrack]="track"
|
||||||
|
[localParticipant]="localParticipant"
|
||||||
|
(trackUnpublished)="onLocalDataTrackUnpublished($event)"
|
||||||
|
(newTrackEvent)="onTrackEvent($event)">
|
||||||
|
</app-data-track>
|
||||||
|
}
|
||||||
|
@for (track of remoteDataTracks; track track.info.sid) {
|
||||||
|
<app-data-track
|
||||||
|
[index]="index"
|
||||||
|
[remoteDataTrack]="track"
|
||||||
|
(newTrackEvent)="onTrackEvent($event)">
|
||||||
|
</app-data-track>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
<div class="audio-tracks-container">
|
<div class="audio-tracks-container">
|
||||||
@for (trackPublication of participant.audioTrackPublications| keyvalue; track trackPublication) {
|
@for (trackPublication of participant.audioTrackPublications| keyvalue; track trackPublication) {
|
||||||
<app-audio-track
|
<app-audio-track
|
||||||
|
|
|
||||||
|
|
@ -10,15 +10,18 @@ import {
|
||||||
CreateLocalTracksOptions,
|
CreateLocalTracksOptions,
|
||||||
DataPacket_Kind,
|
DataPacket_Kind,
|
||||||
LocalAudioTrack,
|
LocalAudioTrack,
|
||||||
|
LocalDataTrack,
|
||||||
LocalParticipant,
|
LocalParticipant,
|
||||||
LocalTrack,
|
LocalTrack,
|
||||||
LocalTrackPublication,
|
LocalTrackPublication,
|
||||||
LocalVideoTrack,
|
LocalVideoTrack,
|
||||||
Participant,
|
Participant,
|
||||||
ParticipantEvent,
|
ParticipantEvent,
|
||||||
|
RemoteDataTrack,
|
||||||
RemoteTrack,
|
RemoteTrack,
|
||||||
RemoteTrackPublication,
|
RemoteTrackPublication,
|
||||||
Room,
|
Room,
|
||||||
|
RoomEvent,
|
||||||
ScreenShareCaptureOptions,
|
ScreenShareCaptureOptions,
|
||||||
SubscriptionError,
|
SubscriptionError,
|
||||||
Track,
|
Track,
|
||||||
|
|
@ -38,13 +41,14 @@ import {
|
||||||
import { OptionsDialogComponent } from '../dialogs/options-dialog/options-dialog.component';
|
import { OptionsDialogComponent } from '../dialogs/options-dialog/options-dialog.component';
|
||||||
import { VideoTrackComponent } from '../video-track/video-track.component';
|
import { VideoTrackComponent } from '../video-track/video-track.component';
|
||||||
import { AudioTrackComponent } from '../audio-track/audio-track.component';
|
import { AudioTrackComponent } from '../audio-track/audio-track.component';
|
||||||
|
import { DataTrackComponent } from '../data-track/data-track.component';
|
||||||
import { ParticipantEventCallbacks } from 'node_modules/livekit-client/dist/src/room/participant/Participant';
|
import { ParticipantEventCallbacks } from 'node_modules/livekit-client/dist/src/room/participant/Participant';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-participant',
|
selector: 'app-participant',
|
||||||
templateUrl: './participant.component.html',
|
templateUrl: './participant.component.html',
|
||||||
styleUrl: './participant.component.css',
|
styleUrl: './participant.component.css',
|
||||||
imports: [NgClass, KeyValuePipe, MatIconModule, MatTooltipModule, MatExpansionModule, VideoTrackComponent, AudioTrackComponent],
|
imports: [NgClass, KeyValuePipe, MatIconModule, MatTooltipModule, MatExpansionModule, VideoTrackComponent, AudioTrackComponent, DataTrackComponent],
|
||||||
})
|
})
|
||||||
export class ParticipantComponent {
|
export class ParticipantComponent {
|
||||||
@Input()
|
@Input()
|
||||||
|
|
@ -66,6 +70,11 @@ export class ParticipantComponent {
|
||||||
|
|
||||||
events: TestAppEvent[] = [];
|
events: TestAppEvent[] = [];
|
||||||
|
|
||||||
|
localDataTracks: LocalDataTrack[] = [];
|
||||||
|
remoteDataTracks: RemoteDataTrack[] = [];
|
||||||
|
dataTrackCounter: number = 1;
|
||||||
|
dataTrackName: string = 'data_track_1';
|
||||||
|
|
||||||
createLocalTracksOptions: CreateLocalTracksOptions;
|
createLocalTracksOptions: CreateLocalTracksOptions;
|
||||||
screenShareCaptureOptions: ScreenShareCaptureOptions = {};
|
screenShareCaptureOptions: ScreenShareCaptureOptions = {};
|
||||||
trackPublishOptions?: TrackPublishOptions;
|
trackPublishOptions?: TrackPublishOptions;
|
||||||
|
|
@ -91,6 +100,9 @@ export class ParticipantComponent {
|
||||||
this.trackPublishOptions = JSON.parse(
|
this.trackPublishOptions = JSON.parse(
|
||||||
JSON.stringify(this.room.options.publishDefaults)
|
JSON.stringify(this.room.options.publishDefaults)
|
||||||
);
|
);
|
||||||
|
if (!this.participant.isLocal) {
|
||||||
|
this.setupDataTrackListeners();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onTrackEvent(event: TestAppEvent) {
|
onTrackEvent(event: TestAppEvent) {
|
||||||
|
|
@ -98,6 +110,31 @@ export class ParticipantComponent {
|
||||||
this.cdr.detectChanges();
|
this.cdr.detectChanges();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async addDataTrack() {
|
||||||
|
const localParticipant = this.participant as LocalParticipant;
|
||||||
|
const track = await localParticipant.publishDataTrack({ name: this.dataTrackName });
|
||||||
|
this.localDataTracks.push(track);
|
||||||
|
this.dataTrackCounter++;
|
||||||
|
this.dataTrackName = 'data_track_' + this.dataTrackCounter;
|
||||||
|
this.cdr.detectChanges();
|
||||||
|
}
|
||||||
|
|
||||||
|
openDataTrackOptionsDialog() {
|
||||||
|
const dialogRef = this.dialog.open(OptionsDialogComponent, {
|
||||||
|
data: { dataTrackName: this.dataTrackName },
|
||||||
|
});
|
||||||
|
dialogRef.afterClosed().subscribe((result) => {
|
||||||
|
if (!!result) {
|
||||||
|
this.dataTrackName = result.dataTrackName;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
onLocalDataTrackUnpublished(track: LocalDataTrack) {
|
||||||
|
this.localDataTracks = this.localDataTracks.filter((t) => t !== track);
|
||||||
|
this.cdr.detectChanges();
|
||||||
|
}
|
||||||
|
|
||||||
async addVideoTrack() {
|
async addVideoTrack() {
|
||||||
const options =
|
const options =
|
||||||
this.createLocalTracksOptions.video === true
|
this.createLocalTracksOptions.video === true
|
||||||
|
|
@ -479,4 +516,25 @@ export class ParticipantComponent {
|
||||||
sendDataLossy() {
|
sendDataLossy() {
|
||||||
this.sendLossyDataToOneParticipant.emit(this.participant.identity);
|
this.sendLossyDataToOneParticipant.emit(this.participant.identity);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private setupDataTrackListeners() {
|
||||||
|
this.room.on(
|
||||||
|
RoomEvent.DataTrackPublished,
|
||||||
|
(track: RemoteDataTrack) => {
|
||||||
|
if (track.publisherIdentity === this.participant.identity) {
|
||||||
|
this.remoteDataTracks.push(track);
|
||||||
|
this.cdr.detectChanges();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
this.room.on(
|
||||||
|
RoomEvent.DataTrackUnpublished,
|
||||||
|
(sid: string) => {
|
||||||
|
this.remoteDataTracks = this.remoteDataTracks.filter(
|
||||||
|
(t) => t.info.sid !== sid
|
||||||
|
);
|
||||||
|
this.cdr.detectChanges();
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue