better clips

This commit is contained in:
2023-12-13 13:46:30 +00:00
parent 3998db8ea1
commit d087850f69
4 changed files with 95 additions and 33 deletions

View File

@ -173,19 +173,17 @@ public class NostrController : Controller
return Json(new return Json(new
{ {
id, id = clip.Id,
segments = clip.Select(a => new length = clip.Length
{
idx = a.Index
})
}); });
} }
[HttpPost("clip/{id:guid}")] [HttpPost("clip/{streamId:guid}/{tempClipId:guid}")]
public async Task<IActionResult> MakeClip([FromRoute] Guid id, [FromBody] MakeClipReq req) public async Task<IActionResult> MakeClip([FromRoute] Guid streamId, [FromRoute] Guid tempClipId, [FromQuery] float start,
[FromQuery] float length)
{ {
var pk = GetPubKey(); var pk = GetPubKey();
var clip = await _clipService.MakeClip(pk, req.Segments.Select(a => new ClipSegment(id, a)).ToList(), req.Start, req.Length); var clip = await _clipService.MakeClip(pk, streamId, tempClipId, start, length);
if (clip == default) return StatusCode(500); if (clip == default) return StatusCode(500);
return Json(new return Json(new
@ -194,17 +192,18 @@ public class NostrController : Controller
}); });
} }
[HttpGet("clip/{id:guid}/{idx:int}.ts")] [AllowAnonymous]
public async Task<IActionResult> GetClipSegment([FromRoute] Guid id, [FromRoute] int idx) [HttpGet("clip/{streamId:guid}/{clipId:guid}")]
public IActionResult GetClipSegment([FromRoute] Guid streamId, [FromRoute] Guid clipId)
{ {
var seg = new ClipSegment(id, idx); var seg = new TempClip(streamId, clipId, 0);
if (!System.IO.File.Exists(seg.GetPath())) if (!System.IO.File.Exists(seg.GetPath()))
{ {
return NotFound(); return NotFound();
} }
await using var fs = new FileStream(seg.GetPath(), FileMode.Open, FileAccess.Read); var fs = new FileStream(seg.GetPath(), FileMode.Open, FileAccess.Read);
return File(fs, "video/mp2t"); return File(fs, "video/mp4", enableRangeProcessing: true);
} }
private async Task<User?> GetUser() private async Task<User?> GetUser()

View File

