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; } }