Planned streams
This commit is contained in:
@ -21,7 +21,8 @@ public class PlaylistController : Controller
|
||||
private readonly EdgeSteering _edgeSteering;
|
||||
|
||||
public PlaylistController(Config config, ILogger<PlaylistController> 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<string?> 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;
|
||||
}
|
||||
}
|
||||
}
|
@ -53,20 +53,20 @@ public class UserStream
|
||||
/// </summary>
|
||||
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!;
|
||||
|
||||
/// <summary>
|
||||
/// Publisher edge IP
|
||||
/// </summary>
|
||||
public string EdgeIp { get; set; } = null!;
|
||||
public string? EdgeIp { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Publisher edge client id
|
||||
/// </summary>
|
||||
public string ForwardClientId { get; set; } = null!;
|
||||
public string? ForwardClientId { get; set; }
|
||||
|
||||
public DateTime LastSegment { get; set; } = DateTime.UtcNow;
|
||||
public DateTime? LastSegment { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Total sats charged during this stream
|
||||
|
562
NostrStreamer/Migrations/20240828100209_PlannedUserStreams.Designer.cs
generated
Normal file
562
NostrStreamer/Migrations/20240828100209_PlannedUserStreams.Designer.cs
generated
Normal file
@ -0,0 +1,562 @@
|
||||
// <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("20240828100209_PlannedUserStreams")]
|
||||
partial class PlannedUserStreams
|
||||
{
|
||||
/// <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<decimal>("Fee")
|
||||
.HasColumnType("numeric(20,0)");
|
||||
|
||||
b.Property<string>("Invoice")
|
||||
.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.PushSubscription", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("Auth")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<DateTime>("Created")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("Endpoint")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("Key")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<DateTime>("LastUsed")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("Pubkey")
|
||||
.IsRequired()
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("character varying(64)");
|
||||
|
||||
b.Property<string>("Scope")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("PushSubscriptions");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("NostrStreamer.Database.PushSubscriptionTarget", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("SubscriberPubkey")
|
||||
.IsRequired()
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("character varying(64)");
|
||||
|
||||
b.Property<string>("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<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<DateTime>("Created")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<Guid>("Token")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<Guid>("UserStreamId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("UserStreamId");
|
||||
|
||||
b.ToTable("StreamTickets");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("NostrStreamer.Database.User", b =>
|
||||
{
|
||||
b.Property<string>("PubKey")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<long>("Balance")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<bool>("IsAdmin")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<bool>("IsBlocked")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<string>("StreamKey")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<DateTime?>("TosAccepted")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
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<decimal?>("AdmissionCost")
|
||||
.HasColumnType("numeric");
|
||||
|
||||
b.Property<string>("ContentWarning")
|
||||
.HasColumnType("text");
|
||||
|
||||
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>("Goal")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("Image")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<DateTime?>("LastSegment")
|
||||
.IsRequired()
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<decimal>("Length")
|
||||
.HasColumnType("numeric");
|
||||
|
||||
b.Property<decimal>("MilliSatsCollected")
|
||||
.HasColumnType("numeric");
|
||||
|
||||
b.Property<string>("PubKey")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<DateTime>("Starts")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<int>("State")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<string>("Summary")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("Tags")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("Thumbnail")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("Title")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("EndpointId");
|
||||
|
||||
b.HasIndex("PubKey");
|
||||
|
||||
b.ToTable("Streams");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("NostrStreamer.Database.UserStreamClip", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<DateTime>("Created")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("TakenByPubkey")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("Url")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<Guid>("UserStreamId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("UserStreamId");
|
||||
|
||||
b.ToTable("Clips");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("NostrStreamer.Database.UserStreamForwards", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("Target")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("UserPubkey")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("UserPubkey");
|
||||
|
||||
b.ToTable("Forwards");
|
||||
});
|
||||
|
||||
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.UserStreamKey", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<DateTime>("Created")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<DateTime?>("Expires")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("Key")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<Guid>("StreamId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("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<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.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
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,60 @@
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace NostrStreamer.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class PlannedUserStreams : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropForeignKey(
|
||||
name: "FK_Streams_Endpoints_EndpointId",
|
||||
table: "Streams");
|
||||
|
||||
migrationBuilder.AlterColumn<Guid>(
|
||||
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");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropForeignKey(
|
||||
name: "FK_Streams_Endpoints_EndpointId",
|
||||
table: "Streams");
|
||||
|
||||
migrationBuilder.AlterColumn<Guid>(
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
@ -227,7 +227,7 @@ namespace NostrStreamer.Migrations
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<Guid>("EndpointId")
|
||||
b.Property<Guid?>("EndpointId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<DateTime?>("Ends")
|
||||
@ -247,7 +247,8 @@ namespace NostrStreamer.Migrations
|
||||
b.Property<string>("Image")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<DateTime>("LastSegment")
|
||||
b.Property<DateTime?>("LastSegment")
|
||||
.IsRequired()
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<decimal>("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")
|
||||
|
@ -7,6 +7,10 @@
|
||||
<DockerDefaultTargetOS>Linux</DockerDefaultTargetOS>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
|
||||
<WarningsAsErrors>;NU1605;SYSLIB0011;CS8602</WarningsAsErrors>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Protobuf Include="proto/*.proto" GrpcServices="Client" ProtoRoot="proto" />
|
||||
<Protobuf Include="proto2/games.proto" ProtoRoot="proto2" />
|
||||
|
@ -47,6 +47,7 @@ public class ClipGenerator
|
||||
public async Task<List<ClipSegment>> GetClipSegments(UserStream stream)
|
||||
{
|
||||
var ret = new List<ClipSegment>();
|
||||
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<string?> 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;
|
||||
}
|
||||
}
|
||||
}
|
@ -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));
|
||||
}
|
||||
|
@ -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()));
|
||||
}
|
||||
|
@ -55,10 +55,12 @@ public class NostrStreamManager : IStreamManager
|
||||
public Task<List<string>> OnForward()
|
||||
{
|
||||
TestCanStream();
|
||||
var fwds = new List<string>
|
||||
var fwds = new List<string>();
|
||||
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()
|
||||
|
@ -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));
|
||||
|
||||
|
@ -14,11 +14,13 @@ public abstract class BaseThumbnailService
|
||||
Logger = logger;
|
||||
}
|
||||
|
||||
protected async Task<string> GenerateThumbnail(UserStream stream)
|
||||
protected async Task<string?> 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;
|
||||
}
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user