From 55c96a73b93c53983c1ada73dc4988e2e036fbc1 Mon Sep 17 00:00:00 2001 From: Kieran Date: Mon, 31 Jul 2023 12:07:51 +0100 Subject: [PATCH] Use on_hls segments --- .../Controllers/PlaylistController.cs | 47 ++++++++++++++++--- NostrStreamer/Controllers/SRSController.cs | 13 ++--- NostrStreamer/NostrStreamer.csproj | 1 + NostrStreamer/Services/Dvr/S3DvrStore.cs | 46 ++++++++++++++---- .../StreamManager/NostrStreamManager.cs | 4 +- 5 files changed, 88 insertions(+), 23 deletions(-) diff --git a/NostrStreamer/Controllers/PlaylistController.cs b/NostrStreamer/Controllers/PlaylistController.cs index 9278b6c..0107851 100644 --- a/NostrStreamer/Controllers/PlaylistController.cs +++ b/NostrStreamer/Controllers/PlaylistController.cs @@ -1,4 +1,5 @@ using System.Text.RegularExpressions; +using MediaFormatLibrary.MP4; using Microsoft.AspNetCore.Mvc; using NostrStreamer.Database; using NostrStreamer.Services; @@ -18,7 +19,8 @@ public class PlaylistController : Controller private readonly ThumbnailService _thumbnailService; public PlaylistController(Config config, ILogger logger, - HttpClient client, SrsApi srsApi, ViewCounter viewCounter, StreamManagerFactory streamManagerFactory, ThumbnailService thumbnailService) + HttpClient client, SrsApi srsApi, ViewCounter viewCounter, StreamManagerFactory streamManagerFactory, + ThumbnailService thumbnailService) { _config = config; _logger = logger; @@ -177,7 +179,7 @@ public class PlaylistController : Controller } [HttpGet("recording/{id:guid}.m3u8")] - public async Task RecordingPlaylist([FromRoute]Guid id) + public async Task RecordingPlaylist([FromRoute] Guid id) { try { @@ -189,14 +191,14 @@ public class PlaylistController : Controller await using var sw = new StreamWriter(Response.Body); await sw.WriteLineAsync("#EXTM3U"); await sw.WriteLineAsync("#EXT-X-PLAYLIST-TYPE:VOD"); - await sw.WriteLineAsync("#EXT-X-TARGETDURATION:30"); - await sw.WriteLineAsync("#EXT-X-VERSION:4"); + await sw.WriteLineAsync("#EXT-X-TARGETDURATION:4"); + await sw.WriteLineAsync("#EXT-X-VERSION:6"); await sw.WriteLineAsync("#EXT-X-MEDIA-SEQUENCE:0"); - await sw.WriteLineAsync("#EXT-X-INDEPENDENT-SEGMENTS"); + //await sw.WriteLineAsync($"#EXT-X-MAP:URI=\"{id}_init.mp4\""); foreach (var seg in userStream.Recordings.OrderBy(a => a.Timestamp)) { - await sw.WriteLineAsync($"#EXTINF:{seg.Duration:0.0####},"); + await sw.WriteLineAsync($"#EXTINF:{seg.Duration},"); await sw.WriteLineAsync($"#EXT-X-PROGRAM-DATE-TIME:{seg.Timestamp:yyyy-MM-ddTHH:mm:ss.fffzzz}"); await sw.WriteLineAsync(seg.Url); } @@ -208,7 +210,38 @@ public class PlaylistController : Controller Response.StatusCode = 404; } } - + + [HttpGet("recording/{id:guid}_init.mp4")] + public async Task GenerateInitTrack([FromRoute] Guid id) + { + try + { + var streamManager = await _streamManagerFactory.ForStream(id); + var userStream = streamManager.GetStream(); + + var firstFrag = await _client.GetStreamAsync(userStream.Recordings.First().Url); + var tmpFrag = Path.GetTempFileName(); + await firstFrag.CopyToAsync(new FileStream(tmpFrag, FileMode.Open, FileAccess.ReadWrite)); + + var frag = MP4Stream.Open(tmpFrag, FileMode.Open, FileAccess.Read); + var boxes = frag.ReadRootBoxes(); + + Response.ContentType = "video/mp4"; + using var outStream = new MemoryStream(); + foreach (var box in boxes.Take(2)) + { + box.WriteBytes(outStream); + } + + outStream.Seek(0, SeekOrigin.Begin); + await outStream.CopyToAsync(Response.Body); + } + catch + { + Response.StatusCode = 404; + } + } + private async Task GetHlsCtx(UserStream stream) { var path = $"/{stream.Endpoint.App}/source/{stream.User.StreamKey}.m3u8"; diff --git a/NostrStreamer/Controllers/SRSController.cs b/NostrStreamer/Controllers/SRSController.cs index 90bbbd8..c827d98 100644 --- a/NostrStreamer/Controllers/SRSController.cs +++ b/NostrStreamer/Controllers/SRSController.cs @@ -75,17 +75,18 @@ public class SrsController : Controller return new(); } - if (req.Action == "on_hls" && req.Duration.HasValue && !string.IsNullOrEmpty(req.ClientId)) + if (req.Action == "on_hls" && req.Duration.HasValue && !string.IsNullOrEmpty(req.ClientId) && !string.IsNullOrEmpty(req.File)) { await streamManager.ConsumeQuota(req.Duration.Value); - return new(); - } - - if (req.Action == "on_dvr" && !string.IsNullOrEmpty(req.File)) - { await streamManager.OnDvr(new Uri(_config.SrsHttpHost, $"{req.App}/{Path.GetFileName(req.File)}")); return new(); } + + /*if (req.Action == "on_dvr" && !string.IsNullOrEmpty(req.File)) + { + await streamManager.OnDvr(new Uri(_config.SrsHttpHost, $"{req.App}/{Path.GetFileName(req.File)}")); + return new(); + }*/ } else { diff --git a/NostrStreamer/NostrStreamer.csproj b/NostrStreamer/NostrStreamer.csproj index 933a367..1caa582 100644 --- a/NostrStreamer/NostrStreamer.csproj +++ b/NostrStreamer/NostrStreamer.csproj @@ -38,6 +38,7 @@ runtime; build; native; contentfiles; analyzers; buildtransitive + diff --git a/NostrStreamer/Services/Dvr/S3DvrStore.cs b/NostrStreamer/Services/Dvr/S3DvrStore.cs index 05cd69a..ede3827 100644 --- a/NostrStreamer/Services/Dvr/S3DvrStore.cs +++ b/NostrStreamer/Services/Dvr/S3DvrStore.cs @@ -1,3 +1,4 @@ +using System.Diagnostics; using Amazon.S3; using Amazon.S3.Model; using FFMpegCore; @@ -9,27 +10,51 @@ 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) + public S3DvrStore(Config config, HttpClient httpClient, ILogger logger) { _httpClient = httpClient; + _logger = logger; _config = config.DvrStore; _client = config.DvrStore.CreateClient(); } public async Task UploadRecording(Uri source) { + var sw = Stopwatch.StartNew(); + var tmpFile = Path.GetTempFileName(); var recordingId = Guid.NewGuid(); - var dvrSeg = await _httpClient.GetStreamAsync(source); - + + /* + 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); - await dvrSeg.CopyToAsync(fs); + 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); - fs.Seek(0, SeekOrigin.Begin); - - var key = $"{recordingId}.mp4"; + var tsProbe = sw.Elapsed; + + sw.Restart(); + var ext = Path.GetExtension(source.AbsolutePath); + var key = $"{recordingId}{ext}"; await _client.PutObjectAsync(new PutObjectRequest { BucketName = _config.BucketName, @@ -37,7 +62,7 @@ public class S3DvrStore : IDvrStore InputStream = fs, AutoCloseStream = false, AutoResetStreamPosition = false, - ContentType = "video/mp4", + ContentType = ext == ".ts" ? "video/mp2t" : "video/mp4", DisablePayloadSigning = _config.DisablePayloadSigning }); @@ -48,11 +73,16 @@ public class S3DvrStore : IDvrStore Expires = new DateTime(3000, 1, 1) }); + var tsUpload = sw.Elapsed; + var ret = new UriBuilder(url) { Scheme = _config.ServiceUrl.Scheme }; + _logger.LogInformation("download={tc:#,##0}ms, probe={pc:#,##0}ms, upload={uc:#,##0}ms", tsDownload.TotalMilliseconds, + tsProbe.TotalMilliseconds, tsUpload.TotalMilliseconds); + return new(ret.Uri, probe.Duration.TotalSeconds); } } diff --git a/NostrStreamer/Services/StreamManager/NostrStreamManager.cs b/NostrStreamer/Services/StreamManager/NostrStreamManager.cs index c8f09dd..26367c5 100644 --- a/NostrStreamer/Services/StreamManager/NostrStreamManager.cs +++ b/NostrStreamer/Services/StreamManager/NostrStreamManager.cs @@ -138,7 +138,7 @@ public class NostrStreamManager : IStreamManager public async Task OnDvr(Uri segment) { - var matches = new Regex("\\.(\\d+)\\.[\\w]{2,4}$").Match(segment.AbsolutePath); + //var matches = new Regex("\\.(\\d+)\\.[\\w]{2,4}$").Match(segment.AbsolutePath); var result = await _dvrStore.UploadRecording(segment); _context.Db.Recordings.Add(new() @@ -146,7 +146,7 @@ public class NostrStreamManager : IStreamManager UserStreamId = _context.UserStream.Id, Url = result.Result.ToString(), Duration = result.Duration, - Timestamp = DateTimeOffset.FromUnixTimeMilliseconds(long.Parse(matches.Groups[1].Value)).UtcDateTime + Timestamp = DateTime.UtcNow //DateTimeOffset.FromUnixTimeMilliseconds(long.Parse(matches.Groups[1].Value)).UtcDateTime }); await _context.Db.SaveChangesAsync();