diff --git a/NostrStreamer/Config.cs b/NostrStreamer/Config.cs index 941e090..a8d77ed 100644 --- a/NostrStreamer/Config.cs +++ b/NostrStreamer/Config.cs @@ -56,6 +56,8 @@ public class Config public Uri SnortApi { get; init; } = null!; public Uri? DiscordLiveWebhook { get; init; } + + public int RetainRecordingsDays { get; init; } = 90; } public class VapidKeyDetails diff --git a/NostrStreamer/Database/Configuration/UserStreamRecordingConfiguration.cs b/NostrStreamer/Database/Configuration/UserStreamRecordingConfiguration.cs index d58de43..d6d40e6 100644 --- a/NostrStreamer/Database/Configuration/UserStreamRecordingConfiguration.cs +++ b/NostrStreamer/Database/Configuration/UserStreamRecordingConfiguration.cs @@ -18,7 +18,7 @@ public class UserStreamRecordingConfiguration : IEntityTypeConfiguration a.Stream) - .WithMany() + .WithMany(a => a.Recordings) .HasForeignKey(a => a.UserStreamId); } } diff --git a/NostrStreamer/Database/UserStream.cs b/NostrStreamer/Database/UserStream.cs index 72435b0..3df9a8a 100644 --- a/NostrStreamer/Database/UserStream.cs +++ b/NostrStreamer/Database/UserStream.cs @@ -51,6 +51,8 @@ public class UserStream public decimal Length { get; set; } public List Guests { get; init; } = new(); + + public List Recordings { get; init; } = new(); } public enum UserStreamState diff --git a/NostrStreamer/Extensions.cs b/NostrStreamer/Extensions.cs index a3a7157..04894b4 100644 --- a/NostrStreamer/Extensions.cs +++ b/NostrStreamer/Extensions.cs @@ -112,7 +112,13 @@ public static class Extensions } return new NostrEventIdentifier(ev.Id!, ev.Pubkey, null, ev.Kind); - } + } + + public static NostrIdentifier ToIdentifier(this UserStream stream) + { + var ev = NostrJson.Deserialize(stream.Event); + return ev!.ToIdentifier(); + } } public class Variant diff --git a/NostrStreamer/Migrations/20230725123250_Endpoints.cs b/NostrStreamer/Migrations/20230725123250_Endpoints.cs index 8761b8c..76a176a 100644 --- a/NostrStreamer/Migrations/20230725123250_Endpoints.cs +++ b/NostrStreamer/Migrations/20230725123250_Endpoints.cs @@ -53,6 +53,9 @@ namespace NostrStreamer.Migrations principalTable: "Endpoints", principalColumn: "Id", onDelete: ReferentialAction.Cascade); + + migrationBuilder.Sql( + "INSERT INTO public.\"Endpoints\"(\"Id\", \"Name\", \"App\", \"Forward\", \"Cost\", \"Capabilities\") VALUES(gen_random_uuid(), 'basic', 'basic', 'base.in.zap.stream', 1000, '{variant:source,dvr:source}');"); } /// diff --git a/NostrStreamer/Migrations/20240226120459_StreamRecordings.Designer.cs b/NostrStreamer/Migrations/20240226120459_StreamRecordings.Designer.cs new file mode 100644 index 0000000..cc65393 --- /dev/null +++ b/NostrStreamer/Migrations/20240226120459_StreamRecordings.Designer.cs @@ -0,0 +1,466 @@ +// +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("20240226120459_StreamRecordings")] + partial class StreamRecordings + { + /// + 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") + .HasColumnType("text"); + + b.Property("IsPaid") + .HasColumnType("boolean"); + + b.Property("Nostr") + .HasColumnType("text"); + + b.Property("PubKey") + .IsRequired() + .HasColumnType("text"); + + b.Property("Type") + .HasColumnType("integer"); + + b.HasKey("PaymentHash"); + + b.HasIndex("PubKey"); + + b.ToTable("Payments"); + }); + + modelBuilder.Entity("NostrStreamer.Database.PushSubscription", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Auth") + .IsRequired() + .HasColumnType("text"); + + b.Property("Created") + .HasColumnType("timestamp with time zone"); + + b.Property("Endpoint") + .IsRequired() + .HasColumnType("text"); + + b.Property("Key") + .IsRequired() + .HasColumnType("text"); + + b.Property("LastUsed") + .HasColumnType("timestamp with time zone"); + + b.Property("Pubkey") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("Scope") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("PushSubscriptions"); + }); + + modelBuilder.Entity("NostrStreamer.Database.PushSubscriptionTarget", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("SubscriberPubkey") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("TargetPubkey") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.HasKey("Id"); + + b.HasIndex("TargetPubkey"); + + b.HasIndex("SubscriberPubkey", "TargetPubkey") + .IsUnique(); + + b.ToTable("PushSubscriptionTargets"); + }); + + modelBuilder.Entity("NostrStreamer.Database.User", b => + { + b.Property("PubKey") + .HasColumnType("text"); + + b.Property("Balance") + .HasColumnType("bigint"); + + b.Property("ContentWarning") + .HasColumnType("text"); + + b.Property("Goal") + .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("TosAccepted") + .HasColumnType("timestamp with time zone"); + + b.Property("Version") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("xid") + .HasColumnName("xmin"); + + b.HasKey("PubKey"); + + b.ToTable("Users"); + }); + + modelBuilder.Entity("NostrStreamer.Database.UserStream", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("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("LastSegment") + .HasColumnType("timestamp with time zone"); + + b.Property("Length") + .HasColumnType("numeric"); + + b.Property("MilliSatsCollected") + .HasColumnType("numeric"); + + b.Property("PubKey") + .IsRequired() + .HasColumnType("text"); + + b.Property("Starts") + .HasColumnType("timestamp with time zone"); + + b.Property("State") + .HasColumnType("integer"); + + b.Property("StreamId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Thumbnail") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("EndpointId"); + + b.HasIndex("PubKey"); + + b.ToTable("Streams"); + }); + + modelBuilder.Entity("NostrStreamer.Database.UserStreamClip", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Created") + .HasColumnType("timestamp with time zone"); + + b.Property("TakenByPubkey") + .IsRequired() + .HasColumnType("text"); + + b.Property("Url") + .IsRequired() + .HasColumnType("text"); + + b.Property("UserStreamId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("UserStreamId"); + + b.ToTable("Clips"); + }); + + modelBuilder.Entity("NostrStreamer.Database.UserStreamForwards", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("Target") + .IsRequired() + .HasColumnType("text"); + + b.Property("UserPubkey") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("UserPubkey"); + + b.ToTable("Forwards"); + }); + + modelBuilder.Entity("NostrStreamer.Database.UserStreamGuest", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("PubKey") + .IsRequired() + .HasColumnType("text"); + + b.Property("Relay") + .HasColumnType("text"); + + b.Property("Role") + .HasColumnType("text"); + + b.Property("Sig") + .HasColumnType("text"); + + b.Property("StreamId") + .HasColumnType("uuid"); + + b.Property("ZapSplit") + .HasColumnType("numeric"); + + b.HasKey("Id"); + + b.HasIndex("StreamId", "PubKey") + .IsUnique(); + + b.ToTable("Guests"); + }); + + modelBuilder.Entity("NostrStreamer.Database.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.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.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("Streams"); + }); + + modelBuilder.Entity("NostrStreamer.Database.UserStream", b => + { + b.Navigation("Guests"); + + b.Navigation("Recordings"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/NostrStreamer/Migrations/20240226120459_StreamRecordings.cs b/NostrStreamer/Migrations/20240226120459_StreamRecordings.cs new file mode 100644 index 0000000..83ad410 --- /dev/null +++ b/NostrStreamer/Migrations/20240226120459_StreamRecordings.cs @@ -0,0 +1,22 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace NostrStreamer.Migrations +{ + /// + public partial class StreamRecordings : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + + } + } +} diff --git a/NostrStreamer/Migrations/StreamerContextModelSnapshot.cs b/NostrStreamer/Migrations/StreamerContextModelSnapshot.cs index 838b2cc..76a4363 100644 --- a/NostrStreamer/Migrations/StreamerContextModelSnapshot.cs +++ b/NostrStreamer/Migrations/StreamerContextModelSnapshot.cs @@ -434,7 +434,7 @@ namespace NostrStreamer.Migrations modelBuilder.Entity("NostrStreamer.Database.UserStreamRecording", b => { b.HasOne("NostrStreamer.Database.UserStream", "Stream") - .WithMany() + .WithMany("Recordings") .HasForeignKey("UserStreamId") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); @@ -454,6 +454,8 @@ namespace NostrStreamer.Migrations modelBuilder.Entity("NostrStreamer.Database.UserStream", b => { b.Navigation("Guests"); + + b.Navigation("Recordings"); }); #pragma warning restore 612, 618 } diff --git a/NostrStreamer/Program.cs b/NostrStreamer/Program.cs index 7d65847..a0fc669 100644 --- a/NostrStreamer/Program.cs +++ b/NostrStreamer/Program.cs @@ -105,6 +105,7 @@ internal static class Program // dvr services services.AddTransient(); + services.AddHostedService(); // thumbnail services services.AddTransient(); diff --git a/NostrStreamer/Services/Background/RecordingDeleter.cs b/NostrStreamer/Services/Background/RecordingDeleter.cs new file mode 100644 index 0000000..5ed47ba --- /dev/null +++ b/NostrStreamer/Services/Background/RecordingDeleter.cs @@ -0,0 +1,70 @@ +using Microsoft.EntityFrameworkCore; +using NostrStreamer.Database; +using NostrStreamer.Services.Dvr; + +namespace NostrStreamer.Services.Background; + +public class RecordingDeleter : BackgroundService +{ + private readonly ILogger _logger; + private readonly IServiceScopeFactory _scopeFactory; + private readonly Config _config; + + public RecordingDeleter(ILogger logger, IServiceScopeFactory scopeFactory, Config config) + { + _logger = logger; + _scopeFactory = scopeFactory; + _config = config; + } + + protected override async Task ExecuteAsync(CancellationToken stoppingToken) + { + while (!stoppingToken.IsCancellationRequested) + { + try + { + using var scope = _scopeFactory.CreateScope(); + await using var db = scope.ServiceProvider.GetRequiredService(); + var dvrStore = scope.ServiceProvider.GetRequiredService(); + + var olderThan = DateTime.UtcNow.Subtract(TimeSpan.FromDays(_config.RetainRecordingsDays)); + var toDelete = await db.Streams + .AsNoTracking() + .Where(a => a.Starts < olderThan) + .Where(a => a.Recordings.Count > 0) + .ToListAsync(cancellationToken: stoppingToken); + + _logger.LogInformation("Starting delete of {n:###0} stream recordings", toDelete.Count); + + foreach (var stream in toDelete) + { + try + { + var streamRecordings = await db.Streams + .AsNoTracking() + .Include(a => a.Recordings) + .SingleAsync(a => a.Id == stream.Id, cancellationToken: stoppingToken); + var deleted = await dvrStore.DeleteRecordings(streamRecordings); + + await db.Recordings + .Where(a => deleted.Contains(a.Id)) + .ExecuteDeleteAsync(cancellationToken: stoppingToken); + + _logger.LogInformation("Deleted {n}/{m} recordings from stream {id}", deleted.Count, + streamRecordings.Recordings.Count, stream.Id); + } + catch (Exception ex) + { + _logger.LogWarning(ex, "Failed to delete stream recordings {id} {msg}", stream.Id, ex.Message); + } + } + } + catch (Exception ex) + { + _logger.LogError(ex, "Failed to run background deleter"); + } + + await Task.Delay(TimeSpan.FromMinutes(30), stoppingToken); + } + } +} \ No newline at end of file diff --git a/NostrStreamer/Services/Dvr/IDvrStore.cs b/NostrStreamer/Services/Dvr/IDvrStore.cs index 251193b..447aced 100644 --- a/NostrStreamer/Services/Dvr/IDvrStore.cs +++ b/NostrStreamer/Services/Dvr/IDvrStore.cs @@ -11,6 +11,13 @@ public interface IDvrStore /// /// Task UploadRecording(UserStream stream, Uri source); + + /// + /// Delete all recordings from the storage by stream + /// + /// + /// List of deleted recordings + Task> DeleteRecordings(UserStream stream); } public record UploadResult(Uri Result, double Duration); diff --git a/NostrStreamer/Services/Dvr/S3DvrStore.cs b/NostrStreamer/Services/Dvr/S3DvrStore.cs index 4083db7..1ee499c 100644 --- a/NostrStreamer/Services/Dvr/S3DvrStore.cs +++ b/NostrStreamer/Services/Dvr/S3DvrStore.cs @@ -86,10 +86,30 @@ public class S3DvrStore : IDvrStore // cleanup temp file fs.Close(); File.Delete(tmpFile); - - _logger.LogInformation("download={tc:#,##0}ms, probe={pc:#,##0}ms, upload={uc:#,##0}ms", tsDownload.TotalMilliseconds, + + _logger.LogInformation("download={tc:#,##0}ms, probe={pc:#,##0}ms, upload={uc:#,##0}ms", + tsDownload.TotalMilliseconds, tsProbe.TotalMilliseconds, tsUpload.TotalMilliseconds); return new(ub.Uri, probe.Duration.TotalSeconds); } -} + + public async Task> DeleteRecordings(UserStream stream) + { + var deleted = new List(); + foreach (var batch in stream.Recordings.Select((a, i) => (Batch: i / 1000, Item: a)).GroupBy(a => a.Batch)) + { + var res = await _client.DeleteObjectsAsync(new() + { + BucketName = _config.BucketName, + Objects = batch.Select(a => new KeyVersion() + { + Key = $"{stream.Id}/{a.Item.Id}.ts" + }).ToList() + }); + deleted.AddRange(res.DeletedObjects.Select(a => Guid.Parse(Path.GetFileNameWithoutExtension(a.Key)))); + } + + return deleted; + } +} \ No newline at end of file diff --git a/NostrStreamer/Services/StreamEventBuilder.cs b/NostrStreamer/Services/StreamEventBuilder.cs index 08d9254..f915918 100644 --- a/NostrStreamer/Services/StreamEventBuilder.cs +++ b/NostrStreamer/Services/StreamEventBuilder.cs @@ -102,8 +102,24 @@ public class StreamEventBuilder return ev.Sign(pk); } + public NostrEvent CreateDm(UserStream stream, string message) + { + var pk = NostrPrivateKey.FromBech32(_config.PrivateKey); + var ev = new NostrEvent + { + Kind = NostrKind.EncryptedDm, + Content = message, + CreatedAt = DateTime.Now, + Tags = new NostrEventTags( + new NostrEventTag("p", stream.PubKey) + ) + }; + + return ev.EncryptDirect(pk, NostrPublicKey.FromHex(stream.PubKey)).Sign(pk); + } + public void BroadcastEvent(NostrEvent ev) { _nostrClient.Send(new NostrEventRequest(ev)); } -} +} \ No newline at end of file diff --git a/NostrStreamer/Services/StreamManager/NostrStreamManager.cs b/NostrStreamer/Services/StreamManager/NostrStreamManager.cs index 2c0ee4f..aaf0a69 100644 --- a/NostrStreamer/Services/StreamManager/NostrStreamManager.cs +++ b/NostrStreamer/Services/StreamManager/NostrStreamManager.cs @@ -107,6 +107,16 @@ public class NostrStreamManager : IStreamManager _logger.LogInformation("Stream stopped for: {pubkey}", _context.User.PubKey); await UpdateStreamState(UserStreamState.Ended); + + // send DM with link to summary + var msg = + (_context.UserStream.Thumbnail != default ? $"{_context.UserStream.Thumbnail}\n" : "") + + $"Your stream summary is available here: https://zap.stream/summary/{_context.UserStream.ToIdentifier().ToBech32()}\n\n" + + $"You paid {_context.UserStream.MilliSatsCollected / 1000:#,##0.###} sats for this stream!\n\n" + + $"You streamed for {_context.UserStream.Length / 60:#,##0} mins!"; + + var chat = _eventBuilder.CreateDm(_context.UserStream, msg); + _eventBuilder.BroadcastEvent(chat); } public async Task ConsumeQuota(double duration) diff --git a/NostrStreamer/appsettings.json b/NostrStreamer/appsettings.json index 723858d..48b7402 100644 --- a/NostrStreamer/appsettings.json +++ b/NostrStreamer/appsettings.json @@ -29,13 +29,13 @@ "MacaroonPath": "/Users/kieran/.polar/networks/1/volumes/lnd/bob/data/chain/bitcoin/regtest/admin.macaroon" }, "S3Store": { - "ServiceUrl": "http://localhost:9010", - "AccessKey": "TQcxug1ZAXfnZ5bvc9n5", - "SecretKey": "p7EK4qew6DBkBPqrpRPuJgTOc6ChUlfIcEdAwE7K", - "PublicHost": "http://localhost:9010" + "ServiceUrl": "http://localhost:9000", + "AccessKey": "GfvDg9FoarDnNZj5axOq", + "SecretKey": "1XtAmUcJDPHWEarFLmOx75z6Ok3FyMEdP7cvCkQP", + "PublicHost": "http://localhost:9000" }, "SnortApi": "https://api.snort.social", - "GeoIpDatabase": "/Users/kieran/Downloads/GeoLite2-City_20230801/GeoLite2-City.mmdb", + "GeoIpDatabase": "C:\\Users\\Kieran\\Downloads\\GeoLite2-City.mmdb", "Edges": [ { "Name": "US0", diff --git a/docker-compose.yaml b/docker-compose.yaml index a6c41dc..f51ecf2 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -1,5 +1,6 @@ volumes: minio-dvr: + postgres: services: srs-origin: image: ossrs/srs:5 @@ -29,18 +30,20 @@ services: - "POSTGRES_HOST_AUTH_METHOD=trust" ports: - "5431:5432" + volumes: + - "postgres:/var/lib/postgresql/data" minio: image: quay.io/minio/minio command: - "server" - "/data" - "--console-address" - - ":9001" + - ":9011" environment: - - "MINIO_SERVER_URL=http://localhost:9010" + - "MINIO_SERVER_URL=http://localhost:9000" ports: - - "9010:9000" - - "9011:9001" + - "9000:9000" + - "9011:9011" volumes: - "minio-dvr:/data" redis: