using System.Diagnostics; using Amazon.S3; using Amazon.S3.Model; using FFMpegCore; using NostrStreamer.Database; namespace NostrStreamer.Services.Dvr; public class S3DvrStore : IDvrStore { private readonly AmazonS3Client _client; private readonly S3BlobConfig _config; private readonly HttpClient _httpClient; private readonly ILogger _logger; public S3DvrStore(Config config, HttpClient httpClient, ILogger logger) { _httpClient = httpClient; _logger = logger; _config = config.S3Store; _client = config.S3Store.CreateClient(); } public async Task UploadRecording(UserStream stream, Uri source) { var sw = Stopwatch.StartNew(); var tmpFile = Path.GetTempFileName(); var recordingId = Guid.NewGuid(); /* var cmdTranscode = FFMpegArguments.FromUrlInput(source) .OutputToFile(tmpFile, true, o => { o.WithCustomArgument("-movflags frag_keyframe+empty_moov"); o.CopyChannel(Channel.All); o.ForceFormat("mp4"); }); _logger.LogInformation("Transcoding with: {cmd}", cmdTranscode.Arguments); await cmdTranscode.ProcessAsynchronously(); var tsTranscode = sw.Elapsed;*/ sw.Restart(); await using var fs = new FileStream(tmpFile, FileMode.Create, FileAccess.ReadWrite); var dl = await _httpClient.GetStreamAsync(source); await dl.CopyToAsync(fs); await fs.FlushAsync(); fs.Seek(0, SeekOrigin.Begin); var tsDownload = sw.Elapsed; sw.Restart(); var probe = await FFProbe.AnalyseAsync(tmpFile); var tsProbe = sw.Elapsed; sw.Restart(); var ext = Path.GetExtension(source.AbsolutePath); var key = $"{stream.Id}/{recordingId}{ext}"; await _client.PutObjectAsync(new PutObjectRequest { BucketName = _config.BucketName, Key = key, InputStream = fs, AutoCloseStream = false, AutoResetStreamPosition = false, ContentType = ext == ".ts" ? "video/mp2t" : "video/mp4", DisablePayloadSigning = _config.DisablePayloadSigning }); var url = _client.GetPreSignedURL(new() { BucketName = _config.BucketName, Key = key, Expires = new DateTime(3000, 1, 1) }); var ub = new UriBuilder(url) { Scheme = _config.PublicHost.Scheme, Host = _config.PublicHost.Host, Port = _config.PublicHost.Port }; var tsUpload = sw.Elapsed; // cleanup temp file fs.Close(); File.Delete(tmpFile); _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; } }