Planned streams

This commit is contained in:
2024-08-28 11:02:47 +01:00
parent 51393b20f2
commit d6b55aef00
12 changed files with 695 additions and 32 deletions

View File

@ -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;
}
}
}

View File

@ -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

View 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
}
}
}

View File

@ -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);
}
}
}

View File

@ -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")

View File

@ -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" />

View File

@ -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;
}
}
}

View File

@ -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));
}

View File

@ -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()));
}

View File

@ -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()

View File

@ -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));

View File

@ -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;
}
}
}