Compare commits
10 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 6d16baf052 | |||
| 8b05e85683 | |||
| 6373468050 | |||
| d116e6a652 | |||
| cb3c837f0b | |||
| fb7edeae0b | |||
| 9890848c6c | |||
| 73659ce0a0 | |||
| f8b992ca1d | |||
| 75eff0dd29 |
@@ -0,0 +1,15 @@
|
|||||||
|
# These are supported funding model platforms
|
||||||
|
|
||||||
|
github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
|
||||||
|
patreon: stoolscc
|
||||||
|
open_collective: # Replace with a single Open Collective username
|
||||||
|
ko_fi: # Replace with a single Ko-fi username
|
||||||
|
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
|
||||||
|
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
|
||||||
|
liberapay: # Replace with a single Liberapay username
|
||||||
|
issuehunt: # Replace with a single IssueHunt username
|
||||||
|
lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry
|
||||||
|
polar: # Replace with a single Polar username
|
||||||
|
buy_me_a_coffee: # Replace with a single Buy Me a Coffee username
|
||||||
|
thanks_dev: # Replace with a single thanks.dev username
|
||||||
|
custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
|
||||||
@@ -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.
|
||||||
+2
-1
@@ -1,6 +1,6 @@
|
|||||||
cmake_minimum_required(VERSION 3.16...3.28)
|
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 11)
|
||||||
set(CMAKE_C_STANDARD_REQUIRED ON)
|
set(CMAKE_C_STANDARD_REQUIRED ON)
|
||||||
@@ -22,6 +22,7 @@ add_library(easy-irl-stream MODULE
|
|||||||
src/webhook.c
|
src/webhook.c
|
||||||
src/srtla-server.c
|
src/srtla-server.c
|
||||||
src/remote-settings.c
|
src/remote-settings.c
|
||||||
|
src/watermark.c
|
||||||
src/obfuscation.cpp
|
src/obfuscation.cpp
|
||||||
src/help-dialog.cpp
|
src/help-dialog.cpp
|
||||||
src/stats-dialog.cpp
|
src/stats-dialog.cpp
|
||||||
|
|||||||
@@ -12,11 +12,10 @@ OBS Studio plugin for IRL streamers. Receives an RTMP or SRT stream directly in
|
|||||||
- DuckDNS integration
|
- DuckDNS integration
|
||||||
- Cross-platform (Windows, macOS, Linux)
|
- 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)**.
|
For setup instructions, FAQ and downloads visit **[stools.cc/p/easy-irl-stream](https://stools.cc/p/easy-irl-stream)**.
|
||||||
|
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
This project is licensed under the [GNU General Public License v2.0](LICENSE).
|
This project is licensed under the [GNU General Public License v2.0](LICENSE).
|
||||||
|
|||||||
@@ -107,23 +107,6 @@ if (Test-Path $dll) {
|
|||||||
Write-Host ""
|
Write-Host ""
|
||||||
Write-Host "BUILD SUCCESSFUL: easy-irl-stream.dll ($size KB)" -ForegroundColor Green
|
Write-Host "BUILD SUCCESSFUL: easy-irl-stream.dll ($size KB)" -ForegroundColor Green
|
||||||
Write-Host "Output: $dll"
|
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 {
|
} else {
|
||||||
Write-Host "BUILD FAILED" -ForegroundColor Red
|
Write-Host "BUILD FAILED" -ForegroundColor Red
|
||||||
exit 1
|
exit 1
|
||||||
|
|||||||
+37
-10
@@ -1,5 +1,24 @@
|
|||||||
#include "event-handler.h"
|
#include "event-handler.h"
|
||||||
#include "webhook.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 ---- */
|
/* ---- queued tasks executed on the UI thread ---- */
|
||||||
|
|
||||||
@@ -118,8 +137,10 @@ static void fire_low_quality_actions(struct irl_source_data *data)
|
|||||||
queue_scene_switch(scene);
|
queue_scene_switch(scene);
|
||||||
queue_overlay(overlay, true);
|
queue_overlay(overlay, true);
|
||||||
|
|
||||||
if (webhook && webhook[0])
|
if (webhook && webhook[0]) {
|
||||||
webhook_send_async(webhook, "low_quality", src_copy);
|
struct webhook_event_data ed = snapshot_event_data(data);
|
||||||
|
webhook_send_async(webhook, "low_quality", src_copy, &ed);
|
||||||
|
}
|
||||||
if (cmd && cmd[0])
|
if (cmd && cmd[0])
|
||||||
webhook_execute_command_async(cmd);
|
webhook_execute_command_async(cmd);
|
||||||
|
|
||||||
@@ -153,8 +174,10 @@ static void fire_quality_recovered_actions(struct irl_source_data *data)
|
|||||||
queue_scene_switch(scene);
|
queue_scene_switch(scene);
|
||||||
queue_overlay(overlay, false);
|
queue_overlay(overlay, false);
|
||||||
|
|
||||||
if (webhook && webhook[0])
|
if (webhook && webhook[0]) {
|
||||||
webhook_send_async(webhook, "quality_recovered", src_copy);
|
struct webhook_event_data ed = snapshot_event_data(data);
|
||||||
|
webhook_send_async(webhook, "quality_recovered", src_copy, &ed);
|
||||||
|
}
|
||||||
|
|
||||||
bfree(scene);
|
bfree(scene);
|
||||||
bfree(overlay);
|
bfree(overlay);
|
||||||
@@ -193,8 +216,10 @@ static void fire_disconnect_actions(struct irl_source_data *data)
|
|||||||
queue_overlay(overlay, true);
|
queue_overlay(overlay, true);
|
||||||
queue_recording(rec_action);
|
queue_recording(rec_action);
|
||||||
|
|
||||||
if (webhook && webhook[0])
|
if (webhook && webhook[0]) {
|
||||||
webhook_send_async(webhook, "disconnect", src_copy);
|
struct webhook_event_data ed = snapshot_event_data(data);
|
||||||
|
webhook_send_async(webhook, "disconnect", src_copy, &ed);
|
||||||
|
}
|
||||||
if (cmd && cmd[0])
|
if (cmd && cmd[0])
|
||||||
webhook_execute_command_async(cmd);
|
webhook_execute_command_async(cmd);
|
||||||
|
|
||||||
@@ -229,8 +254,10 @@ static void fire_reconnect_actions(struct irl_source_data *data)
|
|||||||
queue_scene_switch(scene);
|
queue_scene_switch(scene);
|
||||||
queue_overlay(overlay, false);
|
queue_overlay(overlay, false);
|
||||||
|
|
||||||
if (webhook && webhook[0])
|
if (webhook && webhook[0]) {
|
||||||
webhook_send_async(webhook, "reconnect", src_copy);
|
struct webhook_event_data ed = snapshot_event_data(data);
|
||||||
|
webhook_send_async(webhook, "reconnect", src_copy, &ed);
|
||||||
|
}
|
||||||
if (cmd && cmd[0])
|
if (cmd && cmd[0])
|
||||||
webhook_execute_command_async(cmd);
|
webhook_execute_command_async(cmd);
|
||||||
|
|
||||||
@@ -347,7 +374,7 @@ void event_handler_tick(struct irl_source_data *data)
|
|||||||
if (state == CONN_STATE_CONNECTED)
|
if (state == CONN_STATE_CONNECTED)
|
||||||
check_quality(data);
|
check_quality(data);
|
||||||
|
|
||||||
if (state != CONN_STATE_DISCONNECTED)
|
if (state != CONN_STATE_DISCONNECTED && state != CONN_STATE_LISTENING)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
pthread_mutex_lock(&data->mutex);
|
pthread_mutex_lock(&data->mutex);
|
||||||
@@ -356,7 +383,7 @@ void event_handler_tick(struct irl_source_data *data)
|
|||||||
int timeout = data->disconnect_timeout_sec;
|
int timeout = data->disconnect_timeout_sec;
|
||||||
pthread_mutex_unlock(&data->mutex);
|
pthread_mutex_unlock(&data->mutex);
|
||||||
|
|
||||||
if (already_fired || timeout <= 0)
|
if (already_fired || timeout <= 0 || disc_time == 0)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
uint64_t elapsed_ns = os_gettime_ns() - disc_time;
|
uint64_t elapsed_ns = os_gettime_ns() - disc_time;
|
||||||
|
|||||||
@@ -56,6 +56,8 @@ struct HelpStrings {
|
|||||||
const char *srtla_step4;
|
const char *srtla_step4;
|
||||||
const char *faq_q7;
|
const char *faq_q7;
|
||||||
const char *faq_a7;
|
const char *faq_a7;
|
||||||
|
const char *faq_q8;
|
||||||
|
const char *faq_a8;
|
||||||
};
|
};
|
||||||
|
|
||||||
static const HelpStrings LANG_DE = {
|
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>"
|
"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>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)!",
|
"<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 – stools.cc</i> "
|
||||||
|
"unten rechts im Bild eingeblendet. Als "
|
||||||
|
"<a href='https://www.patreon.com/checkout/stoolscc?rid=27957669'>Patreon-Unterstützer</a> "
|
||||||
|
"wird das Wasserzeichen automatisch entfernt.",
|
||||||
};
|
};
|
||||||
|
|
||||||
static const HelpStrings LANG_EN = {
|
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>"
|
"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>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)!",
|
"<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 – 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,
|
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_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_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_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>";
|
+ "</body></html>";
|
||||||
}
|
}
|
||||||
@@ -388,3 +401,98 @@ extern "C" void update_dialog_show(const char *new_version, const char *locale)
|
|||||||
open_url(url);
|
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);
|
||||||
|
}
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ void help_dialog_show(const char *local_ip, const char *external_ip,
|
|||||||
const char *version, const char *locale);
|
const char *version, const char *locale);
|
||||||
|
|
||||||
void update_dialog_show(const char *new_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
|
#ifdef __cplusplus
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -47,6 +47,9 @@ static void build_url(struct irl_source_data *data, char *buf, size_t sz)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (data->srt_streamid && data->srt_streamid[0])
|
||||||
|
dstr_catf(&url, "&streamid=%s", data->srt_streamid);
|
||||||
|
|
||||||
snprintf(buf, sz, "%s", url.array);
|
snprintf(buf, sz, "%s", url.array);
|
||||||
dstr_free(&url);
|
dstr_free(&url);
|
||||||
}
|
}
|
||||||
@@ -152,6 +155,8 @@ static void *ingest_thread_func(void *arg)
|
|||||||
data->stats_connect_time_ns = os_gettime_ns();
|
data->stats_connect_time_ns = os_gettime_ns();
|
||||||
data->stats_total_frames = 0;
|
data->stats_total_frames = 0;
|
||||||
data->stats_total_bytes = 0;
|
data->stats_total_bytes = 0;
|
||||||
|
data->dec_vid_pkt_count = 0;
|
||||||
|
data->dec_vid_frame_count = 0;
|
||||||
event_handler_on_connect(data);
|
event_handler_on_connect(data);
|
||||||
|
|
||||||
AVPacket *pkt = av_packet_alloc();
|
AVPacket *pkt = av_packet_alloc();
|
||||||
|
|||||||
@@ -115,6 +115,7 @@ static void *irl_source_create(obs_data_t *settings, obs_source_t *source)
|
|||||||
data->video_stream_idx = -1;
|
data->video_stream_idx = -1;
|
||||||
data->audio_stream_idx = -1;
|
data->audio_stream_idx = -1;
|
||||||
data->active = true;
|
data->active = true;
|
||||||
|
data->show_watermark = true;
|
||||||
|
|
||||||
pthread_mutex_init(&data->mutex, NULL);
|
pthread_mutex_init(&data->mutex, NULL);
|
||||||
|
|
||||||
|
|||||||
@@ -115,6 +115,13 @@ struct irl_source_data {
|
|||||||
char *webhook_url;
|
char *webhook_url;
|
||||||
char *custom_command;
|
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) */
|
/* Stats (written by ingest thread, read by UI) */
|
||||||
char stats_video_codec[32];
|
char stats_video_codec[32];
|
||||||
char stats_audio_codec[32];
|
char stats_audio_codec[32];
|
||||||
|
|||||||
+27
-11
@@ -1,4 +1,5 @@
|
|||||||
#include "media-decoder.h"
|
#include "media-decoder.h"
|
||||||
|
#include "watermark.h"
|
||||||
|
|
||||||
bool decoder_open(struct irl_source_data *data)
|
bool decoder_open(struct irl_source_data *data)
|
||||||
{
|
{
|
||||||
@@ -173,6 +174,10 @@ static void output_video_frame(struct irl_source_data *data, AVFrame *frame)
|
|||||||
obs_fmt, full_range);
|
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
|
enum video_colorspace cs = (h >= 720) ? VIDEO_CS_709
|
||||||
: VIDEO_CS_601;
|
: VIDEO_CS_601;
|
||||||
|
|
||||||
@@ -228,6 +233,18 @@ static void output_video_frame(struct irl_source_data *data, AVFrame *frame)
|
|||||||
frame->linesize, 0, h, data->video_dst_data,
|
frame->linesize, 0, h, data->video_dst_data,
|
||||||
data->video_dst_linesize);
|
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;
|
enum video_colorspace cs = (h >= 720) ? VIDEO_CS_709 : VIDEO_CS_601;
|
||||||
|
|
||||||
struct obs_source_frame obs_frame = {0};
|
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)
|
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 &&
|
if (pkt->stream_index == data->video_stream_idx &&
|
||||||
data->video_dec_ctx) {
|
data->video_dec_ctx) {
|
||||||
int send_ret =
|
int send_ret =
|
||||||
avcodec_send_packet(data->video_dec_ctx, pkt);
|
avcodec_send_packet(data->video_dec_ctx, pkt);
|
||||||
vid_pkt_count++;
|
data->dec_vid_pkt_count++;
|
||||||
|
|
||||||
if (send_ret < 0) {
|
if (send_ret < 0) {
|
||||||
if (vid_pkt_count <= 5)
|
if (data->dec_vid_pkt_count <= 5)
|
||||||
blog(LOG_WARNING,
|
blog(LOG_WARNING,
|
||||||
"[%s] avcodec_send_packet failed: %d (pkt #%d, size=%d)",
|
"[%s] avcodec_send_packet failed: %d (pkt #%d, size=%d)",
|
||||||
PLUGIN_NAME, send_ret,
|
PLUGIN_NAME, send_ret,
|
||||||
vid_pkt_count, pkt->size);
|
data->dec_vid_pkt_count, pkt->size);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
AVFrame *frame = av_frame_alloc();
|
AVFrame *frame = av_frame_alloc();
|
||||||
while (avcodec_receive_frame(data->video_dec_ctx, frame) == 0) {
|
while (avcodec_receive_frame(data->video_dec_ctx, frame) == 0) {
|
||||||
vid_frame_count++;
|
data->dec_vid_frame_count++;
|
||||||
if (vid_frame_count <= 3 ||
|
if (data->dec_vid_frame_count <= 3 ||
|
||||||
(vid_frame_count % 300 == 0))
|
(data->dec_vid_frame_count % 300 == 0))
|
||||||
blog(LOG_DEBUG,
|
blog(LOG_DEBUG,
|
||||||
"[%s] Video frame #%d decoded (fmt=%d %dx%d)",
|
"[%s] Video frame #%d decoded (fmt=%d %dx%d)",
|
||||||
PLUGIN_NAME, vid_frame_count,
|
PLUGIN_NAME,
|
||||||
|
data->dec_vid_frame_count,
|
||||||
frame->format,
|
frame->format,
|
||||||
frame->width, frame->height);
|
frame->width, frame->height);
|
||||||
output_video_frame(data, frame);
|
output_video_frame(data, frame);
|
||||||
@@ -338,7 +353,8 @@ bool decoder_decode_packet(struct irl_source_data *data, AVPacket *pkt)
|
|||||||
}
|
}
|
||||||
av_frame_free(&frame);
|
av_frame_free(&frame);
|
||||||
|
|
||||||
if (vid_pkt_count == 30 && vid_frame_count == 0)
|
if (data->dec_vid_pkt_count == 30 &&
|
||||||
|
data->dec_vid_frame_count == 0)
|
||||||
blog(LOG_WARNING,
|
blog(LOG_WARNING,
|
||||||
"[%s] 30 video packets sent but 0 frames decoded",
|
"[%s] 30 video packets sent but 0 frames decoded",
|
||||||
PLUGIN_NAME);
|
PLUGIN_NAME);
|
||||||
|
|||||||
+105
-35
@@ -260,6 +260,9 @@ static void *ip_detect_thread(void *arg)
|
|||||||
|
|
||||||
/* ---- Update check ---- */
|
/* ---- Update check ---- */
|
||||||
|
|
||||||
|
static volatile bool g_update_required = false;
|
||||||
|
static char g_remote_version[64] = "";
|
||||||
|
|
||||||
struct update_mem_buf {
|
struct update_mem_buf {
|
||||||
char *data;
|
char *data;
|
||||||
size_t size;
|
size_t size;
|
||||||
@@ -296,18 +299,46 @@ struct update_ctx {
|
|||||||
#include "help-dialog.hpp"
|
#include "help-dialog.hpp"
|
||||||
#include "stats-dialog.hpp"
|
#include "stats-dialog.hpp"
|
||||||
|
|
||||||
static void task_show_update_dialog(void *param)
|
static int curl_debug_cb(CURL *handle, curl_infotype type, char *data,
|
||||||
|
size_t size, void *userptr)
|
||||||
{
|
{
|
||||||
struct update_ctx *ctx = param;
|
(void)handle;
|
||||||
update_dialog_show(ctx->version, obs_get_locale());
|
(void)userptr;
|
||||||
free(ctx);
|
|
||||||
|
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 void *update_check_thread(void *arg)
|
static bool check_update_blocking(void)
|
||||||
{
|
{
|
||||||
UNUSED_PARAMETER(arg);
|
|
||||||
os_sleep_ms(5000);
|
|
||||||
|
|
||||||
char url[256];
|
char url[256];
|
||||||
snprintf(url, sizeof(url), "%s%s%s",
|
snprintf(url, sizeof(url), "%s%s%s",
|
||||||
obf_https_prefix(), obf_stools_host(),
|
obf_https_prefix(), obf_stools_host(),
|
||||||
@@ -317,54 +348,69 @@ static void *update_check_thread(void *arg)
|
|||||||
snprintf(ua, sizeof(ua), "%s%s", obf_ua_prefix(), PLUGIN_VERSION);
|
snprintf(ua, sizeof(ua), "%s%s", obf_ua_prefix(), PLUGIN_VERSION);
|
||||||
|
|
||||||
CURL *curl = curl_easy_init();
|
CURL *curl = curl_easy_init();
|
||||||
if (!curl) return NULL;
|
if (!curl) return false;
|
||||||
|
|
||||||
struct update_mem_buf buf = {NULL, 0};
|
struct update_mem_buf buf = {NULL, 0};
|
||||||
|
char errbuf[CURL_ERROR_SIZE] = "";
|
||||||
|
|
||||||
curl_easy_setopt(curl, CURLOPT_URL, url);
|
curl_easy_setopt(curl, CURLOPT_URL, url);
|
||||||
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, update_write_cb);
|
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, update_write_cb);
|
||||||
curl_easy_setopt(curl, CURLOPT_WRITEDATA, &buf);
|
curl_easy_setopt(curl, CURLOPT_WRITEDATA, &buf);
|
||||||
curl_easy_setopt(curl, CURLOPT_TIMEOUT, 10L);
|
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_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);
|
CURLcode res = curl_easy_perform(curl);
|
||||||
long http_code = 0;
|
long http_code = 0;
|
||||||
curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &http_code);
|
curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &http_code);
|
||||||
curl_easy_cleanup(curl);
|
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) {
|
if (res != CURLE_OK || http_code != 200 || !buf.data) {
|
||||||
free(buf.data);
|
free(buf.data);
|
||||||
return NULL;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Parse {"version":"x.y.z"} */
|
|
||||||
const char *vkey = strstr(buf.data, "\"version\"");
|
const char *vkey = strstr(buf.data, "\"version\"");
|
||||||
if (!vkey) { free(buf.data); return NULL; }
|
if (!vkey) { free(buf.data); return false; }
|
||||||
const char *vstart = strchr(vkey + 9, '"');
|
const char *vstart = strchr(vkey + 9, '"');
|
||||||
if (!vstart) { free(buf.data); return NULL; }
|
if (!vstart) { free(buf.data); return false; }
|
||||||
vstart++;
|
vstart++;
|
||||||
const char *vend = strchr(vstart, '"');
|
const char *vend = strchr(vstart, '"');
|
||||||
if (!vend || vend - vstart > 60) { free(buf.data); return NULL; }
|
if (!vend || vend - vstart > 60) { free(buf.data); return false; }
|
||||||
|
|
||||||
char remote_ver[64];
|
|
||||||
size_t vlen = (size_t)(vend - vstart);
|
size_t vlen = (size_t)(vend - vstart);
|
||||||
memcpy(remote_ver, vstart, vlen);
|
memcpy(g_remote_version, vstart, vlen);
|
||||||
remote_ver[vlen] = '\0';
|
g_remote_version[vlen] = '\0';
|
||||||
free(buf.data);
|
free(buf.data);
|
||||||
|
|
||||||
if (compare_versions(remote_ver, PLUGIN_VERSION) > 0) {
|
return compare_versions(g_remote_version, 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));
|
static void task_show_forced_update(void *param)
|
||||||
if (ctx) {
|
{
|
||||||
snprintf(ctx->version, sizeof(ctx->version), "%s",
|
struct update_ctx *ctx = param;
|
||||||
remote_ver);
|
forced_update_show(ctx->version, obs_get_locale());
|
||||||
obs_queue_task(OBS_TASK_UI, task_show_update_dialog,
|
free(ctx);
|
||||||
ctx, false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return NULL;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ---- Tools menu ---- */
|
/* ---- Tools menu ---- */
|
||||||
@@ -388,22 +434,46 @@ bool obs_module_load(void)
|
|||||||
{
|
{
|
||||||
curl_global_init(CURL_GLOBAL_DEFAULT);
|
curl_global_init(CURL_GLOBAL_DEFAULT);
|
||||||
|
|
||||||
|
curl_version_info_data *vi = curl_version_info(CURLVERSION_NOW);
|
||||||
|
if (vi) {
|
||||||
|
blog(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);
|
obs_register_source(&irl_source_info);
|
||||||
|
|
||||||
g_ip_thread_active = true;
|
g_ip_thread_active = true;
|
||||||
if (pthread_create(&g_ip_thread, NULL, ip_detect_thread, NULL) != 0)
|
if (pthread_create(&g_ip_thread, NULL, ip_detect_thread, NULL) != 0)
|
||||||
g_ip_thread_active = false;
|
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);
|
blog(LOG_INFO, "[%s] Plugin loaded (v%s)", PLUGIN_NAME, PLUGIN_VERSION);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
void obs_module_post_load(void)
|
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(),
|
obs_frontend_add_tools_menu_item(tr_tools_menu_help(),
|
||||||
tools_menu_cb, NULL);
|
tools_menu_cb, NULL);
|
||||||
obs_frontend_add_tools_menu_item(tr_tools_menu_stats(),
|
obs_frontend_add_tools_menu_item(tr_tools_menu_stats(),
|
||||||
|
|||||||
+105
-4
@@ -14,6 +14,94 @@
|
|||||||
|
|
||||||
#include <curl/curl.h>
|
#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';
|
||||||
|
|
||||||
|
blog(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 ---- */
|
/* ---- cURL helpers ---- */
|
||||||
|
|
||||||
struct mem_buf {
|
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);
|
snprintf(ua, sizeof(ua), "%s%s", obf_ua_prefix(), PLUGIN_VERSION);
|
||||||
|
|
||||||
struct mem_buf buf = {NULL, 0};
|
struct mem_buf buf = {NULL, 0};
|
||||||
|
char errbuf[CURL_ERROR_SIZE] = "";
|
||||||
|
|
||||||
curl_easy_setopt(curl, CURLOPT_URL, url);
|
curl_easy_setopt(curl, CURLOPT_URL, url);
|
||||||
curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
|
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_WRITEDATA, &buf);
|
||||||
curl_easy_setopt(curl, CURLOPT_TIMEOUT, 15L);
|
curl_easy_setopt(curl, CURLOPT_TIMEOUT, 15L);
|
||||||
curl_easy_setopt(curl, CURLOPT_USERAGENT, ua);
|
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);
|
CURLcode res = curl_easy_perform(curl);
|
||||||
long http_code = 0;
|
long http_code = 0;
|
||||||
@@ -68,8 +159,10 @@ static char *api_get(const char *path, const char *token)
|
|||||||
curl_easy_cleanup(curl);
|
curl_easy_cleanup(curl);
|
||||||
|
|
||||||
if (res != CURLE_OK) {
|
if (res != CURLE_OK) {
|
||||||
blog(LOG_WARNING, "[%s] API GET %s failed: %s",
|
blog(LOG_WARNING, "[%s] API GET %s failed: %s (%s)",
|
||||||
PLUGIN_NAME, path, curl_easy_strerror(res));
|
PLUGIN_NAME, path, curl_easy_strerror(res),
|
||||||
|
errbuf[0] ? errbuf : "no details");
|
||||||
|
maybe_show_ssl_error(res, errbuf);
|
||||||
free(buf.data);
|
free(buf.data);
|
||||||
return NULL;
|
return NULL;
|
||||||
}
|
}
|
||||||
@@ -102,11 +195,15 @@ static bool api_post(const char *path, const char *token, const char *json_body)
|
|||||||
char ua[128];
|
char ua[128];
|
||||||
snprintf(ua, sizeof(ua), "%s%s", obf_ua_prefix(), PLUGIN_VERSION);
|
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_URL, url);
|
||||||
curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
|
curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
|
||||||
curl_easy_setopt(curl, CURLOPT_POSTFIELDS, json_body);
|
curl_easy_setopt(curl, CURLOPT_POSTFIELDS, json_body);
|
||||||
curl_easy_setopt(curl, CURLOPT_TIMEOUT, 15L);
|
curl_easy_setopt(curl, CURLOPT_TIMEOUT, 15L);
|
||||||
curl_easy_setopt(curl, CURLOPT_USERAGENT, ua);
|
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);
|
CURLcode res = curl_easy_perform(curl);
|
||||||
long http_code = 0;
|
long http_code = 0;
|
||||||
@@ -115,8 +212,10 @@ static bool api_post(const char *path, const char *token, const char *json_body)
|
|||||||
curl_easy_cleanup(curl);
|
curl_easy_cleanup(curl);
|
||||||
|
|
||||||
if (res != CURLE_OK) {
|
if (res != CURLE_OK) {
|
||||||
blog(LOG_WARNING, "[%s] API POST %s failed: %s",
|
blog(LOG_WARNING, "[%s] API POST %s failed: %s (%s)",
|
||||||
PLUGIN_NAME, path, curl_easy_strerror(res));
|
PLUGIN_NAME, path, curl_easy_strerror(res),
|
||||||
|
errbuf[0] ? errbuf : "no details");
|
||||||
|
maybe_show_ssl_error(res, errbuf);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (http_code != 200) {
|
if (http_code != 200) {
|
||||||
@@ -221,6 +320,8 @@ 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_enabled = json_get_bool(json, "srtlaEnabled", data->srtla_enabled);
|
||||||
data->srtla_port = json_get_int(json, "srtlaPort", data->srtla_port);
|
data->srtla_port = json_get_int(json, "srtlaPort", data->srtla_port);
|
||||||
|
|
||||||
|
data->show_watermark = !json_get_bool(json, "patreon", false);
|
||||||
|
|
||||||
bfree(data->duckdns_domain);
|
bfree(data->duckdns_domain);
|
||||||
data->duckdns_domain = json_get_string(json, "duckdnsDomain");
|
data->duckdns_domain = json_get_string(json, "duckdnsDomain");
|
||||||
|
|
||||||
|
|||||||
+15
-1
@@ -82,7 +82,7 @@ private:
|
|||||||
bool m_de;
|
bool m_de;
|
||||||
QLabel *m_dot, *m_status;
|
QLabel *m_dot, *m_status;
|
||||||
QLabel *m_lbls[4], *m_vals[4];
|
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;
|
QTimer *m_timer;
|
||||||
int64_t m_prevFrames = 0;
|
int64_t m_prevFrames = 0;
|
||||||
uint64_t m_prevTime = 0;
|
uint64_t m_prevTime = 0;
|
||||||
@@ -181,9 +181,11 @@ private:
|
|||||||
m_videoLine = makeLabel("-", -1, false, "");
|
m_videoLine = makeLabel("-", -1, false, "");
|
||||||
m_audioLine = makeLabel("-", -1, false, "");
|
m_audioLine = makeLabel("-", -1, false, "");
|
||||||
m_serverLine = makeLabel("-", -1, false, dim);
|
m_serverLine = makeLabel("-", -1, false, dim);
|
||||||
|
m_ipLine = makeLabel("-", -1, false, dim);
|
||||||
root->addWidget(m_videoLine);
|
root->addWidget(m_videoLine);
|
||||||
root->addWidget(m_audioLine);
|
root->addWidget(m_audioLine);
|
||||||
root->addWidget(m_serverLine);
|
root->addWidget(m_serverLine);
|
||||||
|
root->addWidget(m_ipLine);
|
||||||
|
|
||||||
root->addStretch();
|
root->addStretch();
|
||||||
}
|
}
|
||||||
@@ -288,6 +290,17 @@ private:
|
|||||||
s += QString(" \u00b7 SRTLA \u2713 (:%1)")
|
s += QString(" \u00b7 SRTLA \u2713 (:%1)")
|
||||||
.arg(srtla_p);
|
.arg(srtla_p);
|
||||||
m_serverLine->setText(s);
|
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()
|
void setNoSource()
|
||||||
@@ -301,6 +314,7 @@ private:
|
|||||||
m_videoLine->setText("-");
|
m_videoLine->setText("-");
|
||||||
m_audioLine->setText("-");
|
m_audioLine->setText("-");
|
||||||
m_serverLine->setText("-");
|
m_serverLine->setText("-");
|
||||||
|
m_ipLine->setText("-");
|
||||||
m_fps = 0;
|
m_fps = 0;
|
||||||
m_prevFrames = 0;
|
m_prevFrames = 0;
|
||||||
m_prevTime = 0;
|
m_prevTime = 0;
|
||||||
|
|||||||
+171
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <libavutil/frame.h>
|
||||||
|
|
||||||
|
void watermark_draw(AVFrame *frame);
|
||||||
+119
-132
@@ -6,183 +6,170 @@
|
|||||||
#include <stdio.h>
|
#include <stdio.h>
|
||||||
#include <stdlib.h>
|
#include <stdlib.h>
|
||||||
#include <time.h>
|
#include <time.h>
|
||||||
|
#include <curl/curl.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
|
|
||||||
|
|
||||||
struct webhook_args {
|
struct webhook_args {
|
||||||
char *url;
|
char *url;
|
||||||
char *event_name;
|
char *json_body;
|
||||||
char *source_name;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
struct cmd_args {
|
struct cmd_args {
|
||||||
char *command;
|
char *command;
|
||||||
};
|
};
|
||||||
|
|
||||||
static bool parse_url(const char *url, char *host, size_t host_sz,
|
static size_t discard_response(void *ptr, size_t size, size_t nmemb,
|
||||||
char *port, size_t port_sz, char *path, size_t path_sz)
|
void *userdata)
|
||||||
{
|
{
|
||||||
const char *p = url;
|
(void)ptr;
|
||||||
|
(void)userdata;
|
||||||
if (strncmp(p, "http://", 7) == 0) {
|
return size * nmemb;
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static void webhook_do_send(const char *url, const char *event_name,
|
static int webhook_curl_debug_cb(CURL *handle, curl_infotype type, char *data,
|
||||||
const char *source_name)
|
size_t size, void *userptr)
|
||||||
{
|
{
|
||||||
char host[256] = {0};
|
(void)handle;
|
||||||
char port_str[16] = {0};
|
(void)userptr;
|
||||||
char path[512] = {0};
|
|
||||||
|
|
||||||
if (!parse_url(url, host, sizeof(host), port_str, sizeof(port_str),
|
const char *prefix;
|
||||||
path, sizeof(path))) {
|
switch (type) {
|
||||||
blog(LOG_WARNING, "[%s] Webhook: invalid URL '%s'",
|
case CURLINFO_TEXT:
|
||||||
"Easy IRL Stream", url);
|
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';
|
||||||
|
|
||||||
|
blog(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) {
|
||||||
|
blog(LOG_WARNING, "[%s] Webhook: curl_easy_init failed",
|
||||||
|
"Easy IRL Stream");
|
||||||
return;
|
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
|
#ifdef _WIN32
|
||||||
WSADATA wsa;
|
curl_easy_setopt(curl, CURLOPT_SSLVERSION,
|
||||||
WSAStartup(MAKEWORD(2, 2), &wsa);
|
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
|
#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};
|
CURLcode res = curl_easy_perform(curl);
|
||||||
struct addrinfo *res = NULL;
|
if (res != CURLE_OK) {
|
||||||
hints.ai_family = AF_INET;
|
blog(LOG_WARNING, "[%s] Webhook failed (%s): %s (%s)",
|
||||||
hints.ai_socktype = SOCK_STREAM;
|
"Easy IRL Stream", url, curl_easy_strerror(res),
|
||||||
|
errbuf[0] ? errbuf : "no details");
|
||||||
if (getaddrinfo(host, port_str, &hints, &res) != 0) {
|
} else {
|
||||||
blog(LOG_WARNING, "[%s] Webhook: DNS lookup failed for '%s'",
|
long http_code = 0;
|
||||||
"Easy IRL Stream", host);
|
curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &http_code);
|
||||||
return;
|
blog(LOG_DEBUG, "[%s] Webhook sent: %s (HTTP %ld)",
|
||||||
|
"Easy IRL Stream", url, http_code);
|
||||||
}
|
}
|
||||||
|
|
||||||
sock_t sock = socket(res->ai_family, res->ai_socktype,
|
curl_slist_free_all(headers);
|
||||||
res->ai_protocol);
|
curl_easy_cleanup(curl);
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static void *webhook_thread_func(void *arg)
|
static void *webhook_thread_func(void *arg)
|
||||||
{
|
{
|
||||||
struct webhook_args *wa = 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->url);
|
||||||
bfree(wa->event_name);
|
bfree(wa->json_body);
|
||||||
bfree(wa->source_name);
|
|
||||||
bfree(wa);
|
bfree(wa);
|
||||||
return NULL;
|
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,
|
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])
|
if (!url || !url[0])
|
||||||
return;
|
return;
|
||||||
|
|
||||||
struct webhook_args *wa = bzalloc(sizeof(*wa));
|
struct webhook_args *wa = bzalloc(sizeof(*wa));
|
||||||
wa->url = bstrdup(url);
|
wa->url = bstrdup(url);
|
||||||
wa->event_name = bstrdup(event_name);
|
wa->json_body = build_json_body(event_name, source_name, extra);
|
||||||
wa->source_name = bstrdup(source_name);
|
|
||||||
|
|
||||||
pthread_t thread;
|
pthread_t thread;
|
||||||
if (pthread_create(&thread, NULL, webhook_thread_func, wa) == 0) {
|
if (pthread_create(&thread, NULL, webhook_thread_func, wa) == 0) {
|
||||||
pthread_detach(thread);
|
pthread_detach(thread);
|
||||||
} else {
|
} else {
|
||||||
bfree(wa->url);
|
bfree(wa->url);
|
||||||
bfree(wa->event_name);
|
bfree(wa->json_body);
|
||||||
bfree(wa->source_name);
|
|
||||||
bfree(wa);
|
bfree(wa);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+12
-1
@@ -1,6 +1,17 @@
|
|||||||
#pragma once
|
#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,
|
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);
|
void webhook_execute_command_async(const char *command);
|
||||||
|
|||||||
Reference in New Issue
Block a user