Use on_hls segments
This commit is contained in:
@ -1,4 +1,5 @@
|
|||||||
using System.Text.RegularExpressions;
|
using System.Text.RegularExpressions;
|
||||||
|
using MediaFormatLibrary.MP4;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using NostrStreamer.Database;
|
using NostrStreamer.Database;
|
||||||
using NostrStreamer.Services;
|
using NostrStreamer.Services;
|
||||||
@ -18,7 +19,8 @@ public class PlaylistController : Controller
|
|||||||
private readonly ThumbnailService _thumbnailService;
|
private readonly ThumbnailService _thumbnailService;
|
||||||
|
|
||||||
public PlaylistController(Config config, ILogger<PlaylistController> logger,
|
public PlaylistController(Config config, ILogger<PlaylistController> logger,
|
||||||
HttpClient client, SrsApi srsApi, ViewCounter viewCounter, StreamManagerFactory streamManagerFactory, ThumbnailService thumbnailService)
|
HttpClient client, SrsApi srsApi, ViewCounter viewCounter, StreamManagerFactory streamManagerFactory,
|
||||||
|
ThumbnailService thumbnailService)
|
||||||
{
|
{
|
||||||
_config = config;
|
_config = config;
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
@ -189,14 +191,14 @@ public class PlaylistController : Controller
|
|||||||
await using var sw = new StreamWriter(Response.Body);
|
await using var sw = new StreamWriter(Response.Body);
|
||||||
await sw.WriteLineAsync("#EXTM3U");
|
await sw.WriteLineAsync("#EXTM3U");
|
||||||
await sw.WriteLineAsync("#EXT-X-PLAYLIST-TYPE:VOD");
|
await sw.WriteLineAsync("#EXT-X-PLAYLIST-TYPE:VOD");
|
||||||
await sw.WriteLineAsync("#EXT-X-TARGETDURATION:30");
|
await sw.WriteLineAsync("#EXT-X-TARGETDURATION:4");
|
||||||
await sw.WriteLineAsync("#EXT-X-VERSION:4");
|
await sw.WriteLineAsync("#EXT-X-VERSION:6");
|
||||||
await sw.WriteLineAsync("#EXT-X-MEDIA-SEQUENCE:0");
|
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))
|
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($"#EXT-X-PROGRAM-DATE-TIME:{seg.Timestamp:yyyy-MM-ddTHH:mm:ss.fffzzz}");
|
||||||
await sw.WriteLineAsync(seg.Url);
|
await sw.WriteLineAsync(seg.Url);
|
||||||
}
|
}
|
||||||
@ -209,6 +211,37 @@ public class PlaylistController : Controller
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[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<string?> GetHlsCtx(UserStream stream)
|
private async Task<string?> GetHlsCtx(UserStream stream)
|
||||||
{
|
{
|
||||||
var path = $"/{stream.Endpoint.App}/source/{stream.User.StreamKey}.m3u8";
|
var path = $"/{stream.Endpoint.App}/source/{stream.User.StreamKey}.m3u8";
|
||||||
|
@ -75,17 +75,18 @@ public class SrsController : Controller
|
|||||||
return new();
|
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);
|
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)}"));
|
await streamManager.OnDvr(new Uri(_config.SrsHttpHost, $"{req.App}/{Path.GetFileName(req.File)}"));
|
||||||
return new();
|
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
|
else
|
||||||
{
|
{
|
||||||
|
@ -38,6 +38,7 @@
|
|||||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
</PackageReference>
|
</PackageReference>
|
||||||
<PackageReference Include="LNURL" Version="0.0.30" />
|
<PackageReference Include="LNURL" Version="0.0.30" />
|
||||||
|
<PackageReference Include="MediaFormatLibrary.Lib" Version="1.0.0" />
|
||||||
<PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="6.0.19" />
|
<PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="6.0.19" />
|
||||||
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="7.0.8" />
|
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="7.0.8" />
|
||||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="7.0.8">
|
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="7.0.8">
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
using System.Diagnostics;
|
||||||
using Amazon.S3;
|
using Amazon.S3;
|
||||||
using Amazon.S3.Model;
|
using Amazon.S3.Model;
|
||||||
using FFMpegCore;
|
using FFMpegCore;
|
||||||
@ -9,27 +10,51 @@ public class S3DvrStore : IDvrStore
|
|||||||
private readonly AmazonS3Client _client;
|
private readonly AmazonS3Client _client;
|
||||||
private readonly S3BlobConfig _config;
|
private readonly S3BlobConfig _config;
|
||||||
private readonly HttpClient _httpClient;
|
private readonly HttpClient _httpClient;
|
||||||
|
private readonly ILogger<S3DvrStore> _logger;
|
||||||
|
|
||||||
public S3DvrStore(Config config, HttpClient httpClient)
|
public S3DvrStore(Config config, HttpClient httpClient, ILogger<S3DvrStore> logger)
|
||||||
{
|
{
|
||||||
_httpClient = httpClient;
|
_httpClient = httpClient;
|
||||||
|
_logger = logger;
|
||||||
_config = config.DvrStore;
|
_config = config.DvrStore;
|
||||||
_client = config.DvrStore.CreateClient();
|
_client = config.DvrStore.CreateClient();
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<UploadResult> UploadRecording(Uri source)
|
public async Task<UploadResult> UploadRecording(Uri source)
|
||||||
{
|
{
|
||||||
|
var sw = Stopwatch.StartNew();
|
||||||
|
|
||||||
var tmpFile = Path.GetTempFileName();
|
var tmpFile = Path.GetTempFileName();
|
||||||
var recordingId = Guid.NewGuid();
|
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 using var fs = new FileStream(tmpFile, FileMode.Create, FileAccess.ReadWrite);
|
||||||
await dvrSeg.CopyToAsync(fs);
|
var dl = await _httpClient.GetStreamAsync(source);
|
||||||
fs.Seek(0, SeekOrigin.Begin);
|
await dl.CopyToAsync(fs);
|
||||||
var probe = await FFProbe.AnalyseAsync(tmpFile);
|
await fs.FlushAsync();
|
||||||
fs.Seek(0, SeekOrigin.Begin);
|
fs.Seek(0, SeekOrigin.Begin);
|
||||||
|
var tsDownload = sw.Elapsed;
|
||||||
|
|
||||||
var key = $"{recordingId}.mp4";
|
sw.Restart();
|
||||||
|
var probe = await FFProbe.AnalyseAsync(tmpFile);
|
||||||
|
var tsProbe = sw.Elapsed;
|
||||||
|
|
||||||
|
sw.Restart();
|
||||||
|
var ext = Path.GetExtension(source.AbsolutePath);
|
||||||
|
var key = $"{recordingId}{ext}";
|
||||||
await _client.PutObjectAsync(new PutObjectRequest
|
await _client.PutObjectAsync(new PutObjectRequest
|
||||||
{
|
{
|
||||||
BucketName = _config.BucketName,
|
BucketName = _config.BucketName,
|
||||||
@ -37,7 +62,7 @@ public class S3DvrStore : IDvrStore
|
|||||||
InputStream = fs,
|
InputStream = fs,
|
||||||
AutoCloseStream = false,
|
AutoCloseStream = false,
|
||||||
AutoResetStreamPosition = false,
|
AutoResetStreamPosition = false,
|
||||||
ContentType = "video/mp4",
|
ContentType = ext == ".ts" ? "video/mp2t" : "video/mp4",
|
||||||
DisablePayloadSigning = _config.DisablePayloadSigning
|
DisablePayloadSigning = _config.DisablePayloadSigning
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -48,11 +73,16 @@ public class S3DvrStore : IDvrStore
|
|||||||
Expires = new DateTime(3000, 1, 1)
|
Expires = new DateTime(3000, 1, 1)
|
||||||
});
|
});
|
||||||
|
|
||||||
|
var tsUpload = sw.Elapsed;
|
||||||
|
|
||||||
var ret = new UriBuilder(url)
|
var ret = new UriBuilder(url)
|
||||||
{
|
{
|
||||||
Scheme = _config.ServiceUrl.Scheme
|
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);
|
return new(ret.Uri, probe.Duration.TotalSeconds);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -138,7 +138,7 @@ public class NostrStreamManager : IStreamManager
|
|||||||
|
|
||||||
public async Task OnDvr(Uri segment)
|
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);
|
var result = await _dvrStore.UploadRecording(segment);
|
||||||
_context.Db.Recordings.Add(new()
|
_context.Db.Recordings.Add(new()
|
||||||
@ -146,7 +146,7 @@ public class NostrStreamManager : IStreamManager
|
|||||||
UserStreamId = _context.UserStream.Id,
|
UserStreamId = _context.UserStream.Id,
|
||||||
Url = result.Result.ToString(),
|
Url = result.Result.ToString(),
|
||||||
Duration = result.Duration,
|
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();
|
await _context.Db.SaveChangesAsync();
|
||||||
|
Reference in New Issue
Block a user