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;
|
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
|
* Starts a LiveKit server RTC SDK participant (a minimal program at
|
||||||
* src/test/resources/<sdk>-publisher) in a plain runtime container
|
* src/test/resources/<sdk>-publisher) in a plain runtime container
|
||||||
|
|
@ -375,73 +379,59 @@ public class OpenViduTestE2e {
|
||||||
* given codec (vp8, h264, vp9 or av1) as one plain RTP encoding: without
|
* 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.
|
* 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
|
* The container mounts the program read-only at /app, copies it to /work and
|
||||||
* and runs it there, with a per-SDK dependency cache mounted at /cache so
|
* runs it there, with a per-SDK dependency cache mounted at /cache so
|
||||||
* repeated runs skip downloads. Blocks until the participant has published
|
* repeated runs skip downloads. Returns once the program logs
|
||||||
* its track (its TRACK_PUBLISHED log line). The container is stopped
|
* TRACK_PUBLISHED. The container is stopped automatically on test dispose.
|
||||||
* automatically on test dispose.
|
|
||||||
*/
|
*/
|
||||||
public void startServerSdkPublisher(String sdk, String roomName, String codec) throws Exception {
|
public void startServerSdkPublisher(String sdk, String roomName, String codec, int layers) throws Exception {
|
||||||
startServerSdkPublisher(sdk, roomName, codec, false);
|
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 image;
|
||||||
final String runCommand;
|
final String runCommand;
|
||||||
int startupTimeoutMinutes = 5;
|
int startupTimeoutMinutes = 5;
|
||||||
Map<String, String> env = new HashMap<>();
|
Map<String, String> env = new HashMap<>();
|
||||||
switch (sdk) {
|
switch (sdk) {
|
||||||
case "go":
|
case "go":
|
||||||
image = "golang:1.27";
|
image = "golang:1.27";
|
||||||
runCommand = "go run .";
|
runCommand = "go run .";
|
||||||
env.put("GOMODCACHE", "/cache/gomod");
|
env.put("GOMODCACHE", "/cache/gomod");
|
||||||
env.put("GOCACHE", "/cache/gobuild");
|
env.put("GOCACHE", "/cache/gobuild");
|
||||||
break;
|
break;
|
||||||
case "node":
|
case "node":
|
||||||
image = "node:24";
|
image = "node:24";
|
||||||
runCommand = "npm install --no-audit --no-fund --loglevel=error && node main.mjs";
|
runCommand = "npm install --no-audit --no-fund --loglevel=error && node main.mjs";
|
||||||
env.put("npm_config_cache", "/cache/npm");
|
env.put("npm_config_cache", "/cache/npm");
|
||||||
break;
|
break;
|
||||||
case "python":
|
case "python":
|
||||||
image = "python:3.14";
|
image = "python:3.14";
|
||||||
runCommand = "pip install -q -r requirements.txt && python main.py";
|
runCommand = "pip install -q -r requirements.txt && python main.py";
|
||||||
env.put("PIP_CACHE_DIR", "/cache/pip");
|
env.put("PIP_CACHE_DIR", "/cache/pip");
|
||||||
break;
|
break;
|
||||||
case "rust":
|
case "rust":
|
||||||
image = "rust:1";
|
image = "rust:1";
|
||||||
// webrtc-sys links a prebuilt libwebrtc that requires clang++ >= 21
|
// webrtc-sys links a prebuilt libwebrtc that requires clang++ >= 21
|
||||||
// (apt.llvm.org); the first run then compiles the whole livekit
|
// (apt.llvm.org); the first run then compiles the whole livekit
|
||||||
// crate graph
|
// crate graph
|
||||||
runCommand = "apt-get update -qq >/dev/null && apt-get install -y -qq lsb-release gnupg >/dev/null"
|
runCommand = "apt-get update -qq >/dev/null && apt-get install -y -qq lsb-release gnupg >/dev/null"
|
||||||
+ " && wget -qO /tmp/llvm.sh https://apt.llvm.org/llvm.sh && bash /tmp/llvm.sh 21 >/dev/null 2>&1"
|
+ " && wget -qO /tmp/llvm.sh https://apt.llvm.org/llvm.sh && bash /tmp/llvm.sh 21 >/dev/null 2>&1"
|
||||||
+ " && cargo run --release --locked";
|
+ " && cargo run --release --locked";
|
||||||
env.put("CARGO_HOME", "/cache/cargo");
|
env.put("CARGO_HOME", "/cache/cargo");
|
||||||
env.put("CARGO_TARGET_DIR", "/cache/cargo-target");
|
env.put("CARGO_TARGET_DIR", "/cache/cargo-target");
|
||||||
env.put("CC", "clang-21");
|
env.put("CC", "clang-21");
|
||||||
env.put("CXX", "clang++-21");
|
env.put("CXX", "clang++-21");
|
||||||
startupTimeoutMinutes = 30;
|
startupTimeoutMinutes = 30;
|
||||||
break;
|
break;
|
||||||
case "dotnet":
|
case "dotnet":
|
||||||
image = "mcr.microsoft.com/dotnet/sdk:10.0";
|
image = "mcr.microsoft.com/dotnet/sdk:10.0";
|
||||||
runCommand = "dotnet run -c Release";
|
runCommand = "dotnet run -c Release";
|
||||||
env.put("NUGET_PACKAGES", "/cache/nuget");
|
env.put("NUGET_PACKAGES", "/cache/nuget");
|
||||||
env.put("DOTNET_CLI_TELEMETRY_OPTOUT", "1");
|
env.put("DOTNET_CLI_TELEMETRY_OPTOUT", "1");
|
||||||
startupTimeoutMinutes = 10;
|
startupTimeoutMinutes = 10;
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
throw new IllegalArgumentException("Unknown server SDK publisher: " + sdk);
|
throw new IllegalArgumentException("Unknown server SDK publisher: " + sdk);
|
||||||
}
|
}
|
||||||
// Publishers receive a ready-made token: it keeps the programs shorter
|
// Publishers receive a ready-made token: it keeps the programs shorter
|
||||||
// (no per-SDK token dependency) and works with any api secret length
|
// (no per-SDK token dependency) and works with any api secret length
|
||||||
|
|
@ -451,7 +441,7 @@ public class OpenViduTestE2e {
|
||||||
env.put("LIVEKIT_URL", LIVEKIT_URL);
|
env.put("LIVEKIT_URL", LIVEKIT_URL);
|
||||||
env.put("LIVEKIT_TOKEN", accessToken.toJwt());
|
env.put("LIVEKIT_TOKEN", accessToken.toJwt());
|
||||||
env.put("VIDEO_CODEC", codec);
|
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();
|
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);
|
Path cacheDir = Paths.get(System.getProperty("java.io.tmpdir"), "openvidu-e2e-sdk-cache", sdk);
|
||||||
|
|
@ -475,27 +465,37 @@ public class OpenViduTestE2e {
|
||||||
// encode raw frames themselves.
|
// encode raw frames themselves.
|
||||||
final String encoder;
|
final String encoder;
|
||||||
switch (codec) {
|
switch (codec) {
|
||||||
case "h264":
|
case "h264":
|
||||||
encoder = "-c:v libx264 -profile:v baseline -level 3.1 -x264-params keyint=30:scenecut=0 -f h264";
|
encoder = "-c:v libx264 -profile:v baseline -level 3.1 -x264-params keyint=30:scenecut=0 -f h264";
|
||||||
break;
|
break;
|
||||||
case "vp8":
|
case "vp8":
|
||||||
encoder = "-c:v libvpx -g 30 -f ivf";
|
encoder = "-c:v libvpx -g 30 -f ivf";
|
||||||
break;
|
break;
|
||||||
case "vp9":
|
case "vp9":
|
||||||
encoder = "-c:v libvpx-vp9 -g 30 -f ivf";
|
encoder = "-c:v libvpx-vp9 -g 30 -f ivf";
|
||||||
break;
|
break;
|
||||||
case "av1":
|
case "av1":
|
||||||
encoder = "-c:v libaom-av1 -usage realtime -cpu-used 8 -g 30 -f ivf";
|
encoder = "-c:v libaom-av1 -usage realtime -cpu-used 8 -g 30 -f ivf";
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
throw new IllegalArgumentException("Unknown video codec: " + codec);
|
throw new IllegalArgumentException("Unknown video codec: " + codec);
|
||||||
}
|
}
|
||||||
Path mediaDir = Files.createTempDirectory("go-publisher-media");
|
Path mediaDir = Files.createTempDirectory("go-publisher-media");
|
||||||
// One file per layer: the single encoding, or the three simulcast
|
// One file per layer: the single encoding or the simulcast layers
|
||||||
// layers (must match the sizes declared by go-publisher/main.go)
|
// (must match the sizes declared by go-publisher/main.go)
|
||||||
Map<String, String> layerSizes = multiLayer
|
final Map<String, String> layerSizes;
|
||||||
? Map.of("VIDEO_FILE_LOW", "320x240", "VIDEO_FILE_HIGH", "640x480")
|
switch (layers) {
|
||||||
: Map.of("VIDEO_FILE", "640x480");
|
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()) {
|
for (Map.Entry<String, String> layer : layerSizes.entrySet()) {
|
||||||
Path videoFile = mediaDir
|
Path videoFile = mediaDir
|
||||||
.resolve("test-video-" + layer.getValue() + "." + ("h264".equals(codec) ? "h264" : "ivf"));
|
.resolve("test-video-" + layer.getValue() + "." + ("h264".equals(codec) ? "h264" : "ivf"));
|
||||||
|
|
|
||||||
|
|
@ -64,83 +64,87 @@ public class OpenViduTestAppE2eServerSdkTest extends AbstractOpenViduTestappE2eT
|
||||||
this.closeAllRooms(LK);
|
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.
|
* {vp8, h264, vp9, av1} x {1, 2, 3} layers, single-layer cases first. System
|
||||||
* System properties sdk.codecs / sdk.layers (comma-separated) restrict the
|
* properties sdk.codecs / sdk.layers (comma-separated) restrict the matrix,
|
||||||
* matrix, e.g. -Dsdk.layers=multi -Dsdk.codecs=vp9,av1
|
* e.g. -Dsdk.layers=2,3 -Dsdk.codecs=vp9,av1
|
||||||
*/
|
*/
|
||||||
static Stream<Arguments> serverSdkPublisherMatrix() {
|
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(","));
|
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)
|
.flatMap(layers -> Stream.of("vp8", "h264", "vp9", "av1").filter(codecsFilter::contains)
|
||||||
.map(codec -> Arguments.of(codec, layers)));
|
.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")
|
@MethodSource("serverSdkPublisherMatrix")
|
||||||
@DisplayName("Go SDK publisher to Chrome and Firefox subscribers")
|
@DisplayName("Go SDK publisher to Chrome and Firefox early and late subscribers")
|
||||||
void goSdkPublisherToBrowserSubscribersTest(String codec, String layers) throws Exception {
|
void goSdkPublisherToBrowserSubscribersTest(String codec, int layers) throws Exception {
|
||||||
serverSdkPublisherToBrowserSubscribersAux("go", codec, layers);
|
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")
|
@MethodSource("serverSdkPublisherMatrix")
|
||||||
@DisplayName("Node SDK publisher to Chrome and Firefox subscribers")
|
@DisplayName("Node SDK publisher to Chrome and Firefox early and late subscribers")
|
||||||
void nodeSdkPublisherToBrowserSubscribersTest(String codec, String layers) throws Exception {
|
void nodeSdkPublisherToBrowserSubscribersTest(String codec, int layers) throws Exception {
|
||||||
serverSdkPublisherToBrowserSubscribersAux("node", codec, layers);
|
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")
|
@MethodSource("serverSdkPublisherMatrix")
|
||||||
@DisplayName("Python SDK publisher to Chrome and Firefox subscribers")
|
@DisplayName("Python SDK publisher to Chrome and Firefox early and late subscribers")
|
||||||
void pythonSdkPublisherToBrowserSubscribersTest(String codec, String layers) throws Exception {
|
void pythonSdkPublisherToBrowserSubscribersTest(String codec, int layers) throws Exception {
|
||||||
serverSdkPublisherToBrowserSubscribersAux("python", codec, layers);
|
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")
|
@MethodSource("serverSdkPublisherMatrix")
|
||||||
@DisplayName("Rust SDK publisher to Chrome and Firefox subscribers")
|
@DisplayName("Rust SDK publisher to Chrome and Firefox early and late subscribers")
|
||||||
void rustSdkPublisherToBrowserSubscribersTest(String codec, String layers) throws Exception {
|
void rustSdkPublisherToBrowserSubscribersTest(String codec, int layers) throws Exception {
|
||||||
serverSdkPublisherToBrowserSubscribersAux("rust", codec, layers);
|
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")
|
@MethodSource("serverSdkPublisherMatrix")
|
||||||
@DisplayName(".NET SDK publisher to Chrome and Firefox subscribers")
|
@DisplayName(".NET SDK publisher to Chrome and Firefox early and late subscribers")
|
||||||
void dotnetSdkPublisherToBrowserSubscribersTest(String codec, String layers) throws Exception {
|
void dotnetSdkPublisherToBrowserSubscribersTest(String codec, int layers) throws Exception {
|
||||||
// Requires Livekit.Rtc.Dotnet >= 0.1.4 (TrackPublishOptions.VideoCodec)
|
// Requires Livekit.Rtc.Dotnet >= 0.1.4 (TrackPublishOptions.VideoCodec)
|
||||||
serverSdkPublisherToBrowserSubscribersAux("dotnet", codec, layers);
|
serverSdkPublisherToBrowserSubscribersAux("dotnet", codec, layers);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A Chrome browser and a Firefox browser join the room as subscriber-only
|
* A subscriber-only participant of the room: the testapp instance (index
|
||||||
* participants and a LiveKit server RTC SDK participant
|
* within its browser page) that hosts it, and its participant name.
|
||||||
* (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.
|
|
||||||
*/
|
*/
|
||||||
private void serverSdkPublisherToBrowserSubscribersAux(String sdk, String codec, String layers)
|
private record Subscriber(OpenViduTestappUser user, int instance, String name) {
|
||||||
throws Exception {
|
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 expectedCodec = "video/" + codec.toUpperCase();
|
||||||
final String publisherIdentity = sdk + "-publisher";
|
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
|
// The Go SDK forwards pre-encoded samples (no encoder), so its only
|
||||||
// multi-layer shape is RID simulcast — and LiveKit does not support RID
|
// 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)),
|
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");
|
"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"),
|
log.info("{} SDK {} {}-layer publisher to Chrome and Firefox early and late subscribers", sdk, codec,
|
||||||
setupBrowserAndConnectToOpenViduTestapp("firefox"));
|
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) {
|
// Early subscribers: in the room before the SDK publishes
|
||||||
this.addSubscriber(user, false);
|
List<Subscriber> earlySubscribers = List.of(joinAsSubscriberOnly(chrome, "early"),
|
||||||
WebElement participantNameInput = user.getDriver().findElement(By.id("participant-name-input-0"));
|
joinAsSubscriberOnly(firefox, "early"));
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.startServerSdkPublisher(sdk, "TestRoom", codec, multiLayer);
|
this.startServerSdkPublisher(sdk, "TestRoom", codec, layers);
|
||||||
|
|
||||||
// The server's authoritative view of the publication (RoomService API):
|
// The server's authoritative view of the publication (RoomService API):
|
||||||
// the LiveKit TrackInfo of a video publication lists one layer per
|
// the LiveKit TrackInfo of a video publication lists one layer per
|
||||||
// simulcast layer or SVC spatial layer, so one plain encoding (no
|
// simulcast layer or SVC spatial layer, so one plain encoding (no
|
||||||
// simulcast, no SVC / L1T1) has exactly one layer and a two-layer
|
// simulcast, no SVC / L1T1) has exactly one layer and a two- or
|
||||||
// publish (simulcast or SVC L2T2) has two
|
// three-layer publish (simulcast or SVC L2T2 / L3T3) has two or three
|
||||||
TrackInfo trackInfo = this.getPublishedVideoTrackInfo("TestRoom", publisherIdentity);
|
TrackInfo trackInfo = this.getPublishedVideoTrackInfo("TestRoom", publisherIdentity);
|
||||||
final int expectedLayers = multiLayer ? 2 : 1;
|
Assertions.assertEquals(layers, trackInfo.getLayersCount(), "Expected " + layers
|
||||||
Assertions.assertEquals(expectedLayers, trackInfo.getLayersCount(), "Expected " + expectedLayers
|
|
||||||
+ " video layer(s) in the track published by the " + sdk + " SDK, but the server reports "
|
+ " video layer(s) in the track published by the " + sdk + " SDK, but the server reports "
|
||||||
+ trackInfo.getLayersList());
|
+ trackInfo.getLayersList());
|
||||||
Assertions.assertEquals(expectedLayers, trackInfo.getCodecs(0).getLayersCount(), "Expected " + expectedLayers
|
Assertions.assertEquals(layers, trackInfo.getCodecs(0).getLayersCount(),
|
||||||
+ " video layer(s) for codec " + expectedCodec + " published by the " + sdk + " SDK");
|
"Expected " + layers + " video layer(s) for codec " + expectedCodec + " published by the " + sdk
|
||||||
// VP8/H264 multi-layer publishes are simulcast and VP9/AV1 ones are SVC
|
+ " SDK");
|
||||||
// L2T2, except for the Go SDK: it forwards pre-encoded samples (no
|
Assertions.assertEquals(layers, sortedLayerWidths(trackInfo).size(),
|
||||||
// encoder, so no SVC) and publishes simulcast for every codec
|
"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));
|
final boolean expectSimulcast = multiLayer && ("vp8".equals(codec) || "h264".equals(codec) || "go".equals(sdk));
|
||||||
if (!multiLayer || expectSimulcast) {
|
if (!multiLayer || expectSimulcast) {
|
||||||
Assertions.assertEquals(expectSimulcast, trackInfo.getSimulcast(),
|
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");
|
"The track published by the " + sdk + " SDK should not be SVC");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Both browsers must receive the track, with its codec and its layers
|
for (Subscriber subscriber : earlySubscribers) {
|
||||||
for (OpenViduTestappUser user : subscribers) {
|
assertSubscriberReceivesVideo(subscriber, sdk, publisherIdentity, expectedCodec, trackInfo);
|
||||||
assertSubscriberReceivesVideo(user, sdk, publisherIdentity, expectedCodec, trackInfo);
|
|
||||||
}
|
}
|
||||||
|
assertSubscribersSwitchLayers(earlySubscribers, trackInfo);
|
||||||
|
|
||||||
for (OpenViduTestappUser user : subscribers) {
|
// Late subscribers: the publisher has been streaming for a while
|
||||||
gracefullyLeaveParticipants(user, 1);
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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. */
|
/** "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
|
* 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()) {
|
if (!user.getDriver().findElements(By.cssSelector("app-info-dialog")).isEmpty()) {
|
||||||
user.getDriver().findElement(By.cssSelector("#close-dialog-btn")).click();
|
user.getDriver().findElement(By.cssSelector("#close-dialog-btn")).click();
|
||||||
Thread.sleep(300);
|
Thread.sleep(300);
|
||||||
}
|
}
|
||||||
user.getDriver().findElement(By.cssSelector("#openvidu-instance-0 #max-video-quality")).click();
|
this.selectMatOption(user, subscriber.instanceSelector() + " #max-video-quality", quality);
|
||||||
this.waitAndClick(user, "mat-option.mode-" + quality);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The subscriber-only participant of the given browser must receive the video
|
* The subscriber-only participant must receive the video track published by
|
||||||
* track published by the SDK participant: media actually flowing, with the
|
* the SDK participant: media actually flowing, with the expected codec, with
|
||||||
* expected codec, with the layers the server reports in trackInfo. With more
|
* the layers the server reports in trackInfo.
|
||||||
* than one layer the subscriber switches to the LOW and then to the HIGH
|
|
||||||
* layer, each recognised by its frame width.
|
|
||||||
*/
|
*/
|
||||||
private void assertSubscriberReceivesVideo(OpenViduTestappUser user, String sdk, String publisherIdentity,
|
private void assertSubscriberReceivesVideo(Subscriber subscriber, String sdk, String publisherIdentity,
|
||||||
String expectedCodec, TrackInfo trackInfo) throws Exception {
|
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));
|
user.getWaiter().until(
|
||||||
Assertions.assertTrue(assertAllElementsHaveTracks(user, "video", false, true),
|
ExpectedConditions.numberOfElementsToBe(By.cssSelector(subscriber.instanceSelector() + " video"), 1));
|
||||||
browser + ": HTMLVideoElements were expected to have only one video track");
|
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
|
// 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);
|
waitUntilVideoLayersNotEmpty(user, subscriberVideo);
|
||||||
this.waitUntilSubscriberBytesReceivedIncreasing(user, subscriberVideo);
|
this.waitUntilSubscriberBytesReceivedIncreasing(user, subscriberVideo);
|
||||||
this.waitUntilSubscriberFramesPerSecondNotZero(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
|
// And with the codec the publisher actually sends (a Producer bound to
|
||||||
// the wrong codec makes every subscriber negotiate that wrong codec)
|
// the wrong codec makes every subscriber negotiate that wrong codec)
|
||||||
Assertions.assertEquals(expectedCodec, this.getSubscriberVideoCodec(user, subscriberVideo),
|
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
|
// And with the layers the server reports, in the track info received by
|
||||||
// the subscriber
|
// the subscriber
|
||||||
JsonArray subscriberLayers = this.getRemoteVideoTrackInfoLayers(user, publisherIdentity);
|
JsonArray subscriberLayers = this.getRemoteVideoTrackInfoLayers(subscriber, publisherIdentity);
|
||||||
Assertions.assertEquals(trackInfo.getLayersCount(), subscriberLayers.size(),
|
Assertions.assertEquals(trackInfo.getLayersCount(), subscriberLayers.size(),
|
||||||
"Expected " + trackInfo.getLayersCount() + " video layer(s) in the track published by the " + sdk
|
"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
|
* With more than one published layer, the given subscribers (all of them
|
||||||
// only changes through the max-video-quality selector. Receiving the
|
* receiving the track already) switch together to the LOW layer, then (with
|
||||||
// LOW layer and then the HIGH layer — each recognised by the frame
|
* three layers) to the MEDIUM one and then to the HIGH one, each subscriber
|
||||||
// width the publisher declared for it in the TrackInfo, and each
|
* recognising each layer by its frame width. With adaptiveStream disabled the
|
||||||
// actually decoding (framesPerSecond > 0: an undecodable layer still
|
* received layer only changes through the max-video-quality selector, and
|
||||||
// reports its frame size) — proves that the publisher sends every
|
* each layer must actually decode (framesPerSecond > 0: an undecodable layer
|
||||||
// layer and that the SFU forwards the requested one. The declared
|
* still reports its frame size): this proves that the publisher sends every
|
||||||
// widths are reliable because the multi-layer publishers capture at
|
* layer and that the SFU forwards the requested one, also the middle spatial
|
||||||
// 15 fps: at 30 fps libwebrtc's CPU adaptation can scale every layer
|
* layer, which neither LOW nor HIGH can clamp to. The declared widths are
|
||||||
// down under load
|
* reliable because the multi-layer publishers capture at 15 fps: at 30 fps
|
||||||
this.selectQualityAndAwaitLayer(user, subscriberVideo, "LOW", lowestLayerWidth(trackInfo));
|
* libwebrtc's CPU adaptation can scale every layer down under load.
|
||||||
this.selectQualityAndAwaitLayer(user, subscriberVideo, "HIGH", highestLayerWidth(trackInfo));
|
*/
|
||||||
|
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
|
* (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.
|
* can take longer than one wait window to ramp back up to a higher layer.
|
||||||
*/
|
*/
|
||||||
private void selectQualityAndAwaitLayer(OpenViduTestappUser user, WebElement subscriberVideo, String quality,
|
private void selectQualityAndAwaitLayer(Subscriber subscriber, String quality, int expectedFrameWidth)
|
||||||
int expectedFrameWidth) throws Exception {
|
throws Exception {
|
||||||
|
OpenViduTestappUser user = subscriber.user();
|
||||||
|
WebElement subscriberVideo = subscriber.remoteVideo();
|
||||||
for (int attempt = 1; attempt <= 2; attempt++) {
|
for (int attempt = 1; attempt <= 2; attempt++) {
|
||||||
this.selectSubscriberVideoQuality(user, quality);
|
this.selectSubscriberVideoQuality(subscriber, quality);
|
||||||
try {
|
try {
|
||||||
this.waitUntilSubscriberFrameWidthIs(user, subscriberVideo, expectedFrameWidth);
|
this.waitUntilSubscriberFrameWidthIs(user, subscriberVideo, expectedFrameWidth);
|
||||||
break;
|
break;
|
||||||
|
|
@ -298,23 +349,15 @@ public class OpenViduTestAppE2eServerSdkTest extends AbstractOpenViduTestappE2eT
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Width of the lowest-quality published layer, from the server's TrackInfo.
|
* Distinct widths of the published layers, lowest first, from the server's
|
||||||
* The layers are picked by width, not by their VideoQuality label: the SDKs
|
* TrackInfo. The layers are picked by width, not by their VideoQuality label:
|
||||||
* label a two-layer publish inconsistently (simulcast: LOW + MEDIUM; SVC
|
* the SDKs label a two-layer publish inconsistently (simulcast: LOW + MEDIUM;
|
||||||
* L2T2: MEDIUM + HIGH; the Go program: LOW + HIGH), while the subscriber's
|
* SVC L2T2: MEDIUM + HIGH; the Go program: LOW + HIGH), while the
|
||||||
* LOW/HIGH selection always clamps to the lowest/highest available layer.
|
* 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) {
|
private List<Integer> sortedLayerWidths(TrackInfo trackInfo) {
|
||||||
return trackInfo.getLayersList().stream().mapToInt(VideoLayer::getWidth).min()
|
return trackInfo.getLayersList().stream().map(VideoLayer::getWidth).distinct().sorted().toList();
|
||||||
.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));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -341,11 +384,11 @@ public class OpenViduTestAppE2eServerSdkTest extends AbstractOpenViduTestappE2eT
|
||||||
/**
|
/**
|
||||||
* Video layers (VideoLayer[] of the LiveKit TrackInfo) of the first video
|
* Video layers (VideoLayer[] of the LiveKit TrackInfo) of the first video
|
||||||
* track published by the remote participant with the given identity, as seen
|
* 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) {
|
private JsonArray getRemoteVideoTrackInfoLayers(Subscriber subscriber, String participantIdentity) {
|
||||||
String layers = (String) ((JavascriptExecutor) user.getDriver()).executeScript(
|
String layers = (String) ((JavascriptExecutor) subscriber.user().getDriver()).executeScript(
|
||||||
"var room = window['room_0'];"
|
"var room = window['room_" + subscriber.instance() + "'];"
|
||||||
+ "var participant = room.remoteParticipants.get(arguments[0]);"
|
+ "var participant = room.remoteParticipants.get(arguments[0]);"
|
||||||
+ "var publication = participant.videoTrackPublications.values().next().value;"
|
+ "var publication = participant.videoTrackPublications.values().next().value;"
|
||||||
+ "return JSON.stringify(publication.trackInfo.layers);",
|
+ "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.
|
// Minimal LiveKit .NET SDK publisher used by OpenViduTestAppE2eServerSdkTest.
|
||||||
// Joins the room of LIVEKIT_TOKEN and publishes a single video track with codec
|
// See ../README.md
|
||||||
// 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
|
|
||||||
using LiveKit.Rtc;
|
using LiveKit.Rtc;
|
||||||
using Proto = LiveKit.Proto;
|
using Proto = LiveKit.Proto;
|
||||||
|
|
||||||
var codec = Environment.GetEnvironmentVariable("VIDEO_CODEC")!;
|
var codec = Environment.GetEnvironmentVariable("VIDEO_CODEC")!;
|
||||||
var multiLayer = Environment.GetEnvironmentVariable("VIDEO_LAYERS") == "multi";
|
var layers = int.Parse(Environment.GetEnvironmentVariable("VIDEO_LAYERS") ?? "1");
|
||||||
int width = 640;
|
// The SDK only splits a simulcast source in three layers from 960 px wide
|
||||||
int height = 480;
|
int width = layers == 3 ? 1280 : 640;
|
||||||
|
int height = layers == 3 ? 720 : 480;
|
||||||
|
|
||||||
var room = new Room();
|
var room = new Room();
|
||||||
await room.ConnectAsync(
|
await room.ConnectAsync(
|
||||||
Environment.GetEnvironmentVariable("LIVEKIT_URL")!,
|
Environment.GetEnvironmentVariable("LIVEKIT_URL")!,
|
||||||
Environment.GetEnvironmentVariable("LIVEKIT_TOKEN")!,
|
Environment.GetEnvironmentVariable("LIVEKIT_TOKEN")!,
|
||||||
new RoomOptions { AutoSubscribe = false });
|
new RoomOptions { AutoSubscribe = false, Dynacast = false });
|
||||||
|
|
||||||
var videoSource = new VideoSource(width, height);
|
var videoSource = new VideoSource(width, height);
|
||||||
var videoTrack = LocalVideoTrack.Create("dotnet-video", videoSource);
|
var videoTrack = LocalVideoTrack.Create("dotnet-video", videoSource);
|
||||||
|
|
@ -31,11 +24,11 @@ var options = new TrackPublishOptions
|
||||||
};
|
};
|
||||||
if (codec == "vp8" || codec == "h264")
|
if (codec == "vp8" || codec == "h264")
|
||||||
{
|
{
|
||||||
options.Simulcast = multiLayer;
|
options.Simulcast = layers > 1;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
options.ScalabilityMode = multiLayer ? "L2T2" : "L1T1";
|
options.ScalabilityMode = $"L{layers}T{layers}";
|
||||||
}
|
}
|
||||||
await room.LocalParticipant!.PublishTrackAsync(videoTrack, options);
|
await room.LocalParticipant!.PublishTrackAsync(videoTrack, options);
|
||||||
|
|
||||||
|
|
@ -55,6 +48,6 @@ while (true)
|
||||||
data[i + 3] = 255;
|
data[i + 3] = 255;
|
||||||
}
|
}
|
||||||
videoSource.CaptureFrame(new VideoFrame(width, height, Proto.VideoBufferType.Rgba, data));
|
videoSource.CaptureFrame(new VideoFrame(width, height, Proto.VideoBufferType.Rgba, data));
|
||||||
// 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 Task.Delay(multiLayer ? 66 : 33);
|
await Task.Delay(layers > 1 ? 66 : 33);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,21 +1,5 @@
|
||||||
// Minimal LiveKit Go SDK publisher used by OpenViduTestAppE2eTest.
|
// Minimal LiveKit Go SDK publisher used by OpenViduTestAppE2eServerSdkTest
|
||||||
//
|
// See ../README.md
|
||||||
// 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*
|
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
|
@ -50,9 +34,19 @@ type simulcastLayer struct {
|
||||||
fileEnv string
|
fileEnv string
|
||||||
}
|
}
|
||||||
|
|
||||||
var simulcastLayers = []simulcastLayer{
|
// Simulcast layers per VIDEO_LAYERS value, lowest first (the sizes must match
|
||||||
{livekit.VideoQuality_LOW, 320, 240, "VIDEO_FILE_LOW"},
|
// the files generated by OpenViduTestE2e#startServerSdkPublisher). The last
|
||||||
{livekit.VideoQuality_HIGH, 640, 480, "VIDEO_FILE_HIGH"},
|
// 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() {
|
func main() {
|
||||||
|
|
@ -68,9 +62,17 @@ func main() {
|
||||||
}
|
}
|
||||||
defer room.Disconnect()
|
defer room.Disconnect()
|
||||||
|
|
||||||
if os.Getenv("VIDEO_LAYERS") == "multi" {
|
layers := os.Getenv("VIDEO_LAYERS")
|
||||||
tracks := make([]*lksdk.LocalTrack, 0, len(simulcastLayers))
|
if layers == "" {
|
||||||
for _, layer := range simulcastLayers {
|
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",
|
track, err := lksdk.NewLocalSampleTrack(codec, lksdk.WithSimulcast("go-video",
|
||||||
&livekit.VideoLayer{Quality: layer.quality, Width: layer.width, Height: layer.height}))
|
&livekit.VideoLayer{Quality: layer.quality, Width: layer.width, Height: layer.height}))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -78,16 +80,17 @@ func main() {
|
||||||
}
|
}
|
||||||
tracks = append(tracks, track)
|
tracks = append(tracks, track)
|
||||||
}
|
}
|
||||||
|
top := layerDefs[len(layerDefs)-1]
|
||||||
if _, err = room.LocalParticipant.PublishSimulcastTrack(tracks, &lksdk.TrackPublicationOptions{
|
if _, err = room.LocalParticipant.PublishSimulcastTrack(tracks, &lksdk.TrackPublicationOptions{
|
||||||
Name: "go-video",
|
Name: "go-video",
|
||||||
VideoWidth: 640,
|
VideoWidth: int(top.width),
|
||||||
VideoHeight: 480,
|
VideoHeight: int(top.height),
|
||||||
}); err != nil {
|
}); err != nil {
|
||||||
log.Fatalf("could not publish simulcast track: %v", err)
|
log.Fatalf("could not publish simulcast track: %v", err)
|
||||||
}
|
}
|
||||||
// The Java test waits for this exact log line before asserting.
|
// The Java test waits for this exact log line before asserting.
|
||||||
fmt.Println("TRACK_PUBLISHED")
|
fmt.Println("TRACK_PUBLISHED")
|
||||||
for i, layer := range simulcastLayers {
|
for i, layer := range layerDefs {
|
||||||
go loopVideoFile(tracks[i], os.Getenv(layer.fileEnv))
|
go loopVideoFile(tracks[i], os.Getenv(layer.fileEnv))
|
||||||
}
|
}
|
||||||
select {}
|
select {}
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,5 @@
|
||||||
// Minimal LiveKit Node RTC SDK publisher used by OpenViduTestAppE2eTest.
|
// Minimal LiveKit Node SDK publisher used by OpenViduTestAppE2eServerSdkTest
|
||||||
// Joins the room of LIVEKIT_TOKEN and publishes a single video track with codec
|
// See ../README.md
|
||||||
// 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
|
|
||||||
import {
|
import {
|
||||||
Room,
|
Room,
|
||||||
LocalVideoTrack,
|
LocalVideoTrack,
|
||||||
|
|
@ -19,9 +12,9 @@ import {
|
||||||
} from '@livekit/rtc-node';
|
} from '@livekit/rtc-node';
|
||||||
|
|
||||||
const codec = process.env.VIDEO_CODEC;
|
const codec = process.env.VIDEO_CODEC;
|
||||||
const multiLayer = process.env.VIDEO_LAYERS === 'multi';
|
const layers = Number(process.env.VIDEO_LAYERS || '1');
|
||||||
const WIDTH = 640;
|
// The SDK only splits a simulcast source in three layers from 960 px wide
|
||||||
const HEIGHT = 480;
|
const [WIDTH, HEIGHT] = layers === 3 ? [1280, 720] : [640, 480];
|
||||||
|
|
||||||
const room = new Room();
|
const room = new Room();
|
||||||
await room.connect(process.env.LIVEKIT_URL, process.env.LIVEKIT_TOKEN, {
|
await room.connect(process.env.LIVEKIT_URL, process.env.LIVEKIT_TOKEN, {
|
||||||
|
|
@ -36,9 +29,9 @@ const options = new TrackPublishOptions({
|
||||||
source: TrackSource.SOURCE_CAMERA,
|
source: TrackSource.SOURCE_CAMERA,
|
||||||
});
|
});
|
||||||
if (codec === 'vp8' || codec === 'h264') {
|
if (codec === 'vp8' || codec === 'h264') {
|
||||||
options.simulcast = multiLayer;
|
options.simulcast = layers > 1;
|
||||||
} else {
|
} else {
|
||||||
options.scalabilityMode = multiLayer ? 'L2T2' : 'L1T1';
|
options.scalabilityMode = `L${layers}T${layers}`;
|
||||||
}
|
}
|
||||||
await room.localParticipant.publishTrack(track, options);
|
await room.localParticipant.publishTrack(track, options);
|
||||||
|
|
||||||
|
|
@ -59,4 +52,4 @@ setInterval(() => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
source.captureFrame(new VideoFrame(buf, WIDTH, HEIGHT, VideoBufferType.RGBA));
|
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.
|
# Minimal LiveKit Python SDK publisher used by OpenViduTestAppE2eServerSdkTest
|
||||||
# Joins the room of LIVEKIT_TOKEN and publishes a single video track with codec
|
# See ../README.md
|
||||||
# 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
|
|
||||||
import asyncio
|
import asyncio
|
||||||
import os
|
import os
|
||||||
|
|
||||||
from livekit import rtc
|
from livekit import rtc
|
||||||
|
|
||||||
MULTI_LAYER = os.environ.get("VIDEO_LAYERS") == "multi"
|
LAYERS = int(os.environ.get("VIDEO_LAYERS", "1"))
|
||||||
WIDTH = 640
|
# The SDK only splits a simulcast source in three layers from 960 px wide
|
||||||
HEIGHT = 480
|
WIDTH, HEIGHT = (1280, 720) if LAYERS == 3 else (640, 480)
|
||||||
|
|
||||||
|
|
||||||
async def main():
|
async def main():
|
||||||
codec = os.environ["VIDEO_CODEC"]
|
codec = os.environ["VIDEO_CODEC"]
|
||||||
|
|
||||||
room = rtc.Room()
|
room = rtc.Room()
|
||||||
await room.connect(
|
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)
|
source = rtc.VideoSource(WIDTH, HEIGHT)
|
||||||
|
|
@ -32,9 +26,9 @@ async def main():
|
||||||
source=rtc.TrackSource.SOURCE_CAMERA,
|
source=rtc.TrackSource.SOURCE_CAMERA,
|
||||||
)
|
)
|
||||||
if codec in ("vp8", "h264"):
|
if codec in ("vp8", "h264"):
|
||||||
options.simulcast = MULTI_LAYER
|
options.simulcast = LAYERS > 1
|
||||||
else:
|
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)
|
await room.local_participant.publish_track(track, options)
|
||||||
|
|
||||||
# The Java test waits for this exact log line before asserting
|
# The Java test waits for this exact log line before asserting
|
||||||
|
|
@ -45,7 +39,8 @@ async def main():
|
||||||
n = (n + 7) % 256
|
n = (n + 7) % 256
|
||||||
data = bytes((n, 255 - n, (n * 3) % 256, 255)) * (WIDTH * HEIGHT)
|
data = bytes((n, 255 - n, (n * 3) % 256, 255)) * (WIDTH * HEIGHT)
|
||||||
source.capture_frame(rtc.VideoFrame(WIDTH, HEIGHT, rtc.VideoBufferType.RGBA, data))
|
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__":
|
if __name__ == "__main__":
|
||||||
|
|
|
||||||
|
|
@ -464,9 +464,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "crossbeam-utils"
|
name = "crossbeam-utils"
|
||||||
version = "0.8.22"
|
version = "0.8.23"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "61803da095bee82a81bb1a452ecc25d3b2f1416d1897eb86430c6159ef717c17"
|
checksum = "a31eee39dddec8330830986fcd7625edb5a24ec90ea038215273bbc3adb08ac6"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "crypto-common"
|
name = "crypto-common"
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,5 @@
|
||||||
// Minimal LiveKit Rust SDK publisher used by OpenViduTestAppE2eTest.
|
// Minimal LiveKit Rust SDK publisher used by OpenViduTestAppE2eServerSdkTest
|
||||||
// Joins the room of LIVEKIT_TOKEN and publishes a single video track with codec
|
// See ../README.md
|
||||||
// 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
|
|
||||||
use std::{env, time::Duration};
|
use std::{env, time::Duration};
|
||||||
|
|
||||||
use livekit::options::{TrackPublishOptions, VideoCodec};
|
use livekit::options::{TrackPublishOptions, VideoCodec};
|
||||||
|
|
@ -18,8 +11,9 @@ use livekit::{Room, RoomOptions};
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() {
|
async fn main() {
|
||||||
let codec = env::var("VIDEO_CODEC").unwrap();
|
let codec = env::var("VIDEO_CODEC").unwrap();
|
||||||
let multi_layer = env::var("VIDEO_LAYERS").map(|v| v == "multi").unwrap_or(false);
|
let layers: u32 = env::var("VIDEO_LAYERS").ok().and_then(|v| v.parse().ok()).unwrap_or(1);
|
||||||
let (width, height): (u32, u32) = (640, 480);
|
// 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 {
|
let mut options = TrackPublishOptions {
|
||||||
source: TrackSource::Camera,
|
source: TrackSource::Camera,
|
||||||
|
|
@ -33,15 +27,18 @@ async fn main() {
|
||||||
..Default::default()
|
..Default::default()
|
||||||
};
|
};
|
||||||
if codec == "vp8" || codec == "h264" {
|
if codec == "vp8" || codec == "h264" {
|
||||||
options.simulcast = multi_layer;
|
options.simulcast = layers > 1;
|
||||||
} else {
|
} 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(
|
let (room, mut _events) = Room::connect(
|
||||||
&env::var("LIVEKIT_URL").unwrap(),
|
&env::var("LIVEKIT_URL").unwrap(),
|
||||||
&env::var("LIVEKIT_TOKEN").unwrap(),
|
&env::var("LIVEKIT_TOKEN").unwrap(),
|
||||||
RoomOptions::default(),
|
room_options,
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
.expect("could not connect to room");
|
.expect("could not connect to room");
|
||||||
|
|
@ -73,7 +70,7 @@ async fn main() {
|
||||||
data_u.fill(255 - n);
|
data_u.fill(255 - n);
|
||||||
data_v.fill(n.wrapping_mul(3));
|
data_v.fill(n.wrapping_mul(3));
|
||||||
source.capture_frame(&frame);
|
source.capture_frame(&frame);
|
||||||
// 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
|
||||||
tokio::time::sleep(Duration::from_millis(if multi_layer { 66 } else { 33 })).await;
|
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-form-field id="max-video-quality" class="video-btn quality-option" matTooltip="Set video quality" matTooltipClass="custom-tooltip">
|
||||||
<mat-select [(value)]="maxVideoQuality" (selectionChange)="onQualityChange()">
|
<mat-select [(value)]="maxVideoQuality" (selectionChange)="onQualityChange()">
|
||||||
@for (q of ['LOW', 'MEDIUM', 'HIGH']; track q) {
|
@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-select>
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue