From d6b55aef00b28cd2257f8379710dd26d39e5dddc Mon Sep 17 00:00:00 2001 From: kieran Date: Wed, 28 Aug 2024 11:02:47 +0100 Subject: [PATCH] Planned streams --- .../Controllers/PlaylistController.cs | 46 +- NostrStreamer/Database/UserStream.cs | 10 +- ...40828100209_PlannedUserStreams.Designer.cs | 562 ++++++++++++++++++ .../20240828100209_PlannedUserStreams.cs | 60 ++ .../StreamerContextModelSnapshot.cs | 9 +- NostrStreamer/NostrStreamer.csproj | 4 + NostrStreamer/Services/Clips/ClipGenerator.cs | 4 +- NostrStreamer/Services/LndNode.cs | 2 +- NostrStreamer/Services/StreamEventBuilder.cs | 4 +- .../StreamManager/NostrStreamManager.cs | 17 +- .../StreamManager/StreamManagerFactory.cs | 1 + .../Thumbnail/BaseThumbnailService.cs | 8 +- 12 files changed, 695 insertions(+), 32 deletions(-) create mode 100644 NostrStreamer/Migrations/20240828100209_PlannedUserStreams.Designer.cs create mode 100644 NostrStreamer/Migrations/20240828100209_PlannedUserStreams.cs diff --git a/NostrStreamer/Controllers/PlaylistController.cs b/NostrStreamer/Controllers/PlaylistController.cs index 3ee33ac..11c9b22 100644 --- a/NostrStreamer/Controllers/PlaylistController.cs +++ b/NostrStreamer/Controllers/PlaylistController.cs @@ -21,7 +21,8 @@ public class PlaylistController : Controller private readonly EdgeSteering _edgeSteering; public PlaylistController(Config config, ILogger logger, - HttpClient client, SrsApi srsApi, ViewCounter viewCounter, StreamManagerFactory streamManagerFactory, EdgeSteering edgeSteering) + HttpClient client, SrsApi srsApi, ViewCounter viewCounter, StreamManagerFactory streamManagerFactory, + EdgeSteering edgeSteering) { _config = config; _logger = logger; @@ -34,12 +35,18 @@ public class PlaylistController : Controller [ResponseCache(Duration = 1, Location = ResponseCacheLocation.Any)] [HttpGet("{variant}/{id}.m3u8")] - public async Task RewritePlaylist([FromRoute] Guid id, [FromRoute] string variant, [FromQuery(Name = "hls_ctx")] string hlsCtx) + public async Task RewritePlaylist([FromRoute] Guid id, [FromRoute] string variant, + [FromQuery(Name = "hls_ctx")] string hlsCtx) { try { var streamManager = await _streamManagerFactory.ForStream(id); var userStream = streamManager.GetStream(); + if (userStream.Endpoint == default) + { + Response.StatusCode = 404; + return; + } var path = $"/{userStream.Endpoint.App}/{variant}/{userStream.Key}.m3u8"; var ub = new UriBuilder(_config.SrsHttpHost) @@ -113,6 +120,11 @@ public class PlaylistController : Controller var edge = _edgeSteering.GetEdge(HttpContext); var streamManager = await _streamManagerFactory.ForStream(id); var userStream = streamManager.GetStream(); + if (userStream.Endpoint == default) + { + Response.StatusCode = 404; + return; + } var hlsCtx = await GetHlsCtx(userStream); if (string.IsNullOrEmpty(hlsCtx)) @@ -132,18 +144,23 @@ public class PlaylistController : Controller var stream = streams.FirstOrDefault(a => a.Name == userStream.Key && a.App == $"{userStream.Endpoint.App}/{variant.SourceName}"); - var resArg = stream?.Video != default ? $"RESOLUTION={stream.Video?.Width}x{stream.Video?.Height}" : - variant.ToResolutionArg(); + var resArg = stream?.Video != default + ? $"RESOLUTION={stream.Video?.Width}x{stream.Video?.Height}" + : variant.ToResolutionArg(); var bandwidthArg = variant.ToBandwidthArg(); - var averageBandwidthArg = stream?.Kbps?.Recv30s.HasValue ?? false ? $"AVERAGE-BANDWIDTH={stream.Kbps.Recv30s * 1000}" : ""; + var averageBandwidthArg = stream?.Kbps?.Recv30s.HasValue ?? false + ? $"AVERAGE-BANDWIDTH={stream.Kbps.Recv30s * 1000}" + : ""; var codecArg = "CODECS=\"avc1.640028,mp4a.40.2\""; - var allArgs = new[] {bandwidthArg, averageBandwidthArg, resArg, codecArg}.Where(a => !string.IsNullOrEmpty(a)); + var allArgs = + new[] { bandwidthArg, averageBandwidthArg, resArg, codecArg }.Where(a => !string.IsNullOrEmpty(a)); await sw.WriteLineAsync( $"#EXT-X-STREAM-INF:{string.Join(",", allArgs)}"); - var path = $"{variant.SourceName}/{userStream.Id}.m3u8{(!string.IsNullOrEmpty(hlsCtx) ? $"?hls_ctx={hlsCtx}" : "")}"; + var path = + $"{variant.SourceName}/{userStream.Id}.m3u8{(!string.IsNullOrEmpty(hlsCtx) ? $"?hls_ctx={hlsCtx}" : "")}"; if (edge != default) { var u = new Uri(edge.Url, path); @@ -170,6 +187,11 @@ public class PlaylistController : Controller { var streamManager = await _streamManagerFactory.ForStream(id); var userStream = streamManager.GetStream(); + if (userStream.Endpoint == default) + { + Response.StatusCode = 404; + return; + } var path = $"/{userStream.Endpoint.App}/{variant}/{userStream.Key}-{segment}"; await ProxyRequest(path); @@ -253,6 +275,8 @@ public class PlaylistController : Controller private async Task GetHlsCtx(UserStream stream) { + if (stream.Endpoint == default) return default; + var path = $"/{stream.Endpoint.App}/source/{stream.Key}.m3u8"; var ub = new Uri(_config.SrsHttpHost, path); var req = CreateProxyRequest(ub); @@ -292,11 +316,13 @@ public class PlaylistController : Controller private HttpRequestMessage CreateProxyRequest(Uri u) { var req = new HttpRequestMessage(HttpMethod.Get, u); - if (Request.Headers.TryGetValue("X-Forwarded-For", out var xff) || HttpContext.Connection.RemoteIpAddress != default) + if (Request.Headers.TryGetValue("X-Forwarded-For", out var xff) || + HttpContext.Connection.RemoteIpAddress != default) { - req.Headers.Add("X-Forwarded-For", xff.Count > 0 ? xff.ToString() : HttpContext.Connection.RemoteIpAddress!.ToString()); + req.Headers.Add("X-Forwarded-For", + xff.Count > 0 ? xff.ToString() : HttpContext.Connection.RemoteIpAddress!.ToString()); } return req; } -} +} \ No newline at end of file diff --git a/NostrStreamer/Database/UserStream.cs b/NostrStreamer/Database/UserStream.cs index 046548f..05d14d7 100644 --- a/NostrStreamer/Database/UserStream.cs +++ b/NostrStreamer/Database/UserStream.cs @@ -53,20 +53,20 @@ public class UserStream /// public string? Thumbnail { get; set; } - public Guid EndpointId { get; set; } - public IngestEndpoint Endpoint { get; init; } = null!; + public Guid? EndpointId { get; set; } + public IngestEndpoint? Endpoint { get; init; } = null!; /// /// Publisher edge IP /// - public string EdgeIp { get; set; } = null!; + public string? EdgeIp { get; set; } /// /// Publisher edge client id /// - public string ForwardClientId { get; set; } = null!; + public string? ForwardClientId { get; set; } - public DateTime LastSegment { get; set; } = DateTime.UtcNow; + public DateTime? LastSegment { get; set; } /// /// Total sats charged during this stream diff --git a/NostrStreamer/Migrations/20240828100209_PlannedUserStreams.Designer.cs b/NostrStreamer/Migrations/20240828100209_PlannedUserStreams.Designer.cs new file mode 100644 index 0000000..2e22c49 --- /dev/null +++ b/NostrStreamer/Migrations/20240828100209_PlannedUserStreams.Designer.cs @@ -0,0 +1,562 @@ +// +using System; +using System.Collections.Generic; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using NostrStreamer.Database; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace NostrStreamer.Migrations +{ + [DbContext(typeof(StreamerContext))] + [Migration("20240828100209_PlannedUserStreams")] + partial class PlannedUserStreams + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "7.0.8") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("NostrStreamer.Database.IngestEndpoint", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("App") + .IsRequired() + .HasColumnType("text"); + + b.Property>("Capabilities") + .IsRequired() + .HasColumnType("text[]"); + + b.Property("Cost") + .HasColumnType("integer"); + + b.Property("Forward") + .IsRequired() + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("App") + .IsUnique(); + + b.ToTable("Endpoints"); + }); + + modelBuilder.Entity("NostrStreamer.Database.Payment", b => + { + b.Property("PaymentHash") + .HasColumnType("text"); + + b.Property("Amount") + .HasColumnType("numeric(20,0)"); + + b.Property("Created") + .HasColumnType("timestamp with time zone"); + + b.Property("Fee") + .HasColumnType("numeric(20,0)"); + + b.Property("Invoice") + .HasColumnType("text"); + + b.Property("IsPaid") + .HasColumnType("boolean"); + + b.Property("Nostr") + .HasColumnType("text"); + + b.Property("PubKey") + .IsRequired() + .HasColumnType("text"); + + b.Property("Type") + .HasColumnType("integer"); + + b.HasKey("PaymentHash"); + + b.HasIndex("PubKey"); + + b.ToTable("Payments"); + }); + + modelBuilder.Entity("NostrStreamer.Database.PushSubscription", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Auth") + .IsRequired() + .HasColumnType("text"); + + b.Property("Created") + .HasColumnType("timestamp with time zone"); + + b.Property("Endpoint") + .IsRequired() + .HasColumnType("text"); + + b.Property("Key") + .IsRequired() + .HasColumnType("text"); + + b.Property("LastUsed") + .HasColumnType("timestamp with time zone"); + + b.Property("Pubkey") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("Scope") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("PushSubscriptions"); + }); + + modelBuilder.Entity("NostrStreamer.Database.PushSubscriptionTarget", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("SubscriberPubkey") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("TargetPubkey") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.HasKey("Id"); + + b.HasIndex("TargetPubkey"); + + b.HasIndex("SubscriberPubkey", "TargetPubkey") + .IsUnique(); + + b.ToTable("PushSubscriptionTargets"); + }); + + modelBuilder.Entity("NostrStreamer.Database.StreamTickets", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Created") + .HasColumnType("timestamp with time zone"); + + b.Property("Token") + .HasColumnType("uuid"); + + b.Property("UserStreamId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("UserStreamId"); + + b.ToTable("StreamTickets"); + }); + + modelBuilder.Entity("NostrStreamer.Database.User", b => + { + b.Property("PubKey") + .HasColumnType("text"); + + b.Property("Balance") + .HasColumnType("bigint"); + + b.Property("IsAdmin") + .HasColumnType("boolean"); + + b.Property("IsBlocked") + .HasColumnType("boolean"); + + b.Property("StreamKey") + .IsRequired() + .HasColumnType("text"); + + b.Property("TosAccepted") + .HasColumnType("timestamp with time zone"); + + b.Property("Version") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("xid") + .HasColumnName("xmin"); + + b.HasKey("PubKey"); + + b.ToTable("Users"); + }); + + modelBuilder.Entity("NostrStreamer.Database.UserStream", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AdmissionCost") + .HasColumnType("numeric"); + + b.Property("ContentWarning") + .HasColumnType("text"); + + b.Property("EdgeIp") + .IsRequired() + .HasColumnType("text"); + + b.Property("EndpointId") + .HasColumnType("uuid"); + + b.Property("Ends") + .HasColumnType("timestamp with time zone"); + + b.Property("Event") + .IsRequired() + .HasColumnType("text"); + + b.Property("ForwardClientId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Goal") + .HasColumnType("text"); + + b.Property("Image") + .HasColumnType("text"); + + b.Property("LastSegment") + .IsRequired() + .HasColumnType("timestamp with time zone"); + + b.Property("Length") + .HasColumnType("numeric"); + + b.Property("MilliSatsCollected") + .HasColumnType("numeric"); + + b.Property("PubKey") + .IsRequired() + .HasColumnType("text"); + + b.Property("Starts") + .HasColumnType("timestamp with time zone"); + + b.Property("State") + .HasColumnType("integer"); + + b.Property("Summary") + .HasColumnType("text"); + + b.Property("Tags") + .HasColumnType("text"); + + b.Property("Thumbnail") + .HasColumnType("text"); + + b.Property("Title") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("EndpointId"); + + b.HasIndex("PubKey"); + + b.ToTable("Streams"); + }); + + modelBuilder.Entity("NostrStreamer.Database.UserStreamClip", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Created") + .HasColumnType("timestamp with time zone"); + + b.Property("TakenByPubkey") + .IsRequired() + .HasColumnType("text"); + + b.Property("Url") + .IsRequired() + .HasColumnType("text"); + + b.Property("UserStreamId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("UserStreamId"); + + b.ToTable("Clips"); + }); + + modelBuilder.Entity("NostrStreamer.Database.UserStreamForwards", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("Target") + .IsRequired() + .HasColumnType("text"); + + b.Property("UserPubkey") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("UserPubkey"); + + b.ToTable("Forwards"); + }); + + modelBuilder.Entity("NostrStreamer.Database.UserStreamGuest", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("PubKey") + .IsRequired() + .HasColumnType("text"); + + b.Property("Relay") + .HasColumnType("text"); + + b.Property("Role") + .HasColumnType("text"); + + b.Property("Sig") + .HasColumnType("text"); + + b.Property("StreamId") + .HasColumnType("uuid"); + + b.Property("ZapSplit") + .HasColumnType("numeric"); + + b.HasKey("Id"); + + b.HasIndex("StreamId", "PubKey") + .IsUnique(); + + b.ToTable("Guests"); + }); + + modelBuilder.Entity("NostrStreamer.Database.UserStreamKey", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Created") + .HasColumnType("timestamp with time zone"); + + b.Property("Expires") + .HasColumnType("timestamp with time zone"); + + b.Property("Key") + .IsRequired() + .HasColumnType("text"); + + b.Property("StreamId") + .HasColumnType("uuid"); + + b.Property("UserPubkey") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("StreamId") + .IsUnique(); + + b.HasIndex("UserPubkey"); + + b.ToTable("StreamKeys"); + }); + + modelBuilder.Entity("NostrStreamer.Database.UserStreamRecording", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Duration") + .HasColumnType("double precision"); + + b.Property("Timestamp") + .HasColumnType("timestamp with time zone"); + + b.Property("Url") + .IsRequired() + .HasColumnType("text"); + + b.Property("UserStreamId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("UserStreamId"); + + b.ToTable("Recordings"); + }); + + modelBuilder.Entity("NostrStreamer.Database.Payment", b => + { + b.HasOne("NostrStreamer.Database.User", "User") + .WithMany("Payments") + .HasForeignKey("PubKey") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("NostrStreamer.Database.StreamTickets", b => + { + b.HasOne("NostrStreamer.Database.UserStream", "UserStream") + .WithMany() + .HasForeignKey("UserStreamId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("UserStream"); + }); + + modelBuilder.Entity("NostrStreamer.Database.UserStream", b => + { + b.HasOne("NostrStreamer.Database.IngestEndpoint", "Endpoint") + .WithMany() + .HasForeignKey("EndpointId"); + + b.HasOne("NostrStreamer.Database.User", "User") + .WithMany("Streams") + .HasForeignKey("PubKey") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Endpoint"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("NostrStreamer.Database.UserStreamClip", b => + { + b.HasOne("NostrStreamer.Database.UserStream", "UserStream") + .WithMany() + .HasForeignKey("UserStreamId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("UserStream"); + }); + + modelBuilder.Entity("NostrStreamer.Database.UserStreamForwards", b => + { + b.HasOne("NostrStreamer.Database.User", "User") + .WithMany("Forwards") + .HasForeignKey("UserPubkey") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("NostrStreamer.Database.UserStreamGuest", b => + { + b.HasOne("NostrStreamer.Database.UserStream", "Stream") + .WithMany("Guests") + .HasForeignKey("StreamId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Stream"); + }); + + modelBuilder.Entity("NostrStreamer.Database.UserStreamKey", b => + { + b.HasOne("NostrStreamer.Database.UserStream", "UserStream") + .WithOne("StreamKey") + .HasForeignKey("NostrStreamer.Database.UserStreamKey", "StreamId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("NostrStreamer.Database.User", "User") + .WithMany("StreamKeys") + .HasForeignKey("UserPubkey") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + + b.Navigation("UserStream"); + }); + + modelBuilder.Entity("NostrStreamer.Database.UserStreamRecording", b => + { + b.HasOne("NostrStreamer.Database.UserStream", "Stream") + .WithMany("Recordings") + .HasForeignKey("UserStreamId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Stream"); + }); + + modelBuilder.Entity("NostrStreamer.Database.User", b => + { + b.Navigation("Forwards"); + + b.Navigation("Payments"); + + b.Navigation("StreamKeys"); + + b.Navigation("Streams"); + }); + + modelBuilder.Entity("NostrStreamer.Database.UserStream", b => + { + b.Navigation("Guests"); + + b.Navigation("Recordings"); + + b.Navigation("StreamKey"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/NostrStreamer/Migrations/20240828100209_PlannedUserStreams.cs b/NostrStreamer/Migrations/20240828100209_PlannedUserStreams.cs new file mode 100644 index 0000000..2c85e4b --- /dev/null +++ b/NostrStreamer/Migrations/20240828100209_PlannedUserStreams.cs @@ -0,0 +1,60 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace NostrStreamer.Migrations +{ + /// + public partial class PlannedUserStreams : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "FK_Streams_Endpoints_EndpointId", + table: "Streams"); + + migrationBuilder.AlterColumn( + name: "EndpointId", + table: "Streams", + type: "uuid", + nullable: true, + oldClrType: typeof(Guid), + oldType: "uuid"); + + migrationBuilder.AddForeignKey( + name: "FK_Streams_Endpoints_EndpointId", + table: "Streams", + column: "EndpointId", + principalTable: "Endpoints", + principalColumn: "Id"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "FK_Streams_Endpoints_EndpointId", + table: "Streams"); + + migrationBuilder.AlterColumn( + name: "EndpointId", + table: "Streams", + type: "uuid", + nullable: false, + defaultValue: new Guid("00000000-0000-0000-0000-000000000000"), + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true); + + migrationBuilder.AddForeignKey( + name: "FK_Streams_Endpoints_EndpointId", + table: "Streams", + column: "EndpointId", + principalTable: "Endpoints", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + } + } +} diff --git a/NostrStreamer/Migrations/StreamerContextModelSnapshot.cs b/NostrStreamer/Migrations/StreamerContextModelSnapshot.cs index b84bb67..6d0f5d7 100644 --- a/NostrStreamer/Migrations/StreamerContextModelSnapshot.cs +++ b/NostrStreamer/Migrations/StreamerContextModelSnapshot.cs @@ -227,7 +227,7 @@ namespace NostrStreamer.Migrations .IsRequired() .HasColumnType("text"); - b.Property("EndpointId") + b.Property("EndpointId") .HasColumnType("uuid"); b.Property("Ends") @@ -247,7 +247,8 @@ namespace NostrStreamer.Migrations b.Property("Image") .HasColumnType("text"); - b.Property("LastSegment") + b.Property("LastSegment") + .IsRequired() .HasColumnType("timestamp with time zone"); b.Property("Length") @@ -457,9 +458,7 @@ namespace NostrStreamer.Migrations { b.HasOne("NostrStreamer.Database.IngestEndpoint", "Endpoint") .WithMany() - .HasForeignKey("EndpointId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); + .HasForeignKey("EndpointId"); b.HasOne("NostrStreamer.Database.User", "User") .WithMany("Streams") diff --git a/NostrStreamer/NostrStreamer.csproj b/NostrStreamer/NostrStreamer.csproj index 1f96bd4..069aeee 100644 --- a/NostrStreamer/NostrStreamer.csproj +++ b/NostrStreamer/NostrStreamer.csproj @@ -7,6 +7,10 @@ Linux + + ;NU1605;SYSLIB0011;CS8602 + + diff --git a/NostrStreamer/Services/Clips/ClipGenerator.cs b/NostrStreamer/Services/Clips/ClipGenerator.cs index 25c5987..8d8a2ce 100644 --- a/NostrStreamer/Services/Clips/ClipGenerator.cs +++ b/NostrStreamer/Services/Clips/ClipGenerator.cs @@ -47,6 +47,7 @@ public class ClipGenerator public async Task> GetClipSegments(UserStream stream) { var ret = new List(); + if (stream.Endpoint == default) return ret; var ctx = await GetHlsCtx(stream); var path = $"/{stream.Endpoint.App}/source/{stream.User.StreamKey}.m3u8?hls_ctx={ctx}"; var ub = new Uri(_config.SrsHttpHost, path); @@ -89,6 +90,7 @@ public class ClipGenerator private async Task GetHlsCtx(UserStream stream) { + if (stream.Endpoint == default) return null; var path = $"/{stream.Endpoint.App}/source/{stream.User.StreamKey}.m3u8"; var ub = new Uri(_config.SrsHttpHost, path); var req = new HttpRequestMessage(HttpMethod.Get, ub); @@ -115,4 +117,4 @@ public class ClipGenerator return default; } -} +} \ No newline at end of file diff --git a/NostrStreamer/Services/LndNode.cs b/NostrStreamer/Services/LndNode.cs index ca2e42e..d6c0f0c 100644 --- a/NostrStreamer/Services/LndNode.cs +++ b/NostrStreamer/Services/LndNode.cs @@ -146,7 +146,7 @@ public class LndNode { req.Memo = decoded.ShortDescription; } - else if (decoded.DescriptionHash != default) + else if (decoded.DescriptionHash != null) { req.DescriptionHash = ByteString.CopyFrom(decoded.DescriptionHash.ToBytes(false)); } diff --git a/NostrStreamer/Services/StreamEventBuilder.cs b/NostrStreamer/Services/StreamEventBuilder.cs index 64216d2..4a55f7c 100644 --- a/NostrStreamer/Services/StreamEventBuilder.cs +++ b/NostrStreamer/Services/StreamEventBuilder.cs @@ -58,8 +58,8 @@ public class StreamEventBuilder } else if (status == "ended") { - if (stream.Endpoint.Capabilities - .Any(a => a.StartsWith("dvr:", StringComparison.InvariantCultureIgnoreCase))) + if (stream.Endpoint?.Capabilities + .Any(a => a.StartsWith("dvr:", StringComparison.InvariantCultureIgnoreCase)) ?? false) { tags.Add(new("recording", new Uri(_config.DataHost, $"recording/{stream.Id}.m3u8").ToString())); } diff --git a/NostrStreamer/Services/StreamManager/NostrStreamManager.cs b/NostrStreamer/Services/StreamManager/NostrStreamManager.cs index 8350f40..1a4b74d 100644 --- a/NostrStreamer/Services/StreamManager/NostrStreamManager.cs +++ b/NostrStreamer/Services/StreamManager/NostrStreamManager.cs @@ -55,10 +55,12 @@ public class NostrStreamManager : IStreamManager public Task> OnForward() { TestCanStream(); - var fwds = new List + var fwds = new List(); + if (_context.UserStream.Endpoint != default) { - $"rtmp://127.0.0.1:1935/{_context.UserStream.Endpoint.App}/{_context.StreamKey}?vhost={_context.UserStream.Endpoint.Forward}" - }; + fwds.Add( + $"rtmp://127.0.0.1:1935/{_context.UserStream.Endpoint.App}/{_context.StreamKey}?vhost={_context.UserStream.Endpoint.Forward}"); + } var dataProtector = _dataProtectionProvider.CreateProtector("forward-targets"); foreach (var f in _context.User.Forwards) @@ -125,6 +127,8 @@ public class NostrStreamManager : IStreamManager public async Task ConsumeQuota(double duration) { const long balanceAlertThreshold = 500_000; + + if (_context.UserStream.Endpoint == default) return; var cost = (long)Math.Ceiling(_context.UserStream.Endpoint.Cost * (duration / 60d)); if (cost > 0) { @@ -165,7 +169,10 @@ public class NostrStreamManager : IStreamManager if (_context.User.Balance <= 0) { _logger.LogInformation("Kicking stream due to low balance"); - await _context.EdgeApi.KickClient(_context.UserStream.ForwardClientId); + if (!string.IsNullOrEmpty(_context.UserStream.ForwardClientId)) + { + await _context.EdgeApi.KickClient(_context.UserStream.ForwardClientId); + } } } @@ -195,7 +202,7 @@ public class NostrStreamManager : IStreamManager try { - if (_context.UserStream.Endpoint.Capabilities.Contains("dvr:source")) + if (_context.UserStream.Endpoint?.Capabilities.Contains("dvr:source") ?? false) { var result = await _dvrStore.UploadRecording(_context.UserStream, segment); _context.Db.Recordings.Add(new() diff --git a/NostrStreamer/Services/StreamManager/StreamManagerFactory.cs b/NostrStreamer/Services/StreamManager/StreamManagerFactory.cs index 4b04a7c..59d5698 100644 --- a/NostrStreamer/Services/StreamManager/StreamManagerFactory.cs +++ b/NostrStreamer/Services/StreamManager/StreamManagerFactory.cs @@ -166,6 +166,7 @@ public class StreamManagerFactory .FirstOrDefaultAsync(a => (a.StreamKey != default && a.StreamKey.Key == info.StreamKey) || (a.User.StreamKey.Equals(info.StreamKey) && + a.Endpoint != null && a.Endpoint.App.Equals(info.App) && a.State == UserStreamState.Live)); diff --git a/NostrStreamer/Services/Thumbnail/BaseThumbnailService.cs b/NostrStreamer/Services/Thumbnail/BaseThumbnailService.cs index 4c6f4ec..4703fcc 100644 --- a/NostrStreamer/Services/Thumbnail/BaseThumbnailService.cs +++ b/NostrStreamer/Services/Thumbnail/BaseThumbnailService.cs @@ -14,11 +14,13 @@ public abstract class BaseThumbnailService Logger = logger; } - protected async Task GenerateThumbnail(UserStream stream) + protected async Task GenerateThumbnail(UserStream stream) { + if (stream.Endpoint == default) return default; var path = Path.ChangeExtension(Path.GetTempFileName(), ".jpg"); var cmd = FFMpegArguments - .FromUrlInput(new Uri(Config.RtmpHost, $"{stream.Endpoint.App}/source/{stream.User.StreamKey}?vhost=hls.zap.stream")) + .FromUrlInput(new Uri(Config.RtmpHost, + $"{stream.Endpoint.App}/source/{stream.User.StreamKey}?vhost=hls.zap.stream")) .OutputToFile(path, true, o => { o.ForceFormat("image2").WithCustomArgument("-vframes 1"); }) .CancellableThrough(new CancellationTokenSource(TimeSpan.FromSeconds(10)).Token); @@ -26,4 +28,4 @@ public abstract class BaseThumbnailService await cmd.ProcessAsynchronously(); return path; } -} +} \ No newline at end of file