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
pabloFuente 2026-09-05 21:10:56 +02:00
parent 6e78fc0ec1
commit 2fe3bd87f2
10 changed files with 411 additions and 306 deletions

View File

@ -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"));

View File

@ -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);",

View File

@ -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.

View File

@ -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);
}

View File

@ -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 {}

View File

@ -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)

View File

@ -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__":

View File

@ -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"

View File

@ -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;
}
}

View File

@ -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>