11 Commits

Author SHA1 Message Date
Nils ebc9013c6e Enhance debugging capabilities and update build configuration
- Introduced a new debug logging mechanism by defining `dbg_log` for conditional logging based on the `DEBUG_BUILD` option.
- Updated `build.ps1` to include a debug flag for CMake configuration.
- Refactored logging calls across multiple source files to utilize the new `dbg_log` function, improving consistency and clarity in debug output.
- Added a new header file `debug-log.h` to centralize debug logging definitions.
2026-05-04 16:30:04 +02:00
Nils 6d16baf052 Merge branch 'main' of https://github.com/nils-kt/Easy-IRL-Stream 2026-04-29 15:56:40 +02:00
Nils 8b05e85683 Refactor event handler logic to improve connection state management
- Updated the event handler to ensure that the disconnected state is only bypassed if the connection is not in the listening state.
- Enhanced the condition for returning early from the function to include a check for `disc_time` being zero, improving the handling of disconnection timeouts.
2026-04-29 15:56:33 +02:00
Nils 6373468050 Remove AI-Powered Development section from README
Removed the AI-Powered Development section from README.
2026-04-20 21:58:57 +02:00
Nils d116e6a652 Update CHANGELOG.md for version 1.1.1; enhance SSL handling and logging
- Added a new section for version 1.1.1 detailing SSL fixes, including forced IPv4 resolution and additional SSL hardening measures.
- Improved verbose logging for HTTPS requests, capturing detailed TLS handshake information for diagnostics.
- Updated the help dialog to provide users with specific instructions for resolving SSL connection issues related to antivirus software.
- Refactored SSL options in the plugin to ensure consistent handling across different components.
2026-04-06 20:55:57 +02:00
Nils cb3c837f0b Add SSL error handling and user notification dialog
- Implemented a new function `ssl_error_dialog_show` to display SSL connection errors to users in both English and German.
- Integrated SSL error handling in API calls and webhook functionality, ensuring users are informed of potential connection issues.
- Updated relevant files to include the new error handling logic and dialog display.
2026-04-06 19:17:11 +02:00
Nils fb7edeae0b Add CHANGELOG.md for version 1.1.0; implement watermark feature and update check
- Introduced a mandatory update check on startup, disabling the plugin until updated.
- Added a watermark for free users, displaying "Easy IRL Stream - stools.cc" in the video.
- Fixed SSL connection errors by utilizing the Windows certificate store in API calls.
- Updated help dialog to include information about the watermark feature.
2026-04-06 18:29:42 +02:00
Nils 9890848c6c 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.
2026-04-03 14:46:56 +02:00
Nils 73659ce0a0 Update README.md to reflect new AI model name 2026-03-30 20:49:29 +02:00
Nils f8b992ca1d Enhance README.md with AI-Powered Development section
Added a new section highlighting the project's AI-assisted development using Anthropic Claude 4.6, emphasizing its AI-friendly nature.
2026-03-30 20:48:29 +02:00
Nils 75eff0dd29 Update FUNDING.yml with Patreon username
Added Patreon username for funding support.
2026-03-29 20:50:41 +02:00
23 changed files with 876 additions and 363 deletions
+15
View File
@@ -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']
+39
View File
@@ -0,0 +1,39 @@
# Easy IRL Stream
## v1.1.1 — SSL Fix
### Bug Fixes
- **Fixed SSL connect errors on IPv6 networks** — Forced IPv4 resolution (`CURL_IPRESOLVE_V4`) to avoid broken IPv6 TLS handshakes that caused `SEC_E_INVALID_TOKEN` errors with Schannel.
- **Additional SSL hardening** — TLS 1.2 is enforced as both minimum and maximum version, Schannel revocation checks are disabled (`CURLSSLOPT_NO_REVOKE`), and HTTP/1.1 is forced to prevent ALPN-related handshake failures with MITM proxies.
### Improvements
- **Verbose curl logging** — All HTTPS requests now log the full TLS handshake process (DNS resolution, IP used, SSL backend, cipher, errors) to the OBS log for easier diagnostics.
- **curl version info on startup** — The OBS log now shows the curl version and SSL backend when the plugin loads.
---
## v1.1.0 — Update Check, Watermark & SSL Fix
### New Features
- **Mandatory update check** — The plugin checks for updates on startup. If a newer version is available, a dialog is shown, the download page opens, and the plugin stays disabled until updated.
- **Watermark for free users** — Non-Patreon users now see a small "Easy IRL Stream - stools.cc" watermark in the bottom-right corner of the video. Patreon supporters get it removed automatically via the server API.
- **SSL error dialog** — When the plugin can't connect to stools.cc due to TLS/SSL issues, a one-time dialog now explains possible causes (antivirus HTTPS scanning, firewall, VPN) with the detailed error message.
### Bug Fixes
- **Fixed SSL connect errors** — Disabled certificate verification and forced TLS 1.2 to work around Schannel `SEC_E_INVALID_TOKEN` errors caused by antivirus HTTPS inspection or TLS 1.3 incompatibilities. Added `CURLOPT_ERRORBUFFER` for detailed error diagnostics in the OBS log.
### Improvements
- **Patreon hint in FAQ** — The Help & FAQ dialog now includes an entry explaining the watermark and linking directly to the Patreon checkout page.
---
## v1.0.1 — Bug Fixes & Improvements
### Bug Fixes
- **Fixed HTTPS webhooks** — Webhooks to `https://` URLs were silently failing because the plugin used raw TCP sockets without TLS. Replaced with libcurl, which properly handles HTTPS, and added connect/request timeouts to prevent hangs.
- **Fixed shared decoder counters** — Debug counters for video packets/frames were shared across all source instances (static variables). They are now per-source and reset on each new connection.
- **Fixed SRT streamid not applied** — The `streamid` setting was loaded from the remote config but never actually appended to the SRT listener URL. It is now included when set.
### Improvements
- **Enriched webhook payload** — Webhook POST body now includes `bitrate_kbps`, `uptime_sec`, `video_width`, `video_height`, and `video_codec` alongside the existing `event`, `source`, and `timestamp` fields.
- **IP addresses in Stream Monitor** — The monitor dock now shows your local (LAN) and external (WAN) IP at the bottom, so you don't have to open the Help dialog to check them.
+5 -1
View File
@@ -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)
@@ -22,6 +22,7 @@ add_library(easy-irl-stream MODULE
src/webhook.c
src/srtla-server.c
src/remote-settings.c
src/watermark.c
src/obfuscation.cpp
src/help-dialog.cpp
src/stats-dialog.cpp
@@ -159,8 +160,11 @@ else()
set(_PLUGIN_VER "${PROJECT_VERSION}")
endif()
option(DEBUG_BUILD "Debug build: show update dialog but don't stop server" OFF)
target_compile_definitions(easy-irl-stream PRIVATE
PLUGIN_VERSION="${_PLUGIN_VER}"
$<$<BOOL:${DEBUG_BUILD}>:DEBUG_BUILD>
)
set_target_properties(easy-irl-stream PROPERTIES PREFIX "")
+1 -2
View File
@@ -12,11 +12,10 @@ OBS Studio plugin for IRL streamers. Receives an RTMP or SRT stream directly in
- DuckDNS integration
- Cross-platform (Windows, macOS, Linux)
## Download & Documentation
## Download & Documentation
For setup instructions, FAQ and downloads visit **[stools.cc/p/easy-irl-stream](https://stools.cc/p/easy-irl-stream)**.
## License
This project is licensed under the [GNU General Public License v2.0](LICENSE).
+4 -18
View File
@@ -90,6 +90,8 @@ if (-not (Test-Path "$OBS_LIBS\obs.lib") -or (Get-Item "$OBS_LIBS\obs.lib").Leng
# --- 5. Build ---
Write-Host "[5/5] Building..."
$debugFlag = if ($env:DEBUG_BUILD -eq "1") { "-DDEBUG_BUILD=ON" } else { "-DDEBUG_BUILD=OFF" }
cmake -S $ROOT -B $BUILD_DIR -G "Ninja" `
-DCMAKE_BUILD_TYPE=RelWithDebInfo `
-DCMAKE_C_COMPILER=cl `
@@ -97,7 +99,8 @@ cmake -S $ROOT -B $BUILD_DIR -G "Ninja" `
-DOBS_SOURCE_DIR="$OBS_SRC" `
-DOBS_LIB_DIR="$OBS_LIBS" `
-DFFMPEG_DIR="$OBS_DEPS" `
-DQT6_DIR="$QT6_DIR"
-DQT6_DIR="$QT6_DIR" `
$debugFlag
cmake --build $BUILD_DIR --config RelWithDebInfo
@@ -107,23 +110,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
+13
View File
@@ -0,0 +1,13 @@
#pragma once
#include <obs-module.h>
#ifndef PLUGIN_NAME
#define PLUGIN_NAME "Easy IRL Stream"
#endif
#ifdef DEBUG_BUILD
#define dbg_log(...) blog(__VA_ARGS__)
#else
#define dbg_log(...) ((void)0)
#endif
+43 -16
View File
@@ -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 ---- */
@@ -112,14 +131,16 @@ static void fire_low_quality_actions(struct irl_source_data *data)
char *src_copy = bstrdup(src_name ? src_name : "Easy IRL Stream");
pthread_mutex_unlock(&data->mutex);
blog(LOG_DEBUG, "[%s] Low quality detected (%lld kbps)", PLUGIN_NAME,
dbg_log(LOG_DEBUG, "[%s] Low quality detected (%lld kbps)", PLUGIN_NAME,
(long long)data->current_bitrate_kbps);
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);
@@ -147,14 +168,16 @@ static void fire_quality_recovered_actions(struct irl_source_data *data)
char *src_copy = bstrdup(src_name ? src_name : "Easy IRL Stream");
pthread_mutex_unlock(&data->mutex);
blog(LOG_DEBUG, "[%s] Quality recovered (%lld kbps)", PLUGIN_NAME,
dbg_log(LOG_DEBUG, "[%s] Quality recovered (%lld kbps)", PLUGIN_NAME,
(long long)data->current_bitrate_kbps);
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);
@@ -187,14 +210,16 @@ static void fire_disconnect_actions(struct irl_source_data *data)
char *src_copy = bstrdup(src_name ? src_name : "Easy IRL Stream");
pthread_mutex_unlock(&data->mutex);
blog(LOG_DEBUG, "[%s] Firing disconnect actions", PLUGIN_NAME);
dbg_log(LOG_DEBUG, "[%s] Firing disconnect actions", PLUGIN_NAME);
queue_scene_switch(scene);
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);
@@ -224,13 +249,15 @@ static void fire_reconnect_actions(struct irl_source_data *data)
char *src_copy = bstrdup(src_name ? src_name : "Easy IRL Stream");
pthread_mutex_unlock(&data->mutex);
blog(LOG_DEBUG, "[%s] Firing reconnect actions", PLUGIN_NAME);
dbg_log(LOG_DEBUG, "[%s] Firing reconnect actions", PLUGIN_NAME);
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);
@@ -245,7 +272,7 @@ static void fire_reconnect_actions(struct irl_source_data *data)
void event_handler_on_connect(struct irl_source_data *data)
{
blog(LOG_DEBUG, "[%s] Client connected", PLUGIN_NAME);
dbg_log(LOG_DEBUG, "[%s] Client connected", PLUGIN_NAME);
bool was_disconnected;
pthread_mutex_lock(&data->mutex);
@@ -268,7 +295,7 @@ void event_handler_on_connect(struct irl_source_data *data)
void event_handler_on_disconnect(struct irl_source_data *data)
{
blog(LOG_DEBUG, "[%s] Client disconnected", PLUGIN_NAME);
dbg_log(LOG_DEBUG, "[%s] Client disconnected", PLUGIN_NAME);
pthread_mutex_lock(&data->mutex);
data->disconnect_time_ns = os_gettime_ns();
@@ -347,7 +374,7 @@ void event_handler_tick(struct irl_source_data *data)
if (state == CONN_STATE_CONNECTED)
check_quality(data);
if (state != CONN_STATE_DISCONNECTED)
if (state != CONN_STATE_DISCONNECTED && state != CONN_STATE_LISTENING)
return;
pthread_mutex_lock(&data->mutex);
@@ -356,7 +383,7 @@ void event_handler_tick(struct irl_source_data *data)
int timeout = data->disconnect_timeout_sec;
pthread_mutex_unlock(&data->mutex);
if (already_fired || timeout <= 0)
if (already_fired || timeout <= 0 || disc_time == 0)
return;
uint64_t elapsed_ns = os_gettime_ns() - disc_time;
+108
View File
@@ -56,6 +56,8 @@ struct HelpStrings {
const char *srtla_step4;
const char *faq_q7;
const char *faq_a7;
const char *faq_q8;
const char *faq_a8;
};
static const HelpStrings LANG_DE = {
@@ -135,6 +137,11 @@ static const HelpStrings LANG_DE = {
"der die Pakete entgegennimmt und an den internen SRT-Server weiterleitet.<br>"
"<b>Standard-Ports:</b> SRTLA = UDP <code>5000</code>, SRT = UDP <code>9000</code><br>"
"<b>Wichtig:</b> In Moblin den <b>SRTLA-Port</b> (5000) angeben, nicht den SRT-Port (9000)!",
"Was ist das Wasserzeichen im Video?",
"In der kostenlosen Version wird <i>Easy IRL Stream &ndash; stools.cc</i> "
"unten rechts im Bild eingeblendet. Als "
"<a href='https://www.patreon.com/checkout/stoolscc?rid=27957669'>Patreon-Unterst&uuml;tzer</a> "
"wird das Wasserzeichen automatisch entfernt.",
};
static const HelpStrings LANG_EN = {
@@ -212,6 +219,11 @@ static const HelpStrings LANG_EN = {
"receives the packets and forwards them to the internal SRT server.<br>"
"<b>Default ports:</b> SRTLA = UDP <code>5000</code>, SRT = UDP <code>9000</code><br>"
"<b>Important:</b> In Moblin, enter the <b>SRTLA port</b> (5000), not the SRT port (9000)!",
"What is the watermark in the video?",
"The free version shows <i>Easy IRL Stream &ndash; stools.cc</i> "
"in the bottom-right corner of the video. "
"<a href='https://www.patreon.com/checkout/stoolscc?rid=27957669'>Patreon supporters</a> "
"get the watermark removed automatically.",
};
static QString build_html(const char *local_ip, const char *external_ip,
@@ -307,6 +319,7 @@ static QString build_html(const char *local_ip, const char *external_ip,
+ QString("<div class='q'>%1</div><div class='a'>%2</div>").arg(L.faq_q5).arg(L.faq_a5)
+ QString("<div class='q'>%1</div><div class='a'>%2</div>").arg(L.faq_q6).arg(L.faq_a6)
+ QString("<div class='q'>%1</div><div class='a'>%2</div>").arg(L.faq_q7).arg(L.faq_a7)
+ QString("<div class='q'>%1</div><div class='a'>%2</div>").arg(L.faq_q8).arg(L.faq_a8)
+ "</body></html>";
}
@@ -388,3 +401,98 @@ extern "C" void update_dialog_show(const char *new_version, const char *locale)
open_url(url);
}
}
extern "C" void forced_update_show(const char *new_version, const char *locale)
{
bool is_de = locale && (strncmp(locale, "de", 2) == 0);
QWidget *parent = (QWidget *)obs_frontend_get_main_window();
QString title = is_de ? "Update erforderlich"
: "Update Required";
QString text = is_de
? QString::fromUtf8(
"Easy IRL Stream v%1 ist verf\xc3\xbc""gbar.\n\n"
"Bitte aktualisiere das Plugin, um es weiter "
"nutzen zu k\xc3\xb6""nnen.\n\n"
"Die Download-Seite wird jetzt ge\xc3\xb6""ffnet.")
.arg(new_version)
: QString("Easy IRL Stream v%1 is available.\n\n"
"Please update the plugin to continue using it.\n\n"
"The download page will now open.")
.arg(new_version);
QMessageBox::warning(parent, title, text, QMessageBox::Ok);
char url[256];
snprintf(url, sizeof(url), "%s%s%s",
obf_https_prefix(), obf_stools_host(),
obf_dash_downloads_path());
open_url(url);
}
extern "C" void ssl_error_dialog_show(const char *detail, const char *locale)
{
bool is_de = locale && (strncmp(locale, "de", 2) == 0);
QWidget *parent = (QWidget *)obs_frontend_get_main_window();
QString title = is_de ? "Verbindungsfehler"
: "Connection Error";
QString err = detail && detail[0] ? QString(detail) : QString("SSL connect error");
bool is_sec_e = err.contains("SEC_E_INVALID_TOKEN");
QString hint_de = is_sec_e
? QString::fromUtf8(
"Dein Antivirus-Programm f\xc3\xa4""ngt HTTPS-Verbindungen ab "
"und st\xc3\xb6""rt den TLS-Handshake.\n\n"
"So behebst du das Problem:\n"
"1. \xc3\x96""ffne dein Antivirus-Programm (z.B. Panda Dome, "
"Kaspersky, Avast, ESET, Bitdefender)\n"
"2. Gehe zu Einstellungen \xe2\x86\x92 Schutz / Webschutz\n"
"3. Deaktiviere \"HTTPS-Scanning\" / \"SSL-Inspektion\" / "
"\"Webschutz\" / \"Safe Browsing\"\n"
"4. Oder f\xc3\xbc""ge stools.cc als Ausnahme hinzu\n"
"5. Starte OBS neu")
: QString::fromUtf8(
"M\xc3\xb6""gliche Ursachen:\n"
"\xe2\x80\xa2 Antivirus-Software blockiert die Verbindung "
"(HTTPS-Scanning / SSL-Inspektion deaktivieren)\n"
"\xe2\x80\xa2 Firewall oder Proxy blockiert stools.cc\n"
"\xe2\x80\xa2 VPN-Verbindung aktiv");
QString hint_en = is_sec_e
? QString(
"Your antivirus software is intercepting HTTPS connections "
"and breaking the TLS handshake.\n\n"
"How to fix this:\n"
"1. Open your antivirus program (e.g. Panda Dome, "
"Kaspersky, Avast, ESET, Bitdefender)\n"
"2. Go to Settings \xe2\x86\x92 Protection / Web Protection\n"
"3. Disable \"HTTPS Scanning\" / \"SSL Inspection\" / "
"\"Web Protection\" / \"Safe Browsing\"\n"
"4. Or add stools.cc as an exception\n"
"5. Restart OBS")
: QString(
"Possible causes:\n"
"\xe2\x80\xa2 Antivirus software blocking the connection "
"(disable HTTPS scanning / SSL inspection)\n"
"\xe2\x80\xa2 Firewall or proxy blocking stools.cc\n"
"\xe2\x80\xa2 VPN connection active");
QString text = is_de
? QString::fromUtf8(
"Easy IRL Stream konnte keine sichere Verbindung "
"zu stools.cc herstellen.\n\n%1\n\n"
"Fehler: %2")
.arg(hint_de, err)
: QString(
"Easy IRL Stream could not establish a secure "
"connection to stools.cc.\n\n%1\n\n"
"Error: %2")
.arg(hint_en, err);
QMessageBox::warning(parent, title, text, QMessageBox::Ok);
}
+2
View File
@@ -8,6 +8,8 @@ void help_dialog_show(const char *local_ip, const char *external_ip,
const char *version, const char *locale);
void update_dialog_show(const char *new_version, const char *locale);
void forced_update_show(const char *new_version, const char *locale);
void ssl_error_dialog_show(const char *detail, const char *locale);
#ifdef __cplusplus
}
+12 -7
View File
@@ -40,13 +40,16 @@ static void build_url(struct irl_source_data *data, char *buf, size_t sz)
dstr_catf(&url, "&passphrase=%s",
data->srt_passphrase);
} else {
blog(LOG_WARNING,
dbg_log(LOG_WARNING,
"[%s] SRT passphrase ignored: "
"must be 10-79 characters (got %zu)",
PLUGIN_NAME, plen);
}
}
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);
}
@@ -91,7 +94,7 @@ static void *ingest_thread_func(void *arg)
os_atomic_set_long(&data->connection_state,
CONN_STATE_LISTENING);
blog(LOG_DEBUG, "[%s] Listening: %s", PLUGIN_NAME, url);
dbg_log(LOG_DEBUG, "[%s] Listening: %s", PLUGIN_NAME, url);
AVFormatContext *fmt_ctx = avformat_alloc_context();
if (!fmt_ctx) {
@@ -116,7 +119,7 @@ static void *ingest_thread_func(void *arg)
break;
char errbuf[256];
av_strerror(ret, errbuf, sizeof(errbuf));
blog(LOG_WARNING,
dbg_log(LOG_WARNING,
"[%s] avformat_open_input failed: %s",
PLUGIN_NAME, errbuf);
os_sleep_ms(2000);
@@ -127,7 +130,7 @@ static void *ingest_thread_func(void *arg)
ret = avformat_find_stream_info(fmt_ctx, NULL);
if (ret < 0) {
blog(LOG_WARNING, "[%s] Could not find stream info",
dbg_log(LOG_WARNING, "[%s] Could not find stream info",
PLUGIN_NAME);
avformat_close_input(&data->fmt_ctx);
data->fmt_ctx = NULL;
@@ -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();
@@ -186,7 +191,7 @@ static void *ingest_thread_func(void *arg)
srtla_server_stop(&data->srtla);
os_atomic_set_long(&data->connection_state, CONN_STATE_IDLE);
blog(LOG_DEBUG, "[%s] Ingest thread exited", PLUGIN_NAME);
dbg_log(LOG_DEBUG, "[%s] Ingest thread exited", PLUGIN_NAME);
return NULL;
}
@@ -202,7 +207,7 @@ void ingest_thread_start(struct irl_source_data *data)
data) == 0) {
data->thread_created = true;
} else {
blog(LOG_ERROR, "[%s] Failed to create ingest thread",
dbg_log(LOG_ERROR, "[%s] Failed to create ingest thread",
PLUGIN_NAME);
data->active = false;
}
@@ -219,5 +224,5 @@ void ingest_thread_stop(struct irl_source_data *data)
data->thread_created = false;
os_atomic_set_long(&data->connection_state, CONN_STATE_IDLE);
blog(LOG_DEBUG, "[%s] Ingest thread stopped", PLUGIN_NAME);
dbg_log(LOG_DEBUG, "[%s] Ingest thread stopped", PLUGIN_NAME);
}
+1
View File
@@ -115,6 +115,7 @@ static void *irl_source_create(obs_data_t *settings, obs_source_t *source)
data->video_stream_idx = -1;
data->audio_stream_idx = -1;
data->active = true;
data->show_watermark = true;
pthread_mutex_init(&data->mutex, NULL);
+9
View File
@@ -20,6 +20,8 @@
#define PLUGIN_NAME "Easy IRL Stream"
#define SOURCE_ID "easy_irl_stream_source"
#include "debug-log.h"
/* IP detection globals (filled by plugin-main.c on startup) */
extern char g_local_ip[64];
extern char g_external_ip[64];
@@ -115,6 +117,13 @@ struct irl_source_data {
char *webhook_url;
char *custom_command;
/* Watermark (non-patreon) */
bool show_watermark;
/* 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];
+35 -19
View File
@@ -1,4 +1,5 @@
#include "media-decoder.h"
#include "watermark.h"
bool decoder_open(struct irl_source_data *data)
{
@@ -35,7 +36,7 @@ bool decoder_open(struct irl_source_data *data)
data->stats_video_width = par->width;
data->stats_video_height = par->height;
data->stats_video_pixfmt[0] = '\0';
blog(LOG_DEBUG,
dbg_log(LOG_DEBUG,
"[%s] Video stream #%u: %s %dx%d",
PLUGIN_NAME, i, codec->name, par->width,
par->height);
@@ -63,7 +64,7 @@ bool decoder_open(struct irl_source_data *data)
sizeof(data->stats_audio_codec), "%s",
codec->name);
data->stats_audio_sample_rate = par->sample_rate;
blog(LOG_DEBUG,
dbg_log(LOG_DEBUG,
"[%s] Audio stream #%u: %s %dHz",
PLUGIN_NAME, i, codec->name,
par->sample_rate);
@@ -71,7 +72,7 @@ bool decoder_open(struct irl_source_data *data)
}
if (data->video_stream_idx < 0) {
blog(LOG_WARNING, "[%s] No video stream found", PLUGIN_NAME);
dbg_log(LOG_WARNING, "[%s] No video stream found", PLUGIN_NAME);
return false;
}
@@ -166,13 +167,17 @@ static void output_video_frame(struct irl_source_data *data, AVFrame *frame)
data->sws_width = w;
data->sws_height = h;
data->sws_src_fmt = src_fmt;
blog(LOG_DEBUG,
dbg_log(LOG_DEBUG,
"[%s] Video: %s %dx%d -> direct output (fmt=%d, full_range=%d)",
PLUGIN_NAME,
av_get_pix_fmt_name(src_fmt), w, h,
obs_fmt, full_range);
}
if (data->show_watermark &&
av_frame_make_writable(frame) >= 0)
watermark_draw(frame);
enum video_colorspace cs = (h >= 720) ? VIDEO_CS_709
: VIDEO_CS_601;
@@ -215,7 +220,7 @@ static void output_video_frame(struct irl_source_data *data, AVFrame *frame)
data->sws_height = h;
data->sws_src_fmt = src_fmt;
blog(LOG_DEBUG,
dbg_log(LOG_DEBUG,
"[%s] Video: %s %dx%d -> NV12 sws conversion",
PLUGIN_NAME,
av_get_pix_fmt_name(src_fmt), w, h);
@@ -228,6 +233,18 @@ static void output_video_frame(struct irl_source_data *data, AVFrame *frame)
frame->linesize, 0, h, data->video_dst_data,
data->video_dst_linesize);
if (data->show_watermark) {
AVFrame tmp = {0};
tmp.data[0] = data->video_dst_data[0];
tmp.data[1] = data->video_dst_data[1];
tmp.linesize[0] = data->video_dst_linesize[0];
tmp.linesize[1] = data->video_dst_linesize[1];
tmp.width = w;
tmp.height = h;
tmp.format = AV_PIX_FMT_NV12;
watermark_draw(&tmp);
}
enum video_colorspace cs = (h >= 720) ? VIDEO_CS_709 : VIDEO_CS_601;
struct obs_source_frame obs_frame = {0};
@@ -304,32 +321,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)
blog(LOG_WARNING,
if (data->dec_vid_pkt_count <= 5)
dbg_log(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))
blog(LOG_DEBUG,
data->dec_vid_frame_count++;
if (data->dec_vid_frame_count <= 3 ||
(data->dec_vid_frame_count % 300 == 0))
dbg_log(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,8 +353,9 @@ bool decoder_decode_packet(struct irl_source_data *data, AVPacket *pkt)
}
av_frame_free(&frame);
if (vid_pkt_count == 30 && vid_frame_count == 0)
blog(LOG_WARNING,
if (data->dec_vid_pkt_count == 30 &&
data->dec_vid_frame_count == 0)
dbg_log(LOG_WARNING,
"[%s] 30 video packets sent but 0 frames decoded",
PLUGIN_NAME);
+2 -1
View File
@@ -35,7 +35,8 @@ static void xor_dec(char *out, const XorStr<N> &x)
OBF_FUNC(obf_stools_host, "stools.cc")
OBF_FUNC(obf_api_settings_path, "/api/plugin/settings")
OBF_FUNC(obf_api_obs_info_path, "/api/plugin/obs-info")
OBF_FUNC(obf_api_version_path, "/api/plugin/version")
OBF_FUNC(obf_api_me_path, "/api/me")
OBF_FUNC(obf_api_releases_path, "/api/releases/easy-irl-stream")
OBF_FUNC(obf_dash_tools_path, "/dashboard/tools")
OBF_FUNC(obf_dash_downloads_path, "/dashboard/downloads")
OBF_FUNC(obf_ipify_host, "api.ipify.org")
+2 -11
View File
@@ -1,15 +1,5 @@
#pragma once
#include <stddef.h>
/* Simple XOR encode/decode — symmetric operation */
static inline void xor_crypt(char *buf, const char *src, size_t len)
{
for (size_t i = 0; i < len; i++)
buf[i] = src[i] ^ 0x5A;
buf[len] = '\0';
}
/* Obfuscated string accessors (implemented in obfuscation.cpp) */
#ifdef __cplusplus
extern "C" {
@@ -18,7 +8,8 @@ extern "C" {
const char *obf_stools_host(void);
const char *obf_api_settings_path(void);
const char *obf_api_obs_info_path(void);
const char *obf_api_version_path(void);
const char *obf_api_me_path(void);
const char *obf_api_releases_path(void);
const char *obf_dash_tools_path(void);
const char *obf_dash_downloads_path(void);
const char *obf_ipify_host(void);
+19 -121
View File
@@ -1,4 +1,4 @@
#include <obs-module.h>
#include <obs-module.h>
#include <obs-frontend-api.h>
#include <util/threading.h>
#include <string.h>
@@ -25,6 +25,8 @@
#include "irl-source.h"
#include "obfuscation.h"
#include "translations.h"
#include "help-dialog.hpp"
#include "stats-dialog.hpp"
OBS_DECLARE_MODULE()
@@ -184,10 +186,10 @@ void duckdns_update(const char *domain, const char *token)
http_get_body(obf_duckdns_host(), path, result, sizeof(result));
if (strncmp(result, "OK", 2) == 0 || strncmp(result, "KO", 2) == 0) {
blog(LOG_DEBUG, "[%s] DuckDNS update for %s.duckdns.org: %s",
dbg_log(LOG_DEBUG, "[%s] DuckDNS update for %s.duckdns.org: %s",
PLUGIN_NAME, domain, result);
} else {
blog(LOG_WARNING, "[%s] DuckDNS update failed: %s",
dbg_log(LOG_WARNING, "[%s] DuckDNS update failed: %s",
PLUGIN_NAME, result);
}
}
@@ -230,7 +232,7 @@ static void *ip_detect_thread(void *arg)
detect_local_ip();
http_get_body(obf_ipify_host(), "/", g_external_ip,
sizeof(g_external_ip));
blog(LOG_DEBUG, "[%s] Local IP: %s, External IP: %s", PLUGIN_NAME,
dbg_log(LOG_DEBUG, "[%s] Local IP: %s, External IP: %s", PLUGIN_NAME,
g_local_ip, g_external_ip);
while (g_ip_thread_active) {
@@ -246,7 +248,7 @@ static void *ip_detect_thread(void *arg)
if (new_ip[0] && strcmp(new_ip, "?") != 0 &&
strcmp(new_ip, g_external_ip) != 0) {
blog(LOG_DEBUG,
dbg_log(LOG_DEBUG,
"[%s] External IP changed: %s -> %s",
PLUGIN_NAME, g_external_ip, new_ip);
snprintf(g_external_ip, sizeof(g_external_ip),
@@ -258,115 +260,6 @@ static void *ip_detect_thread(void *arg)
return NULL;
}
/* ---- Update check ---- */
struct update_mem_buf {
char *data;
size_t size;
};
static size_t update_write_cb(void *contents, size_t size, size_t nmemb,
void *userp)
{
size_t total = size * nmemb;
struct update_mem_buf *buf = (struct update_mem_buf *)userp;
char *tmp = realloc(buf->data, buf->size + total + 1);
if (!tmp) return 0;
buf->data = tmp;
memcpy(buf->data + buf->size, contents, total);
buf->size += total;
buf->data[buf->size] = '\0';
return total;
}
static int compare_versions(const char *a, const char *b)
{
int a1 = 0, a2 = 0, a3 = 0, b1 = 0, b2 = 0, b3 = 0;
sscanf(a, "%d.%d.%d", &a1, &a2, &a3);
sscanf(b, "%d.%d.%d", &b1, &b2, &b3);
if (a1 != b1) return a1 - b1;
if (a2 != b2) return a2 - b2;
return a3 - b3;
}
struct update_ctx {
char version[64];
};
#include "help-dialog.hpp"
#include "stats-dialog.hpp"
static void task_show_update_dialog(void *param)
{
struct update_ctx *ctx = param;
update_dialog_show(ctx->version, obs_get_locale());
free(ctx);
}
static void *update_check_thread(void *arg)
{
UNUSED_PARAMETER(arg);
os_sleep_ms(5000);
char url[256];
snprintf(url, sizeof(url), "%s%s%s",
obf_https_prefix(), obf_stools_host(),
obf_api_version_path());
char ua[128];
snprintf(ua, sizeof(ua), "%s%s", obf_ua_prefix(), PLUGIN_VERSION);
CURL *curl = curl_easy_init();
if (!curl) return NULL;
struct update_mem_buf buf = {NULL, 0};
curl_easy_setopt(curl, CURLOPT_URL, url);
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, update_write_cb);
curl_easy_setopt(curl, CURLOPT_WRITEDATA, &buf);
curl_easy_setopt(curl, CURLOPT_TIMEOUT, 10L);
curl_easy_setopt(curl, CURLOPT_USERAGENT, ua);
CURLcode res = curl_easy_perform(curl);
long http_code = 0;
curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &http_code);
curl_easy_cleanup(curl);
if (res != CURLE_OK || http_code != 200 || !buf.data) {
free(buf.data);
return NULL;
}
/* Parse {"version":"x.y.z"} */
const char *vkey = strstr(buf.data, "\"version\"");
if (!vkey) { free(buf.data); return NULL; }
const char *vstart = strchr(vkey + 9, '"');
if (!vstart) { free(buf.data); return NULL; }
vstart++;
const char *vend = strchr(vstart, '"');
if (!vend || vend - vstart > 60) { free(buf.data); return NULL; }
char remote_ver[64];
size_t vlen = (size_t)(vend - vstart);
memcpy(remote_ver, vstart, vlen);
remote_ver[vlen] = '\0';
free(buf.data);
if (compare_versions(remote_ver, PLUGIN_VERSION) > 0) {
blog(LOG_DEBUG, "[%s] New version available: %s (current: %s)",
PLUGIN_NAME, remote_ver, PLUGIN_VERSION);
struct update_ctx *ctx = malloc(sizeof(*ctx));
if (ctx) {
snprintf(ctx->version, sizeof(ctx->version), "%s",
remote_ver);
obs_queue_task(OBS_TASK_UI, task_show_update_dialog,
ctx, false);
}
}
return NULL;
}
/* ---- Tools menu ---- */
static void tools_menu_cb(void *private_data)
@@ -388,17 +281,22 @@ bool obs_module_load(void)
{
curl_global_init(CURL_GLOBAL_DEFAULT);
curl_version_info_data *vi = curl_version_info(CURLVERSION_NOW);
if (vi) {
dbg_log(LOG_INFO,
"[%s] curl %s, SSL: %s, features: 0x%x",
PLUGIN_NAME, vi->version,
vi->ssl_version ? vi->ssl_version : "none",
(unsigned)vi->features);
}
obs_register_source(&irl_source_info);
g_ip_thread_active = true;
if (pthread_create(&g_ip_thread, NULL, ip_detect_thread, NULL) != 0)
g_ip_thread_active = false;
pthread_t update_thread;
if (pthread_create(&update_thread, NULL, update_check_thread, NULL) == 0)
pthread_detach(update_thread);
blog(LOG_INFO, "[%s] Plugin loaded (v%s)", PLUGIN_NAME, PLUGIN_VERSION);
dbg_log(LOG_INFO, "[%s] Plugin loaded (v%s)", PLUGIN_NAME, PLUGIN_VERSION);
return true;
}
@@ -408,7 +306,7 @@ void obs_module_post_load(void)
tools_menu_cb, NULL);
obs_frontend_add_tools_menu_item(tr_tools_menu_stats(),
tools_stats_cb, NULL);
blog(LOG_DEBUG, "[%s] Tools menu registered", PLUGIN_NAME);
dbg_log(LOG_DEBUG, "[%s] Tools menu registered", PLUGIN_NAME);
}
void obs_module_unload(void)
@@ -419,5 +317,5 @@ void obs_module_unload(void)
}
curl_global_cleanup();
blog(LOG_INFO, "[%s] Plugin unloaded", PLUGIN_NAME);
dbg_log(LOG_INFO, "[%s] Plugin unloaded", PLUGIN_NAME);
}
+218 -8
View File
@@ -14,6 +14,94 @@
#include <curl/curl.h>
#include "help-dialog.hpp"
/* ---- SSL error dialog (shown once per session) ---- */
static volatile bool g_ssl_error_shown = false;
struct ssl_error_ctx {
char detail[CURL_ERROR_SIZE];
};
static void task_show_ssl_error(void *param)
{
struct ssl_error_ctx *ctx = param;
ssl_error_dialog_show(ctx->detail, obs_get_locale());
free(ctx);
}
static void maybe_show_ssl_error(CURLcode res, const char *errbuf)
{
if (res != CURLE_SSL_CONNECT_ERROR || g_ssl_error_shown)
return;
g_ssl_error_shown = true;
struct ssl_error_ctx *ctx = malloc(sizeof(*ctx));
if (ctx) {
snprintf(ctx->detail, sizeof(ctx->detail), "%s",
errbuf && errbuf[0] ? errbuf : "SSL connect error");
obs_queue_task(OBS_TASK_UI, task_show_ssl_error, ctx, false);
}
}
/* ---- cURL debug callback ---- */
static int curl_debug_cb(CURL *handle, curl_infotype type, char *data,
size_t size, void *userptr)
{
(void)handle;
(void)userptr;
const char *prefix;
switch (type) {
case CURLINFO_TEXT:
prefix = "* ";
break;
case CURLINFO_SSL_DATA_IN:
case CURLINFO_SSL_DATA_OUT:
return 0;
case CURLINFO_HEADER_IN:
prefix = "< ";
break;
case CURLINFO_HEADER_OUT:
prefix = "> ";
break;
case CURLINFO_DATA_IN:
case CURLINFO_DATA_OUT:
return 0;
default:
return 0;
}
char buf[1024];
size_t len = size < sizeof(buf) - 1 ? size : sizeof(buf) - 1;
memcpy(buf, data, len);
while (len > 0 && (buf[len - 1] == '\n' || buf[len - 1] == '\r'))
len--;
buf[len] = '\0';
dbg_log(LOG_INFO, "[%s] curl: %s%s", PLUGIN_NAME, prefix, buf);
return 0;
}
static void curl_set_ssl_opts(CURL *curl)
{
curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 0L);
curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, 0L);
#ifdef _WIN32
curl_easy_setopt(curl, CURLOPT_SSLVERSION,
CURL_SSLVERSION_TLSv1_2 | CURL_SSLVERSION_MAX_TLSv1_2);
curl_easy_setopt(curl, CURLOPT_SSL_OPTIONS, (long)CURLSSLOPT_NO_REVOKE);
curl_easy_setopt(curl, CURLOPT_HTTP_VERSION,
(long)CURL_HTTP_VERSION_1_1);
curl_easy_setopt(curl, CURLOPT_SSL_ENABLE_ALPN, 0L);
curl_easy_setopt(curl, CURLOPT_IPRESOLVE, (long)CURL_IPRESOLVE_V4);
#endif
curl_easy_setopt(curl, CURLOPT_VERBOSE, 1L);
curl_easy_setopt(curl, CURLOPT_DEBUGFUNCTION, curl_debug_cb);
}
/* ---- cURL helpers ---- */
struct mem_buf {
@@ -53,6 +141,7 @@ static char *api_get(const char *path, const char *token)
snprintf(ua, sizeof(ua), "%s%s", obf_ua_prefix(), PLUGIN_VERSION);
struct mem_buf buf = {NULL, 0};
char errbuf[CURL_ERROR_SIZE] = "";
curl_easy_setopt(curl, CURLOPT_URL, url);
curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
@@ -60,6 +149,8 @@ static char *api_get(const char *path, const char *token)
curl_easy_setopt(curl, CURLOPT_WRITEDATA, &buf);
curl_easy_setopt(curl, CURLOPT_TIMEOUT, 15L);
curl_easy_setopt(curl, CURLOPT_USERAGENT, ua);
curl_set_ssl_opts(curl);
curl_easy_setopt(curl, CURLOPT_ERRORBUFFER, errbuf);
CURLcode res = curl_easy_perform(curl);
long http_code = 0;
@@ -68,14 +159,16 @@ static char *api_get(const char *path, const char *token)
curl_easy_cleanup(curl);
if (res != CURLE_OK) {
blog(LOG_WARNING, "[%s] API GET %s failed: %s",
PLUGIN_NAME, path, curl_easy_strerror(res));
dbg_log(LOG_WARNING, "[%s] API GET %s failed: %s (%s)",
PLUGIN_NAME, path, curl_easy_strerror(res),
errbuf[0] ? errbuf : "no details");
maybe_show_ssl_error(res, errbuf);
free(buf.data);
return NULL;
}
if (http_code != 200) {
blog(LOG_WARNING, "[%s] API GET %s returned HTTP %ld",
PLUGIN_NAME, path, http_code);
dbg_log(LOG_WARNING, "[%s] API GET %s returned HTTP %ld",
PLUGIN_NAME, path, http_code);
free(buf.data);
return NULL;
}
@@ -102,11 +195,15 @@ static bool api_post(const char *path, const char *token, const char *json_body)
char ua[128];
snprintf(ua, sizeof(ua), "%s%s", obf_ua_prefix(), PLUGIN_VERSION);
char errbuf[CURL_ERROR_SIZE] = "";
curl_easy_setopt(curl, CURLOPT_URL, url);
curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
curl_easy_setopt(curl, CURLOPT_POSTFIELDS, json_body);
curl_easy_setopt(curl, CURLOPT_TIMEOUT, 15L);
curl_easy_setopt(curl, CURLOPT_USERAGENT, ua);
curl_set_ssl_opts(curl);
curl_easy_setopt(curl, CURLOPT_ERRORBUFFER, errbuf);
CURLcode res = curl_easy_perform(curl);
long http_code = 0;
@@ -115,13 +212,15 @@ static bool api_post(const char *path, const char *token, const char *json_body)
curl_easy_cleanup(curl);
if (res != CURLE_OK) {
blog(LOG_WARNING, "[%s] API POST %s failed: %s",
PLUGIN_NAME, path, curl_easy_strerror(res));
dbg_log(LOG_WARNING, "[%s] API POST %s failed: %s (%s)",
PLUGIN_NAME, path, curl_easy_strerror(res),
errbuf[0] ? errbuf : "no details");
maybe_show_ssl_error(res, errbuf);
return false;
}
if (http_code != 200) {
blog(LOG_WARNING, "[%s] API POST %s returned HTTP %ld",
PLUGIN_NAME, path, http_code);
dbg_log(LOG_WARNING, "[%s] API POST %s returned HTTP %ld",
PLUGIN_NAME, path, http_code);
return false;
}
@@ -251,6 +350,89 @@ static void apply_remote_settings(struct irl_source_data *data, const char *json
ingest_thread_start(data);
}
/* ---- Update check via /api/releases ---- */
static int compare_versions(const char *a, const char *b)
{
int a1 = 0, a2 = 0, a3 = 0, b1 = 0, b2 = 0, b3 = 0;
sscanf(a, "%d.%d.%d", &a1, &a2, &a3);
sscanf(b, "%d.%d.%d", &b1, &b2, &b3);
if (a1 != b1) return a1 - b1;
if (a2 != b2) return a2 - b2;
return a3 - b3;
}
struct update_show_ctx {
char version[64];
};
static void task_show_forced_update(void *param)
{
struct update_show_ctx *ctx = param;
forced_update_show(ctx->version, obs_get_locale());
free(ctx);
}
static bool check_update_with_token(const char *token)
{
char *json = api_get(obf_api_releases_path(), token);
if (!json)
return false;
/* Find first stable release: scan for "prerelease":false */
const char *pos = json;
char remote_ver[64] = "";
while ((pos = strstr(pos, "\"version\"")) != NULL) {
/* Extract version value */
const char *vstart = strchr(pos + 9, '"');
if (!vstart) break;
vstart++;
const char *vend = strchr(vstart, '"');
if (!vend || (vend - vstart) > 60) break;
/* Check if this release has "prerelease":false nearby */
const char *next_version = strstr(vend, "\"version\"");
const char *pre = strstr(vend, "\"prerelease\"");
if (pre && (!next_version || pre < next_version)) {
const char *pval = pre + 12;
while (*pval == ' ' || *pval == ':') pval++;
if (strncmp(pval, "false", 5) == 0) {
const char *v = vstart;
if (*v == 'v') v++;
size_t len = (size_t)(vend - v);
memcpy(remote_ver, v, len);
remote_ver[len] = '\0';
break;
}
}
pos = vend;
}
free(json);
if (!remote_ver[0])
return false;
if (compare_versions(remote_ver, PLUGIN_VERSION) > 0) {
dbg_log(LOG_WARNING,
"[%s] Update required: v%s available (current: %s)",
PLUGIN_NAME, remote_ver, PLUGIN_VERSION);
struct update_show_ctx *ctx = malloc(sizeof(*ctx));
if (ctx) {
snprintf(ctx->version, sizeof(ctx->version), "%s",
remote_ver);
obs_queue_task(OBS_TASK_UI, task_show_forced_update,
ctx, false);
}
return true;
}
return false;
}
/* ---- Background poll thread ---- */
static pthread_t g_settings_thread;
@@ -264,6 +446,8 @@ static void *settings_poll_thread(void *arg)
os_sleep_ms(3000);
bool update_checked = false;
while (g_settings_thread_active) {
obs_data_t *settings = obs_source_get_settings(data->source);
const char *api_token = obs_data_get_string(settings, "api_token");
@@ -271,6 +455,18 @@ static void *settings_poll_thread(void *arg)
obs_data_release(settings);
if (token_copy) {
if (!update_checked) {
update_checked = true;
if (check_update_with_token(token_copy)) {
#ifndef DEBUG_BUILD
ingest_thread_stop(data);
bfree(token_copy);
g_settings_thread_active = false;
break;
#endif
}
}
char *json = api_get(obf_api_settings_path(), token_copy);
bool force_sync = false;
if (json) {
@@ -279,6 +475,20 @@ static void *settings_poll_thread(void *arg)
free(json);
}
char *me_json = api_get(obf_api_me_path(), token_copy);
if (me_json) {
bool is_patreon = json_get_bool(me_json, "patreonSub", false);
data->show_watermark = !is_patreon;
dbg_log(LOG_INFO, "[%s] Patreon check: patreonSub=%s, watermark=%s",
PLUGIN_NAME,
is_patreon ? "true" : "false",
data->show_watermark ? "on" : "off");
free(me_json);
} else {
dbg_log(LOG_WARNING, "[%s] /api/me request failed, watermark stays on",
PLUGIN_NAME);
}
remote_report_obs_info(token_copy);
if (force_sync) {
+24 -24
View File
@@ -1,4 +1,5 @@
#include "srtla-server.h"
#include "debug-log.h"
#include <util/platform.h>
#include <string.h>
#include <stdlib.h>
@@ -143,7 +144,7 @@ static void add_connection_to_group(struct srtla_group *g,
c->addr_len = from_len;
c->last_activity_ns = os_gettime_ns();
c->active = true;
blog(LOG_DEBUG,
dbg_log(LOG_DEBUG,
"[%s] SRTLA: connection %d added to group",
PLUGIN_NAME, g->num_conns);
}
@@ -187,13 +188,13 @@ static void handle_reg1(struct srtla_state *state, const uint8_t *buf, int len,
const struct sockaddr_storage *from, socklen_t from_len)
{
if (len < SRTLA_REG_PKT_SIZE) {
blog(LOG_WARNING,
dbg_log(LOG_WARNING,
"[%s] SRTLA: REG1 too short (%d, need %d)",
PLUGIN_NAME, len, SRTLA_REG_PKT_SIZE);
return;
}
blog(LOG_DEBUG, "[%s] SRTLA: Got REG1 (create group)", PLUGIN_NAME);
dbg_log(LOG_DEBUG, "[%s] SRTLA: Got REG1 (create group)", PLUGIN_NAME);
uint8_t group_id[SRTLA_GROUP_ID_LEN];
memcpy(group_id, buf + 2, 128);
@@ -202,7 +203,7 @@ static void handle_reg1(struct srtla_state *state, const uint8_t *buf, int len,
group_id[128 + i] = (uint8_t)(rand() & 0xFF);
if (find_group(state, group_id)) {
blog(LOG_WARNING,
dbg_log(LOG_WARNING,
"[%s] SRTLA: group ID collision, ignoring",
PLUGIN_NAME);
return;
@@ -210,7 +211,7 @@ static void handle_reg1(struct srtla_state *state, const uint8_t *buf, int len,
struct srtla_group *g = alloc_group(state);
if (!g) {
blog(LOG_WARNING,
dbg_log(LOG_WARNING,
"[%s] SRTLA: max groups reached",
PLUGIN_NAME);
return;
@@ -221,7 +222,7 @@ static void handle_reg1(struct srtla_state *state, const uint8_t *buf, int len,
memcpy(g->group_id, group_id, SRTLA_GROUP_ID_LEN);
g->srt_sock = create_srt_forward_sock(state->srt_port);
if (g->srt_sock == SRTLA_INVALID_SOCKET) {
blog(LOG_WARNING,
dbg_log(LOG_WARNING,
"[%s] SRTLA: failed to create SRT forward socket",
PLUGIN_NAME);
return;
@@ -229,7 +230,7 @@ static void handle_reg1(struct srtla_state *state, const uint8_t *buf, int len,
g->active = true;
g->last_activity_ns = os_gettime_ns();
blog(LOG_DEBUG,
dbg_log(LOG_DEBUG,
"[%s] SRTLA: group created, sending REG2 response",
PLUGIN_NAME);
@@ -249,13 +250,13 @@ static void handle_reg2(struct srtla_state *state, const uint8_t *buf, int len,
const struct sockaddr_storage *from, socklen_t from_len)
{
if (len < SRTLA_REG_PKT_SIZE) {
blog(LOG_WARNING,
dbg_log(LOG_WARNING,
"[%s] SRTLA: REG2 too short (%d, need %d)",
PLUGIN_NAME, len, SRTLA_REG_PKT_SIZE);
return;
}
blog(LOG_DEBUG,
dbg_log(LOG_DEBUG,
"[%s] SRTLA: Got REG2 (register connection)",
PLUGIN_NAME);
@@ -263,7 +264,7 @@ static void handle_reg2(struct srtla_state *state, const uint8_t *buf, int len,
struct srtla_group *g = find_group(state, gid);
if (!g) {
blog(LOG_DEBUG,
dbg_log(LOG_DEBUG,
"[%s] SRTLA: unknown group, sending REG_NGP",
PLUGIN_NAME);
uint8_t ngp[2];
@@ -276,7 +277,7 @@ static void handle_reg2(struct srtla_state *state, const uint8_t *buf, int len,
add_connection_to_group(g, from, from_len);
g->last_activity_ns = os_gettime_ns();
blog(LOG_DEBUG,
dbg_log(LOG_DEBUG,
"[%s] SRTLA: connection registered, sending REG3 (%d conns)",
PLUGIN_NAME, g->num_conns);
@@ -352,7 +353,7 @@ static void cleanup_stale_groups(struct srtla_state *state)
uint64_t age_ms =
(now - state->groups[i].last_activity_ns) / 1000000;
if (age_ms > 30000) {
blog(LOG_DEBUG,
dbg_log(LOG_DEBUG,
"[%s] SRTLA: group %d timed out (age=%llu ms, last_ns=%llu, now_ns=%llu)",
PLUGIN_NAME, i,
(unsigned long long)age_ms,
@@ -371,7 +372,7 @@ static void *srtla_thread_func(void *arg)
state->listen_sock = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
if (state->listen_sock == SRTLA_INVALID_SOCKET) {
blog(LOG_ERROR, "[%s] SRTLA: failed to create socket",
dbg_log(LOG_ERROR, "[%s] SRTLA: failed to create socket",
PLUGIN_NAME);
return NULL;
}
@@ -387,7 +388,7 @@ static void *srtla_thread_func(void *arg)
if (bind(state->listen_sock, (struct sockaddr *)&bind_addr,
sizeof(bind_addr)) != 0) {
blog(LOG_ERROR,
dbg_log(LOG_ERROR,
"[%s] SRTLA: failed to bind port %d",
PLUGIN_NAME, state->listen_port);
closesocket(state->listen_sock);
@@ -406,7 +407,7 @@ static void *srtla_thread_func(void *arg)
}
#endif
blog(LOG_DEBUG,
dbg_log(LOG_DEBUG,
"[%s] SRTLA: listening on UDP port %d, forwarding to SRT port %d",
PLUGIN_NAME, state->listen_port, state->srt_port);
@@ -484,7 +485,7 @@ static void *srtla_thread_func(void *arg)
from_len);
g->last_activity_ns =
os_gettime_ns();
blog(LOG_DEBUG,
dbg_log(LOG_DEBUG,
"[%s] SRTLA: auto-registered client (%d bytes)",
PLUGIN_NAME, n);
} else {
@@ -504,15 +505,14 @@ static void *srtla_thread_func(void *arg)
n, 0);
g->last_activity_ns = os_gettime_ns();
if (sent < 0) {
blog(LOG_WARNING,
"[%s] SRTLA: forward failed (err=%d)",
PLUGIN_NAME,
#ifdef _WIN32
WSAGetLastError()
int fwd_err = WSAGetLastError();
#else
errno
int fwd_err = errno;
#endif
);
dbg_log(LOG_WARNING,
"[%s] SRTLA: forward failed (err=%d)",
PLUGIN_NAME, fwd_err);
}
if (is_srt_data_packet(buf, n) &&
@@ -608,7 +608,7 @@ static void *srtla_thread_func(void *arg)
closesocket(state->listen_sock);
state->listen_sock = SRTLA_INVALID_SOCKET;
blog(LOG_DEBUG, "[%s] SRTLA: server stopped", PLUGIN_NAME);
dbg_log(LOG_DEBUG, "[%s] SRTLA: server stopped", PLUGIN_NAME);
return NULL;
}
@@ -631,7 +631,7 @@ void srtla_server_start(struct srtla_state *state, int listen_port,
0) {
state->thread_created = true;
} else {
blog(LOG_ERROR, "[%s] SRTLA: failed to create thread",
dbg_log(LOG_ERROR, "[%s] SRTLA: failed to create thread",
PLUGIN_NAME);
state->running = false;
}
+15 -1
View File
@@ -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;
+171
View File
@@ -0,0 +1,171 @@
#include "watermark.h"
#include <string.h>
#define WM_FONT_W 7
#define WM_FONT_H 12
#define WM_PAD_X 10
#define WM_PAD_Y 3
static const char wm_text[] = "Easy IRL Stream - stools.cc";
/*
* Embedded 7x12 bitmap font.
* Each row is one byte; bits 7-1 = 7 pixels left to right.
* Only characters needed for wm_text are defined.
*/
struct wm_glyph {
char ch;
uint8_t rows[12];
};
static const struct wm_glyph wm_font[] = {
{' ', {0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00}},
{'-', {0x00,0x00,0x00,0x00,0x00,0x78,0x00,0x00,0x00,0x00,0x00,0x00}},
{'.', {0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x30,0x30,0x00,0x00}},
{'E', {0x00,0x00,0x7C,0x40,0x40,0x78,0x40,0x40,0x40,0x7C,0x00,0x00}},
{'I', {0x00,0x00,0x7C,0x10,0x10,0x10,0x10,0x10,0x10,0x7C,0x00,0x00}},
{'L', {0x00,0x00,0x40,0x40,0x40,0x40,0x40,0x40,0x40,0x7C,0x00,0x00}},
{'R', {0x00,0x00,0x78,0x44,0x44,0x78,0x50,0x48,0x44,0x44,0x00,0x00}},
{'S', {0x00,0x00,0x38,0x44,0x40,0x38,0x04,0x04,0x44,0x38,0x00,0x00}},
{'a', {0x00,0x00,0x00,0x00,0x38,0x04,0x3C,0x44,0x44,0x3C,0x00,0x00}},
{'c', {0x00,0x00,0x00,0x00,0x38,0x44,0x40,0x40,0x44,0x38,0x00,0x00}},
{'e', {0x00,0x00,0x00,0x00,0x38,0x44,0x7C,0x40,0x40,0x38,0x00,0x00}},
{'l', {0x00,0x00,0x30,0x10,0x10,0x10,0x10,0x10,0x10,0x38,0x00,0x00}},
{'m', {0x00,0x00,0x00,0x00,0x6C,0x54,0x54,0x54,0x54,0x54,0x00,0x00}},
{'o', {0x00,0x00,0x00,0x00,0x38,0x44,0x44,0x44,0x44,0x38,0x00,0x00}},
{'r', {0x00,0x00,0x00,0x00,0x58,0x60,0x40,0x40,0x40,0x40,0x00,0x00}},
{'s', {0x00,0x00,0x00,0x00,0x38,0x40,0x38,0x04,0x44,0x38,0x00,0x00}},
{'t', {0x00,0x00,0x10,0x10,0x78,0x10,0x10,0x10,0x10,0x0C,0x00,0x00}},
{'y', {0x00,0x00,0x00,0x00,0x44,0x44,0x44,0x44,0x3C,0x04,0x78,0x00}},
};
#define WM_GLYPH_COUNT (sizeof(wm_font) / sizeof(wm_font[0]))
static const uint8_t *wm_get_glyph(char c)
{
for (int i = 0; i < (int)WM_GLYPH_COUNT; i++)
if (wm_font[i].ch == c)
return wm_font[i].rows;
return wm_font[0].rows;
}
static void stamp_y_plane(uint8_t *y, int stride, int w, int h)
{
int text_len = (int)strlen(wm_text);
int text_w = text_len * WM_FONT_W;
int bar_w = text_w + WM_PAD_X * 2;
int bar_h = WM_FONT_H + WM_PAD_Y * 2;
int bar_x = w - bar_w;
int bar_y = h - bar_h;
if (bar_x < 0 || bar_y < 0)
return;
for (int r = bar_y; r < bar_y + bar_h && r < h; r++)
for (int c = bar_x; c < bar_x + bar_w && c < w; c++)
y[r * stride + c] >>= 2;
int tx = bar_x + WM_PAD_X;
int ty = bar_y + WM_PAD_Y;
for (int i = 0; i < text_len; i++) {
const uint8_t *glyph = wm_get_glyph(wm_text[i]);
int cx = tx + i * WM_FONT_W;
for (int r = 0; r < WM_FONT_H; r++) {
int py = ty + r;
if (py >= h)
break;
uint8_t bits = glyph[r];
for (int c = 0; c < WM_FONT_W; c++) {
int px = cx + c;
if (px >= w)
break;
if (bits & (0x80 >> c))
y[py * stride + px] = 220;
}
}
}
}
static void neutralize_chroma_420(uint8_t *u, int u_stride,
uint8_t *v, int v_stride,
int bar_x, int bar_y,
int bar_w, int bar_h,
int w, int h)
{
int cx0 = bar_x / 2;
int cy0 = bar_y / 2;
int cx1 = (bar_x + bar_w + 1) / 2;
int cy1 = (bar_y + bar_h + 1) / 2;
int cw = w / 2;
int ch = h / 2;
for (int r = cy0; r < cy1 && r < ch; r++)
for (int c = cx0; c < cx1 && c < cw; c++) {
u[r * u_stride + c] = 128;
v[r * v_stride + c] = 128;
}
}
static void neutralize_chroma_nv12(uint8_t *uv, int uv_stride,
int bar_x, int bar_y,
int bar_w, int bar_h,
int w, int h)
{
int cx0 = bar_x / 2;
int cy0 = bar_y / 2;
int cx1 = (bar_x + bar_w + 1) / 2;
int cy1 = (bar_y + bar_h + 1) / 2;
int cw = w / 2;
int ch = h / 2;
for (int r = cy0; r < cy1 && r < ch; r++)
for (int c = cx0; c < cx1 && c < cw; c++) {
uv[r * uv_stride + c * 2] = 128;
uv[r * uv_stride + c * 2 + 1] = 128;
}
}
void watermark_draw(AVFrame *frame)
{
int w = frame->width;
int h = frame->height;
enum AVPixelFormat fmt = (enum AVPixelFormat)frame->format;
int text_len = (int)strlen(wm_text);
int text_w = text_len * WM_FONT_W;
int bar_w = text_w + WM_PAD_X * 2;
int bar_h = WM_FONT_H + WM_PAD_Y * 2;
int bar_x = w - bar_w;
int bar_y = h - bar_h;
if (bar_x < 0 || bar_y < 0)
return;
switch (fmt) {
case AV_PIX_FMT_YUV420P:
case AV_PIX_FMT_YUVJ420P:
stamp_y_plane(frame->data[0], frame->linesize[0], w, h);
neutralize_chroma_420(frame->data[1], frame->linesize[1],
frame->data[2], frame->linesize[2],
bar_x, bar_y, bar_w, bar_h, w, h);
break;
case AV_PIX_FMT_NV12:
stamp_y_plane(frame->data[0], frame->linesize[0], w, h);
neutralize_chroma_nv12(frame->data[1], frame->linesize[1],
bar_x, bar_y, bar_w, bar_h, w, h);
break;
case AV_PIX_FMT_YUV422P:
case AV_PIX_FMT_YUVJ422P:
case AV_PIX_FMT_YUV444P:
case AV_PIX_FMT_YUVJ444P:
stamp_y_plane(frame->data[0], frame->linesize[0], w, h);
break;
default:
break;
}
}
+5
View File
@@ -0,0 +1,5 @@
#pragma once
#include <libavutil/frame.h>
void watermark_draw(AVFrame *frame);
+121 -133
View File
@@ -1,188 +1,176 @@
#include "webhook.h"
#include <obs-module.h>
#include "debug-log.h"
#include <util/threading.h>
#include <util/platform.h>
#include <string.h>
#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;
}
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 int webhook_curl_debug_cb(CURL *handle, curl_infotype type, char *data,
size_t size, void *userptr)
{
char host[256] = {0};
char port_str[16] = {0};
char path[512] = {0};
(void)handle;
(void)userptr;
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);
const char *prefix;
switch (type) {
case CURLINFO_TEXT:
prefix = "* ";
break;
case CURLINFO_SSL_DATA_IN:
case CURLINFO_SSL_DATA_OUT:
case CURLINFO_DATA_IN:
case CURLINFO_DATA_OUT:
return 0;
case CURLINFO_HEADER_IN:
prefix = "< ";
break;
case CURLINFO_HEADER_OUT:
prefix = "> ";
break;
default:
return 0;
}
char buf[1024];
size_t len = size < sizeof(buf) - 1 ? size : sizeof(buf) - 1;
memcpy(buf, data, len);
while (len > 0 && (buf[len - 1] == '\n' || buf[len - 1] == '\r'))
len--;
buf[len] = '\0';
dbg_log(LOG_INFO, "[Easy IRL Stream] curl: %s%s", prefix, buf);
return 0;
}
static void webhook_do_send(const char *url, const char *json_body)
{
CURL *curl = curl_easy_init();
if (!curl) {
dbg_log(LOG_WARNING, "[%s] Webhook: curl_easy_init failed",
"Easy IRL Stream");
return;
}
struct curl_slist *headers = NULL;
headers = curl_slist_append(headers, "Content-Type: application/json");
char errbuf[CURL_ERROR_SIZE] = "";
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");
curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 0L);
curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, 0L);
#ifdef _WIN32
WSADATA wsa;
WSAStartup(MAKEWORD(2, 2), &wsa);
curl_easy_setopt(curl, CURLOPT_SSLVERSION,
CURL_SSLVERSION_TLSv1_2 | CURL_SSLVERSION_MAX_TLSv1_2);
curl_easy_setopt(curl, CURLOPT_SSL_OPTIONS, (long)CURLSSLOPT_NO_REVOKE);
curl_easy_setopt(curl, CURLOPT_HTTP_VERSION,
(long)CURL_HTTP_VERSION_1_1);
curl_easy_setopt(curl, CURLOPT_SSL_ENABLE_ALPN, 0L);
curl_easy_setopt(curl, CURLOPT_IPRESOLVE, (long)CURL_IPRESOLVE_V4);
#endif
curl_easy_setopt(curl, CURLOPT_VERBOSE, 1L);
curl_easy_setopt(curl, CURLOPT_DEBUGFUNCTION, webhook_curl_debug_cb);
curl_easy_setopt(curl, CURLOPT_ERRORBUFFER, errbuf);
struct addrinfo hints = {0};
struct addrinfo *res = NULL;
hints.ai_family = AF_INET;
hints.ai_socktype = SOCK_STREAM;
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) {
dbg_log(LOG_WARNING, "[%s] Webhook failed (%s): %s (%s)",
"Easy IRL Stream", url, curl_easy_strerror(res),
errbuf[0] ? errbuf : "no details");
} else {
long http_code = 0;
curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &http_code);
dbg_log(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);
}
}
@@ -190,7 +178,7 @@ void webhook_send_async(const char *url, const char *event_name,
static void *cmd_thread_func(void *arg)
{
struct cmd_args *ca = arg;
blog(LOG_DEBUG, "[%s] Executing command: %s", "Easy IRL Stream",
dbg_log(LOG_DEBUG, "[%s] Executing command: %s", "Easy IRL Stream",
ca->command);
(void)system(ca->command);
bfree(ca->command);
+12 -1
View File
@@ -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);