Program Listing for File webrtc_streamer_test.cpp#
↰ Return to documentation for file (Test/commit/webrtc_streamer_test.cpp)
#include "kataglyphis_test_support.h"
#include <gtest/gtest.h>
#include <algorithm>
#include <chrono>
#include <condition_variable>
#include <initializer_list>
#include <mutex>
#include <optional>
#include <string>
#include <string_view>
#include <utility>
#include <vector>
import kataglyphis.config_loader;
import kataglyphis.gstreamer_pipeline;
import kataglyphis.webrtc_streamer;
#include "gstreamer_test_support.h"
namespace webrtc = kataglyphis::webrtc;
namespace cfg = kataglyphis::config;
using kataglyphis::test::TempFile;
namespace {
// Port 1 refuses the connection at once, so a started stream fails fast and deterministically.
constexpr std::string_view kRefusingServer = "ws://127.0.0.1:1";
auto test_pattern_config() -> webrtc::StreamConfig
{
webrtc::StreamConfig config;
config.source = webrtc::VideoSource::TestPattern;
config.encoder = webrtc::VideoEncoder::VP8;
config.signalling_server_uri = std::string(kRefusingServer);
config.width = 64;
config.height = 48;
config.framerate = 15;
return config;
}
}// namespace
// Skips where neither webrtcsink nor webrtcbin exists, which WebRTCStreamer::initialize() demands.
#define KATAGLYPHIS_REQUIRE_WEBRTC() \
do { \
if (!webrtc::WebRTCStreamer::initialize()) { \
GTEST_SKIP() << "neither webrtcsink nor webrtcbin is installed here"; \
} \
} while (false)
TEST(WebRtcStreamConfig, CopiesEveryFieldFromTheSettings)
{
cfg::WebRTCConfig settings;
settings.signaling_server_url = "wss://signal.example/ws";
settings.video = { 1920, 1080, 60, 6000 };
settings.stream.source = "v4l2";
settings.stream.encoder = "vp9";
settings.stream.device = "/dev/video3";
settings.stream.camera_id = "imx219";
settings.stream.input_path = "/clips/a.mp4";
settings.stream.input_uri = "rtsp://cam/stream";
settings.stream.peer_id = "peer";
settings.stream.producer_id = "producer";
settings.stun_servers = { "stun:a", "stun:b" };
settings.turn_servers = { "turn:c" };
const auto config = webrtc::create_stream_config_from_webrtc_config(settings);
EXPECT_EQ(config.source, webrtc::VideoSource::V4L2);
EXPECT_EQ(config.encoder, webrtc::VideoEncoder::VP9);
EXPECT_EQ(config.signalling_server_uri, "wss://signal.example/ws");
EXPECT_EQ(config.width, 1920U);
EXPECT_EQ(config.height, 1080U);
EXPECT_EQ(config.framerate, 60U);
EXPECT_EQ(config.bitrate_kbps, 6000U);
EXPECT_EQ(config.v4l2_device, "/dev/video3");
EXPECT_EQ(config.camera_id, "imx219");
EXPECT_EQ(config.input_path, "/clips/a.mp4");
EXPECT_EQ(config.input_uri, "rtsp://cam/stream");
EXPECT_EQ(config.peer_id, "peer");
EXPECT_EQ(config.producer_id, "producer");
EXPECT_EQ(config.stun_servers, (std::vector<std::string>{ "stun:a", "stun:b" }));
EXPECT_EQ(config.turn_servers, (std::vector<std::string>{ "turn:c" }));
}
TEST(WebRtcStreamConfig, SourceNamesMapToTheirSources)
{
const std::initializer_list<std::pair<std::string_view, webrtc::VideoSource>> names{
{ "libcamera", webrtc::VideoSource::Libcamera },
{ "v4l2", webrtc::VideoSource::V4L2 },
{ "test", webrtc::VideoSource::TestPattern },
{ "file", webrtc::VideoSource::File },
{ "uri", webrtc::VideoSource::Uri },
};
for (const auto &[name, source] : names) {
cfg::WebRTCConfig settings;
settings.stream.source = std::string(name);
EXPECT_EQ(webrtc::create_stream_config_from_webrtc_config(settings, webrtc::VideoSource::File).source, source)
<< name;
}
}
TEST(WebRtcStreamConfig, EncoderNamesMapToTheirEncoders)
{
const std::initializer_list<std::pair<std::string_view, webrtc::VideoEncoder>> names{
{ "h264-hw", webrtc::VideoEncoder::H264_Hardware },
{ "h264-sw", webrtc::VideoEncoder::H264_Software },
{ "vp8", webrtc::VideoEncoder::VP8 },
{ "vp9", webrtc::VideoEncoder::VP9 },
};
for (const auto &[name, encoder] : names) {
cfg::WebRTCConfig settings;
settings.stream.encoder = std::string(name);
const auto config = webrtc::create_stream_config_from_webrtc_config(
settings, webrtc::VideoSource::Libcamera, webrtc::VideoEncoder::VP8);
EXPECT_EQ(config.encoder, encoder) << name;
}
}
TEST(WebRtcStreamConfig, UnknownNamesFallBackToTheArguments)
{
cfg::WebRTCConfig settings;
settings.stream.source = "webcam";
settings.stream.encoder = "h265";
const auto config = webrtc::create_stream_config_from_webrtc_config(
settings, webrtc::VideoSource::TestPattern, webrtc::VideoEncoder::VP9);
EXPECT_EQ(config.source, webrtc::VideoSource::TestPattern);
EXPECT_EQ(config.encoder, webrtc::VideoEncoder::VP9);
}
TEST(WebRtcStreamConfig, OverrideModeIgnoresTheSettingsNames)
{
cfg::WebRTCConfig settings;
settings.stream.source = "v4l2";
settings.stream.encoder = "vp9";
const auto config = webrtc::create_stream_config_from_webrtc_config(settings,
webrtc::VideoSource::Uri,
webrtc::VideoEncoder::H264_Software,
webrtc::ConfigOverrideMode::Override,
webrtc::ConfigOverrideMode::Override);
EXPECT_EQ(config.source, webrtc::VideoSource::Uri);
EXPECT_EQ(config.encoder, webrtc::VideoEncoder::H264_Software);
const auto mixed = webrtc::create_stream_config_from_webrtc_config(settings,
webrtc::VideoSource::Uri,
webrtc::VideoEncoder::H264_Software,
webrtc::ConfigOverrideMode::UseConfig,
webrtc::ConfigOverrideMode::Override);
EXPECT_EQ(mixed.source, webrtc::VideoSource::V4L2);
EXPECT_EQ(mixed.encoder, webrtc::VideoEncoder::H264_Software);
}
TEST(WebRtcStreamer, AnUnconfiguredStreamerRefusesToRun)
{
webrtc::WebRTCStreamer streamer;
EXPECT_EQ(streamer.get_state(), webrtc::StreamState::Idle);
EXPECT_FALSE(streamer.is_streaming());
EXPECT_TRUE(streamer.get_producer_id().empty());
EXPECT_EQ(kataglyphis::test::error_of(streamer.start()), webrtc::WebRTCError::PipelineCreationFailed);
EXPECT_EQ(kataglyphis::test::error_of(streamer.pause()), webrtc::WebRTCError::PipelineCreationFailed);
EXPECT_EQ(kataglyphis::test::error_of(streamer.resume()), webrtc::WebRTCError::PipelineCreationFailed);
EXPECT_TRUE(streamer.stop().has_value());
EXPECT_TRUE(streamer.set_bitrate(4000).has_value());
}
TEST(WebRtcStreamer, ConfigureRejectsIncompleteSettings)
{
KATAGLYPHIS_REQUIRE_WEBRTC();
auto zero_width = test_pattern_config();
zero_width.width = 0;
auto zero_height = test_pattern_config();
zero_height.height = 0;
auto zero_rate = test_pattern_config();
zero_rate.framerate = 0;
auto no_server = test_pattern_config();
no_server.signalling_server_uri.clear();
auto v4l2_without_device = test_pattern_config();
v4l2_without_device.source = webrtc::VideoSource::V4L2;
v4l2_without_device.v4l2_device.clear();
auto file_without_path = test_pattern_config();
file_without_path.source = webrtc::VideoSource::File;
auto uri_without_uri = test_pattern_config();
uri_without_uri.source = webrtc::VideoSource::Uri;
for (const auto &config :
{ zero_width, zero_height, zero_rate, no_server, v4l2_without_device, file_without_path, uri_without_uri }) {
webrtc::WebRTCStreamer streamer;
EXPECT_EQ(kataglyphis::test::error_of(streamer.configure(config)), webrtc::WebRTCError::InvalidConfiguration);
EXPECT_EQ(kataglyphis::test::error_of(streamer.start()), webrtc::WebRTCError::PipelineCreationFailed);
}
}
TEST(WebRtcStreamer, ConfiguresATestPatternStream)
{
KATAGLYPHIS_REQUIRE_WEBRTC();
KATAGLYPHIS_REQUIRE_GSTREAMER_ELEMENTS("videotestsrc", "videoconvert", "webrtcsink");
auto config = test_pattern_config();
config.producer_id = "kataglyphis-test";
webrtc::WebRTCStreamer streamer;
ASSERT_TRUE(streamer.configure(config).has_value());
EXPECT_EQ(streamer.get_producer_id(), "kataglyphis-test");
EXPECT_EQ(streamer.get_state(), webrtc::StreamState::Idle);
// Configuring again replaces the pipeline and keeps the streamer usable.
config.producer_id.clear();
config.encoder = webrtc::VideoEncoder::H264_Software;
ASSERT_TRUE(streamer.configure(config).has_value());
EXPECT_TRUE(streamer.get_producer_id().starts_with("stream-"));
EXPECT_TRUE(streamer.stop().has_value());
}
TEST(WebRtcStreamer, ConfiguresAFileStream)
{
KATAGLYPHIS_REQUIRE_WEBRTC();
KATAGLYPHIS_REQUIRE_GSTREAMER_ELEMENTS(
"uridecodebin", "queue", "videoconvert", "videoscale", "videorate", "webrtcsink");
auto config = test_pattern_config();
config.source = webrtc::VideoSource::File;
// Configure builds the pipeline without opening the file, so a name with quotes and spaces is enough.
config.input_path = "clips/a \"quoted\" name.mp4";
webrtc::WebRTCStreamer streamer;
EXPECT_TRUE(streamer.configure(config).has_value());
}
TEST(WebRtcStreamer, AStartWithoutASignallingServerEndsInError)
{
KATAGLYPHIS_REQUIRE_WEBRTC();
KATAGLYPHIS_REQUIRE_GSTREAMER_ELEMENTS("videotestsrc", "videoconvert", "webrtcsink");
webrtc::WebRTCStreamer streamer;
std::mutex mutex;
std::condition_variable changed;
std::vector<webrtc::StreamState> states;
std::optional<webrtc::WebRTCError> reported;
streamer.set_state_callback([&](webrtc::StreamState, webrtc::StreamState next) {
{
std::scoped_lock lock(mutex);
states.push_back(next);
}
changed.notify_all();
});
streamer.set_error_callback([&](webrtc::WebRTCError error, const std::string &) {
{
std::scoped_lock lock(mutex);
reported = error;
}
changed.notify_all();
});
ASSERT_TRUE(streamer.configure(test_pattern_config()).has_value());
const auto started = streamer.start();
{
std::unique_lock lock(mutex);
// A PLAYING state change may still land after the error, so the history is checked, not the final state.
const bool failed = changed.wait_for(lock, kataglyphis::test::kStreamTimeout, [&] {
if (started.has_value()) { return reported.has_value(); }
return std::ranges::find(states, webrtc::StreamState::Error) != states.end();
});
EXPECT_TRUE(failed);
ASSERT_FALSE(states.empty());
EXPECT_EQ(states.front(), webrtc::StreamState::Connecting);
EXPECT_NE(std::ranges::find(states, webrtc::StreamState::Error), states.end());
// A failure after start() returned comes through the bus, and so through the error callback.
if (started.has_value()) { EXPECT_EQ(reported, webrtc::WebRTCError::MediaError); }
}
EXPECT_TRUE(streamer.stop().has_value());
EXPECT_EQ(streamer.get_state(), webrtc::StreamState::Idle);
}
TEST(WebRtcStreamer, FactoriesConfigureTheirSources)
{
KATAGLYPHIS_REQUIRE_WEBRTC();
KATAGLYPHIS_REQUIRE_GSTREAMER_ELEMENTS("videotestsrc", "videoconvert", "webrtcsink");
const auto test_stream = webrtc::create_test_webrtc_stream(std::string(kRefusingServer));
ASSERT_TRUE(test_stream.has_value());
EXPECT_TRUE(test_stream->get_producer_id().starts_with("stream-"));
EXPECT_EQ(test_stream->get_state(), webrtc::StreamState::Idle);
EXPECT_EQ(kataglyphis::test::error_of(webrtc::create_v4l2_webrtc_stream("", "")),
webrtc::WebRTCError::InvalidConfiguration);
EXPECT_EQ(kataglyphis::test::error_of(webrtc::create_libcamera_webrtc_stream(std::string(kRefusingServer), 0)),
webrtc::WebRTCError::InvalidConfiguration);
if (kataglyphis::test::missing_gstreamer_element({ "v4l2src" }).empty()) {
EXPECT_TRUE(
webrtc::create_v4l2_webrtc_stream(std::string(kRefusingServer), "/dev/video0", 64, 48, 15).has_value());
}
if (!kataglyphis::test::missing_gstreamer_element({ "libcamerasrc" }).empty()) {
EXPECT_EQ(
kataglyphis::test::error_of(webrtc::create_libcamera_webrtc_stream(std::string(kRefusingServer), 64, 48, 15)),
webrtc::WebRTCError::PipelineCreationFailed);
}
}
TEST(WebRtcStreamer, AConfigFileDrivesTheFactory)
{
KATAGLYPHIS_REQUIRE_WEBRTC();
KATAGLYPHIS_REQUIRE_GSTREAMER_ELEMENTS("videotestsrc", "videoconvert", "webrtcsink");
const TempFile settings("kataglyphis-webrtc-stream",
".json",
R"({"signalingServerUrl": "ws://127.0.0.1:1", "video": {"defaultWidth": 64, "defaultHeight": 48},
"stream": {"source": "test", "encoder": "vp8", "producerId": "from-file"}})");
const auto from_file = webrtc::create_webrtc_stream_from_config(settings.path());
ASSERT_TRUE(from_file.has_value());
EXPECT_EQ(from_file->get_producer_id(), "from-file");
// A missing file falls back to the defaults, here with the source forced to the test pattern.
const auto from_defaults = webrtc::create_webrtc_stream_from_config(kataglyphis::test::missing_path("w.json"),
webrtc::VideoSource::TestPattern,
webrtc::VideoEncoder::VP8,
webrtc::ConfigOverrideMode::Override,
webrtc::ConfigOverrideMode::Override);
ASSERT_TRUE(from_defaults.has_value());
EXPECT_TRUE(from_defaults->get_producer_id().starts_with("stream-"));
}