From 6d1190bee849f1ed634a35a3cbec80b5c3888573 Mon Sep 17 00:00:00 2001 From: Joseph Tam Date: Tue, 1 Oct 2024 21:22:47 +0800 Subject: [PATCH] feat: add RtmpUrlParser --- .../Program.cs | 14 ++- .../RtmpUrlParser.cs | 100 ++++++++++++++++++ 2 files changed, 110 insertions(+), 4 deletions(-) create mode 100644 src/LiveStreamingServerNet.Rtmp.Client/RtmpUrlParser.cs diff --git a/samples/LiveStreamingServerNet.RtmpClientPlayDemo/Program.cs b/samples/LiveStreamingServerNet.RtmpClientPlayDemo/Program.cs index fa97461b..078e5869 100644 --- a/samples/LiveStreamingServerNet.RtmpClientPlayDemo/Program.cs +++ b/samples/LiveStreamingServerNet.RtmpClientPlayDemo/Program.cs @@ -15,19 +15,25 @@ public class Program /// public static async Task Main() { + var rtmpUrl = "rtmp://127.0.0.1/live/demo"; + var parsedRtmpUrl = await RtmpUrlParser.ParseAsync(rtmpUrl); + + var serverEndPoint = parsedRtmpUrl.ServerEndPoint; + var information = new Dictionary { ["tcUrl"] = parsedRtmpUrl.TcUrl }; + var appName = parsedRtmpUrl.AppName; + var streamName = parsedRtmpUrl.StreamName; + var rtmpClient = RtmpClientBuilder.Create() .ConfigureLogging(options => options.AddConsole().SetMinimumLevel(LogLevel.Trace)) .Build(); var logger = rtmpClient.Services.GetRequiredService>(); - var serverEndPoint = new IPEndPoint(IPAddress.Parse("127.0.0.1"), 1935); - var information = new Dictionary { ["tcUrl"] = "rtmp://127.0.0.1/live" }; - await rtmpClient.ConnectAsync(serverEndPoint, "live", information); + await rtmpClient.ConnectAsync(serverEndPoint, appName, information); var rtmpStream = await rtmpClient.CreateStreamAsync(); - await RecordStreamAsFlvAsync("demo", rtmpClient, rtmpStream, logger); + await RecordStreamAsFlvAsync(streamName, rtmpClient, rtmpStream, logger); } private static async Task RecordStreamAsFlvAsync(string streamName, IRtmpClient rtmpClient, IRtmpStream rtmpStream, ILogger logger) diff --git a/src/LiveStreamingServerNet.Rtmp.Client/RtmpUrlParser.cs b/src/LiveStreamingServerNet.Rtmp.Client/RtmpUrlParser.cs new file mode 100644 index 00000000..2d0d2cee --- /dev/null +++ b/src/LiveStreamingServerNet.Rtmp.Client/RtmpUrlParser.cs @@ -0,0 +1,100 @@ +using System.Net.Sockets; +using System.Net; +using System.Text.RegularExpressions; +using LiveStreamingServerNet.Networking; + +namespace LiveStreamingServerNet.Rtmp.Client +{ + public static class RtmpUrlParser + { + private static Regex _regex = new Regex( + @"^(?rtmp[s]?)://(?\[[^\]]+\]|[^:/]+)(?::(?\d+))?/(?[^/]+)/(?.+)$", + RegexOptions.IgnoreCase | RegexOptions.Compiled + ); + + public static async ValueTask ParseAsync(string rtmpUrl) + { + if (string.IsNullOrWhiteSpace(rtmpUrl)) + throw new ArgumentException("RTMP URL cannot be null or empty."); + + rtmpUrl = rtmpUrl.Trim(); + + var match = _regex.Match(rtmpUrl); + + if (!match.Success) + throw new ArgumentException("Invalid RTMP URL format."); + + var schemeRaw = match.Groups["scheme"].Value.ToLower(); + var hostnameRaw = match.Groups["hostname"].Value; + var portStr = match.Groups["port"].Value; + var appName = match.Groups["appName"].Value; + var streamName = match.Groups["streamName"].Value; + + if (!Enum.TryParse(schemeRaw, true, out var scheme)) + throw new ArgumentException("Invalid RTMP scheme in RTMP URL."); + + var hostname = NormalizeHostname(hostnameRaw); + + var port = portStr switch + { + var str when (string.IsNullOrEmpty(str)) => scheme == RtmpScheme.RTMPS ? 443 : 1935, + _ => int.TryParse(portStr, out var p) ? p : throw new ArgumentException("Invalid port number in RTMP URL.") + }; + + var ipAddress = await GetIpAddressAsync(hostname); + var serverEndPoint = new ServerEndPoint(new IPEndPoint(ipAddress, port), scheme == RtmpScheme.RTMPS); + var tcUrl = $"{schemeRaw}://{hostname}:{port}/{appName}"; + return new ParsedRtmpUrl(serverEndPoint, appName, streamName, tcUrl); + } + + private static string NormalizeHostname(string hostnameRaw) + { + if (hostnameRaw.StartsWith("[") && hostnameRaw.EndsWith("]")) + return hostnameRaw.Substring(1, hostnameRaw.Length - 2); + + return hostnameRaw; + } + + private static async ValueTask GetIpAddressAsync(string hostname) + { + if (IPAddress.TryParse(hostname, out var ipAddress) && ipAddress != null) + return ipAddress; + + return await ResolveHostnameToIpAddressAsync(hostname); + } + + private static async Task ResolveHostnameToIpAddressAsync(string hostname) + { + try + { + var addresses = await Dns.GetHostAddressesAsync(hostname); + + foreach (var address in addresses) + { + if (address.AddressFamily == AddressFamily.InterNetwork) + return address; + } + + foreach (var address in addresses) + { + if (address.AddressFamily == AddressFamily.InterNetworkV6) + return address; + } + + throw new SocketException((int)SocketError.HostNotFound); + } + catch (SocketException ex) + { + throw new SocketException((int)ex.SocketErrorCode); + } + } + + private enum RtmpScheme + { + RTMP, + RTMPS + } + } + + public record ParsedRtmpUrl(ServerEndPoint ServerEndPoint, string AppName, string StreamName, string TcUrl); +}