Kick from edge forward
This commit is contained in:
24
NostrStreamer/Controllers/AdminController.cs
Normal file
24
NostrStreamer/Controllers/AdminController.cs
Normal file
@ -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<AdminController> _logger;
|
||||
private readonly StreamManagerFactory _streamManagerFactory;
|
||||
|
||||
public AdminController(ILogger<AdminController> 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();
|
||||
}
|
||||
}
|
@ -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
|
||||
|
@ -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")
|
||||
|
@ -24,6 +24,12 @@ public class UserStreamConfiguration : IEntityTypeConfiguration<UserStream>
|
||||
|
||||
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);
|
||||
|
@ -28,6 +28,16 @@ public class UserStream
|
||||
public Guid EndpointId { get; init; }
|
||||
public IngestEndpoint Endpoint { get; init; } = null!;
|
||||
|
||||
/// <summary>
|
||||
/// Publisher edge IP
|
||||
/// </summary>
|
||||
public string EdgeIp { get; set; } = null!;
|
||||
|
||||
/// <summary>
|
||||
/// Publisher edge client id
|
||||
/// </summary>
|
||||
public string ForwardClientId { get; set; } = null!;
|
||||
|
||||
public List<UserStreamGuest> Guests { get; init; } = new();
|
||||
public List<UserStreamRecording> Recordings { get; init; } = new();
|
||||
}
|
||||
|
312
NostrStreamer/Migrations/20230731145446_ForwardClientDetails.Designer.cs
generated
Normal file
312
NostrStreamer/Migrations/20230731145446_ForwardClientDetails.Designer.cs
generated
Normal file
@ -0,0 +1,312 @@
|
||||
// <auto-generated />
|
||||
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
|
||||
{
|
||||
/// <inheritdoc />
|
||||
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<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("App")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<List<string>>("Capabilities")
|
||||
.IsRequired()
|
||||
.HasColumnType("text[]");
|
||||
|
||||
b.Property<int>("Cost")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<string>("Forward")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("App")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("Endpoints");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("NostrStreamer.Database.Payment", b =>
|
||||
{
|
||||
b.Property<string>("PaymentHash")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<decimal>("Amount")
|
||||
.HasColumnType("numeric(20,0)");
|
||||
|
||||
b.Property<DateTime>("Created")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("Invoice")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<bool>("IsPaid")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<string>("Nostr")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("PubKey")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<int>("Type")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.HasKey("PaymentHash");
|
||||
|
||||
b.HasIndex("PubKey");
|
||||
|
||||
b.ToTable("Payments");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("NostrStreamer.Database.User", b =>
|
||||
{
|
||||
b.Property<string>("PubKey")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<long>("Balance")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<string>("ContentWarning")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("Image")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("StreamKey")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("Summary")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("Tags")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("Title")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<uint>("Version")
|
||||
.IsConcurrencyToken()
|
||||
.ValueGeneratedOnAddOrUpdate()
|
||||
.HasColumnType("xid")
|
||||
.HasColumnName("xmin");
|
||||
|
||||
b.HasKey("PubKey");
|
||||
|
||||
b.ToTable("Users");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("NostrStreamer.Database.UserStream", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("EdgeIp")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<Guid>("EndpointId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<DateTime?>("Ends")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("Event")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("ForwardClientId")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("PubKey")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("Recording")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<DateTime>("Starts")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<int>("State")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<string>("StreamId")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("EndpointId");
|
||||
|
||||
b.HasIndex("PubKey");
|
||||
|
||||
b.ToTable("Streams");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("NostrStreamer.Database.UserStreamGuest", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("PubKey")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("Relay")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("Role")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("Sig")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<Guid>("StreamId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<decimal>("ZapSplit")
|
||||
.HasColumnType("numeric");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("StreamId", "PubKey")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("Guests");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("NostrStreamer.Database.UserStreamRecording", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<double>("Duration")
|
||||
.HasColumnType("double precision");
|
||||
|
||||
b.Property<DateTime>("Timestamp")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("Url")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<Guid>("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
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,40 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace NostrStreamer.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class ForwardClientDetails : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "EdgeIp",
|
||||
table: "Streams",
|
||||
type: "text",
|
||||
nullable: false,
|
||||
defaultValue: "");
|
||||
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "ForwardClientId",
|
||||
table: "Streams",
|
||||
type: "text",
|
||||
nullable: false,
|
||||
defaultValue: "");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "EdgeIp",
|
||||
table: "Streams");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "ForwardClientId",
|
||||
table: "Streams");
|
||||
}
|
||||
}
|
||||
}
|
@ -135,6 +135,10 @@ namespace NostrStreamer.Migrations
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("EdgeIp")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<Guid>("EndpointId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
@ -145,6 +149,10 @@ namespace NostrStreamer.Migrations
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("ForwardClientId")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("PubKey")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
@ -27,9 +27,10 @@ public class BackgroundStreamManager : BackgroundService
|
||||
var db = scope.ServiceProvider.GetRequiredService<StreamerContext>();
|
||||
var srs = scope.ServiceProvider.GetRequiredService<SrsApi>();
|
||||
|
||||
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);
|
||||
|
||||
|
@ -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<List<Stream>> ListStreams()
|
||||
|
@ -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()));
|
||||
|
||||
|
@ -73,4 +73,10 @@ public interface IStreamManager
|
||||
/// <param name="segment"></param>
|
||||
/// <returns></returns>
|
||||
Task OnDvr(Uri segment);
|
||||
|
||||
/// <summary>
|
||||
/// Republish stream event
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public Task UpdateEvent();
|
||||
}
|
@ -12,16 +12,14 @@ public class NostrStreamManager : IStreamManager
|
||||
private readonly ILogger<NostrStreamManager> _logger;
|
||||
private readonly StreamManagerContext _context;
|
||||
private readonly StreamEventBuilder _eventBuilder;
|
||||
private readonly SrsApi _srsApi;
|
||||
private readonly IDvrStore _dvrStore;
|
||||
|
||||
public NostrStreamManager(ILogger<NostrStreamManager> 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);
|
||||
}
|
||||
}
|
||||
|
||||
@ -152,6 +150,11 @@ public class NostrStreamManager : IStreamManager
|
||||
await _context.Db.SaveChangesAsync();
|
||||
}
|
||||
|
||||
public async Task UpdateEvent()
|
||||
{
|
||||
await UpdateStreamState(_context.UserStream.State);
|
||||
}
|
||||
|
||||
public async Task UpdateViewers()
|
||||
{
|
||||
if (_context.UserStream.State is not UserStreamState.Live) return;
|
||||
|
@ -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!;
|
||||
}
|
||||
|
@ -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!;
|
||||
}
|
||||
|
@ -11,72 +11,20 @@ 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<IStreamManager> ForStream(Guid id)
|
||||
{
|
||||
var currentStream = 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");
|
||||
|
||||
var ctx = new StreamManagerContext
|
||||
{
|
||||
Db = _db,
|
||||
UserStream = currentStream
|
||||
};
|
||||
|
||||
return new NostrStreamManager(_loggerFactory.CreateLogger<NostrStreamManager>(), ctx, _eventBuilder, _srsApi, _dvrStore);
|
||||
}
|
||||
|
||||
public async Task<IStreamManager> ForCurrentStream(string pubkey)
|
||||
{
|
||||
var currentStream = 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");
|
||||
|
||||
var ctx = new StreamManagerContext
|
||||
{
|
||||
Db = _db,
|
||||
UserStream = currentStream
|
||||
};
|
||||
|
||||
return new NostrStreamManager(_loggerFactory.CreateLogger<NostrStreamManager>(), ctx, _eventBuilder, _srsApi, _dvrStore);
|
||||
}
|
||||
|
||||
public async Task<IStreamManager> ForStream(StreamInfo info)
|
||||
{
|
||||
var stream = await _db.Streams
|
||||
.AsNoTracking()
|
||||
.Include(a => a.User)
|
||||
.Include(a => a.Endpoint)
|
||||
.Include(a => a.Recordings)
|
||||
.FirstOrDefaultAsync(a =>
|
||||
a.StreamId.Equals(info.StreamId) &&
|
||||
a.User.StreamKey.Equals(info.StreamKey) &&
|
||||
a.Endpoint.App.Equals(info.App));
|
||||
|
||||
if (stream == default)
|
||||
public async Task<IStreamManager> CreateStream(StreamInfo info)
|
||||
{
|
||||
var user = await _db.Users
|
||||
.AsNoTracking()
|
||||
@ -90,41 +38,119 @@ public class StreamManagerFactory
|
||||
|
||||
if (ep == default) throw new Exception("No endpoint found");
|
||||
|
||||
// create new stream entry for source only
|
||||
if (info.Variant == "source")
|
||||
if (await _db.Streams.CountAsync(a => a.State == UserStreamState.Live && a.PubKey == user.PubKey) != 0)
|
||||
{
|
||||
stream = new()
|
||||
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 = info.StreamId,
|
||||
State = UserStreamState.Planned
|
||||
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<HttpClient>(), new Uri($"http://{stream.EdgeIp}:1985"))
|
||||
};
|
||||
|
||||
return new NostrStreamManager(_loggerFactory.CreateLogger<NostrStreamManager>(), ctx, _eventBuilder, _dvrStore);
|
||||
}
|
||||
|
||||
// replace again with new values
|
||||
stream = new()
|
||||
public async Task<IStreamManager> ForStream(Guid id)
|
||||
{
|
||||
Id = stream?.Id ?? Guid.NewGuid(),
|
||||
User = user,
|
||||
Endpoint = ep,
|
||||
StreamId = info.StreamId,
|
||||
State = UserStreamState.Planned,
|
||||
var stream = await _db.Streams
|
||||
.AsNoTracking()
|
||||
.Include(a => a.User)
|
||||
.Include(a => a.Endpoint)
|
||||
.Include(a => a.Recordings)
|
||||
.FirstOrDefaultAsync(a => a.Id == id);
|
||||
|
||||
if (stream == default) throw new Exception("No live stream");
|
||||
|
||||
var ctx = new StreamManagerContext
|
||||
{
|
||||
Db = _db,
|
||||
UserStream = stream,
|
||||
EdgeApi = new SrsApi(_serviceProvider.GetRequiredService<HttpClient>(), new Uri($"http://{stream.EdgeIp}:1985"))
|
||||
};
|
||||
|
||||
return new NostrStreamManager(_loggerFactory.CreateLogger<NostrStreamManager>(), ctx, _eventBuilder, _dvrStore);
|
||||
}
|
||||
|
||||
public async Task<IStreamManager> ForCurrentStream(string pubkey)
|
||||
{
|
||||
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 (stream == default) throw new Exception("No live stream");
|
||||
|
||||
var ctx = new StreamManagerContext
|
||||
{
|
||||
Db = _db,
|
||||
UserStream = stream,
|
||||
EdgeApi = new SrsApi(_serviceProvider.GetRequiredService<HttpClient>(), new Uri($"http://{stream.EdgeIp}:1985"))
|
||||
};
|
||||
|
||||
return new NostrStreamManager(_loggerFactory.CreateLogger<NostrStreamManager>(), ctx, _eventBuilder, _dvrStore);
|
||||
}
|
||||
|
||||
public async Task<IStreamManager> ForStream(StreamInfo info)
|
||||
{
|
||||
var stream = await _db.Streams
|
||||
.AsNoTracking()
|
||||
.Include(a => a.User)
|
||||
.Include(a => a.Endpoint)
|
||||
.Include(a => a.Recordings)
|
||||
.OrderByDescending(a => a.Starts)
|
||||
.FirstOrDefaultAsync(a =>
|
||||
a.User.StreamKey.Equals(info.StreamKey) &&
|
||||
a.Endpoint.App.Equals(info.App) &&
|
||||
a.State == UserStreamState.Live);
|
||||
|
||||
if (stream == default)
|
||||
{
|
||||
throw new Exception("No stream found");
|
||||
}
|
||||
|
||||
var ctx = new StreamManagerContext
|
||||
{
|
||||
Db = _db,
|
||||
UserStream = stream,
|
||||
StreamInfo = info
|
||||
StreamInfo = info,
|
||||
EdgeApi = new SrsApi(_serviceProvider.GetRequiredService<HttpClient>(), new Uri($"http://{stream.EdgeIp}:1985"))
|
||||
};
|
||||
|
||||
return new NostrStreamManager(_loggerFactory.CreateLogger<NostrStreamManager>(), ctx, _eventBuilder, _srsApi, _dvrStore);
|
||||
return new NostrStreamManager(_loggerFactory.CreateLogger<NostrStreamManager>(), ctx, _eventBuilder, _dvrStore);
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -3,7 +3,8 @@
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft.AspNetCore": "Warning",
|
||||
"Microsoft.EntityFrameworkCore": "Warning"
|
||||
"Microsoft.EntityFrameworkCore": "Warning",
|
||||
"System.Net.Http.HttpClient": "Error"
|
||||
}
|
||||
},
|
||||
"AllowedHosts": "*",
|
||||
|
@ -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:
|
||||
|
@ -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;
|
||||
|
Reference in New Issue
Block a user