using System;
using System.Net.Sockets;
using System.Threading;
using System.Threading.Tasks;
using ytLive.Helpers;
namespace ytLive.Services;
///
/// TASK 32ยท5: can we actually reach YouTube's ingest right now? One TCP connect, nothing else.
/// This is prevention, not recovery: "I forgot the VPN" used to produce a broadcast that YouTube
/// created and nobody ever saw โ the app looked live while the push had nowhere to go. Checking the
/// socket before the API call means the failure is a message, not a ghost broadcast.
///
public interface IIngestProbe
{
/// True when the ingest host accepted a TCP connection. False on any failure โ the
/// point is a fast, unambiguous answer, not a diagnosis.
Task CanReachAsync(string rtmpUrl, CancellationToken cancellationToken = default);
}
/// Default probe: parse the RTMP URL, connect to the host on its port (1935 for rtmp) with a
/// short timeout. Failures are logged and reported as "not reachable".
public sealed class TcpIngestProbe : IIngestProbe
{
public static readonly TimeSpan DefaultTimeout = TimeSpan.FromSeconds(2);
private readonly TimeSpan _timeout;
public TcpIngestProbe(TimeSpan? timeout = null) => _timeout = timeout ?? DefaultTimeout;
public async Task CanReachAsync(string rtmpUrl, CancellationToken cancellationToken = default)
{
if (!TryParse(rtmpUrl, out var host, out var port))
{
AppLog.Write($"Ingest probe: '{rtmpUrl}' is not a usable RTMP URL");
return false;
}
using var timeoutCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
timeoutCts.CancelAfter(_timeout);
try
{
using var client = new TcpClient();
await client.ConnectAsync(host, port, timeoutCts.Token).ConfigureAwait(false);
return client.Connected;
}
catch (OperationCanceledException)
{
AppLog.Write($"Ingest probe: {host}:{port} did not answer within {_timeout.TotalSeconds:0.#} s");
return false;
}
catch (Exception ex)
{
AppLog.Write($"Ingest probe: {host}:{port} unreachable: {ex.Message}");
return false;
}
}
/// rtmp://a.rtmp.youtube.com/live2/key โ (a.rtmp.youtube.com, 1935).
/// rtmps uses 443, which is what YouTube's secure ingest listens on.
internal static bool TryParse(string? rtmpUrl, out string host, out int port)
{
host = string.Empty;
port = 1935;
if (string.IsNullOrWhiteSpace(rtmpUrl)) return false;
if (!Uri.TryCreate(rtmpUrl, UriKind.Absolute, out var uri)) return false;
if (string.IsNullOrWhiteSpace(uri.Host)) return false;
host = uri.Host;
port = uri.IsDefaultPort
? (uri.Scheme.Equals("rtmps", StringComparison.OrdinalIgnoreCase) ? 443 : 1935)
: uri.Port;
return true;
}
}