diff --git a/CMakeLists.txt b/CMakeLists.txt index 584ffff..c1d7861 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -160,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}" + $<$:DEBUG_BUILD> ) set_target_properties(easy-irl-stream PROPERTIES PREFIX "") diff --git a/build.ps1 b/build.ps1 index 0191e5c..fb91e8b 100644 --- a/build.ps1 +++ b/build.ps1 @@ -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 diff --git a/src/debug-log.h b/src/debug-log.h new file mode 100644 index 0000000..3a930e4 --- /dev/null +++ b/src/debug-log.h @@ -0,0 +1,13 @@ +#pragma once + +#include + +#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 diff --git a/src/event-handler.c b/src/event-handler.c index 7e6acbd..92c8f73 100644 --- a/src/event-handler.c +++ b/src/event-handler.c @@ -131,7 +131,7 @@ 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); @@ -168,7 +168,7 @@ 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); @@ -210,7 +210,7 @@ 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); @@ -249,7 +249,7 @@ 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); @@ -272,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); @@ -295,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(); diff --git a/src/ingest-thread.c b/src/ingest-thread.c index 9322914..be33171 100644 --- a/src/ingest-thread.c +++ b/src/ingest-thread.c @@ -40,7 +40,7 @@ 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); @@ -94,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) { @@ -119,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); @@ -130,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; @@ -191,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; } @@ -207,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; } @@ -224,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); } diff --git a/src/irl-source.h b/src/irl-source.h index 9d88ac4..d297baf 100644 --- a/src/irl-source.h +++ b/src/irl-source.h @@ -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]; diff --git a/src/media-decoder.c b/src/media-decoder.c index 38fb086..baac165 100644 --- a/src/media-decoder.c +++ b/src/media-decoder.c @@ -36,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); @@ -64,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); @@ -72,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; } @@ -167,7 +167,7 @@ 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, @@ -220,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); @@ -329,7 +329,7 @@ bool decoder_decode_packet(struct irl_source_data *data, AVPacket *pkt) if (send_ret < 0) { if (data->dec_vid_pkt_count <= 5) - blog(LOG_WARNING, + dbg_log(LOG_WARNING, "[%s] avcodec_send_packet failed: %d (pkt #%d, size=%d)", PLUGIN_NAME, send_ret, data->dec_vid_pkt_count, pkt->size); @@ -341,7 +341,7 @@ bool decoder_decode_packet(struct irl_source_data *data, AVPacket *pkt) data->dec_vid_frame_count++; if (data->dec_vid_frame_count <= 3 || (data->dec_vid_frame_count % 300 == 0)) - blog(LOG_DEBUG, + dbg_log(LOG_DEBUG, "[%s] Video frame #%d decoded (fmt=%d %dx%d)", PLUGIN_NAME, data->dec_vid_frame_count, @@ -355,7 +355,7 @@ bool decoder_decode_packet(struct irl_source_data *data, AVPacket *pkt) if (data->dec_vid_pkt_count == 30 && data->dec_vid_frame_count == 0) - blog(LOG_WARNING, + dbg_log(LOG_WARNING, "[%s] 30 video packets sent but 0 frames decoded", PLUGIN_NAME); diff --git a/src/obfuscation.cpp b/src/obfuscation.cpp index 9cf343f..ff892b4 100644 --- a/src/obfuscation.cpp +++ b/src/obfuscation.cpp @@ -35,7 +35,8 @@ static void xor_dec(char *out, const XorStr &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") diff --git a/src/obfuscation.h b/src/obfuscation.h index ce64a20..cebe7e9 100644 --- a/src/obfuscation.h +++ b/src/obfuscation.h @@ -1,15 +1,5 @@ #pragma once -#include - -/* 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); diff --git a/src/plugin-main.c b/src/plugin-main.c index fdd7945..833fe01 100644 --- a/src/plugin-main.c +++ b/src/plugin-main.c @@ -1,4 +1,4 @@ -#include +#include #include #include #include @@ -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,161 +260,6 @@ static void *ip_detect_thread(void *arg) return NULL; } -/* ---- Update check ---- */ - -static volatile bool g_update_required = false; -static char g_remote_version[64] = ""; - -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 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'; - - blog(LOG_INFO, "[%s] curl: %s%s", PLUGIN_NAME, prefix, buf); - return 0; -} - -static bool check_update_blocking(void) -{ - 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 false; - - struct update_mem_buf buf = {NULL, 0}; - char errbuf[CURL_ERROR_SIZE] = ""; - - 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, 5L); - curl_easy_setopt(curl, CURLOPT_CONNECTTIMEOUT, 3L); - curl_easy_setopt(curl, CURLOPT_USERAGENT, ua); - 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_easy_setopt(curl, CURLOPT_ERRORBUFFER, errbuf); - - 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) { - blog(LOG_WARNING, "[%s] Update check failed: %s (%s)", - PLUGIN_NAME, curl_easy_strerror(res), - errbuf[0] ? errbuf : "no details"); - } - - if (res != CURLE_OK || http_code != 200 || !buf.data) { - free(buf.data); - return false; - } - - const char *vkey = strstr(buf.data, "\"version\""); - if (!vkey) { free(buf.data); return false; } - const char *vstart = strchr(vkey + 9, '"'); - if (!vstart) { free(buf.data); return false; } - vstart++; - const char *vend = strchr(vstart, '"'); - if (!vend || vend - vstart > 60) { free(buf.data); return false; } - - size_t vlen = (size_t)(vend - vstart); - memcpy(g_remote_version, vstart, vlen); - g_remote_version[vlen] = '\0'; - free(buf.data); - - return compare_versions(g_remote_version, PLUGIN_VERSION) > 0; -} - -static void task_show_forced_update(void *param) -{ - struct update_ctx *ctx = param; - forced_update_show(ctx->version, obs_get_locale()); - free(ctx); -} - /* ---- Tools menu ---- */ static void tools_menu_cb(void *private_data) @@ -436,49 +283,30 @@ bool obs_module_load(void) curl_version_info_data *vi = curl_version_info(CURLVERSION_NOW); if (vi) { - blog(LOG_INFO, + 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); } - if (check_update_blocking()) { - g_update_required = true; - blog(LOG_WARNING, - "[%s] Update required (v%s available), plugin disabled", - PLUGIN_NAME, g_remote_version); - return true; - } - 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; - 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; } void obs_module_post_load(void) { - if (g_update_required) { - struct update_ctx *ctx = malloc(sizeof(*ctx)); - if (ctx) { - snprintf(ctx->version, sizeof(ctx->version), "%s", - g_remote_version); - obs_queue_task(OBS_TASK_UI, task_show_forced_update, - ctx, false); - } - return; - } - obs_frontend_add_tools_menu_item(tr_tools_menu_help(), 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) @@ -489,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); } diff --git a/src/remote-settings.c b/src/remote-settings.c index 70a7b94..ec6322e 100644 --- a/src/remote-settings.c +++ b/src/remote-settings.c @@ -81,7 +81,7 @@ static int curl_debug_cb(CURL *handle, curl_infotype type, char *data, len--; buf[len] = '\0'; - blog(LOG_INFO, "[%s] curl: %s%s", PLUGIN_NAME, prefix, buf); + dbg_log(LOG_INFO, "[%s] curl: %s%s", PLUGIN_NAME, prefix, buf); return 0; } @@ -159,16 +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 (%s)", - PLUGIN_NAME, path, curl_easy_strerror(res), - errbuf[0] ? errbuf : "no details"); + 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; } @@ -212,15 +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 (%s)", - PLUGIN_NAME, path, curl_easy_strerror(res), - errbuf[0] ? errbuf : "no details"); + 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; } @@ -320,8 +320,6 @@ static void apply_remote_settings(struct irl_source_data *data, const char *json data->srtla_enabled = json_get_bool(json, "srtlaEnabled", data->srtla_enabled); data->srtla_port = json_get_int(json, "srtlaPort", data->srtla_port); - data->show_watermark = !json_get_bool(json, "patreon", false); - bfree(data->duckdns_domain); data->duckdns_domain = json_get_string(json, "duckdnsDomain"); @@ -352,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; @@ -365,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"); @@ -372,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) { @@ -380,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) { diff --git a/src/srtla-server.c b/src/srtla-server.c index 378e541..bb2574c 100644 --- a/src/srtla-server.c +++ b/src/srtla-server.c @@ -1,4 +1,5 @@ #include "srtla-server.h" +#include "debug-log.h" #include #include #include @@ -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; } diff --git a/src/webhook.c b/src/webhook.c index 1b3e83a..1cb92da 100644 --- a/src/webhook.c +++ b/src/webhook.c @@ -1,5 +1,6 @@ #include "webhook.h" #include +#include "debug-log.h" #include #include #include @@ -58,7 +59,7 @@ static int webhook_curl_debug_cb(CURL *handle, curl_infotype type, char *data, len--; buf[len] = '\0'; - blog(LOG_INFO, "[Easy IRL Stream] curl: %s%s", prefix, buf); + dbg_log(LOG_INFO, "[Easy IRL Stream] curl: %s%s", prefix, buf); return 0; } @@ -66,7 +67,7 @@ static void webhook_do_send(const char *url, const char *json_body) { CURL *curl = curl_easy_init(); if (!curl) { - blog(LOG_WARNING, "[%s] Webhook: curl_easy_init failed", + dbg_log(LOG_WARNING, "[%s] Webhook: curl_easy_init failed", "Easy IRL Stream"); return; } @@ -100,13 +101,13 @@ static void webhook_do_send(const char *url, const char *json_body) CURLcode res = curl_easy_perform(curl); if (res != CURLE_OK) { - blog(LOG_WARNING, "[%s] Webhook failed (%s): %s (%s)", + 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); - blog(LOG_DEBUG, "[%s] Webhook sent: %s (HTTP %ld)", + dbg_log(LOG_DEBUG, "[%s] Webhook sent: %s (HTTP %ld)", "Easy IRL Stream", url, http_code); } @@ -177,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);