mirror of https://github.com/OpenVidu/openvidu.git
openvidu-test-e2e: server SDK publishers with 1, 2 and 3 layers to early and late browser subscribers
Each SDK (go, node, python, rust, dotnet) publishes each codec with one plain encoding, two layers (simulcast / SVC L2T2) and three layers (simulcast / SVC L3T3) to a Chrome and a Firefox browser, each hosting a subscriber that joined before the publish and one that joins afterwards. Subscribers switch between the LOW, MEDIUM and HIGH layers.pull/907/head
parent
6e78fc0ec1
commit
2fe3bd87f2
|
|
@ -368,6 +368,10 @@ public class OpenViduTestE2e {
|
|||
return "srt://" + srtServerIp + ":" + RTSP_SRT_PORT;
|
||||
}
|
||||
|
||||
public void startServerSdkPublisher(String sdk, String roomName, String codec) throws Exception {
|
||||
startServerSdkPublisher(sdk, roomName, codec, 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts a LiveKit server RTC SDK participant (a minimal program at
|
||||
* src/test/resources/<sdk>-publisher) in a plain runtime container
|
||||
|
|
@ -375,29 +379,15 @@ public class OpenViduTestE2e {
|
|||
* given codec (vp8, h264, vp9 or av1) as one plain RTP encoding: without
|
||||
* simulcast for VP8/H264 and without SVC (scalabilityMode L1T1) for VP9/AV1.
|
||||
*
|
||||
* The container mounts the program read-only at /app, copies it to /work
|
||||
* and runs it there, with a per-SDK dependency cache mounted at /cache so
|
||||
* repeated runs skip downloads. Blocks until the participant has published
|
||||
* its track (its TRACK_PUBLISHED log line). The container is stopped
|
||||
* automatically on test dispose.
|
||||
* The container mounts the program read-only at /app, copies it to /work and
|
||||
* runs it there, with a per-SDK dependency cache mounted at /cache so
|
||||
* repeated runs skip downloads. Returns once the program logs
|
||||
* TRACK_PUBLISHED. The container is stopped automatically on test dispose.
|
||||
*/
|
||||
public void startServerSdkPublisher(String sdk, String roomName, String codec) throws Exception {
|
||||
startServerSdkPublisher(sdk, roomName, codec, false);
|
||||
public void startServerSdkPublisher(String sdk, String roomName, String codec, int layers) throws Exception {
|
||||
if (layers < 1 || layers > 3) {
|
||||
throw new IllegalArgumentException("Unsupported number of video layers: " + layers);
|
||||
}
|
||||
|
||||
/**
|
||||
* Launches the minimal publisher program of the given LiveKit server RTC SDK
|
||||
* (src/test/resources/<sdk>-publisher) in a plain runtime container, joining
|
||||
* roomName and publishing one video track of the given codec. multiLayer=false
|
||||
* publishes one plain RTP encoding (no simulcast, no SVC); multiLayer=true
|
||||
* publishes two layers from the same 640x480 source: simulcast (480x360 +
|
||||
* 640x480, the SDKs' absolute presets) for VP8/H264 and SVC L2T2 (320x240 +
|
||||
* 640x480) for VP9/AV1 — except the Go SDK, which forwards pre-encoded
|
||||
* samples and publishes two simulcast files (320x240 + 640x480). Returns once
|
||||
* the program logs TRACK_PUBLISHED.
|
||||
*/
|
||||
public void startServerSdkPublisher(String sdk, String roomName, String codec, boolean multiLayer)
|
||||
throws Exception {
|
||||
final String image;
|
||||
final String runCommand;
|
||||
int startupTimeoutMinutes = 5;
|
||||
|
|
@ -451,7 +441,7 @@ public class OpenViduTestE2e {
|
|||
env.put("LIVEKIT_URL", LIVEKIT_URL);
|
||||
env.put("LIVEKIT_TOKEN", accessToken.toJwt());
|
||||
env.put("VIDEO_CODEC", codec);
|
||||
env.put("VIDEO_LAYERS", multiLayer ? "multi" : "single");
|
||||
env.put("VIDEO_LAYERS", String.valueOf(layers));
|
||||
|
||||
String programDir = Paths.get("src/test/resources/" + sdk + "-publisher").toAbsolutePath().toString();
|
||||
Path cacheDir = Paths.get(System.getProperty("java.io.tmpdir"), "openvidu-e2e-sdk-cache", sdk);
|
||||
|
|
@ -491,11 +481,21 @@ public class OpenViduTestE2e {
|
|||
throw new IllegalArgumentException("Unknown video codec: " + codec);
|
||||
}
|
||||
Path mediaDir = Files.createTempDirectory("go-publisher-media");
|
||||
// One file per layer: the single encoding, or the three simulcast
|
||||
// layers (must match the sizes declared by go-publisher/main.go)
|
||||
Map<String, String> layerSizes = multiLayer
|
||||
? Map.of("VIDEO_FILE_LOW", "320x240", "VIDEO_FILE_HIGH", "640x480")
|
||||
: Map.of("VIDEO_FILE", "640x480");
|
||||
// One file per layer: the single encoding or the simulcast layers
|
||||
// (must match the sizes declared by go-publisher/main.go)
|
||||
final Map<String, String> layerSizes;
|
||||
switch (layers) {
|
||||
case 1:
|
||||
layerSizes = Map.of("VIDEO_FILE", "640x480");
|
||||
break;
|
||||
case 2:
|
||||
layerSizes = Map.of("VIDEO_FILE_LOW", "320x240", "VIDEO_FILE_HIGH", "640x480");
|
||||
break;
|
||||
default:
|
||||
layerSizes = Map.of("VIDEO_FILE_LOW", "320x180", "VIDEO_FILE_MEDIUM", "640x360", "VIDEO_FILE_HIGH",
|
||||
"1280x720");
|
||||
break;
|
||||
}
|
||||
for (Map.Entry<String, String> layer : layerSizes.entrySet()) {
|
||||
Path videoFile = mediaDir
|
||||
.resolve("test-video-" + layer.getValue() + "." + ("h264".equals(codec) ? "h264" : "ivf"));
|
||||
|
|
|
|||
|
|
@ -64,83 +64,87 @@ public class OpenViduTestAppE2eServerSdkTest extends AbstractOpenViduTestappE2eT
|
|||
this.closeAllRooms(LK);
|
||||
}
|
||||
|
||||
// Server RTC SDK publishers matrix: each SDK publishes each codec first as
|
||||
// one plain RTP encoding (no simulcast for VP8/H264, no SVC for VP9/AV1)
|
||||
// and then as two layers (high and low quality: simulcast for VP8/H264, SVC
|
||||
// L2T2 for VP9/AV1), and a Chrome subscriber and a Firefox subscriber must
|
||||
// both receive it — for two layers, switching to the LOW and then to the
|
||||
// HIGH layer. The Go SDK single-layer H264 lane reproduces the mediasoup
|
||||
// Producer codec-binding bug: Go SDK publishers declare no codec in their
|
||||
// AddTrackRequest and prefer an H264 variant the server does not support,
|
||||
// so the server's answer ends up VP8 first, the Producer is bound to VP8,
|
||||
// the worker discards every incoming packet and the subscribers receive no
|
||||
// media at all. See MEDIASOUP_CODEC_BINDING_BUG.md
|
||||
|
||||
/**
|
||||
* {vp8, h264, vp9, av1} x {single, multi} layers, single-layer cases first.
|
||||
* System properties sdk.codecs / sdk.layers (comma-separated) restrict the
|
||||
* matrix, e.g. -Dsdk.layers=multi -Dsdk.codecs=vp9,av1
|
||||
* {vp8, h264, vp9, av1} x {1, 2, 3} layers, single-layer cases first. System
|
||||
* properties sdk.codecs / sdk.layers (comma-separated) restrict the matrix,
|
||||
* e.g. -Dsdk.layers=2,3 -Dsdk.codecs=vp9,av1
|
||||
*/
|
||||
static Stream<Arguments> serverSdkPublisherMatrix() {
|
||||
List<String> layersFilter = List.of(System.getProperty("sdk.layers", "single,multi").split(","));
|
||||
List<String> layersFilter = List.of(System.getProperty("sdk.layers", "1,2,3").split(","));
|
||||
List<String> codecsFilter = List.of(System.getProperty("sdk.codecs", "vp8,h264,vp9,av1").split(","));
|
||||
return Stream.of("single", "multi").filter(layersFilter::contains)
|
||||
return Stream.of(1, 2, 3).filter(layers -> layersFilter.contains(String.valueOf(layers)))
|
||||
.flatMap(layers -> Stream.of("vp8", "h264", "vp9", "av1").filter(codecsFilter::contains)
|
||||
.map(codec -> Arguments.of(codec, layers)));
|
||||
}
|
||||
|
||||
@ParameterizedTest(name = "Go SDK {0} {1}-layer publisher to Chrome and Firefox subscribers")
|
||||
@ParameterizedTest(name = "Go SDK {0} {1}-layer publisher to Chrome and Firefox early and late subscribers")
|
||||
@MethodSource("serverSdkPublisherMatrix")
|
||||
@DisplayName("Go SDK publisher to Chrome and Firefox subscribers")
|
||||
void goSdkPublisherToBrowserSubscribersTest(String codec, String layers) throws Exception {
|
||||
@DisplayName("Go SDK publisher to Chrome and Firefox early and late subscribers")
|
||||
void goSdkPublisherToBrowserSubscribersTest(String codec, int layers) throws Exception {
|
||||
serverSdkPublisherToBrowserSubscribersAux("go", codec, layers);
|
||||
}
|
||||
|
||||
@ParameterizedTest(name = "Node SDK {0} {1}-layer publisher to Chrome and Firefox subscribers")
|
||||
@ParameterizedTest(name = "Node SDK {0} {1}-layer publisher to Chrome and Firefox early and late subscribers")
|
||||
@MethodSource("serverSdkPublisherMatrix")
|
||||
@DisplayName("Node SDK publisher to Chrome and Firefox subscribers")
|
||||
void nodeSdkPublisherToBrowserSubscribersTest(String codec, String layers) throws Exception {
|
||||
@DisplayName("Node SDK publisher to Chrome and Firefox early and late subscribers")
|
||||
void nodeSdkPublisherToBrowserSubscribersTest(String codec, int layers) throws Exception {
|
||||
serverSdkPublisherToBrowserSubscribersAux("node", codec, layers);
|
||||
}
|
||||
|
||||
@ParameterizedTest(name = "Python SDK {0} {1}-layer publisher to Chrome and Firefox subscribers")
|
||||
@ParameterizedTest(name = "Python SDK {0} {1}-layer publisher to Chrome and Firefox early and late subscribers")
|
||||
@MethodSource("serverSdkPublisherMatrix")
|
||||
@DisplayName("Python SDK publisher to Chrome and Firefox subscribers")
|
||||
void pythonSdkPublisherToBrowserSubscribersTest(String codec, String layers) throws Exception {
|
||||
@DisplayName("Python SDK publisher to Chrome and Firefox early and late subscribers")
|
||||
void pythonSdkPublisherToBrowserSubscribersTest(String codec, int layers) throws Exception {
|
||||
serverSdkPublisherToBrowserSubscribersAux("python", codec, layers);
|
||||
}
|
||||
|
||||
@ParameterizedTest(name = "Rust SDK {0} {1}-layer publisher to Chrome and Firefox subscribers")
|
||||
@ParameterizedTest(name = "Rust SDK {0} {1}-layer publisher to Chrome and Firefox early and late subscribers")
|
||||
@MethodSource("serverSdkPublisherMatrix")
|
||||
@DisplayName("Rust SDK publisher to Chrome and Firefox subscribers")
|
||||
void rustSdkPublisherToBrowserSubscribersTest(String codec, String layers) throws Exception {
|
||||
@DisplayName("Rust SDK publisher to Chrome and Firefox early and late subscribers")
|
||||
void rustSdkPublisherToBrowserSubscribersTest(String codec, int layers) throws Exception {
|
||||
serverSdkPublisherToBrowserSubscribersAux("rust", codec, layers);
|
||||
}
|
||||
|
||||
@ParameterizedTest(name = ".NET SDK {0} {1}-layer publisher to Chrome and Firefox subscribers")
|
||||
@ParameterizedTest(name = ".NET SDK {0} {1}-layer publisher to Chrome and Firefox early and late subscribers")
|
||||
@MethodSource("serverSdkPublisherMatrix")
|
||||
@DisplayName(".NET SDK publisher to Chrome and Firefox subscribers")
|
||||
void dotnetSdkPublisherToBrowserSubscribersTest(String codec, String layers) throws Exception {
|
||||
@DisplayName(".NET SDK publisher to Chrome and Firefox early and late subscribers")
|
||||
void dotnetSdkPublisherToBrowserSubscribersTest(String codec, int layers) throws Exception {
|
||||
// Requires Livekit.Rtc.Dotnet >= 0.1.4 (TrackPublishOptions.VideoCodec)
|
||||
serverSdkPublisherToBrowserSubscribersAux("dotnet", codec, layers);
|
||||
}
|
||||
|
||||
/**
|
||||
* A Chrome browser and a Firefox browser join the room as subscriber-only
|
||||
* participants and a LiveKit server RTC SDK participant
|
||||
* (startServerSdkPublisher) joins the same room ("TestRoom" is the testapp
|
||||
* default) publishing a single video track with the given codec: as one plain
|
||||
* RTP encoding (layers "single") or as two layers (layers "multi": high and
|
||||
* low quality — simulcast for VP8/H264, SVC L2T2 for VP9/AV1). Both
|
||||
* subscribers must receive the track's media with that codec, going through
|
||||
* exactly the same steps; with two layers they also switch to the LOW and
|
||||
* then to the HIGH layer.
|
||||
* A subscriber-only participant of the room: the testapp instance (index
|
||||
* within its browser page) that hosts it, and its participant name.
|
||||
*/
|
||||
private void serverSdkPublisherToBrowserSubscribersAux(String sdk, String codec, String layers)
|
||||
throws Exception {
|
||||
private record Subscriber(OpenViduTestappUser user, int instance, String name) {
|
||||
String instanceSelector() {
|
||||
return "#openvidu-instance-" + instance;
|
||||
}
|
||||
|
||||
WebElement remoteVideo() {
|
||||
return user.getDriver().findElement(By.cssSelector(instanceSelector() + " video.remote"));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A Chrome browser and a Firefox browser each connect one subscriber-only
|
||||
* participant to the room; then a LiveKit server RTC SDK participant joins it,
|
||||
* publishing a single video track with the given codec and number of layers:
|
||||
* one plain RTP encoding (1), or two or three layers (simulcast for VP8/H264,
|
||||
* SVC L2T2 / L3T3 for VP9/AV1). Both early subscribers, already subscribed when
|
||||
* the track appears, must receive it with that codec and those layers; with
|
||||
* several layers they switch to the LOW, (MEDIUM) and HIGH layers. Then each
|
||||
* browser adds a second subscriber-only participant as a late subscriber (the
|
||||
* publisher has been streaming for a while, so its first frame depends on the
|
||||
* SFU requesting a keyframe) that goes through the same media and layer checks
|
||||
* while the early subscribers keep the HIGH layer. The room ends up with 5
|
||||
* participants, and both join orders are covered in both browsers.
|
||||
*/
|
||||
private void serverSdkPublisherToBrowserSubscribersAux(String sdk, String codec, int layers) throws Exception {
|
||||
final String expectedCodec = "video/" + codec.toUpperCase();
|
||||
final String publisherIdentity = sdk + "-publisher";
|
||||
final boolean multiLayer = "multi".equals(layers);
|
||||
final boolean multiLayer = layers > 1;
|
||||
|
||||
// The Go SDK forwards pre-encoded samples (no encoder), so its only
|
||||
// multi-layer shape is RID simulcast — and LiveKit does not support RID
|
||||
|
|
@ -149,40 +153,36 @@ public class OpenViduTestAppE2eServerSdkTest extends AbstractOpenViduTestappE2eT
|
|||
Assumptions.assumeFalse(multiLayer && "go".equals(sdk) && ("vp9".equals(codec) || "av1".equals(codec)),
|
||||
"The Go SDK cannot publish SVC, and VP9/AV1 RID simulcast is not a supported LiveKit publish shape");
|
||||
|
||||
List<OpenViduTestappUser> subscribers = List.of(setupBrowserAndConnectToOpenViduTestapp("chrome"),
|
||||
setupBrowserAndConnectToOpenViduTestapp("firefox"));
|
||||
log.info("{} SDK {} {}-layer publisher to Chrome and Firefox early and late subscribers", sdk, codec,
|
||||
layers);
|
||||
|
||||
log.info("{} SDK {} {}-layer publisher to Chrome and Firefox subscribers", sdk, codec, layers);
|
||||
OpenViduTestappUser chrome = setupBrowserAndConnectToOpenViduTestapp("chrome");
|
||||
OpenViduTestappUser firefox = setupBrowserAndConnectToOpenViduTestapp("firefox");
|
||||
|
||||
for (OpenViduTestappUser user : subscribers) {
|
||||
this.addSubscriber(user, false);
|
||||
WebElement participantNameInput = user.getDriver().findElement(By.id("participant-name-input-0"));
|
||||
participantNameInput.clear();
|
||||
participantNameInput.sendKeys(browserName(user) + "-subscriber");
|
||||
user.getDriver().findElements(By.className("connect-btn")).forEach(el -> el.sendKeys(Keys.ENTER));
|
||||
user.getEventManager().waitUntilEventReaches("connected", "RoomEvent", 1);
|
||||
}
|
||||
for (OpenViduTestappUser user : subscribers) {
|
||||
user.getEventManager().waitUntilEventReaches("active", "ParticipantEvent", 1);
|
||||
}
|
||||
// Early subscribers: in the room before the SDK publishes
|
||||
List<Subscriber> earlySubscribers = List.of(joinAsSubscriberOnly(chrome, "early"),
|
||||
joinAsSubscriberOnly(firefox, "early"));
|
||||
|
||||
this.startServerSdkPublisher(sdk, "TestRoom", codec, multiLayer);
|
||||
this.startServerSdkPublisher(sdk, "TestRoom", codec, layers);
|
||||
|
||||
// The server's authoritative view of the publication (RoomService API):
|
||||
// the LiveKit TrackInfo of a video publication lists one layer per
|
||||
// simulcast layer or SVC spatial layer, so one plain encoding (no
|
||||
// simulcast, no SVC / L1T1) has exactly one layer and a two-layer
|
||||
// publish (simulcast or SVC L2T2) has two
|
||||
// simulcast, no SVC / L1T1) has exactly one layer and a two- or
|
||||
// three-layer publish (simulcast or SVC L2T2 / L3T3) has two or three
|
||||
TrackInfo trackInfo = this.getPublishedVideoTrackInfo("TestRoom", publisherIdentity);
|
||||
final int expectedLayers = multiLayer ? 2 : 1;
|
||||
Assertions.assertEquals(expectedLayers, trackInfo.getLayersCount(), "Expected " + expectedLayers
|
||||
Assertions.assertEquals(layers, trackInfo.getLayersCount(), "Expected " + layers
|
||||
+ " video layer(s) in the track published by the " + sdk + " SDK, but the server reports "
|
||||
+ trackInfo.getLayersList());
|
||||
Assertions.assertEquals(expectedLayers, trackInfo.getCodecs(0).getLayersCount(), "Expected " + expectedLayers
|
||||
+ " video layer(s) for codec " + expectedCodec + " published by the " + sdk + " SDK");
|
||||
// VP8/H264 multi-layer publishes are simulcast and VP9/AV1 ones are SVC
|
||||
// L2T2, except for the Go SDK: it forwards pre-encoded samples (no
|
||||
// encoder, so no SVC) and publishes simulcast for every codec
|
||||
Assertions.assertEquals(layers, trackInfo.getCodecs(0).getLayersCount(),
|
||||
"Expected " + layers + " video layer(s) for codec " + expectedCodec + " published by the " + sdk
|
||||
+ " SDK");
|
||||
Assertions.assertEquals(layers, sortedLayerWidths(trackInfo).size(),
|
||||
"Expected " + layers + " video layers of distinct widths published by the " + sdk + " SDK, but got "
|
||||
+ trackInfo.getLayersList());
|
||||
// VP8/H264 multi-layer publishes are simulcast and VP9/AV1 ones are SVC,
|
||||
// except for the Go SDK: it forwards pre-encoded samples (no encoder, so
|
||||
// no SVC) and publishes simulcast for every codec
|
||||
final boolean expectSimulcast = multiLayer && ("vp8".equals(codec) || "h264".equals(codec) || "go".equals(sdk));
|
||||
if (!multiLayer || expectSimulcast) {
|
||||
Assertions.assertEquals(expectSimulcast, trackInfo.getSimulcast(),
|
||||
|
|
@ -194,14 +194,42 @@ public class OpenViduTestAppE2eServerSdkTest extends AbstractOpenViduTestappE2eT
|
|||
"The track published by the " + sdk + " SDK should not be SVC");
|
||||
}
|
||||
|
||||
// Both browsers must receive the track, with its codec and its layers
|
||||
for (OpenViduTestappUser user : subscribers) {
|
||||
assertSubscriberReceivesVideo(user, sdk, publisherIdentity, expectedCodec, trackInfo);
|
||||
for (Subscriber subscriber : earlySubscribers) {
|
||||
assertSubscriberReceivesVideo(subscriber, sdk, publisherIdentity, expectedCodec, trackInfo);
|
||||
}
|
||||
assertSubscribersSwitchLayers(earlySubscribers, trackInfo);
|
||||
|
||||
// Late subscribers: the publisher has been streaming for a while
|
||||
List<Subscriber> lateSubscribers = List.of(joinAsSubscriberOnly(chrome, "late"),
|
||||
joinAsSubscriberOnly(firefox, "late"));
|
||||
for (Subscriber subscriber : lateSubscribers) {
|
||||
assertSubscriberReceivesVideo(subscriber, sdk, publisherIdentity, expectedCodec, trackInfo);
|
||||
}
|
||||
assertSubscribersSwitchLayers(lateSubscribers, trackInfo);
|
||||
|
||||
gracefullyLeaveParticipants(chrome, 2);
|
||||
gracefullyLeaveParticipants(firefox, 2);
|
||||
}
|
||||
|
||||
for (OpenViduTestappUser user : subscribers) {
|
||||
gracefullyLeaveParticipants(user, 1);
|
||||
}
|
||||
/**
|
||||
* Adds a testapp instance to the browser page and connects it to the default
|
||||
* room as a subscriber-only participant named "<Browser>-<role>-subscriber"
|
||||
* (e.g. "Chrome-early-subscriber"), with adaptiveStream disabled, and waits
|
||||
* until it is active.
|
||||
*/
|
||||
private Subscriber joinAsSubscriberOnly(OpenViduTestappUser user, String role) throws Exception {
|
||||
// The new instance takes the next index
|
||||
final int instance = user.getDriver().findElements(By.cssSelector("app-openvidu-instance")).size();
|
||||
final String name = browserName(user) + "-" + role + "-subscriber";
|
||||
this.addSubscriber(user, false);
|
||||
WebElement participantNameInput = user.getDriver().findElement(By.id("participant-name-input-" + instance));
|
||||
participantNameInput.clear();
|
||||
participantNameInput.sendKeys(name);
|
||||
user.getDriver().findElement(By.cssSelector("#openvidu-instance-" + instance + " .connect-btn"))
|
||||
.sendKeys(Keys.ENTER);
|
||||
user.getEventManager().waitUntilEventReaches(instance, "connected", "RoomEvent", 1);
|
||||
user.getEventManager().waitUntilEventReaches(instance, "active", "ParticipantEvent", 1);
|
||||
return new Subscriber(user, instance, name);
|
||||
}
|
||||
|
||||
/** "Chrome", "Firefox"... from the BrowserUser class of the testapp user. */
|
||||
|
|
@ -211,38 +239,43 @@ public class OpenViduTestAppE2eServerSdkTest extends AbstractOpenViduTestappE2eT
|
|||
|
||||
/**
|
||||
* Selects the max video quality (LOW, MEDIUM or HIGH) of the remote track of
|
||||
* the first testapp instance, closing the track info dialog if it is open.
|
||||
* the subscriber's testapp instance, closing the track info dialog if it is
|
||||
* open. The selection is verified (selectMatOption): a click that only opened
|
||||
* or closed the mat-select panel is retried instead of leaving the panel open
|
||||
* over the page, which would block the later clicks on the video controls.
|
||||
*/
|
||||
private void selectSubscriberVideoQuality(OpenViduTestappUser user, String quality) throws InterruptedException {
|
||||
private void selectSubscriberVideoQuality(Subscriber subscriber, String quality) throws InterruptedException {
|
||||
OpenViduTestappUser user = subscriber.user();
|
||||
if (!user.getDriver().findElements(By.cssSelector("app-info-dialog")).isEmpty()) {
|
||||
user.getDriver().findElement(By.cssSelector("#close-dialog-btn")).click();
|
||||
Thread.sleep(300);
|
||||
}
|
||||
user.getDriver().findElement(By.cssSelector("#openvidu-instance-0 #max-video-quality")).click();
|
||||
this.waitAndClick(user, "mat-option.mode-" + quality);
|
||||
this.selectMatOption(user, subscriber.instanceSelector() + " #max-video-quality", quality);
|
||||
}
|
||||
|
||||
/**
|
||||
* The subscriber-only participant of the given browser must receive the video
|
||||
* track published by the SDK participant: media actually flowing, with the
|
||||
* expected codec, with the layers the server reports in trackInfo. With more
|
||||
* than one layer the subscriber switches to the LOW and then to the HIGH
|
||||
* layer, each recognised by its frame width.
|
||||
* The subscriber-only participant must receive the video track published by
|
||||
* the SDK participant: media actually flowing, with the expected codec, with
|
||||
* the layers the server reports in trackInfo.
|
||||
*/
|
||||
private void assertSubscriberReceivesVideo(OpenViduTestappUser user, String sdk, String publisherIdentity,
|
||||
private void assertSubscriberReceivesVideo(Subscriber subscriber, String sdk, String publisherIdentity,
|
||||
String expectedCodec, TrackInfo trackInfo) throws Exception {
|
||||
final String browser = browserName(user);
|
||||
final OpenViduTestappUser user = subscriber.user();
|
||||
final String name = subscriber.name();
|
||||
|
||||
user.getEventManager().waitUntilEventReaches("trackSubscribed", "ParticipantEvent", 1);
|
||||
user.getEventManager().waitUntilEventReaches(subscriber.instance(), "trackSubscribed", "ParticipantEvent",
|
||||
1);
|
||||
|
||||
user.getWaiter().until(ExpectedConditions.numberOfElementsToBe(By.tagName("video"), 1));
|
||||
Assertions.assertTrue(assertAllElementsHaveTracks(user, "video", false, true),
|
||||
browser + ": HTMLVideoElements were expected to have only one video track");
|
||||
user.getWaiter().until(
|
||||
ExpectedConditions.numberOfElementsToBe(By.cssSelector(subscriber.instanceSelector() + " video"), 1));
|
||||
Assertions.assertTrue(assertAllElementsHaveTracks(user, subscriber.instanceSelector() + " video", false, true),
|
||||
name + ": HTMLVideoElements were expected to have only one video track");
|
||||
|
||||
WebElement subscriberVideo = user.getDriver().findElement(By.cssSelector("#openvidu-instance-0 video.remote"));
|
||||
WebElement subscriberVideo = subscriber.remoteVideo();
|
||||
|
||||
// Media must actually reach the subscriber (with the codec-binding bug
|
||||
// present the track subscribes but receives 0 bytes forever)
|
||||
// present the track subscribes but receives 0 bytes forever; a late
|
||||
// subscriber additionally depends on the SFU requesting a keyframe)
|
||||
waitUntilVideoLayersNotEmpty(user, subscriberVideo);
|
||||
this.waitUntilSubscriberBytesReceivedIncreasing(user, subscriberVideo);
|
||||
this.waitUntilSubscriberFramesPerSecondNotZero(user, subscriberVideo);
|
||||
|
|
@ -250,28 +283,44 @@ public class OpenViduTestAppE2eServerSdkTest extends AbstractOpenViduTestappE2eT
|
|||
// And with the codec the publisher actually sends (a Producer bound to
|
||||
// the wrong codec makes every subscriber negotiate that wrong codec)
|
||||
Assertions.assertEquals(expectedCodec, this.getSubscriberVideoCodec(user, subscriberVideo),
|
||||
browser + " subscriber should negotiate the codec the " + sdk + " SDK publisher sends");
|
||||
name + " should negotiate the codec the " + sdk + " SDK publisher sends");
|
||||
|
||||
// And with the layers the server reports, in the track info received by
|
||||
// the subscriber
|
||||
JsonArray subscriberLayers = this.getRemoteVideoTrackInfoLayers(user, publisherIdentity);
|
||||
JsonArray subscriberLayers = this.getRemoteVideoTrackInfoLayers(subscriber, publisherIdentity);
|
||||
Assertions.assertEquals(trackInfo.getLayersCount(), subscriberLayers.size(),
|
||||
"Expected " + trackInfo.getLayersCount() + " video layer(s) in the track published by the " + sdk
|
||||
+ " SDK, but the " + browser + " subscriber sees " + subscriberLayers);
|
||||
+ " SDK, but " + name + " sees " + subscriberLayers);
|
||||
}
|
||||
|
||||
if (trackInfo.getLayersCount() > 1) {
|
||||
// Multiple layers: with adaptiveStream disabled the received layer
|
||||
// only changes through the max-video-quality selector. Receiving the
|
||||
// LOW layer and then the HIGH layer — each recognised by the frame
|
||||
// width the publisher declared for it in the TrackInfo, and each
|
||||
// actually decoding (framesPerSecond > 0: an undecodable layer still
|
||||
// reports its frame size) — proves that the publisher sends every
|
||||
// layer and that the SFU forwards the requested one. The declared
|
||||
// widths are reliable because the multi-layer publishers capture at
|
||||
// 15 fps: at 30 fps libwebrtc's CPU adaptation can scale every layer
|
||||
// down under load
|
||||
this.selectQualityAndAwaitLayer(user, subscriberVideo, "LOW", lowestLayerWidth(trackInfo));
|
||||
this.selectQualityAndAwaitLayer(user, subscriberVideo, "HIGH", highestLayerWidth(trackInfo));
|
||||
/**
|
||||
* With more than one published layer, the given subscribers (all of them
|
||||
* receiving the track already) switch together to the LOW layer, then (with
|
||||
* three layers) to the MEDIUM one and then to the HIGH one, each subscriber
|
||||
* recognising each layer by its frame width. With adaptiveStream disabled the
|
||||
* received layer only changes through the max-video-quality selector, and
|
||||
* each layer must actually decode (framesPerSecond > 0: an undecodable layer
|
||||
* still reports its frame size): this proves that the publisher sends every
|
||||
* layer and that the SFU forwards the requested one, also the middle spatial
|
||||
* layer, which neither LOW nor HIGH can clamp to. The declared widths are
|
||||
* reliable because the multi-layer publishers capture at 15 fps: at 30 fps
|
||||
* libwebrtc's CPU adaptation can scale every layer down under load.
|
||||
*/
|
||||
private void assertSubscribersSwitchLayers(List<Subscriber> subscribers, TrackInfo trackInfo) throws Exception {
|
||||
List<Integer> layerWidths = sortedLayerWidths(trackInfo);
|
||||
if (layerWidths.size() < 2) {
|
||||
return;
|
||||
}
|
||||
for (Subscriber subscriber : subscribers) {
|
||||
this.selectQualityAndAwaitLayer(subscriber, "LOW", layerWidths.get(0));
|
||||
}
|
||||
if (layerWidths.size() > 2) {
|
||||
for (Subscriber subscriber : subscribers) {
|
||||
this.selectQualityAndAwaitLayer(subscriber, "MEDIUM", layerWidths.get(1));
|
||||
}
|
||||
}
|
||||
for (Subscriber subscriber : subscribers) {
|
||||
this.selectQualityAndAwaitLayer(subscriber, "HIGH", layerWidths.get(layerWidths.size() - 1));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -281,10 +330,12 @@ public class OpenViduTestAppE2eServerSdkTest extends AbstractOpenViduTestappE2eT
|
|||
* (framesPerSecond > 0). Retries the selection once: under CPU load the SFU
|
||||
* can take longer than one wait window to ramp back up to a higher layer.
|
||||
*/
|
||||
private void selectQualityAndAwaitLayer(OpenViduTestappUser user, WebElement subscriberVideo, String quality,
|
||||
int expectedFrameWidth) throws Exception {
|
||||
private void selectQualityAndAwaitLayer(Subscriber subscriber, String quality, int expectedFrameWidth)
|
||||
throws Exception {
|
||||
OpenViduTestappUser user = subscriber.user();
|
||||
WebElement subscriberVideo = subscriber.remoteVideo();
|
||||
for (int attempt = 1; attempt <= 2; attempt++) {
|
||||
this.selectSubscriberVideoQuality(user, quality);
|
||||
this.selectSubscriberVideoQuality(subscriber, quality);
|
||||
try {
|
||||
this.waitUntilSubscriberFrameWidthIs(user, subscriberVideo, expectedFrameWidth);
|
||||
break;
|
||||
|
|
@ -298,23 +349,15 @@ public class OpenViduTestAppE2eServerSdkTest extends AbstractOpenViduTestappE2eT
|
|||
}
|
||||
|
||||
/**
|
||||
* Width of the lowest-quality published layer, from the server's TrackInfo.
|
||||
* The layers are picked by width, not by their VideoQuality label: the SDKs
|
||||
* label a two-layer publish inconsistently (simulcast: LOW + MEDIUM; SVC
|
||||
* L2T2: MEDIUM + HIGH; the Go program: LOW + HIGH), while the subscriber's
|
||||
* LOW/HIGH selection always clamps to the lowest/highest available layer.
|
||||
* Distinct widths of the published layers, lowest first, from the server's
|
||||
* TrackInfo. The layers are picked by width, not by their VideoQuality label:
|
||||
* the SDKs label a two-layer publish inconsistently (simulcast: LOW + MEDIUM;
|
||||
* SVC L2T2: MEDIUM + HIGH; the Go program: LOW + HIGH), while the
|
||||
* subscriber's LOW/HIGH selection always clamps to the lowest/highest
|
||||
* available layer and MEDIUM, only used with three layers, is the middle one.
|
||||
*/
|
||||
private int lowestLayerWidth(TrackInfo trackInfo) {
|
||||
return trackInfo.getLayersList().stream().mapToInt(VideoLayer::getWidth).min()
|
||||
.orElseThrow(() -> new AssertionError("No layers in " + trackInfo));
|
||||
}
|
||||
|
||||
/**
|
||||
* Width of the highest-quality published layer, from the server's TrackInfo.
|
||||
*/
|
||||
private int highestLayerWidth(TrackInfo trackInfo) {
|
||||
return trackInfo.getLayersList().stream().mapToInt(VideoLayer::getWidth).max()
|
||||
.orElseThrow(() -> new AssertionError("No layers in " + trackInfo));
|
||||
private List<Integer> sortedLayerWidths(TrackInfo trackInfo) {
|
||||
return trackInfo.getLayersList().stream().map(VideoLayer::getWidth).distinct().sorted().toList();
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -341,11 +384,11 @@ public class OpenViduTestAppE2eServerSdkTest extends AbstractOpenViduTestappE2eT
|
|||
/**
|
||||
* Video layers (VideoLayer[] of the LiveKit TrackInfo) of the first video
|
||||
* track published by the remote participant with the given identity, as seen
|
||||
* by the local participant of the first testapp instance.
|
||||
* by the local participant of the subscriber's testapp instance.
|
||||
*/
|
||||
private JsonArray getRemoteVideoTrackInfoLayers(OpenViduTestappUser user, String participantIdentity) {
|
||||
String layers = (String) ((JavascriptExecutor) user.getDriver()).executeScript(
|
||||
"var room = window['room_0'];"
|
||||
private JsonArray getRemoteVideoTrackInfoLayers(Subscriber subscriber, String participantIdentity) {
|
||||
String layers = (String) ((JavascriptExecutor) subscriber.user().getDriver()).executeScript(
|
||||
"var room = window['room_" + subscriber.instance() + "'];"
|
||||
+ "var participant = room.remoteParticipants.get(arguments[0]);"
|
||||
+ "var publication = participant.videoTrackPublications.values().next().value;"
|
||||
+ "return JSON.stringify(publication.trackInfo.layers);",
|
||||
|
|
|
|||
|
|
@ -0,0 +1,81 @@
|
|||
# Server SDK publishers
|
||||
|
||||
Minimal LiveKit server RTC SDK publisher programs used by
|
||||
`OpenViduTestAppE2eServerSdkTest`, one per SDK:
|
||||
|
||||
| Directory | SDK | Video source |
|
||||
| ------------------- | ----------------------------------------- | ----------------------- |
|
||||
| `go-publisher/` | `github.com/livekit/server-sdk-go/v2` | pre-encoded files |
|
||||
| `node-publisher/` | `@livekit/rtc-node` | synthetic frames (FFI) |
|
||||
| `python-publisher/` | `livekit` (Python `rtc`) | synthetic frames (FFI) |
|
||||
| `rust-publisher/` | `livekit` crate | synthetic frames (FFI) |
|
||||
| `dotnet-publisher/` | `Livekit.Rtc.Dotnet` | synthetic frames (FFI) |
|
||||
|
||||
Each program is launched by `OpenViduTestE2e#startServerSdkPublisher` inside a
|
||||
plain runtime container of its language (see that method for the images, run
|
||||
commands and per-SDK dependency caches). The directory is mounted read-only at
|
||||
`/app`, copied to `/work` and run there. The program must print the exact line
|
||||
`TRACK_PUBLISHED` once the track is published: the Java test waits for it before
|
||||
asserting, and stops the container when the test ends, so the programs never
|
||||
exit on their own.
|
||||
|
||||
## Behaviour
|
||||
|
||||
Every publisher joins the room of `LIVEKIT_TOKEN` with dynacast disabled and
|
||||
publishes a single video track with codec `VIDEO_CODEC` (`vp8`, `h264`, `vp9`
|
||||
or `av1`) and `VIDEO_LAYERS` layers (`1`, `2` or `3`):
|
||||
|
||||
- `VIDEO_LAYERS=1` (default): one plain RTP encoding. No simulcast for
|
||||
VP8/H264, no SVC (scalability mode `L1T1`) for VP9/AV1.
|
||||
- `VIDEO_LAYERS=2` and `3`: simulcast for VP8/H264 and SVC `L2T2` / `L3T3` for
|
||||
VP9/AV1.
|
||||
|
||||
### FFI-based SDKs (node, python, rust, dotnet)
|
||||
|
||||
They encode raw frames themselves and push synthetic animated frames forever.
|
||||
The source is 640x480 for one or two layers and 1280x720 for three layers,
|
||||
because the SDKs only split a simulcast source in three layers from 960 px
|
||||
wide. The SDKs derive the simulcast layers from the source: 480x360 + 640x480
|
||||
from the 640x480 source (4:3 presets) and 320x180 + 640x360 + 1280x720 from the
|
||||
1280x720 one (16:9 presets).
|
||||
|
||||
### Go SDK
|
||||
|
||||
The Go SDK forwards pre-encoded samples and has no encoder. The publisher loops
|
||||
forever the files generated by the Java test with the host's `ffmpeg` (Annex-B
|
||||
for H264, IVF for VP8/VP9/AV1), one file per layer:
|
||||
|
||||
- `VIDEO_LAYERS=1`: one plain RTP encoding from `VIDEO_FILE` (640x480). This is
|
||||
the publish shape of every Go SDK client (`lk` CLI, `lk load-test`, Go
|
||||
agents): the `AddTrackRequest` declares no codec, the exact configuration that
|
||||
triggered the mediasoup Producer codec-binding bug.
|
||||
- `VIDEO_LAYERS=2`: two RID simulcast layers (LOW 320x240, HIGH 640x480) from
|
||||
`VIDEO_FILE_LOW` / `VIDEO_FILE_HIGH`.
|
||||
- `VIDEO_LAYERS=3`: three RID simulcast layers (LOW 320x180, MEDIUM 640x360,
|
||||
HIGH 1280x720) from `VIDEO_FILE_LOW` / `VIDEO_FILE_MEDIUM` /
|
||||
`VIDEO_FILE_HIGH`.
|
||||
|
||||
Because it cannot produce SVC streams, its VP9/AV1 multi-layer cases are
|
||||
skipped by the Java test (RID simulcast of SVC-class codecs is not a supported
|
||||
publish shape). The layer sizes in `go-publisher/main.go` must match the files
|
||||
generated by `startServerSdkPublisher`.
|
||||
|
||||
## Environment variables
|
||||
|
||||
| Variable | Publishers | Meaning |
|
||||
| -------------- | ---------- | ---------------------------------------------------------- |
|
||||
| `LIVEKIT_URL` | all | LiveKit server URL |
|
||||
| `LIVEKIT_TOKEN`| all | Ready-made join token (identity `<sdk>-publisher`) |
|
||||
| `VIDEO_CODEC` | all | `vp8`, `h264`, `vp9` or `av1` |
|
||||
| `VIDEO_LAYERS` | all | `1` (default), `2` or `3` |
|
||||
| `VIDEO_FILE` | go | Pre-encoded 640x480 file for one layer |
|
||||
| `VIDEO_FILE_LOW`, `VIDEO_FILE_MEDIUM`, `VIDEO_FILE_HIGH` | go | Pre-encoded file per simulcast layer |
|
||||
|
||||
The token is generated by the Java test so the programs stay short (no per-SDK
|
||||
token dependency) and work with any API secret length.
|
||||
|
||||
## Other files
|
||||
|
||||
- `logback.xml`: Logback configuration picked up from the test classpath.
|
||||
- `ScreenCapturing.crx`: packaged Chrome screen-capture extension. Not
|
||||
referenced by name from the Java code.
|
||||
|
|
@ -1,26 +1,19 @@
|
|||
// Minimal LiveKit .NET RTC SDK publisher used by OpenViduTestAppE2eTest.
|
||||
// Joins the room of LIVEKIT_TOKEN and publishes a single video track with codec
|
||||
// VIDEO_CODEC (vp8, h264, vp9 or av1). VIDEO_LAYERS=single (default): one plain
|
||||
// RTP encoding (no simulcast for VP8/H264, no SVC = ScalabilityMode L1T1 for
|
||||
// VP9/AV1). VIDEO_LAYERS=multi: two layers, simulcast for VP8/H264 (the SDK
|
||||
// derives 480x360 + 640x480 from the 640x480 source) and SVC L2T2 for
|
||||
// VP9/AV1. Pushes synthetic animated frames forever (the Java test stops the
|
||||
// container).
|
||||
// Requires Livekit.Rtc.Dotnet >= 0.1.4 (TrackPublishOptions.VideoCodec).
|
||||
// Env: LIVEKIT_URL, LIVEKIT_TOKEN, VIDEO_CODEC, VIDEO_LAYERS
|
||||
// Minimal LiveKit .NET SDK publisher used by OpenViduTestAppE2eServerSdkTest.
|
||||
// See ../README.md
|
||||
using LiveKit.Rtc;
|
||||
using Proto = LiveKit.Proto;
|
||||
|
||||
var codec = Environment.GetEnvironmentVariable("VIDEO_CODEC")!;
|
||||
var multiLayer = Environment.GetEnvironmentVariable("VIDEO_LAYERS") == "multi";
|
||||
int width = 640;
|
||||
int height = 480;
|
||||
var layers = int.Parse(Environment.GetEnvironmentVariable("VIDEO_LAYERS") ?? "1");
|
||||
// The SDK only splits a simulcast source in three layers from 960 px wide
|
||||
int width = layers == 3 ? 1280 : 640;
|
||||
int height = layers == 3 ? 720 : 480;
|
||||
|
||||
var room = new Room();
|
||||
await room.ConnectAsync(
|
||||
Environment.GetEnvironmentVariable("LIVEKIT_URL")!,
|
||||
Environment.GetEnvironmentVariable("LIVEKIT_TOKEN")!,
|
||||
new RoomOptions { AutoSubscribe = false });
|
||||
new RoomOptions { AutoSubscribe = false, Dynacast = false });
|
||||
|
||||
var videoSource = new VideoSource(width, height);
|
||||
var videoTrack = LocalVideoTrack.Create("dotnet-video", videoSource);
|
||||
|
|
@ -31,11 +24,11 @@ var options = new TrackPublishOptions
|
|||
};
|
||||
if (codec == "vp8" || codec == "h264")
|
||||
{
|
||||
options.Simulcast = multiLayer;
|
||||
options.Simulcast = layers > 1;
|
||||
}
|
||||
else
|
||||
{
|
||||
options.ScalabilityMode = multiLayer ? "L2T2" : "L1T1";
|
||||
options.ScalabilityMode = $"L{layers}T{layers}";
|
||||
}
|
||||
await room.LocalParticipant!.PublishTrackAsync(videoTrack, options);
|
||||
|
||||
|
|
@ -55,6 +48,6 @@ while (true)
|
|||
data[i + 3] = 255;
|
||||
}
|
||||
videoSource.CaptureFrame(new VideoFrame(width, height, Proto.VideoBufferType.Rgba, data));
|
||||
// 15 fps in multi mode: several software encoders at 30 fps trigger CPU adaptation
|
||||
await Task.Delay(multiLayer ? 66 : 33);
|
||||
// 15 fps with several layers: several software encoders at 30 fps trigger CPU adaptation
|
||||
await Task.Delay(layers > 1 ? 66 : 33);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,21 +1,5 @@
|
|||
// Minimal LiveKit Go SDK publisher used by OpenViduTestAppE2eTest.
|
||||
//
|
||||
// It joins the room of LIVEKIT_TOKEN and publishes one video track with codec
|
||||
// VIDEO_CODEC (vp8, h264, vp9 or av1), looping pre-encoded files forever
|
||||
// (Annex-B for H264, IVF otherwise; the Java test stops the container):
|
||||
//
|
||||
// - VIDEO_LAYERS=single (default): one plain RTP encoding, no simulcast,
|
||||
// from VIDEO_FILE. This is the publish shape of every Go SDK client (lk
|
||||
// CLI, lk load-test, Go agents): the AddTrackRequest declares no codec —
|
||||
// the exact configuration that triggered the mediasoup Producer
|
||||
// codec-binding bug (see MEDIASOUP_CODEC_BINDING_BUG.md).
|
||||
// - VIDEO_LAYERS=multi: two RID simulcast layers (LOW 320x240, HIGH
|
||||
// 640x480) from VIDEO_FILE_LOW / VIDEO_FILE_HIGH. The Go SDK forwards
|
||||
// pre-encoded samples and has no encoder, so it cannot produce SVC
|
||||
// streams; its VP9/AV1 multi-layer cases are skipped by the Java test
|
||||
// (RID simulcast of SVC-class codecs is not a supported publish shape).
|
||||
//
|
||||
// Env: LIVEKIT_URL, LIVEKIT_TOKEN, VIDEO_CODEC, VIDEO_LAYERS, VIDEO_FILE*
|
||||
// Minimal LiveKit Go SDK publisher used by OpenViduTestAppE2eServerSdkTest
|
||||
// See ../README.md
|
||||
package main
|
||||
|
||||
import (
|
||||
|
|
@ -50,9 +34,19 @@ type simulcastLayer struct {
|
|||
fileEnv string
|
||||
}
|
||||
|
||||
var simulcastLayers = []simulcastLayer{
|
||||
// Simulcast layers per VIDEO_LAYERS value, lowest first (the sizes must match
|
||||
// the files generated by OpenViduTestE2e#startServerSdkPublisher). The last
|
||||
// layer is the source size declared in the publication.
|
||||
var simulcastLayers = map[string][]simulcastLayer{
|
||||
"2": {
|
||||
{livekit.VideoQuality_LOW, 320, 240, "VIDEO_FILE_LOW"},
|
||||
{livekit.VideoQuality_HIGH, 640, 480, "VIDEO_FILE_HIGH"},
|
||||
},
|
||||
"3": {
|
||||
{livekit.VideoQuality_LOW, 320, 180, "VIDEO_FILE_LOW"},
|
||||
{livekit.VideoQuality_MEDIUM, 640, 360, "VIDEO_FILE_MEDIUM"},
|
||||
{livekit.VideoQuality_HIGH, 1280, 720, "VIDEO_FILE_HIGH"},
|
||||
},
|
||||
}
|
||||
|
||||
func main() {
|
||||
|
|
@ -68,9 +62,17 @@ func main() {
|
|||
}
|
||||
defer room.Disconnect()
|
||||
|
||||
if os.Getenv("VIDEO_LAYERS") == "multi" {
|
||||
tracks := make([]*lksdk.LocalTrack, 0, len(simulcastLayers))
|
||||
for _, layer := range simulcastLayers {
|
||||
layers := os.Getenv("VIDEO_LAYERS")
|
||||
if layers == "" {
|
||||
layers = "1"
|
||||
}
|
||||
if layers != "1" {
|
||||
layerDefs, ok := simulcastLayers[layers]
|
||||
if !ok {
|
||||
log.Fatalf("unsupported VIDEO_LAYERS %q", layers)
|
||||
}
|
||||
tracks := make([]*lksdk.LocalTrack, 0, len(layerDefs))
|
||||
for _, layer := range layerDefs {
|
||||
track, err := lksdk.NewLocalSampleTrack(codec, lksdk.WithSimulcast("go-video",
|
||||
&livekit.VideoLayer{Quality: layer.quality, Width: layer.width, Height: layer.height}))
|
||||
if err != nil {
|
||||
|
|
@ -78,16 +80,17 @@ func main() {
|
|||
}
|
||||
tracks = append(tracks, track)
|
||||
}
|
||||
top := layerDefs[len(layerDefs)-1]
|
||||
if _, err = room.LocalParticipant.PublishSimulcastTrack(tracks, &lksdk.TrackPublicationOptions{
|
||||
Name: "go-video",
|
||||
VideoWidth: 640,
|
||||
VideoHeight: 480,
|
||||
VideoWidth: int(top.width),
|
||||
VideoHeight: int(top.height),
|
||||
}); err != nil {
|
||||
log.Fatalf("could not publish simulcast track: %v", err)
|
||||
}
|
||||
// The Java test waits for this exact log line before asserting.
|
||||
fmt.Println("TRACK_PUBLISHED")
|
||||
for i, layer := range simulcastLayers {
|
||||
for i, layer := range layerDefs {
|
||||
go loopVideoFile(tracks[i], os.Getenv(layer.fileEnv))
|
||||
}
|
||||
select {}
|
||||
|
|
|
|||
|
|
@ -1,12 +1,5 @@
|
|||
// Minimal LiveKit Node RTC SDK publisher used by OpenViduTestAppE2eTest.
|
||||
// Joins the room of LIVEKIT_TOKEN and publishes a single video track with codec
|
||||
// VIDEO_CODEC (vp8, h264, vp9 or av1). VIDEO_LAYERS=single (default): one plain
|
||||
// RTP encoding (no simulcast for VP8/H264, no SVC = scalabilityMode L1T1 for
|
||||
// VP9/AV1). VIDEO_LAYERS=multi: two layers, simulcast for VP8/H264 (the SDK
|
||||
// derives 480x360 + 640x480 from the 640x480 source) and SVC L2T2 for
|
||||
// VP9/AV1. Pushes synthetic animated frames forever (the Java test stops the
|
||||
// container).
|
||||
// Env: LIVEKIT_URL, LIVEKIT_TOKEN, VIDEO_CODEC, VIDEO_LAYERS
|
||||
// Minimal LiveKit Node SDK publisher used by OpenViduTestAppE2eServerSdkTest
|
||||
// See ../README.md
|
||||
import {
|
||||
Room,
|
||||
LocalVideoTrack,
|
||||
|
|
@ -19,9 +12,9 @@ import {
|
|||
} from '@livekit/rtc-node';
|
||||
|
||||
const codec = process.env.VIDEO_CODEC;
|
||||
const multiLayer = process.env.VIDEO_LAYERS === 'multi';
|
||||
const WIDTH = 640;
|
||||
const HEIGHT = 480;
|
||||
const layers = Number(process.env.VIDEO_LAYERS || '1');
|
||||
// The SDK only splits a simulcast source in three layers from 960 px wide
|
||||
const [WIDTH, HEIGHT] = layers === 3 ? [1280, 720] : [640, 480];
|
||||
|
||||
const room = new Room();
|
||||
await room.connect(process.env.LIVEKIT_URL, process.env.LIVEKIT_TOKEN, {
|
||||
|
|
@ -36,9 +29,9 @@ const options = new TrackPublishOptions({
|
|||
source: TrackSource.SOURCE_CAMERA,
|
||||
});
|
||||
if (codec === 'vp8' || codec === 'h264') {
|
||||
options.simulcast = multiLayer;
|
||||
options.simulcast = layers > 1;
|
||||
} else {
|
||||
options.scalabilityMode = multiLayer ? 'L2T2' : 'L1T1';
|
||||
options.scalabilityMode = `L${layers}T${layers}`;
|
||||
}
|
||||
await room.localParticipant.publishTrack(track, options);
|
||||
|
||||
|
|
@ -59,4 +52,4 @@ setInterval(() => {
|
|||
}
|
||||
}
|
||||
source.captureFrame(new VideoFrame(buf, WIDTH, HEIGHT, VideoBufferType.RGBA));
|
||||
}, multiLayer ? 66 : 33); // 15 fps in multi mode: several software encoders at 30 fps trigger libwebrtc CPU adaptation (layer sizes shrink)
|
||||
}, layers > 1 ? 66 : 33); // 15 fps with several layers: several software encoders at 30 fps trigger libwebrtc CPU adaptation (layer sizes shrink)
|
||||
|
|
|
|||
|
|
@ -1,28 +1,22 @@
|
|||
# Minimal LiveKit Python RTC SDK publisher used by OpenViduTestAppE2eTest.
|
||||
# Joins the room of LIVEKIT_TOKEN and publishes a single video track with codec
|
||||
# VIDEO_CODEC (vp8, h264, vp9 or av1). VIDEO_LAYERS=single (default): one plain
|
||||
# RTP encoding (no simulcast for VP8/H264, no SVC = scalability_mode L1T1 for
|
||||
# VP9/AV1). VIDEO_LAYERS=multi: two layers, simulcast for VP8/H264 (the SDK
|
||||
# derives 480x360 + 640x480 from the 640x480 source) and SVC L2T2 for
|
||||
# VP9/AV1. Pushes synthetic animated frames forever (the Java test stops the
|
||||
# container).
|
||||
# Env: LIVEKIT_URL, LIVEKIT_TOKEN, VIDEO_CODEC, VIDEO_LAYERS
|
||||
# Minimal LiveKit Python SDK publisher used by OpenViduTestAppE2eServerSdkTest
|
||||
# See ../README.md
|
||||
import asyncio
|
||||
import os
|
||||
|
||||
from livekit import rtc
|
||||
|
||||
MULTI_LAYER = os.environ.get("VIDEO_LAYERS") == "multi"
|
||||
WIDTH = 640
|
||||
HEIGHT = 480
|
||||
|
||||
LAYERS = int(os.environ.get("VIDEO_LAYERS", "1"))
|
||||
# The SDK only splits a simulcast source in three layers from 960 px wide
|
||||
WIDTH, HEIGHT = (1280, 720) if LAYERS == 3 else (640, 480)
|
||||
|
||||
async def main():
|
||||
codec = os.environ["VIDEO_CODEC"]
|
||||
|
||||
room = rtc.Room()
|
||||
await room.connect(
|
||||
os.environ["LIVEKIT_URL"], os.environ["LIVEKIT_TOKEN"], options=rtc.RoomOptions(auto_subscribe=False)
|
||||
os.environ["LIVEKIT_URL"],
|
||||
os.environ["LIVEKIT_TOKEN"],
|
||||
options=rtc.RoomOptions(auto_subscribe=False, dynacast=False),
|
||||
)
|
||||
|
||||
source = rtc.VideoSource(WIDTH, HEIGHT)
|
||||
|
|
@ -32,9 +26,9 @@ async def main():
|
|||
source=rtc.TrackSource.SOURCE_CAMERA,
|
||||
)
|
||||
if codec in ("vp8", "h264"):
|
||||
options.simulcast = MULTI_LAYER
|
||||
options.simulcast = LAYERS > 1
|
||||
else:
|
||||
options.scalability_mode = "L2T2" if MULTI_LAYER else "L1T1"
|
||||
options.scalability_mode = f"L{LAYERS}T{LAYERS}"
|
||||
await room.local_participant.publish_track(track, options)
|
||||
|
||||
# The Java test waits for this exact log line before asserting
|
||||
|
|
@ -45,7 +39,8 @@ async def main():
|
|||
n = (n + 7) % 256
|
||||
data = bytes((n, 255 - n, (n * 3) % 256, 255)) * (WIDTH * HEIGHT)
|
||||
source.capture_frame(rtc.VideoFrame(WIDTH, HEIGHT, rtc.VideoBufferType.RGBA, data))
|
||||
await asyncio.sleep(1 / 15 if MULTI_LAYER else 1 / 30) # 15 fps in multi mode: several software encoders at 30 fps trigger CPU adaptation
|
||||
# 15 fps with several layers: several software encoders at 30 fps trigger CPU adaptation
|
||||
await asyncio.sleep(1 / 15 if LAYERS > 1 else 1 / 30)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
|
|
|||
|
|
@ -464,9 +464,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "crossbeam-utils"
|
||||
version = "0.8.22"
|
||||
version = "0.8.23"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "61803da095bee82a81bb1a452ecc25d3b2f1416d1897eb86430c6159ef717c17"
|
||||
checksum = "a31eee39dddec8330830986fcd7625edb5a24ec90ea038215273bbc3adb08ac6"
|
||||
|
||||
[[package]]
|
||||
name = "crypto-common"
|
||||
|
|
|
|||
|
|
@ -1,12 +1,5 @@
|
|||
// Minimal LiveKit Rust SDK publisher used by OpenViduTestAppE2eTest.
|
||||
// Joins the room of LIVEKIT_TOKEN and publishes a single video track with codec
|
||||
// VIDEO_CODEC (vp8, h264, vp9 or av1). VIDEO_LAYERS=single (default): one plain
|
||||
// RTP encoding (no simulcast for VP8/H264, no SVC = scalability_mode L1T1 for
|
||||
// VP9/AV1). VIDEO_LAYERS=multi: two layers, simulcast for VP8/H264 (the SDK
|
||||
// derives 480x360 + 640x480 from the 640x480 source) and SVC L2T2 for
|
||||
// VP9/AV1. Pushes synthetic animated frames forever (the Java test stops the
|
||||
// container).
|
||||
// Env: LIVEKIT_URL, LIVEKIT_TOKEN, VIDEO_CODEC, VIDEO_LAYERS
|
||||
// Minimal LiveKit Rust SDK publisher used by OpenViduTestAppE2eServerSdkTest
|
||||
// See ../README.md
|
||||
use std::{env, time::Duration};
|
||||
|
||||
use livekit::options::{TrackPublishOptions, VideoCodec};
|
||||
|
|
@ -18,8 +11,9 @@ use livekit::{Room, RoomOptions};
|
|||
#[tokio::main]
|
||||
async fn main() {
|
||||
let codec = env::var("VIDEO_CODEC").unwrap();
|
||||
let multi_layer = env::var("VIDEO_LAYERS").map(|v| v == "multi").unwrap_or(false);
|
||||
let (width, height): (u32, u32) = (640, 480);
|
||||
let layers: u32 = env::var("VIDEO_LAYERS").ok().and_then(|v| v.parse().ok()).unwrap_or(1);
|
||||
// The SDK only splits a simulcast source in three layers from 960 px wide
|
||||
let (width, height): (u32, u32) = if layers == 3 { (1280, 720) } else { (640, 480) };
|
||||
|
||||
let mut options = TrackPublishOptions {
|
||||
source: TrackSource::Camera,
|
||||
|
|
@ -33,15 +27,18 @@ async fn main() {
|
|||
..Default::default()
|
||||
};
|
||||
if codec == "vp8" || codec == "h264" {
|
||||
options.simulcast = multi_layer;
|
||||
options.simulcast = layers > 1;
|
||||
} else {
|
||||
options.scalability_mode = Some(if multi_layer { "L2T2" } else { "L1T1" }.to_string());
|
||||
options.scalability_mode = Some(format!("L{layers}T{layers}"));
|
||||
}
|
||||
|
||||
// RoomOptions is #[non_exhaustive]: it cannot be built with a struct expression
|
||||
let mut room_options = RoomOptions::default();
|
||||
room_options.dynacast = false;
|
||||
let (room, mut _events) = Room::connect(
|
||||
&env::var("LIVEKIT_URL").unwrap(),
|
||||
&env::var("LIVEKIT_TOKEN").unwrap(),
|
||||
RoomOptions::default(),
|
||||
room_options,
|
||||
)
|
||||
.await
|
||||
.expect("could not connect to room");
|
||||
|
|
@ -73,7 +70,7 @@ async fn main() {
|
|||
data_u.fill(255 - n);
|
||||
data_v.fill(n.wrapping_mul(3));
|
||||
source.capture_frame(&frame);
|
||||
// 15 fps in multi mode: several software encoders at 30 fps trigger CPU adaptation
|
||||
tokio::time::sleep(Duration::from_millis(if multi_layer { 66 } else { 33 })).await;
|
||||
// 15 fps with several layers: several software encoders at 30 fps trigger CPU adaptation
|
||||
tokio::time::sleep(Duration::from_millis(if layers > 1 { 66 } else { 33 })).await;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -44,7 +44,7 @@
|
|||
<mat-form-field id="max-video-quality" class="video-btn quality-option" matTooltip="Set video quality" matTooltipClass="custom-tooltip">
|
||||
<mat-select [(value)]="maxVideoQuality" (selectionChange)="onQualityChange()">
|
||||
@for (q of ['LOW', 'MEDIUM', 'HIGH']; track q) {
|
||||
<mat-option [value]="q" [ngClass]="'mode-' + q">{{q}}</mat-option>
|
||||
<mat-option [value]="q" [id]="'mat-option-' + q" [ngClass]="'mode-' + q">{{q}}</mat-option>
|
||||
}
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
|
|
|
|||
Loading…
Reference in New Issue