mirror of https://github.com/OpenVidu/openvidu.git
openvidu-test-e2e: download RTSP/SRT ingress media once per run
The RTSP and SRT tests made ffmpeg stream their source from S3 for the whole test, so any network problem broke a test (ffmpeg timed out connecting to S3 in CI). Each file is now downloaded once per test run into a temporary folder, retried with backoff and resumed from the bytes already received, and ffmpeg reads it from a read-only mount. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>dependabot/npm_and_yarn/openvidu-dashboard/minor-and-patch-ddfd6a8e82
parent
dfb7f6aa21
commit
153e11f65d
|
|
@ -3,6 +3,8 @@ package io.openvidu.test.e2e;
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.io.FileNotFoundException;
|
import java.io.FileNotFoundException;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.io.OutputStream;
|
||||||
import java.net.DatagramSocket;
|
import java.net.DatagramSocket;
|
||||||
import java.net.InetAddress;
|
import java.net.InetAddress;
|
||||||
import java.net.ServerSocket;
|
import java.net.ServerSocket;
|
||||||
|
|
@ -13,6 +15,8 @@ import java.net.UnknownHostException;
|
||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
import java.nio.file.Paths;
|
import java.nio.file.Paths;
|
||||||
|
import java.nio.file.StandardCopyOption;
|
||||||
|
import java.nio.file.StandardOpenOption;
|
||||||
import java.time.Duration;
|
import java.time.Duration;
|
||||||
import java.security.KeyManagementException;
|
import java.security.KeyManagementException;
|
||||||
import java.security.NoSuchAlgorithmException;
|
import java.security.NoSuchAlgorithmException;
|
||||||
|
|
@ -63,6 +67,7 @@ import io.openvidu.test.browsers.utils.Ffmpeg;
|
||||||
import livekit.LivekitIngress.IngressInfo;
|
import livekit.LivekitIngress.IngressInfo;
|
||||||
import livekit.LivekitModels.Room;
|
import livekit.LivekitModels.Room;
|
||||||
import okhttp3.OkHttpClient;
|
import okhttp3.OkHttpClient;
|
||||||
|
import okhttp3.Request;
|
||||||
import retrofit2.Response;
|
import retrofit2.Response;
|
||||||
|
|
||||||
public class OpenViduTestE2e {
|
public class OpenViduTestE2e {
|
||||||
|
|
@ -158,6 +163,16 @@ public class OpenViduTestE2e {
|
||||||
// cannot stop a bridged browser from being pinned.
|
// cannot stop a bridged browser from being pinned.
|
||||||
private static final Map<String, String> resolvedHostAddresses = new ConcurrentHashMap<>();
|
private static final Map<String, String> resolvedHostAddresses = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
|
// Media of the RTSP and SRT ingress tests. Each file is downloaded once per test
|
||||||
|
// execution, the first time a test needs it, into a temporary directory that is
|
||||||
|
// deleted when the JVM exits, and ffmpeg reads it from there: streaming it from S3
|
||||||
|
// during the test made any network problem break the stream (e.g. ffmpeg timing
|
||||||
|
// out connecting to S3 and the test failing on the RTSP server never starting).
|
||||||
|
private static final String INGRESS_MEDIA_URL = "https://s3.eu-west-1.amazonaws.com/public.openvidu.io/";
|
||||||
|
private static final int INGRESS_MEDIA_DOWNLOAD_ATTEMPTS = 6;
|
||||||
|
private static Path ingressMediaDir;
|
||||||
|
private static final Map<String, Path> ingressMediaFiles = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
protected static RoomServiceClient LK;
|
protected static RoomServiceClient LK;
|
||||||
protected static IngressServiceClient LK_INGRESS;
|
protected static IngressServiceClient LK_INGRESS;
|
||||||
|
|
||||||
|
|
@ -377,7 +392,7 @@ public class OpenViduTestE2e {
|
||||||
containers.add(rtspServerContainer);
|
containers.add(rtspServerContainer);
|
||||||
|
|
||||||
final String RTSP_PATH = "live";
|
final String RTSP_PATH = "live";
|
||||||
String fileUrl = getFileUrl(videoCodec != null, audioCodec != null, true);
|
String mediaFile = getMediaFile(videoCodec != null, audioCodec != null, true);
|
||||||
String codecs = getCodecs(videoCodec, audioCodec);
|
String codecs = getCodecs(videoCodec, audioCodec);
|
||||||
String rtspServerIp = "host.docker.internal";
|
String rtspServerIp = "host.docker.internal";
|
||||||
|
|
||||||
|
|
@ -386,7 +401,7 @@ public class OpenViduTestE2e {
|
||||||
// many times faster than realtime on fast runners: mediamtx discards most
|
// many times faster than realtime on fast runners: mediamtx discards most
|
||||||
// frames and, when ffmpeg finishes early and exits, terminates the reader
|
// frames and, when ffmpeg finishes early and exits, terminates the reader
|
||||||
// session, killing the ingress mid-test.
|
// session, killing the ingress mid-test.
|
||||||
String ffmpegCommand = "ffmpeg -re -i " + fileUrl + " " + codecs + " "
|
String ffmpegCommand = "ffmpeg -re -i " + mediaFile + " " + codecs + " "
|
||||||
+ " -async 50 -strict -2 -f rtsp -rtsp_transport tcp rtsp://" + rtspServerIp + ":" + rtspPort + "/"
|
+ " -async 50 -strict -2 -f rtsp -rtsp_transport tcp rtsp://" + rtspServerIp + ":" + rtspPort + "/"
|
||||||
+ RTSP_PATH;
|
+ RTSP_PATH;
|
||||||
|
|
||||||
|
|
@ -395,6 +410,7 @@ public class OpenViduTestE2e {
|
||||||
|
|
||||||
GenericContainer<?> ffmpegPublishContainer = new GenericContainer<>(DockerImageName.parse(RTSP_SERVER_IMAGE))
|
GenericContainer<?> ffmpegPublishContainer = new GenericContainer<>(DockerImageName.parse(RTSP_SERVER_IMAGE))
|
||||||
.withCreateContainerCmdModifier(cmd -> cmd.withName("ffmpeg-" + Math.random() * 100000))
|
.withCreateContainerCmdModifier(cmd -> cmd.withName("ffmpeg-" + Math.random() * 100000))
|
||||||
|
.withFileSystemBind(ingressMediaDir.toString(), "/media", BindMode.READ_ONLY)
|
||||||
.withEnv("MTX_PATHS_RTSP_RUNONINIT", ffmpegCommand)
|
.withEnv("MTX_PATHS_RTSP_RUNONINIT", ffmpegCommand)
|
||||||
.withExtraHost("host.docker.internal", "host-gateway")
|
.withExtraHost("host.docker.internal", "host-gateway")
|
||||||
.waitingFor(Wait.forLogMessage(".*Press \\[q\\] to stop.*", 1));
|
.waitingFor(Wait.forLogMessage(".*Press \\[q\\] to stop.*", 1));
|
||||||
|
|
@ -434,11 +450,11 @@ public class OpenViduTestE2e {
|
||||||
unusedRtspPort = socket.getLocalPort();
|
unusedRtspPort = socket.getLocalPort();
|
||||||
}
|
}
|
||||||
|
|
||||||
String fileUrl = getFileUrl(videoCodec != null, audioCodec != null, true);
|
String mediaFile = getMediaFile(videoCodec != null, audioCodec != null, true);
|
||||||
String codecs = getCodecs(videoCodec, audioCodec);
|
String codecs = getCodecs(videoCodec, audioCodec);
|
||||||
|
|
||||||
// -re: see startRtspServer.
|
// -re: see startRtspServer.
|
||||||
String ffmpegCommand = "ffmpeg -re -i " + fileUrl + " " + codecs + " -strict -2 -f mpegts srt://"
|
String ffmpegCommand = "ffmpeg -re -i " + mediaFile + " " + codecs + " -strict -2 -f mpegts srt://"
|
||||||
+ hostGatewayIp
|
+ hostGatewayIp
|
||||||
+ ":" + srtPort + "?mode=listener";
|
+ ":" + srtPort + "?mode=listener";
|
||||||
|
|
||||||
|
|
@ -452,8 +468,9 @@ public class OpenViduTestE2e {
|
||||||
Map.entry("MTX_RTSP", "yes"), Map.entry("MTX_WEBRTC", "no"), Map.entry("MTX_SRT", "no"),
|
Map.entry("MTX_RTSP", "yes"), Map.entry("MTX_WEBRTC", "no"), Map.entry("MTX_SRT", "no"),
|
||||||
Map.entry("MTX_RTMP", "no"), Map.entry("MTX_MOQ", "no"), Map.entry("MTX_API", "no"),
|
Map.entry("MTX_RTMP", "no"), Map.entry("MTX_MOQ", "no"), Map.entry("MTX_API", "no"),
|
||||||
Map.entry("MTX_PATHS_RTSP_RUNONINIT", ffmpegCommand)))
|
Map.entry("MTX_PATHS_RTSP_RUNONINIT", ffmpegCommand)))
|
||||||
|
.withFileSystemBind(ingressMediaDir.toString(), "/media", BindMode.READ_ONLY)
|
||||||
.withNetworkMode("host")
|
.withNetworkMode("host")
|
||||||
.waitingFor(Wait.forLogMessage(".*" + fileUrl + ".+", 1));
|
.waitingFor(Wait.forLogMessage(".*" + Pattern.quote(mediaFile) + ".+", 1));
|
||||||
|
|
||||||
srtServerContainer.start();
|
srtServerContainer.start();
|
||||||
containers.add(srtServerContainer);
|
containers.add(srtServerContainer);
|
||||||
|
|
@ -692,24 +709,112 @@ public class OpenViduTestE2e {
|
||||||
return " " + codecs + " ";
|
return " " + codecs + " ";
|
||||||
}
|
}
|
||||||
|
|
||||||
private String getFileUrl(boolean withVideo, boolean withAudio, boolean lossless) throws Exception {
|
/**
|
||||||
String fileUrl;
|
* @return the path of the media file inside a container that binds
|
||||||
|
* ingressMediaDir to /media
|
||||||
|
*/
|
||||||
|
private String getMediaFile(boolean withVideo, boolean withAudio, boolean lossless) throws Exception {
|
||||||
|
String fileName;
|
||||||
if (withAudio && withVideo) {
|
if (withAudio && withVideo) {
|
||||||
fileUrl = lossless
|
fileName = lossless ? "bbb_sunflower_640x360_30fps_normal_fastdecode.mkv"
|
||||||
? "https://s3.eu-west-1.amazonaws.com/public.openvidu.io/bbb_sunflower_640x360_30fps_normal_fastdecode.mkv"
|
: "bbb_sunflower_1080p_60fps_normal.mp4";
|
||||||
: "https://s3.eu-west-1.amazonaws.com/public.openvidu.io/bbb_sunflower_1080p_60fps_normal.mp4";
|
|
||||||
} else if (!withAudio && withVideo) {
|
} else if (!withAudio && withVideo) {
|
||||||
fileUrl = lossless
|
fileName = lossless ? "bbb_sunflower_640x360_30fps_normal_noaudio_fastdecode.mkv"
|
||||||
? "https://s3.eu-west-1.amazonaws.com/public.openvidu.io/bbb_sunflower_640x360_30fps_normal_noaudio_fastdecode.mkv"
|
: "bbb_sunflower_1080p_60fps_normal_noaudio.mp4";
|
||||||
: "https://s3.eu-west-1.amazonaws.com/public.openvidu.io/bbb_sunflower_1080p_60fps_normal_noaudio.mp4";
|
|
||||||
} else if (withAudio) {
|
} else if (withAudio) {
|
||||||
fileUrl = lossless
|
fileName = lossless ? "bbb_sunflower_1080p_60fps_normal_onlyaudio_fastdecode.flac"
|
||||||
? "https://s3.eu-west-1.amazonaws.com/public.openvidu.io/bbb_sunflower_1080p_60fps_normal_onlyaudio_fastdecode.flac"
|
: "bbb_sunflower_1080p_60fps_normal_onlyaudio.mp3";
|
||||||
: "https://s3.eu-west-1.amazonaws.com/public.openvidu.io/bbb_sunflower_1080p_60fps_normal_onlyaudio.mp3";
|
|
||||||
} else {
|
} else {
|
||||||
throw new Exception("Must have audio or video");
|
throw new Exception("Must have audio or video");
|
||||||
}
|
}
|
||||||
return fileUrl;
|
downloadIngressMedia(fileName);
|
||||||
|
return "/media/" + fileName;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Downloads an ingress media file into ingressMediaDir, once per test execution.
|
||||||
|
*/
|
||||||
|
private static synchronized Path downloadIngressMedia(String fileName) throws Exception {
|
||||||
|
Path file = ingressMediaFiles.get(fileName);
|
||||||
|
if (file != null) {
|
||||||
|
return file;
|
||||||
|
}
|
||||||
|
if (ingressMediaDir == null) {
|
||||||
|
Path dir = Files.createTempDirectory("ingress-media");
|
||||||
|
Runtime.getRuntime().addShutdownHook(new Thread(() -> FileUtils.deleteQuietly(dir.toFile())));
|
||||||
|
ingressMediaDir = dir;
|
||||||
|
}
|
||||||
|
file = ingressMediaDir.resolve(fileName);
|
||||||
|
downloadFile(INGRESS_MEDIA_URL + fileName, file, INGRESS_MEDIA_DOWNLOAD_ATTEMPTS);
|
||||||
|
ingressMediaFiles.put(fileName, file);
|
||||||
|
return file;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Downloads url into file. A failed attempt is retried with an increasing delay,
|
||||||
|
* resuming from the bytes already received (HTTP Range), and a connection that
|
||||||
|
* stops sending data for 60 s counts as failed. The file only gets its final name
|
||||||
|
* once its size matches the one announced by the server.
|
||||||
|
*/
|
||||||
|
private static void downloadFile(String url, Path file, int attempts) throws Exception {
|
||||||
|
Path part = file.resolveSibling(file.getFileName() + ".part");
|
||||||
|
OkHttpClient client = new OkHttpClient.Builder().connectTimeout(Duration.ofSeconds(20))
|
||||||
|
.readTimeout(Duration.ofSeconds(60)).build();
|
||||||
|
log.info("Downloading {} into {}", url, file.getParent());
|
||||||
|
long start = System.currentTimeMillis();
|
||||||
|
for (int attempt = 1;; attempt++) {
|
||||||
|
try {
|
||||||
|
long received = Files.exists(part) ? Files.size(part) : 0;
|
||||||
|
Request.Builder request = new Request.Builder().url(url);
|
||||||
|
if (received > 0) {
|
||||||
|
request.header("Range", "bytes=" + received + "-");
|
||||||
|
}
|
||||||
|
try (okhttp3.Response response = client.newCall(request.build()).execute()) {
|
||||||
|
long total;
|
||||||
|
boolean append;
|
||||||
|
if (response.code() == 206) {
|
||||||
|
// "bytes <first>-<last>/<total>"
|
||||||
|
String range = response.header("Content-Range", "");
|
||||||
|
if (!range.startsWith("bytes " + received + "-")) {
|
||||||
|
Files.deleteIfExists(part);
|
||||||
|
throw new IOException("unexpected Content-Range '" + range + "' resuming from byte " + received);
|
||||||
|
}
|
||||||
|
total = Long.parseLong(range.substring(range.indexOf('/') + 1));
|
||||||
|
append = true;
|
||||||
|
} else if (response.code() == 200) {
|
||||||
|
total = response.body().contentLength();
|
||||||
|
append = false;
|
||||||
|
} else {
|
||||||
|
throw new IOException("HTTP " + response.code());
|
||||||
|
}
|
||||||
|
try (InputStream in = response.body().byteStream();
|
||||||
|
OutputStream out = Files.newOutputStream(part, StandardOpenOption.CREATE,
|
||||||
|
StandardOpenOption.WRITE,
|
||||||
|
append ? StandardOpenOption.APPEND : StandardOpenOption.TRUNCATE_EXISTING)) {
|
||||||
|
in.transferTo(out);
|
||||||
|
}
|
||||||
|
long size = Files.size(part);
|
||||||
|
if (total >= 0 && size != total) {
|
||||||
|
throw new IOException("received " + size + " of " + total + " bytes");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Files.move(part, file, StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.ATOMIC_MOVE);
|
||||||
|
long seconds = Math.max(1, (System.currentTimeMillis() - start) / 1000);
|
||||||
|
log.info("Downloaded {} ({} MB) in {} s, {} attempt(s)", url, Files.size(file) / 1048576, seconds,
|
||||||
|
attempt);
|
||||||
|
return;
|
||||||
|
} catch (IOException e) {
|
||||||
|
long received = Files.exists(part) ? Files.size(part) : 0;
|
||||||
|
if (attempt == attempts) {
|
||||||
|
throw new IOException("Could not download " + url + " after " + attempt + " attempts ("
|
||||||
|
+ received / 1048576 + " MB received)", e);
|
||||||
|
}
|
||||||
|
long delaySeconds = Math.min(5L << (attempt - 1), 60);
|
||||||
|
log.warn("Download of {} failed (attempt {}/{}, {} MB received so far): {}. Retrying in {} s", url,
|
||||||
|
attempt, attempts, received / 1048576, e.toString(), delaySeconds);
|
||||||
|
Thread.sleep(delaySeconds * 1000);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected static void setUpLiveKitClient() throws NoSuchAlgorithmException {
|
protected static void setUpLiveKitClient() throws NoSuchAlgorithmException {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue