webrtc: add liveness check to WebRTC output using data channel

Previously, there was no way to detect if WebRTC clients were still
connected to the stream. This lead to the RTC streams being kept
open indefinitely when clients focibly closed the stream. This can
happen with Chrome, Edge, Safari, etc when the tab is closed or on
mobile devices when the screen is locked or browser closed.

This change adds a data channel to the stream and requires users to
respond to ping requests. While sending a frame the ping might be
sent to the client. If clients do not respond to these ping requests before
the timeout duration the stream is closed and the client is removed
from the server. This causes the client to reinitiate the connection.

The timeout defaults to 30 seconds.

The client needs to send `keepAlive: true` as part of webrtc request.
On the received `ping` send back the `pong` over data-channel.
This commit is contained in:
Joshua Piccari 2023-08-05 14:12:22 -07:00 committed by Kamil Trzciński
parent 9379ffde78
commit e4fe25fdb6
2 changed files with 61 additions and 0 deletions

View File

@ -73,6 +73,14 @@
sdpSemantics: 'unified-plan', sdpSemantics: 'unified-plan',
iceServers: request.iceServers iceServers: request.iceServers
}); });
pc.addEventListener('datachannel', function(e) {
const dc = e.channel;
dc.addEventListener('message', function(e) {
dc.send('pong');
});
});
pc.remote_pc_id = request.id; pc.remote_pc_id = request.id;
pc.addTransceiver('video', { direction: 'recvonly' }); pc.addTransceiver('video', { direction: 'recvonly' });
pc.addEventListener('track', function(evt) { pc.addEventListener('track', function(evt) {

View File

@ -37,6 +37,7 @@ using namespace std::chrono_literals;
class Client; class Client;
static pthread_t heartbeat_thread;
static webrtc_options_t *webrtc_options; static webrtc_options_t *webrtc_options;
static std::set<std::shared_ptr<Client> > webrtc_clients; static std::set<std::shared_ptr<Client> > webrtc_clients;
static std::mutex webrtc_clients_lock; static std::mutex webrtc_clients_lock;
@ -95,6 +96,8 @@ public:
} }
id = "rtc-" + id; id = "rtc-" + id;
name = strdup(id.c_str()); name = strdup(id.c_str());
dc = pc->createDataChannel("pingpong");
last_heartbeat_s = get_monotonic_time_us(NULL, NULL) / 1000 / 1000;
} }
~Client() ~Client()
@ -164,6 +167,7 @@ public:
char *name = NULL; char *name = NULL;
std::string id; std::string id;
std::shared_ptr<rtc::PeerConnection> pc; std::shared_ptr<rtc::PeerConnection> pc;
std::shared_ptr<rtc::DataChannel> dc;
std::shared_ptr<ClientTrackData> video; std::shared_ptr<ClientTrackData> video;
std::mutex lock; std::mutex lock;
std::condition_variable wait_for_complete; std::condition_variable wait_for_complete;
@ -171,6 +175,7 @@ public:
bool has_set_sdp_answer = false; bool has_set_sdp_answer = false;
bool had_key_frame = false; bool had_key_frame = false;
bool requested_key_frame = false; bool requested_key_frame = false;
uint64_t last_heartbeat_s;
}; };
std::shared_ptr<Client> webrtc_find_client(std::string id) std::shared_ptr<Client> webrtc_find_client(std::string id)
@ -260,6 +265,20 @@ static std::shared_ptr<Client> webrtc_peer_connection(rtc::Configuration config,
auto client = std::make_shared<Client>(pc); auto client = std::make_shared<Client>(pc);
auto wclient = std::weak_ptr(client); auto wclient = std::weak_ptr(client);
client->dc->onOpen([wclient]() {
if(auto client = wclient.lock()) {
LOG_DEBUG(client.get(), "data channel onOpen");
}
});
client->dc->onMessage([wclient](auto message) {
auto client = wclient.lock();
if(client && std::holds_alternative<rtc::string>(message)) {
LOG_DEBUG(client.get(), "data channel onMessage: %s", std::get<std::string>(message).c_str());
client->last_heartbeat_s = get_monotonic_time_us(NULL, NULL) / 1000 / 1000;
}
});
pc->onTrack([wclient](std::shared_ptr<rtc::Track> track) { pc->onTrack([wclient](std::shared_ptr<rtc::Track> track) {
if(auto client = wclient.lock()) { if(auto client = wclient.lock()) {
LOG_DEBUG(client.get(), "onTrack: %s", track->mid().c_str()); LOG_DEBUG(client.get(), "onTrack: %s", track->mid().c_str());
@ -471,6 +490,38 @@ static void http_webrtc_remote_candidate(http_worker_t *worker, FILE *stream, co
http_write_response(stream, "200 OK", "application/json", "{}", 0); http_write_response(stream, "200 OK", "application/json", "{}", 0);
} }
static void *heartbeat_checker(void *)
{
uint64_t disconnect = 10;
while (true) {
{
uint64_t now_s = get_monotonic_time_us(NULL, NULL) / 1000 / 1000;
std::unique_lock lk(webrtc_clients_lock);
auto it = webrtc_clients.begin();
while (it != webrtc_clients.end()) {
auto client = *it;
if (!client) {
continue;
}
uint64_t heartbeat_detla = now_s - client->last_heartbeat_s;
if (heartbeat_detla >= disconnect) {
LOG_INFO(client.get(), "No heartbeat from client, removing.");
it = webrtc_clients.erase(it);
} else {
if (heartbeat_detla > disconnect / 2) {
LOG_DEBUG(client.get(), "Checking if client still alive.");
client->dc->send("ping");
}
it++;
}
}
}
sleep(1);
}
return NULL;
}
extern "C" void http_webrtc_offer(http_worker_t *worker, FILE *stream) extern "C" void http_webrtc_offer(http_worker_t *worker, FILE *stream)
{ {
auto message = http_parse_json_body(worker, stream, webrtc_client_max_json_body); auto message = http_parse_json_body(worker, stream, webrtc_client_max_json_body);
@ -507,6 +558,8 @@ extern "C" int webrtc_server(webrtc_options_t *options)
buffer_lock_register_check_streaming(&video_lock, webrtc_h264_needs_buffer); buffer_lock_register_check_streaming(&video_lock, webrtc_h264_needs_buffer);
buffer_lock_register_notify_buffer(&video_lock, webrtc_h264_capture); buffer_lock_register_notify_buffer(&video_lock, webrtc_h264_capture);
pthread_create(&heartbeat_thread, NULL, heartbeat_checker, NULL);
options->running = true; options->running = true;
return 0; return 0;
} }