diff --git a/NostrStreamer/Controllers/AdminController.cs b/NostrStreamer/Controllers/AdminController.cs new file mode 100644 index 0000000..ba3979d --- /dev/null +++ b/NostrStreamer/Controllers/AdminController.cs @@ -0,0 +1,24 @@ +using Microsoft.AspNetCore.Mvc; +using NostrStreamer.Services.StreamManager; + +namespace NostrStreamer.Controllers; + +[Route("/api/admin")] +public class AdminController : Controller +{ + private readonly ILogger _logger; + private readonly StreamManagerFactory _streamManagerFactory; + + public AdminController(ILogger logger, StreamManagerFactory streamManagerFactory) + { + _logger = logger; + _streamManagerFactory = streamManagerFactory; + } + + [HttpPatch("stream/{id:guid}")] + public async Task PublishEvent([FromRoute] Guid id) + { + var stream = await _streamManagerFactory.ForStream(id); + await stream.UpdateEvent(); + } +} diff --git a/NostrStreamer/Controllers/PlaylistController.cs b/NostrStreamer/Controllers/PlaylistController.cs index f5c592d..597c7fc 100644 --- a/NostrStreamer/Controllers/PlaylistController.cs +++ b/NostrStreamer/Controllers/PlaylistController.cs @@ -118,7 +118,7 @@ public class PlaylistController : Controller { var streamManager = await _streamManagerFactory.ForCurrentStream(pubkey); var userStream = streamManager.GetStream(); - return Redirect($"{userStream.Id}.m3u8"); + return Redirect($"stream/{userStream.Id}.m3u8"); } catch (Exception ex) { @@ -128,7 +128,7 @@ public class PlaylistController : Controller return NotFound(); } - [HttpGet("{id:guid}.m3u8")] + [HttpGet("stream/{id:guid}.m3u8")] public async Task CreateMultiBitrate([FromRoute] Guid id) { try diff --git a/NostrStreamer/Controllers/SRSController.cs b/NostrStreamer/Controllers/SRSController.cs index e46e665..fe64adf 100644 --- a/NostrStreamer/Controllers/SRSController.cs +++ b/NostrStreamer/Controllers/SRSController.cs @@ -33,19 +33,19 @@ public class SrsController : Controller } var appSplit = req.App.Split("/"); - var streamManager = await _streamManagerFactory.ForStream(new StreamInfo + var info = new StreamInfo { App = appSplit[0], Variant = appSplit.Length > 1 ? appSplit[1] : "", ClientId = req.ClientId!, - StreamId = req.StreamId ?? req.ClientId!, StreamKey = req.Stream, EdgeIp = req.Ip! - }); - + }; + if (req.Action == "on_forward") { - var urls = await streamManager.OnForward(); + var newStream = await _streamManagerFactory.CreateStream(info); + var urls = await newStream.OnForward(); if (urls.Count > 0) { return new SrsForwardHookReply @@ -63,6 +63,7 @@ public class SrsController : Controller }; } + var streamManager = await _streamManagerFactory.ForStream(info); if (req.App.EndsWith("/source")) { if (req.Action == "on_publish") @@ -132,7 +133,7 @@ public class SrsHook [JsonProperty("client_id")] public string? ClientId { get; set; } - + [JsonProperty("stream_id")] public string? StreamId { get; set; } diff --git a/NostrStreamer/Database/Configuration/UserStreamConfiguration.cs b/NostrStreamer/Database/Configuration/UserStreamConfiguration.cs index f39444f..1a7a601 100644 --- a/NostrStreamer/Database/Configuration/UserStreamConfiguration.cs +++ b/NostrStreamer/Database/Configuration/UserStreamConfiguration.cs @@ -24,6 +24,12 @@ public class UserStreamConfiguration : IEntityTypeConfiguration builder.Property(a => a.Recording); + builder.Property(a => a.EdgeIp) + .IsRequired(); + + builder.Property(a => a.ForwardClientId) + .IsRequired(); + builder.HasOne(a => a.Endpoint) .WithMany() .HasForeignKey(a => a.EndpointId); diff --git a/NostrStreamer/Database/UserStream.cs b/NostrStreamer/Database/UserStream.cs index 260ab80..d860051 100644 --- a/NostrStreamer/Database/UserStream.cs +++ b/NostrStreamer/Database/UserStream.cs @@ -27,6 +27,16 @@ public class UserStream public Guid EndpointId { get; init; } public IngestEndpoint Endpoint { get; init; } = null!; + + /// + /// Publisher edge IP + /// + public string EdgeIp { get; set; } = null!; + + /// + /// Publisher edge client id + /// + public string ForwardClientId { get; set; } = null!; public List Guests { get; init; } = new(); public List Recordings { get; init; } = new(); diff --git a/NostrStreamer/Migrations/20230731145446_ForwardClientDetails.Designer.cs b/NostrStreamer/Migrations/20230731145446_ForwardClientDetails.Designer.cs new file mode 100644 index 0000000..1751bf5 --- /dev/null +++ b/NostrStreamer/Migrations/20230731145446_ForwardClientDetails.Designer.cs @@ -0,0 +1,312 @@ +// +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("20230731145446_ForwardClientDetails")] + partial class ForwardClientDetails + { + /// + 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("Invoice") + .IsRequired() + .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.User", b => + { + b.Property("PubKey") + .HasColumnType("text"); + + b.Property("Balance") + .HasColumnType("bigint"); + + b.Property("ContentWarning") + .HasColumnType("text"); + + b.Property("Image") + .HasColumnType("text"); + + b.Property("StreamKey") + .IsRequired() + .HasColumnType("text"); + + b.Property("Summary") + .HasColumnType("text"); + + b.Property("Tags") + .HasColumnType("text"); + + b.Property("Title") + .HasColumnType("text"); + + 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("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("PubKey") + .IsRequired() + .HasColumnType("text"); + + b.Property("Recording") + .HasColumnType("text"); + + b.Property("Starts") + .HasColumnType("timestamp with time zone"); + + b.Property("State") + .HasColumnType("integer"); + + b.Property("StreamId") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("EndpointId"); + + b.HasIndex("PubKey"); + + b.ToTable("Streams"); + }); + + 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.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.UserStream", b => + { + b.HasOne("NostrStreamer.Database.IngestEndpoint", "Endpoint") + .WithMany() + .HasForeignKey("EndpointId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("NostrStreamer.Database.User", "User") + .WithMany("Streams") + .HasForeignKey("PubKey") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Endpoint"); + + 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.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("Payments"); + + b.Navigation("Streams"); + }); + + modelBuilder.Entity("NostrStreamer.Database.UserStream", b => + { + b.Navigation("Guests"); + + b.Navigation("Recordings"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/NostrStreamer/Migrations/20230731145446_ForwardClientDetails.cs b/NostrStreamer/Migrations/20230731145446_ForwardClientDetails.cs new file mode 100644 index 0000000..b07230d --- /dev/null +++ b/NostrStreamer/Migrations/20230731145446_ForwardClientDetails.cs @@ -0,0 +1,40 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace NostrStreamer.Migrations +{ + /// + public partial class ForwardClientDetails : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "EdgeIp", + table: "Streams", + type: "text", + nullable: false, + defaultValue: ""); + + migrationBuilder.AddColumn( + name: "ForwardClientId", + table: "Streams", + type: "text", + nullable: false, + defaultValue: ""); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "EdgeIp", + table: "Streams"); + + migrationBuilder.DropColumn( + name: "ForwardClientId", + table: "Streams"); + } + } +} diff --git a/NostrStreamer/Migrations/StreamerContextModelSnapshot.cs b/NostrStreamer/Migrations/StreamerContextModelSnapshot.cs index a62d8e2..d72d334 100644 --- a/NostrStreamer/Migrations/StreamerContextModelSnapshot.cs +++ b/NostrStreamer/Migrations/StreamerContextModelSnapshot.cs @@ -135,6 +135,10 @@ namespace NostrStreamer.Migrations .ValueGeneratedOnAdd() .HasColumnType("uuid"); + b.Property("EdgeIp") + .IsRequired() + .HasColumnType("text"); + b.Property("EndpointId") .HasColumnType("uuid"); @@ -145,6 +149,10 @@ namespace NostrStreamer.Migrations .IsRequired() .HasColumnType("text"); + b.Property("ForwardClientId") + .IsRequired() + .HasColumnType("text"); + b.Property("PubKey") .IsRequired() .HasColumnType("text"); diff --git a/NostrStreamer/Services/BackgroundStreamManager.cs b/NostrStreamer/Services/BackgroundStreamManager.cs index 9b0c141..773f305 100644 --- a/NostrStreamer/Services/BackgroundStreamManager.cs +++ b/NostrStreamer/Services/BackgroundStreamManager.cs @@ -27,9 +27,10 @@ public class BackgroundStreamManager : BackgroundService var db = scope.ServiceProvider.GetRequiredService(); var srs = scope.ServiceProvider.GetRequiredService(); + var recentlyEnded = DateTime.UtcNow.Subtract(TimeSpan.FromMinutes(5)); var liveStreams = await db.Streams .AsNoTracking() - .Where(a => a.State == UserStreamState.Live) + .Where(a => a.State == UserStreamState.Live || a.Ends > recentlyEnded) .Select(a => a.Id) .ToListAsync(cancellationToken: stoppingToken); diff --git a/NostrStreamer/Services/SrsApi.cs b/NostrStreamer/Services/SrsApi.cs index 9f4e3fb..2787baf 100644 --- a/NostrStreamer/Services/SrsApi.cs +++ b/NostrStreamer/Services/SrsApi.cs @@ -10,6 +10,14 @@ public class SrsApi { _client = client; _client.BaseAddress = config.SrsApiHost; + _client.Timeout = TimeSpan.FromSeconds(5); + } + + public SrsApi(HttpClient client, Uri baseAddress) + { + _client = client; + _client.BaseAddress = baseAddress; + _client.Timeout = TimeSpan.FromSeconds(5); } public async Task> ListStreams() diff --git a/NostrStreamer/Services/StreamEventBuilder.cs b/NostrStreamer/Services/StreamEventBuilder.cs index 2a4fcc2..2915bf8 100644 --- a/NostrStreamer/Services/StreamEventBuilder.cs +++ b/NostrStreamer/Services/StreamEventBuilder.cs @@ -47,7 +47,7 @@ public class StreamEventBuilder { var viewers = _viewCounter.Current(stream.Id); var starts = new DateTimeOffset(stream.Starts).ToUnixTimeSeconds(); - tags.Add(new("streaming", new Uri(_config.DataHost, $"{stream.Id}.m3u8").ToString())); + tags.Add(new("streaming", new Uri(_config.DataHost, $"stream/{stream.Id}.m3u8").ToString())); tags.Add(new("starts", starts.ToString())); tags.Add(new("current_participants", viewers.ToString())); diff --git a/NostrStreamer/Services/StreamManager/IStreamManager.cs b/NostrStreamer/Services/StreamManager/IStreamManager.cs index b0ba529..33daabd 100644 --- a/NostrStreamer/Services/StreamManager/IStreamManager.cs +++ b/NostrStreamer/Services/StreamManager/IStreamManager.cs @@ -73,4 +73,10 @@ public interface IStreamManager /// /// Task OnDvr(Uri segment); + + /// + /// Republish stream event + /// + /// + public Task UpdateEvent(); } \ No newline at end of file diff --git a/NostrStreamer/Services/StreamManager/NostrStreamManager.cs b/NostrStreamer/Services/StreamManager/NostrStreamManager.cs index a440c9a..98ab273 100644 --- a/NostrStreamer/Services/StreamManager/NostrStreamManager.cs +++ b/NostrStreamer/Services/StreamManager/NostrStreamManager.cs @@ -12,16 +12,14 @@ public class NostrStreamManager : IStreamManager private readonly ILogger _logger; private readonly StreamManagerContext _context; private readonly StreamEventBuilder _eventBuilder; - private readonly SrsApi _srsApi; private readonly IDvrStore _dvrStore; public NostrStreamManager(ILogger logger, StreamManagerContext context, - StreamEventBuilder eventBuilder, SrsApi srsApi, IDvrStore dvrStore) + StreamEventBuilder eventBuilder, IDvrStore dvrStore) { _logger = logger; _context = context; _eventBuilder = eventBuilder; - _srsApi = srsApi; _dvrStore = dvrStore; } @@ -87,7 +85,7 @@ public class NostrStreamManager : IStreamManager if (_context.User.Balance <= 0) { _logger.LogInformation("Kicking stream due to low balance"); - await _srsApi.KickClient(_context.StreamInfo.ClientId); + await _context.EdgeApi.KickClient(_context.UserStream.ForwardClientId); } } @@ -151,6 +149,11 @@ public class NostrStreamManager : IStreamManager await _context.Db.SaveChangesAsync(); } + + public async Task UpdateEvent() + { + await UpdateStreamState(_context.UserStream.State); + } public async Task UpdateViewers() { diff --git a/NostrStreamer/Services/StreamManager/StreamInfo.cs b/NostrStreamer/Services/StreamManager/StreamInfo.cs index 83bd260..a138ddc 100644 --- a/NostrStreamer/Services/StreamManager/StreamInfo.cs +++ b/NostrStreamer/Services/StreamManager/StreamInfo.cs @@ -10,7 +10,5 @@ public class StreamInfo public string ClientId { get; init; } = null!; - public string StreamId { get; init; } = null!; - public string EdgeIp { get; init; } = null!; } diff --git a/NostrStreamer/Services/StreamManager/StreamManagerContext.cs b/NostrStreamer/Services/StreamManager/StreamManagerContext.cs index 7173a83..02b1f5e 100644 --- a/NostrStreamer/Services/StreamManager/StreamManagerContext.cs +++ b/NostrStreamer/Services/StreamManager/StreamManagerContext.cs @@ -8,4 +8,5 @@ public class StreamManagerContext public UserStream UserStream { get; init; } = null!; public User User => UserStream.User; public StreamInfo? StreamInfo { get; init; } + public SrsApi EdgeApi { get; init; } = null!; } diff --git a/NostrStreamer/Services/StreamManager/StreamManagerFactory.cs b/NostrStreamer/Services/StreamManager/StreamManagerFactory.cs index 392e41a..0477b35 100644 --- a/NostrStreamer/Services/StreamManager/StreamManagerFactory.cs +++ b/NostrStreamer/Services/StreamManager/StreamManagerFactory.cs @@ -11,57 +11,118 @@ public class StreamManagerFactory private readonly StreamerContext _db; private readonly ILoggerFactory _loggerFactory; private readonly StreamEventBuilder _eventBuilder; - private readonly SrsApi _srsApi; + private readonly IServiceProvider _serviceProvider; private readonly IDvrStore _dvrStore; public StreamManagerFactory(StreamerContext db, ILoggerFactory loggerFactory, StreamEventBuilder eventBuilder, - SrsApi srsApi, IDvrStore dvrStore) + IServiceProvider serviceProvider, IDvrStore dvrStore) { _db = db; _loggerFactory = loggerFactory; _eventBuilder = eventBuilder; - _srsApi = srsApi; + _serviceProvider = serviceProvider; _dvrStore = dvrStore; } + public async Task CreateStream(StreamInfo info) + { + var user = await _db.Users + .AsNoTracking() + .SingleOrDefaultAsync(a => a.StreamKey.Equals(info.StreamKey)); + + if (user == default) throw new Exception("No user found"); + + var ep = await _db.Endpoints + .AsNoTracking() + .SingleOrDefaultAsync(a => a.App.Equals(info.App)); + + if (ep == default) throw new Exception("No endpoint found"); + + if (await _db.Streams.CountAsync(a => a.State == UserStreamState.Live && a.PubKey == user.PubKey) != 0) + { + throw new Exception("Cannot start a new stream when already live"); + } + + if (user.Balance <= 0) + { + throw new Exception("Cannot start stream with empty balance"); + } + + var stream = new UserStream + { + EndpointId = ep.Id, + PubKey = user.PubKey, + StreamId = "", + State = UserStreamState.Live, + EdgeIp = info.EdgeIp, + ForwardClientId = info.ClientId + }; + + var ev = _eventBuilder.CreateStreamEvent(user, stream); + stream.Event = JsonConvert.SerializeObject(ev, NostrSerializer.Settings); + _db.Streams.Add(stream); + await _db.SaveChangesAsync(); + + var ctx = new StreamManagerContext + { + Db = _db, + UserStream = new() + { + Id = stream.Id, + PubKey = stream.PubKey, + StreamId = stream.StreamId, + State = stream.State, + EdgeIp = stream.EdgeIp, + ForwardClientId = stream.ForwardClientId, + Endpoint = ep, + User = user + }, + EdgeApi = new SrsApi(_serviceProvider.GetRequiredService(), new Uri($"http://{stream.EdgeIp}:1985")) + }; + + return new NostrStreamManager(_loggerFactory.CreateLogger(), ctx, _eventBuilder, _dvrStore); + } + public async Task ForStream(Guid id) { - var currentStream = await _db.Streams + var stream = await _db.Streams .AsNoTracking() .Include(a => a.User) .Include(a => a.Endpoint) .Include(a => a.Recordings) .FirstOrDefaultAsync(a => a.Id == id); - if (currentStream == default) throw new Exception("No live stream"); + if (stream == default) throw new Exception("No live stream"); var ctx = new StreamManagerContext { Db = _db, - UserStream = currentStream + UserStream = stream, + EdgeApi = new SrsApi(_serviceProvider.GetRequiredService(), new Uri($"http://{stream.EdgeIp}:1985")) }; - return new NostrStreamManager(_loggerFactory.CreateLogger(), ctx, _eventBuilder, _srsApi, _dvrStore); + return new NostrStreamManager(_loggerFactory.CreateLogger(), ctx, _eventBuilder, _dvrStore); } public async Task ForCurrentStream(string pubkey) { - var currentStream = await _db.Streams + var stream = await _db.Streams .AsNoTracking() .Include(a => a.User) .Include(a => a.Endpoint) .Include(a => a.Recordings) .FirstOrDefaultAsync(a => a.PubKey.Equals(pubkey) && a.State == UserStreamState.Live); - if (currentStream == default) throw new Exception("No live stream"); + if (stream == default) throw new Exception("No live stream"); var ctx = new StreamManagerContext { Db = _db, - UserStream = currentStream + UserStream = stream, + EdgeApi = new SrsApi(_serviceProvider.GetRequiredService(), new Uri($"http://{stream.EdgeIp}:1985")) }; - return new NostrStreamManager(_loggerFactory.CreateLogger(), ctx, _eventBuilder, _srsApi, _dvrStore); + return new NostrStreamManager(_loggerFactory.CreateLogger(), ctx, _eventBuilder, _dvrStore); } public async Task ForStream(StreamInfo info) @@ -71,60 +132,25 @@ public class StreamManagerFactory .Include(a => a.User) .Include(a => a.Endpoint) .Include(a => a.Recordings) + .OrderByDescending(a => a.Starts) .FirstOrDefaultAsync(a => - a.StreamId.Equals(info.StreamId) && a.User.StreamKey.Equals(info.StreamKey) && - a.Endpoint.App.Equals(info.App)); + a.Endpoint.App.Equals(info.App) && + a.State == UserStreamState.Live); if (stream == default) { - var user = await _db.Users - .AsNoTracking() - .SingleOrDefaultAsync(a => a.StreamKey.Equals(info.StreamKey)); - - if (user == default) throw new Exception("No user found"); - - var ep = await _db.Endpoints - .AsNoTracking() - .SingleOrDefaultAsync(a => a.App.Equals(info.App)); - - if (ep == default) throw new Exception("No endpoint found"); - - // create new stream entry for source only - if (info.Variant == "source") - { - stream = new() - { - EndpointId = ep.Id, - PubKey = user.PubKey, - StreamId = info.StreamId, - State = UserStreamState.Planned - }; - - var ev = _eventBuilder.CreateStreamEvent(user, stream); - stream.Event = JsonConvert.SerializeObject(ev, NostrSerializer.Settings); - _db.Streams.Add(stream); - await _db.SaveChangesAsync(); - } - - // replace again with new values - stream = new() - { - Id = stream?.Id ?? Guid.NewGuid(), - User = user, - Endpoint = ep, - StreamId = info.StreamId, - State = UserStreamState.Planned, - }; + throw new Exception("No stream found"); } var ctx = new StreamManagerContext { Db = _db, UserStream = stream, - StreamInfo = info + StreamInfo = info, + EdgeApi = new SrsApi(_serviceProvider.GetRequiredService(), new Uri($"http://{stream.EdgeIp}:1985")) }; - return new NostrStreamManager(_loggerFactory.CreateLogger(), ctx, _eventBuilder, _srsApi, _dvrStore); + return new NostrStreamManager(_loggerFactory.CreateLogger(), ctx, _eventBuilder, _dvrStore); } } diff --git a/NostrStreamer/Services/ThumbnailService.cs b/NostrStreamer/Services/ThumbnailService.cs index 6170735..ff9b72c 100644 --- a/NostrStreamer/Services/ThumbnailService.cs +++ b/NostrStreamer/Services/ThumbnailService.cs @@ -1,3 +1,4 @@ +using System.Diagnostics; using FFMpegCore; using NostrStreamer.Database; @@ -24,6 +25,7 @@ public class ThumbnailService var path = MapPath(stream.Id); try { + var sw = Stopwatch.StartNew(); var cmd = FFMpegArguments .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"); }) @@ -31,10 +33,12 @@ public class ThumbnailService _logger.LogInformation("Running command {cmd}", cmd.Arguments); await cmd.ProcessAsynchronously(); + sw.Stop(); + _logger.LogInformation("Generated {id} thumb in {n:#,##0}ms", stream.Id, sw.Elapsed.TotalMilliseconds); } catch (Exception ex) { - _logger.LogWarning("Failed to generate thumbnail {msg}", ex.Message); + _logger.LogWarning("Failed to generate {id} thumbnail {msg}", stream.Id, ex.Message); } } diff --git a/NostrStreamer/appsettings.json b/NostrStreamer/appsettings.json index 3533666..28aae52 100644 --- a/NostrStreamer/appsettings.json +++ b/NostrStreamer/appsettings.json @@ -3,7 +3,8 @@ "LogLevel": { "Default": "Information", "Microsoft.AspNetCore": "Warning", - "Microsoft.EntityFrameworkCore": "Warning" + "Microsoft.EntityFrameworkCore": "Warning", + "System.Net.Http.HttpClient": "Error" } }, "AllowedHosts": "*", diff --git a/docker-compose.yaml b/docker-compose.yaml index 6642eab..a0a05e3 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -11,12 +11,12 @@ services: - "9003:8080" - "9004:8000" srs-edge: - image: ossrs/srs:4 + image: ossrs/srs:5 volumes: - "./docker/srs-edge.conf:/usr/local/srs/conf/srs.conf" ports: - "9005:1935" - - "9006:1985" + - "1985:1985" - "9007:8080" - "9008:8000" nostr: diff --git a/docker/srs-origin.conf b/docker/srs-origin.conf index fc3d037..e01a313 100644 --- a/docker/srs-origin.conf +++ b/docker/srs-origin.conf @@ -39,7 +39,7 @@ vhost hls.zap.stream { } dvr { - enabled on; + enabled off; dvr_path ./objs/nginx/html/[app]/[stream].[timestamp].mp4; dvr_plan segment; dvr_duration 30;