Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 9890848c6c | |||
| 73659ce0a0 | |||
| f8b992ca1d | |||
| 75eff0dd29 |
@@ -0,0 +1,15 @@
|
||||
# These are supported funding model platforms
|
||||
|
||||
github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
|
||||
patreon: stoolscc
|
||||
open_collective: # Replace with a single Open Collective username
|
||||
ko_fi: # Replace with a single Ko-fi username
|
||||
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
|
||||
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
|
||||
liberapay: # Replace with a single Liberapay username
|
||||
issuehunt: # Replace with a single IssueHunt username
|
||||
lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry
|
||||
polar: # Replace with a single Polar username
|
||||
buy_me_a_coffee: # Replace with a single Buy Me a Coffee username
|
||||
thanks_dev: # Replace with a single thanks.dev username
|
||||
custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
cmake_minimum_required(VERSION 3.16...3.28)
|
||||
|
||||
project(easy-irl-stream VERSION 1.1.0 LANGUAGES C CXX)
|
||||
project(easy-irl-stream VERSION 1.0.0 LANGUAGES C CXX)
|
||||
|
||||
set(CMAKE_C_STANDARD 11)
|
||||
set(CMAKE_C_STANDARD_REQUIRED ON)
|
||||
|
||||
@@ -17,6 +17,10 @@ OBS Studio plugin for IRL streamers. Receives an RTMP or SRT stream directly in
|
||||
For setup instructions, FAQ and downloads visit **[stools.cc/p/easy-irl-stream](https://stools.cc/p/easy-irl-stream)**.
|
||||
|
||||
|
||||
## AI-Powered Development
|
||||
|
||||
This project was built with the support of **Anthropic Claude Opus 4.6**. We embrace AI-assisted development — this is an AI-friendly project. The future is now.
|
||||
|
||||
## License
|
||||
|
||||
This project is licensed under the [GNU General Public License v2.0](LICENSE).
|
||||
|
||||
@@ -107,23 +107,6 @@ if (Test-Path $dll) {
|
||||
Write-Host ""
|
||||
Write-Host "BUILD SUCCESSFUL: easy-irl-stream.dll ($size KB)" -ForegroundColor Green
|
||||
Write-Host "Output: $dll"
|
||||
Write-Host ""
|
||||
|
||||
$install = Read-Host "Install to OBS? (y/n)"
|
||||
if ($install -eq "y") {
|
||||
$obsPluginDir = "C:\Program Files\obs-studio\obs-plugins\64bit"
|
||||
$obsDataDir = "C:\Program Files\obs-studio\data\obs-plugins\easy-irl-stream\locale"
|
||||
$curlDll = "$OBS_DEPS\bin\libcurl.dll"
|
||||
$script = @"
|
||||
Copy-Item '$dll' '$obsPluginDir\easy-irl-stream.dll' -Force
|
||||
New-Item -ItemType Directory -Force -Path '$obsDataDir' | Out-Null
|
||||
Copy-Item '$ROOT\data\locale\en-US.ini' '$obsDataDir\en-US.ini' -Force
|
||||
Copy-Item '$ROOT\data\locale\de-DE.ini' '$obsDataDir\de-DE.ini' -Force
|
||||
if (Test-Path '$curlDll') { Copy-Item '$curlDll' '$obsPluginDir\libcurl.dll' -Force }
|
||||
"@
|
||||
Start-Process powershell -Verb RunAs -ArgumentList "-NoProfile -Command $script" -Wait
|
||||
Write-Host "Installed." -ForegroundColor Green
|
||||
}
|
||||
} else {
|
||||
Write-Host "BUILD FAILED" -ForegroundColor Red
|
||||
exit 1
|
||||
|
||||
+35
-8
@@ -1,5 +1,24 @@
|
||||
#include "event-handler.h"
|
||||
#include "webhook.h"
|
||||
#include <util/platform.h>
|
||||
|
||||
static struct webhook_event_data snapshot_event_data(struct irl_source_data *data)
|
||||
{
|
||||
struct webhook_event_data ed = {0};
|
||||
ed.bitrate_kbps = data->current_bitrate_kbps;
|
||||
ed.video_width = data->stats_video_width;
|
||||
ed.video_height = data->stats_video_height;
|
||||
ed.video_codec = data->stats_video_codec[0]
|
||||
? data->stats_video_codec
|
||||
: NULL;
|
||||
|
||||
uint64_t conn_ns = data->stats_connect_time_ns;
|
||||
if (conn_ns > 0) {
|
||||
uint64_t now = os_gettime_ns();
|
||||
ed.uptime_sec = (int64_t)((now - conn_ns) / 1000000000ULL);
|
||||
}
|
||||
return ed;
|
||||
}
|
||||
|
||||
/* ---- queued tasks executed on the UI thread ---- */
|
||||
|
||||
@@ -118,8 +137,10 @@ static void fire_low_quality_actions(struct irl_source_data *data)
|
||||
queue_scene_switch(scene);
|
||||
queue_overlay(overlay, true);
|
||||
|
||||
if (webhook && webhook[0])
|
||||
webhook_send_async(webhook, "low_quality", src_copy);
|
||||
if (webhook && webhook[0]) {
|
||||
struct webhook_event_data ed = snapshot_event_data(data);
|
||||
webhook_send_async(webhook, "low_quality", src_copy, &ed);
|
||||
}
|
||||
if (cmd && cmd[0])
|
||||
webhook_execute_command_async(cmd);
|
||||
|
||||
@@ -153,8 +174,10 @@ static void fire_quality_recovered_actions(struct irl_source_data *data)
|
||||
queue_scene_switch(scene);
|
||||
queue_overlay(overlay, false);
|
||||
|
||||
if (webhook && webhook[0])
|
||||
webhook_send_async(webhook, "quality_recovered", src_copy);
|
||||
if (webhook && webhook[0]) {
|
||||
struct webhook_event_data ed = snapshot_event_data(data);
|
||||
webhook_send_async(webhook, "quality_recovered", src_copy, &ed);
|
||||
}
|
||||
|
||||
bfree(scene);
|
||||
bfree(overlay);
|
||||
@@ -193,8 +216,10 @@ static void fire_disconnect_actions(struct irl_source_data *data)
|
||||
queue_overlay(overlay, true);
|
||||
queue_recording(rec_action);
|
||||
|
||||
if (webhook && webhook[0])
|
||||
webhook_send_async(webhook, "disconnect", src_copy);
|
||||
if (webhook && webhook[0]) {
|
||||
struct webhook_event_data ed = snapshot_event_data(data);
|
||||
webhook_send_async(webhook, "disconnect", src_copy, &ed);
|
||||
}
|
||||
if (cmd && cmd[0])
|
||||
webhook_execute_command_async(cmd);
|
||||
|
||||
@@ -229,8 +254,10 @@ static void fire_reconnect_actions(struct irl_source_data *data)
|
||||
queue_scene_switch(scene);
|
||||
queue_overlay(overlay, false);
|
||||
|
||||
if (webhook && webhook[0])
|
||||
webhook_send_async(webhook, "reconnect", src_copy);
|
||||
if (webhook && webhook[0]) {
|
||||
struct webhook_event_data ed = snapshot_event_data(data);
|
||||
webhook_send_async(webhook, "reconnect", src_copy, &ed);
|
||||
}
|
||||
if (cmd && cmd[0])
|
||||
webhook_execute_command_async(cmd);
|
||||
|
||||
|
||||
@@ -47,6 +47,9 @@ static void build_url(struct irl_source_data *data, char *buf, size_t sz)
|
||||
}
|
||||
}
|
||||
|
||||
if (data->srt_streamid && data->srt_streamid[0])
|
||||
dstr_catf(&url, "&streamid=%s", data->srt_streamid);
|
||||
|
||||
snprintf(buf, sz, "%s", url.array);
|
||||
dstr_free(&url);
|
||||
}
|
||||
@@ -152,6 +155,8 @@ static void *ingest_thread_func(void *arg)
|
||||
data->stats_connect_time_ns = os_gettime_ns();
|
||||
data->stats_total_frames = 0;
|
||||
data->stats_total_bytes = 0;
|
||||
data->dec_vid_pkt_count = 0;
|
||||
data->dec_vid_frame_count = 0;
|
||||
event_handler_on_connect(data);
|
||||
|
||||
AVPacket *pkt = av_packet_alloc();
|
||||
|
||||
@@ -115,6 +115,10 @@ struct irl_source_data {
|
||||
char *webhook_url;
|
||||
char *custom_command;
|
||||
|
||||
/* Decoder debug counters (per-source) */
|
||||
int dec_vid_pkt_count;
|
||||
int dec_vid_frame_count;
|
||||
|
||||
/* Stats (written by ingest thread, read by UI) */
|
||||
char stats_video_codec[32];
|
||||
char stats_audio_codec[32];
|
||||
|
||||
+10
-11
@@ -304,32 +304,30 @@ static void output_audio_frame(struct irl_source_data *data, AVFrame *frame)
|
||||
|
||||
bool decoder_decode_packet(struct irl_source_data *data, AVPacket *pkt)
|
||||
{
|
||||
static int vid_pkt_count = 0;
|
||||
static int vid_frame_count = 0;
|
||||
|
||||
if (pkt->stream_index == data->video_stream_idx &&
|
||||
data->video_dec_ctx) {
|
||||
int send_ret =
|
||||
avcodec_send_packet(data->video_dec_ctx, pkt);
|
||||
vid_pkt_count++;
|
||||
data->dec_vid_pkt_count++;
|
||||
|
||||
if (send_ret < 0) {
|
||||
if (vid_pkt_count <= 5)
|
||||
if (data->dec_vid_pkt_count <= 5)
|
||||
blog(LOG_WARNING,
|
||||
"[%s] avcodec_send_packet failed: %d (pkt #%d, size=%d)",
|
||||
PLUGIN_NAME, send_ret,
|
||||
vid_pkt_count, pkt->size);
|
||||
data->dec_vid_pkt_count, pkt->size);
|
||||
return true;
|
||||
}
|
||||
|
||||
AVFrame *frame = av_frame_alloc();
|
||||
while (avcodec_receive_frame(data->video_dec_ctx, frame) == 0) {
|
||||
vid_frame_count++;
|
||||
if (vid_frame_count <= 3 ||
|
||||
(vid_frame_count % 300 == 0))
|
||||
data->dec_vid_frame_count++;
|
||||
if (data->dec_vid_frame_count <= 3 ||
|
||||
(data->dec_vid_frame_count % 300 == 0))
|
||||
blog(LOG_DEBUG,
|
||||
"[%s] Video frame #%d decoded (fmt=%d %dx%d)",
|
||||
PLUGIN_NAME, vid_frame_count,
|
||||
PLUGIN_NAME,
|
||||
data->dec_vid_frame_count,
|
||||
frame->format,
|
||||
frame->width, frame->height);
|
||||
output_video_frame(data, frame);
|
||||
@@ -338,7 +336,8 @@ bool decoder_decode_packet(struct irl_source_data *data, AVPacket *pkt)
|
||||
}
|
||||
av_frame_free(&frame);
|
||||
|
||||
if (vid_pkt_count == 30 && vid_frame_count == 0)
|
||||
if (data->dec_vid_pkt_count == 30 &&
|
||||
data->dec_vid_frame_count == 0)
|
||||
blog(LOG_WARNING,
|
||||
"[%s] 30 video packets sent but 0 frames decoded",
|
||||
PLUGIN_NAME);
|
||||
|
||||
+15
-1
@@ -82,7 +82,7 @@ private:
|
||||
bool m_de;
|
||||
QLabel *m_dot, *m_status;
|
||||
QLabel *m_lbls[4], *m_vals[4];
|
||||
QLabel *m_videoLine, *m_audioLine, *m_serverLine;
|
||||
QLabel *m_videoLine, *m_audioLine, *m_serverLine, *m_ipLine;
|
||||
QTimer *m_timer;
|
||||
int64_t m_prevFrames = 0;
|
||||
uint64_t m_prevTime = 0;
|
||||
@@ -181,9 +181,11 @@ private:
|
||||
m_videoLine = makeLabel("-", -1, false, "");
|
||||
m_audioLine = makeLabel("-", -1, false, "");
|
||||
m_serverLine = makeLabel("-", -1, false, dim);
|
||||
m_ipLine = makeLabel("-", -1, false, dim);
|
||||
root->addWidget(m_videoLine);
|
||||
root->addWidget(m_audioLine);
|
||||
root->addWidget(m_serverLine);
|
||||
root->addWidget(m_ipLine);
|
||||
|
||||
root->addStretch();
|
||||
}
|
||||
@@ -288,6 +290,17 @@ private:
|
||||
s += QString(" \u00b7 SRTLA \u2713 (:%1)")
|
||||
.arg(srtla_p);
|
||||
m_serverLine->setText(s);
|
||||
|
||||
QString ip_text;
|
||||
if (g_local_ip[0] && g_external_ip[0])
|
||||
ip_text = QString("LAN: %1 \u00b7 WAN: %2")
|
||||
.arg(g_local_ip)
|
||||
.arg(g_external_ip);
|
||||
else if (g_local_ip[0])
|
||||
ip_text = QString("LAN: %1").arg(g_local_ip);
|
||||
else
|
||||
ip_text = "-";
|
||||
m_ipLine->setText(ip_text);
|
||||
}
|
||||
|
||||
void setNoSource()
|
||||
@@ -301,6 +314,7 @@ private:
|
||||
m_videoLine->setText("-");
|
||||
m_audioLine->setText("-");
|
||||
m_serverLine->setText("-");
|
||||
m_ipLine->setText("-");
|
||||
m_fps = 0;
|
||||
m_prevFrames = 0;
|
||||
m_prevTime = 0;
|
||||
|
||||
+67
-134
@@ -6,183 +6,116 @@
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <time.h>
|
||||
|
||||
#ifdef _WIN32
|
||||
#include <winsock2.h>
|
||||
#include <ws2tcpip.h>
|
||||
typedef SOCKET sock_t;
|
||||
#define SOCK_INVALID INVALID_SOCKET
|
||||
#define sock_close closesocket
|
||||
#else
|
||||
#include <sys/socket.h>
|
||||
#include <netinet/in.h>
|
||||
#include <netdb.h>
|
||||
#include <unistd.h>
|
||||
typedef int sock_t;
|
||||
#define SOCK_INVALID (-1)
|
||||
#define sock_close close
|
||||
#endif
|
||||
#include <curl/curl.h>
|
||||
|
||||
struct webhook_args {
|
||||
char *url;
|
||||
char *event_name;
|
||||
char *source_name;
|
||||
char *json_body;
|
||||
};
|
||||
|
||||
struct cmd_args {
|
||||
char *command;
|
||||
};
|
||||
|
||||
static bool parse_url(const char *url, char *host, size_t host_sz,
|
||||
char *port, size_t port_sz, char *path, size_t path_sz)
|
||||
static size_t discard_response(void *ptr, size_t size, size_t nmemb,
|
||||
void *userdata)
|
||||
{
|
||||
const char *p = url;
|
||||
|
||||
if (strncmp(p, "http://", 7) == 0) {
|
||||
p += 7;
|
||||
snprintf(port, port_sz, "80");
|
||||
} else if (strncmp(p, "https://", 8) == 0) {
|
||||
p += 8;
|
||||
snprintf(port, port_sz, "443");
|
||||
} else {
|
||||
return false;
|
||||
(void)ptr;
|
||||
(void)userdata;
|
||||
return size * nmemb;
|
||||
}
|
||||
|
||||
const char *slash = strchr(p, '/');
|
||||
const char *colon = strchr(p, ':');
|
||||
|
||||
if (colon && (!slash || colon < slash)) {
|
||||
size_t hlen = (size_t)(colon - p);
|
||||
if (hlen >= host_sz)
|
||||
hlen = host_sz - 1;
|
||||
memcpy(host, p, hlen);
|
||||
host[hlen] = '\0';
|
||||
|
||||
colon++;
|
||||
const char *pend = slash ? slash : colon + strlen(colon);
|
||||
size_t plen = (size_t)(pend - colon);
|
||||
if (plen >= port_sz)
|
||||
plen = port_sz - 1;
|
||||
memcpy(port, colon, plen);
|
||||
port[plen] = '\0';
|
||||
} else {
|
||||
size_t hlen = slash ? (size_t)(slash - p)
|
||||
: strlen(p);
|
||||
if (hlen >= host_sz)
|
||||
hlen = host_sz - 1;
|
||||
memcpy(host, p, hlen);
|
||||
host[hlen] = '\0';
|
||||
}
|
||||
|
||||
if (slash)
|
||||
snprintf(path, path_sz, "%s", slash);
|
||||
else
|
||||
snprintf(path, path_sz, "/");
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
static void webhook_do_send(const char *url, const char *event_name,
|
||||
const char *source_name)
|
||||
static void webhook_do_send(const char *url, const char *json_body)
|
||||
{
|
||||
char host[256] = {0};
|
||||
char port_str[16] = {0};
|
||||
char path[512] = {0};
|
||||
|
||||
if (!parse_url(url, host, sizeof(host), port_str, sizeof(port_str),
|
||||
path, sizeof(path))) {
|
||||
blog(LOG_WARNING, "[%s] Webhook: invalid URL '%s'",
|
||||
"Easy IRL Stream", url);
|
||||
CURL *curl = curl_easy_init();
|
||||
if (!curl) {
|
||||
blog(LOG_WARNING, "[%s] Webhook: curl_easy_init failed",
|
||||
"Easy IRL Stream");
|
||||
return;
|
||||
}
|
||||
|
||||
#ifdef _WIN32
|
||||
WSADATA wsa;
|
||||
WSAStartup(MAKEWORD(2, 2), &wsa);
|
||||
#endif
|
||||
struct curl_slist *headers = NULL;
|
||||
headers = curl_slist_append(headers, "Content-Type: application/json");
|
||||
|
||||
struct addrinfo hints = {0};
|
||||
struct addrinfo *res = NULL;
|
||||
hints.ai_family = AF_INET;
|
||||
hints.ai_socktype = SOCK_STREAM;
|
||||
curl_easy_setopt(curl, CURLOPT_URL, url);
|
||||
curl_easy_setopt(curl, CURLOPT_POSTFIELDS, json_body);
|
||||
curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
|
||||
curl_easy_setopt(curl, CURLOPT_TIMEOUT, 10L);
|
||||
curl_easy_setopt(curl, CURLOPT_CONNECTTIMEOUT, 5L);
|
||||
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, discard_response);
|
||||
curl_easy_setopt(curl, CURLOPT_USERAGENT, "easy-irl-stream-webhook/1.0");
|
||||
|
||||
if (getaddrinfo(host, port_str, &hints, &res) != 0) {
|
||||
blog(LOG_WARNING, "[%s] Webhook: DNS lookup failed for '%s'",
|
||||
"Easy IRL Stream", host);
|
||||
return;
|
||||
CURLcode res = curl_easy_perform(curl);
|
||||
if (res != CURLE_OK) {
|
||||
blog(LOG_WARNING, "[%s] Webhook failed (%s): %s",
|
||||
"Easy IRL Stream", url, curl_easy_strerror(res));
|
||||
} else {
|
||||
long http_code = 0;
|
||||
curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &http_code);
|
||||
blog(LOG_DEBUG, "[%s] Webhook sent: %s (HTTP %ld)",
|
||||
"Easy IRL Stream", url, http_code);
|
||||
}
|
||||
|
||||
sock_t sock = socket(res->ai_family, res->ai_socktype,
|
||||
res->ai_protocol);
|
||||
if (sock == SOCK_INVALID) {
|
||||
freeaddrinfo(res);
|
||||
return;
|
||||
}
|
||||
|
||||
if (connect(sock, res->ai_addr, (int)res->ai_addrlen) != 0) {
|
||||
freeaddrinfo(res);
|
||||
sock_close(sock);
|
||||
return;
|
||||
}
|
||||
freeaddrinfo(res);
|
||||
|
||||
char body[1024];
|
||||
snprintf(body, sizeof(body),
|
||||
"{\"event\":\"%s\",\"source\":\"%s\",\"timestamp\":%lld}",
|
||||
event_name, source_name, (long long)time(NULL));
|
||||
|
||||
char request[2048];
|
||||
snprintf(request, sizeof(request),
|
||||
"POST %s HTTP/1.1\r\n"
|
||||
"Host: %s\r\n"
|
||||
"Content-Type: application/json\r\n"
|
||||
"Content-Length: %d\r\n"
|
||||
"Connection: close\r\n"
|
||||
"\r\n"
|
||||
"%s",
|
||||
path, host, (int)strlen(body), body);
|
||||
|
||||
send(sock, request, (int)strlen(request), 0);
|
||||
|
||||
char buf[512];
|
||||
while (recv(sock, buf, sizeof(buf), 0) > 0) {
|
||||
}
|
||||
|
||||
sock_close(sock);
|
||||
|
||||
blog(LOG_DEBUG, "[%s] Webhook sent: %s -> %s", "Easy IRL Stream",
|
||||
event_name, url);
|
||||
curl_slist_free_all(headers);
|
||||
curl_easy_cleanup(curl);
|
||||
}
|
||||
|
||||
static void *webhook_thread_func(void *arg)
|
||||
{
|
||||
struct webhook_args *wa = arg;
|
||||
webhook_do_send(wa->url, wa->event_name, wa->source_name);
|
||||
webhook_do_send(wa->url, wa->json_body);
|
||||
bfree(wa->url);
|
||||
bfree(wa->event_name);
|
||||
bfree(wa->source_name);
|
||||
bfree(wa->json_body);
|
||||
bfree(wa);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
static char *build_json_body(const char *event_name, const char *source_name,
|
||||
const struct webhook_event_data *extra)
|
||||
{
|
||||
char buf[1024];
|
||||
|
||||
if (extra) {
|
||||
snprintf(buf, sizeof(buf),
|
||||
"{\"event\":\"%s\",\"source\":\"%s\","
|
||||
"\"timestamp\":%lld,"
|
||||
"\"bitrate_kbps\":%lld,"
|
||||
"\"uptime_sec\":%lld,"
|
||||
"\"video_width\":%d,"
|
||||
"\"video_height\":%d,"
|
||||
"\"video_codec\":\"%s\"}",
|
||||
event_name, source_name, (long long)time(NULL),
|
||||
(long long)extra->bitrate_kbps,
|
||||
(long long)extra->uptime_sec,
|
||||
extra->video_width, extra->video_height,
|
||||
extra->video_codec ? extra->video_codec : "");
|
||||
} else {
|
||||
snprintf(buf, sizeof(buf),
|
||||
"{\"event\":\"%s\",\"source\":\"%s\","
|
||||
"\"timestamp\":%lld}",
|
||||
event_name, source_name, (long long)time(NULL));
|
||||
}
|
||||
|
||||
return bstrdup(buf);
|
||||
}
|
||||
|
||||
void webhook_send_async(const char *url, const char *event_name,
|
||||
const char *source_name)
|
||||
const char *source_name,
|
||||
const struct webhook_event_data *extra)
|
||||
{
|
||||
if (!url || !url[0])
|
||||
return;
|
||||
|
||||
struct webhook_args *wa = bzalloc(sizeof(*wa));
|
||||
wa->url = bstrdup(url);
|
||||
wa->event_name = bstrdup(event_name);
|
||||
wa->source_name = bstrdup(source_name);
|
||||
wa->json_body = build_json_body(event_name, source_name, extra);
|
||||
|
||||
pthread_t thread;
|
||||
if (pthread_create(&thread, NULL, webhook_thread_func, wa) == 0) {
|
||||
pthread_detach(thread);
|
||||
} else {
|
||||
bfree(wa->url);
|
||||
bfree(wa->event_name);
|
||||
bfree(wa->source_name);
|
||||
bfree(wa->json_body);
|
||||
bfree(wa);
|
||||
}
|
||||
}
|
||||
|
||||
+12
-1
@@ -1,6 +1,17 @@
|
||||
#pragma once
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
struct webhook_event_data {
|
||||
int64_t bitrate_kbps;
|
||||
int64_t uptime_sec;
|
||||
int video_width;
|
||||
int video_height;
|
||||
const char *video_codec;
|
||||
};
|
||||
|
||||
void webhook_send_async(const char *url, const char *event_name,
|
||||
const char *source_name);
|
||||
const char *source_name,
|
||||
const struct webhook_event_data *extra);
|
||||
|
||||
void webhook_execute_command_async(const char *command);
|
||||
|
||||
Reference in New Issue
Block a user