Redo clips api

This commit is contained in:
2023-12-11 13:04:33 +00:00
parent 79973a3091
commit b75bea89e6
5 changed files with 109 additions and 19 deletions

View File

@ -0,0 +1,16 @@
using Newtonsoft.Json;
namespace NostrStreamer.ApiModel;
public class MakeClipReq
{
[JsonProperty("segments")]
public List<int> Segments { get; init; } = null!;
[JsonProperty("start")]
public float Start { get; init; }
[JsonProperty("length")]
public float Length { get; init; }
}

View File

@ -165,11 +165,27 @@ public class NostrController : Controller
return Ok();
}
[HttpGet("clip/{id:guid}")]
public async Task<IActionResult> GetClipSegments([FromRoute] Guid id)
{
var clip = await _clipService.PrepareClip(id);
if (clip == default) return StatusCode(500);
return Json(new
{
id,
segments = clip.Select(a => new
{
idx = a.Index
})
});
}
[HttpPost("clip/{id:guid}")]
public async Task<IActionResult> CreateClip([FromRoute] Guid id)
public async Task<IActionResult> MakeClip([FromRoute] Guid id, [FromBody] MakeClipReq req)
{
var pk = GetPubKey();
var clip = await _clipService.CreateClip(id, pk);
var clip = await _clipService.MakeClip(pk, req.Segments.Select(a => new ClipSegment(id, a)).ToList(), req.Start, req.Length);
if (clip == default) return StatusCode(500);
return Json(new
@ -178,6 +194,19 @@ public class NostrController : Controller
});
}
[HttpGet("clip/{id:guid}/{idx:int}.ts")]
public async Task<IActionResult> GetClipSegment([FromRoute] Guid id, [FromRoute] int idx)
{
var seg = new ClipSegment(id, idx);
if (!System.IO.File.Exists(seg.GetPath()))
{
return NotFound();
}
await using var fs = new FileStream(seg.GetPath(), FileMode.Open, FileAccess.Read);
return File(fs, "video/mp2t");
}
private async Task<User?> GetUser()
{
var pk = GetPubKey();

View File

@ -1,5 +1,5 @@
using System.Text.RegularExpressions;
using FFMpegCore;
using NostrStreamer.Database;
namespace NostrStreamer.Services.Clips;
@ -7,28 +7,56 @@ public class ClipGenerator
{
private readonly ILogger<ClipGenerator> _logger;
private readonly Config _config;
private readonly HttpClient _client;
public ClipGenerator(ILogger<ClipGenerator> logger, Config config)
public ClipGenerator(ILogger<ClipGenerator> logger, Config config, HttpClient client)
{
_logger = logger;
_config = config;
_client = client;
}
public async Task<string> GenerateClip(UserStream stream)
public async Task<string> CreateClipFromSegments(List<ClipSegment> segments, float start, float length)
{
const int clipLength = 20;
var path = Path.ChangeExtension(Path.GetTempFileName(), ".mp4");
var cmd = FFMpegArguments
.FromUrlInput(new Uri(_config.DataHost, $"stream/{stream.Id}.m3u8"),
inOpt =>
{
inOpt.WithCustomArgument($"-ss -{clipLength}");
})
.OutputToFile(path, true, o => { o.WithDuration(TimeSpan.FromSeconds(clipLength)); })
.FromConcatInput(segments.Select(a => a.GetPath()),
inOpt => { inOpt.Seek(TimeSpan.FromSeconds(start)); })
.OutputToFile(path, true, o => { o.WithDuration(TimeSpan.FromSeconds(length)); })
.CancellableThrough(new CancellationTokenSource(TimeSpan.FromSeconds(60)).Token);
_logger.LogInformation("Running command {cmd}", cmd.Arguments);
await cmd.ProcessAsynchronously();
return path;
}
public async Task<List<ClipSegment>> GetClipSegments(Guid id)
{
var ret = new List<ClipSegment>();
var playlist = new Uri(_config.DataHost, $"stream/{id}.m3u8");
var rsp = await _client.GetStreamAsync(playlist);
using var sr = new StreamReader(rsp);
while (await sr.ReadLineAsync() is { } line)
{
if (line.StartsWith("#EXTINF"))
{
var trackPath = await sr.ReadLineAsync();
var seg = Regex.Match(trackPath!, @"-(\d+)\.ts");
var idx = int.Parse(seg.Groups[1].Value);
var clipSeg = new ClipSegment(id, idx);
var outPath = clipSeg.GetPath();
if (!File.Exists(outPath))
{
var segStream = await _client.GetStreamAsync(new Uri(_config.DataHost, trackPath));
await using var fsOut = new FileStream(outPath, FileMode.Create, FileAccess.ReadWrite);
await segStream.CopyToAsync(fsOut);
}
ret.Add(clipSeg);
}
}
return ret;
}
}

View File

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

View File

@ -20,7 +20,7 @@ public class S3ClipService : IClipService
_context = context;
}
public async Task<ClipResult?> CreateClip(Guid streamId, string takenBy)
public async Task<List<ClipSegment>?> PrepareClip(Guid streamId)
{
var stream = await _context.Streams
.Include(a => a.User)
@ -32,12 +32,19 @@ public class S3ClipService : IClipService
return default;
}
var tmpClip = await _generator.GenerateClip(stream);
return await _generator.GetClipSegments(streamId);
}
public async Task<ClipResult?> MakeClip(string takenBy, List<ClipSegment> segments, float start, float length)
{
if (segments.Count == 0) return default;
var streamId = segments.First().Id;
var clip = await _generator.CreateClipFromSegments(segments, start, length);
var clipId = Guid.NewGuid();
var s3Path = $"{stream.Id}/clips/{clipId}.mp4";
var s3Path = $"{streamId}/clips/{clipId}.mp4";
await using var fs = new FileStream(tmpClip, FileMode.Open, FileAccess.Read);
await using var fs = new FileStream(clip, FileMode.Open, FileAccess.Read);
await _client.PutObjectAsync(new()
{
BucketName = _config.S3Store.BucketName,
@ -67,7 +74,7 @@ public class S3ClipService : IClipService
var clipObj = new UserStreamClip()
{
Id = clipId,
UserStreamId = stream.Id,
UserStreamId = streamId,
TakenByPubkey = takenBy,
Url = ub.Uri.ToString()
};