@ -17,11 +17,24 @@ public class ClipGenerator
_client = client; _client = client;
} }
public async Task<string> CreateClipFromSegments(List<ClipSegment> segments, float start, float length) public async Task<string> CreateClipFromSegments(List<ClipSegment> segments)
{ {
var path = Path.ChangeExtension(Path.GetTempFileName(), ".mp4"); var path = Path.ChangeExtension(Path.GetTempFileName(), ".mp4");
var cmd = FFMpegArguments var cmd = FFMpegArguments
.FromConcatInput(segments.Select(a => a.GetPath()), .FromConcatInput(segments.Select(a => a.GetPath()))
.OutputToFile(path)
.CancellableThrough(new CancellationTokenSource(TimeSpan.FromSeconds(60)).Token);
_logger.LogInformation("Running command {cmd}", cmd.Arguments);
await cmd.ProcessAsynchronously();
return path;
}
public async Task<string> SliceTempClip(TempClip tempClip, float start, float length)
{
var path = Path.ChangeExtension(Path.GetTempFileName(), ".mp4");
var cmd = FFMpegArguments
.FromFileInput(tempClip.GetPath(), true,
inOpt => { inOpt.Seek(TimeSpan.FromSeconds(start)); }) inOpt => { inOpt.Seek(TimeSpan.FromSeconds(start)); })
.OutputToFile(path, true, o => { o.WithDuration(TimeSpan.FromSeconds(length)); }) .OutputToFile(path, true, o => { o.WithDuration(TimeSpan.FromSeconds(length)); })
.CancellableThrough(new CancellationTokenSource(TimeSpan.FromSeconds(60)).Token); .CancellableThrough(new CancellationTokenSource(TimeSpan.FromSeconds(60)).Token);
@ -34,7 +47,8 @@ public class ClipGenerator
public async Task<List<ClipSegment>> GetClipSegments(UserStream stream) public async Task<List<ClipSegment>> GetClipSegments(UserStream stream)
{ {
var ret = new List<ClipSegment>(); var ret = new List<ClipSegment>();
var path = $"/{stream.Endpoint.App}/source/{stream.User.StreamKey}.m3u8"; 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); var ub = new Uri(_config.SrsHttpHost, path);
var rsp = await _client.GetStreamAsync(ub); var rsp = await _client.GetStreamAsync(ub);
@ -44,14 +58,25 @@ public class ClipGenerator
if (line.StartsWith("#EXTINF")) if (line.StartsWith("#EXTINF"))
{ {
var trackPath = await sr.ReadLineAsync(); var trackPath = await sr.ReadLineAsync();
var segLen = Regex.Match(line, @"#EXTINF:([\d\.]+)");
var seg = Regex.Match(trackPath!, @"-(\d+)\.ts"); var seg = Regex.Match(trackPath!, @"-(\d+)\.ts");
var idx = int.Parse(seg.Groups[1].Value); var idx = int.Parse(seg.Groups[1].Value);
var clipSeg = new ClipSegment(stream.Id, idx); var len = float.Parse(segLen.Groups[1].Value);
var clipSeg = new ClipSegment(stream.Id, idx, len);
var outPath = clipSeg.GetPath(); var outPath = clipSeg.GetPath();
var outDir = Path.GetDirectoryName(outPath);
if (!Directory.Exists(outDir))
{
Directory.CreateDirectory(outDir!);
}
if (!File.Exists(outPath)) if (!File.Exists(outPath))
{ {
var segStream = await _client.GetStreamAsync(new Uri(_config.DataHost, trackPath)); var segStream = await _client.GetStreamAsync(new Uri(_config.SrsHttpHost,
$"/{stream.Endpoint.App}/source/{stream.User.StreamKey}-{idx}.ts"));
await using var fsOut = new FileStream(outPath, FileMode.Create, FileAccess.ReadWrite); await using var fsOut = new FileStream(outPath, FileMode.Create, FileAccess.ReadWrite);
await segStream.CopyToAsync(fsOut); await segStream.CopyToAsync(fsOut);
} }
@ -61,4 +86,33 @@ public class ClipGenerator
return ret; return ret;
} }
private async Task<string?> GetHlsCtx(UserStream stream)
{
var path = $"/{stream.Endpoint.App}/source/{stream.User.StreamKey}.m3u8";
var ub = new Uri(_config.SrsHttpHost, path);
var req = new HttpRequestMessage(HttpMethod.Get, ub);
using var rsp = await _client.SendAsync(req);
if (!rsp.IsSuccessStatusCode)
{
return default;
}
using var sr = new StreamReader(await rsp.Content.ReadAsStreamAsync());
while (await sr.ReadLineAsync() is { } line)
{
if (line.StartsWith("#EXT-X-STREAM-INF"))
{
var trackLine = await sr.ReadLineAsync();
var rx = new Regex("\\?hls_ctx=(\\w+)$");
var match = rx.Match(trackLine!);
if (match.Success)
{
return match.Groups[1].Value;
}
}
}
return default;
}
} }

View File

@ -2,17 +2,25 @@ namespace NostrStreamer.Services.Clips;
public interface IClipService public interface IClipService
{ {
Task<List<ClipSegment>?> PrepareClip(Guid streamId); Task<TempClip?> PrepareClip(Guid streamId);
Task<ClipResult?> MakeClip(string takenBy, List<ClipSegment> segments, float start, float length); Task<ClipResult?> MakeClip(string takenBy, Guid streamId, Guid clipId, float start, float length);
} }
public record ClipResult(Uri Url); public record ClipResult(Uri Url, float Length);
public record ClipSegment(Guid Id, int Index) public record ClipSegment(Guid Id, int Index, float Length)
{ {
public string GetPath() public string GetPath()
{ {
return Path.Join(Path.GetTempPath(), Id.ToString(), $"{Index}.ts"); return Path.Join(Path.GetTempPath(), Id.ToString(), $"{Index}.ts");
} }
} }
public record TempClip(Guid StreamId, Guid Id, float Length)
{
public string GetPath()
{
return Path.Join(Path.GetTempPath(), $"{Id}.mp4");
}
}

View File

@ -15,12 +15,11 @@ public class S3ClipService : IClipService
{ {
_generator = generator; _generator = generator;
_client = config.S3Store.CreateClient(); _client = config.S3Store.CreateClient();
;
_config = config; _config = config;
_context = context; _context = context;
} }
public async Task<List<ClipSegment>?> PrepareClip(Guid streamId) public async Task<TempClip?> PrepareClip(Guid streamId)
{ {
var stream = await _context.Streams var stream = await _context.Streams
.Include(a => a.User) .Include(a => a.User)
@ -32,16 +31,18 @@ public class S3ClipService : IClipService
return default; return default;
} }
return await _generator.GetClipSegments(stream); var segments = await _generator.GetClipSegments(stream);
var clip = await _generator.CreateClipFromSegments(segments);
if (string.IsNullOrEmpty(clip)) return default;
var ret = new TempClip(streamId, Guid.NewGuid(), segments.Sum(a => a.Length));
File.Move(clip, ret.GetPath());
return ret;
} }
public async Task<ClipResult?> MakeClip(string takenBy, List<ClipSegment> segments, float start, float length) public async Task<ClipResult?> MakeClip(string takenBy, Guid streamId, Guid clipId, float start, float length)
{ {
if (segments.Count == 0) return default; var clip = await _generator.SliceTempClip(new(streamId, clipId, 0), start, length);
var streamId = segments.First().Id;
var clip = await _generator.CreateClipFromSegments(segments, start, length);
var clipId = Guid.NewGuid();
var s3Path = $"{streamId}/clips/{clipId}.mp4"; var s3Path = $"{streamId}/clips/{clipId}.mp4";
await using var fs = new FileStream(clip, FileMode.Open, FileAccess.Read); await using var fs = new FileStream(clip, FileMode.Open, FileAccess.Read);
@ -82,6 +83,6 @@ public class S3ClipService : IClipService
_context.Clips.Add(clipObj); _context.Clips.Add(clipObj);
await _context.SaveChangesAsync(); await _context.SaveChangesAsync();
return new(ub.Uri); return new(ub.Uri, length);
} }
} }