From 9890848c6cb1dd3fc3f0ba6fa95807e8c357b46c Mon Sep 17 00:00:00 2001 From: Nils <34674720+nils-kt@users.noreply.github.com> Date: Fri, 3 Apr 2026 14:46:56 +0200 Subject: [PATCH] Refactor build script and update project version; enhance webhook functionality - Removed OBS installation prompt from build.ps1. - Updated project version from 1.1.0 to 1.0.0 in CMakeLists.txt. - Added new webhook event data structure and enhanced webhook sending functionality in webhook.c. - Integrated video statistics tracking in event-handler.c and media-decoder.c. - Added IP address display in stats-dialog.cpp. - Improved URL handling and response management in webhook.c. --- CMakeLists.txt | 2 +- build.ps1 | 17 ---- src/event-handler.c | 43 +++++++-- src/ingest-thread.c | 5 ++ src/irl-source.h | 4 + src/media-decoder.c | 21 +++-- src/stats-dialog.cpp | 16 +++- src/webhook.c | 201 +++++++++++++++---------------------------- src/webhook.h | 13 ++- 9 files changed, 149 insertions(+), 173 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 85258b1..c9e7a38 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -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) diff --git a/build.ps1 b/build.ps1 index d9c471a..0191e5c 100644 --- a/build.ps1 +++ b/build.ps1 @@ -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 diff --git a/src/event-handler.c b/src/event-handler.c index 1afe3a6..9687fce 100644 --- a/src/event-handler.c +++ b/src/event-handler.c @@ -1,5 +1,24 @@ #include "event-handler.h" #include "webhook.h" +#include + +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); diff --git a/src/ingest-thread.c b/src/ingest-thread.c index f8fded0..9322914 100644 --- a/src/ingest-thread.c +++ b/src/ingest-thread.c @@ -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(); diff --git a/src/irl-source.h b/src/irl-source.h index 181d3c6..672fc83 100644 --- a/src/irl-source.h +++ b/src/irl-source.h @@ -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]; diff --git a/src/media-decoder.c b/src/media-decoder.c index eff5e51..083333e 100644 --- a/src/media-decoder.c +++ b/src/media-decoder.c @@ -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); diff --git a/src/stats-dialog.cpp b/src/stats-dialog.cpp index a01bebd..094c6cb 100644 --- a/src/stats-dialog.cpp +++ b/src/stats-dialog.cpp @@ -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; diff --git a/src/webhook.c b/src/webhook.c index 2e5dc21..203ae96 100644 --- a/src/webhook.c +++ b/src/webhook.c @@ -6,183 +6,116 @@ #include #include #include - -#ifdef _WIN32 -#include -#include -typedef SOCKET sock_t; -#define SOCK_INVALID INVALID_SOCKET -#define sock_close closesocket -#else -#include -#include -#include -#include -typedef int sock_t; -#define SOCK_INVALID (-1) -#define sock_close close -#endif +#include 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; - } - - 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; + (void)ptr; + (void)userdata; + return size * nmemb; } -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); } } diff --git a/src/webhook.h b/src/webhook.h index 453f419..165e787 100644 --- a/src/webhook.h +++ b/src/webhook.h @@ -1,6 +1,17 @@ #pragma once +#include + +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);