Encoding added Requested Encoding to the request, made request async, some refactoring
This commit is contained in:
@@ -7,14 +7,38 @@ Accept: application/json
|
||||
POST {{Encoder_HostAddress}}/encode
|
||||
Content-Type: multipart/form-data; boundary=WebAppBoundary
|
||||
|
||||
--WebAppBoundary
|
||||
Content-Disposition: form-data; name="encoder"
|
||||
Content-Type: text/plain
|
||||
|
||||
HEVC
|
||||
--WebAppBoundary
|
||||
Content-Disposition: form-data; name="video"; filename="testVid8k.mp4"
|
||||
Content-Type: video/mp4
|
||||
|
||||
< ../EncodingSampleTest/testVid8k.mp4
|
||||
--WebAppBoundary--
|
||||
|
||||
###
|
||||
|
||||
# @timeout 5m (big ass file)
|
||||
POST {{Encoder_HostAddress}}/encode
|
||||
Content-Type: multipart/form-data; boundary=WebAppBoundary
|
||||
|
||||
--WebAppBoundary
|
||||
Content-Disposition: form-data; name="encoder"
|
||||
Content-Type: text/plain
|
||||
|
||||
AV1
|
||||
--WebAppBoundary
|
||||
Content-Disposition: form-data; name="video"; filename="testBigVid.mp4"
|
||||
Content-Type: video/mp4
|
||||
|
||||
< ../EncodingSampleTest/DBER-180.mp4
|
||||
--WebAppBoundary--
|
||||
|
||||
###
|
||||
|
||||
POST {{Encoder_HostAddress}}/encode
|
||||
Content-Type: multipart/form-data; boundary=WebAppBoundary
|
||||
|
||||
|
||||
@@ -14,8 +14,8 @@ public class EncoderService : BackgroundService, IEncoderService {
|
||||
public EncoderService(ILogger<EncoderService> logger, IOptions<FFmpegOptions> ffmpegOptions) {
|
||||
Logger = logger;
|
||||
options = ffmpegOptions.Value;
|
||||
options.FfmpegPath = Path.GetFullPath(Path.Combine(Path.GetDirectoryName(Environment.ProcessPath), options.FfmpegPath));
|
||||
options.TemporaryFilesPath = Path.GetFullPath(Path.Combine(Path.GetDirectoryName(Environment.ProcessPath), options.TemporaryFilesPath));
|
||||
options.FfmpegPath = options.FfmpegPath.AbsoluteOrProcessPath();
|
||||
options.TemporaryFilesPath = options.TemporaryFilesPath.AbsoluteOrProcessPath();
|
||||
Directory.CreateDirectory(options.TemporaryFilesPath); // Ensure the temporary files directory exists
|
||||
JobQueue = new();
|
||||
|
||||
@@ -63,7 +63,7 @@ public class EncoderService : BackgroundService, IEncoderService {
|
||||
|
||||
void ProcessJob(EncodingJob job) {
|
||||
job.Status = JobStatus.InProgress;
|
||||
var file = job.OrigFilePath;
|
||||
var file = job.FilePath;
|
||||
var mediaInfo = FFProbe.Analyse(file);
|
||||
if (mediaInfo.PrimaryVideoStream == null) {
|
||||
job.Status = JobStatus.Failed;
|
||||
@@ -71,7 +71,7 @@ public class EncoderService : BackgroundService, IEncoderService {
|
||||
}
|
||||
var W = mediaInfo.PrimaryVideoStream.Width;
|
||||
var H = mediaInfo.PrimaryVideoStream.Height;
|
||||
string outputPath = Path.Combine(options.TemporaryFilesPath, Path.GetFileName(job.OrigFilePath));
|
||||
string outputPath = Path.Combine(options.TemporaryFilesPath, Path.GetFileName(job.FilePath));
|
||||
int qp = Utils.ToQPValue(W, H);
|
||||
var status = FFMpegArguments.FromFileInput(file, true, args => args.WithHardwareAcceleration())
|
||||
.OutputToFile(outputPath, true, args => args
|
||||
@@ -99,8 +99,11 @@ public class EncoderService : BackgroundService, IEncoderService {
|
||||
if(status) {
|
||||
job.Status = JobStatus.Completed;
|
||||
job.EncodedFilePath = outputPath;
|
||||
File.Delete(file); // Clean up original file
|
||||
} else {
|
||||
job.Status = JobStatus.Failed;
|
||||
// Clean up any partially created output file. Leave original file for retry.
|
||||
if (File.Exists(outputPath)) File.Delete(outputPath);
|
||||
}
|
||||
job.CompletedAt = DateTime.Now;
|
||||
job.Progress = 1.0f;
|
||||
|
||||
@@ -1,9 +1,31 @@
|
||||
namespace Encoder;
|
||||
|
||||
public record EncodingJob(Guid Id, string OrigFilePath) {
|
||||
public enum JobStatus {
|
||||
Pending,
|
||||
InProgress,
|
||||
Completed,
|
||||
Failed
|
||||
}
|
||||
|
||||
|
||||
public enum EncoderType {
|
||||
AV1,
|
||||
HEVC
|
||||
}
|
||||
|
||||
public record EncodingJob {
|
||||
public Guid Id { get; init; }
|
||||
public JobStatus Status { get; set; } = JobStatus.Pending;
|
||||
public DateTime CreatedAt { get; init; } = DateTime.Now;
|
||||
public DateTime? CompletedAt { get; set; } = null;
|
||||
public string FilePath { get; init; }
|
||||
public EncoderType RequestedEncoding { get; init; }
|
||||
public string EncodedFilePath { get; set; } = string.Empty;
|
||||
public float Progress { get; set; } = 0.0f;
|
||||
|
||||
public EncodingJob(Guid id, string filePath, EncoderType requestedEncoding) {
|
||||
Id = id;
|
||||
FilePath = filePath;
|
||||
RequestedEncoding = requestedEncoding;
|
||||
}
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
namespace Encoder;
|
||||
|
||||
public enum JobStatus {
|
||||
Pending,
|
||||
InProgress,
|
||||
Completed,
|
||||
Failed
|
||||
}
|
||||
@@ -9,7 +9,7 @@ builder.Services.AddOpenApi();
|
||||
builder.Services.AddLogging();
|
||||
|
||||
string uploadsPath = builder.Configuration.GetSection("UploadsPath").Get<string>() ?? "./Uploads";
|
||||
if(!Path.IsPathRooted(uploadsPath)) uploadsPath = Path.GetFullPath(Path.Combine(Path.GetDirectoryName(Environment.ProcessPath), uploadsPath));
|
||||
uploadsPath = uploadsPath.AbsoluteOrProcessPath();
|
||||
Directory.CreateDirectory(uploadsPath); // Ensure the uploads directory exists
|
||||
|
||||
builder.Services.Configure<FFmpegOptions>(builder.Configuration.GetSection(FFmpegOptions.SectionName));
|
||||
@@ -26,42 +26,51 @@ app.UseHttpsRedirection();
|
||||
// Get a video file as multipart form data and schedule it for encoding
|
||||
// Get video encoding settings from query parameters
|
||||
// Returns the ID of the job handling the encoding
|
||||
app.MapPost("encode", context =>
|
||||
{
|
||||
app.MapPost("encode", async context => {
|
||||
// Disable request size limit
|
||||
context.Features.Get<IHttpMaxRequestBodySizeFeature>()?.MaxRequestBodySize = null;
|
||||
|
||||
var request = context.Request;
|
||||
if (!request.HasFormContentType) {
|
||||
context.Response.StatusCode = 400;
|
||||
return context.Response.WriteAsync("Invalid content type. Expected multipart/form-data.");
|
||||
await context.Response.WriteAsync("Invalid content type. Expected multipart/form-data.");
|
||||
return;
|
||||
}
|
||||
|
||||
var form = request.Form;
|
||||
var file = form.Files.GetFile("video"); // Contrary to what it seems, the "name" here is the form field name, not the file name
|
||||
|
||||
var encoderType = form["encoder"].FirstOrDefault();
|
||||
if(encoderType == null || !Enum.TryParse<EncoderType>(encoderType, true, out var parsedEncoderType)) {
|
||||
context.Response.StatusCode = 400;
|
||||
await context.Response.WriteAsync("Invalid or missing encoderType parameter.");
|
||||
return;
|
||||
}
|
||||
|
||||
// Contrary to what it seems, the "name" here is the form field name, not the file name
|
||||
var file = form.Files.GetFile("video");
|
||||
if (file == null) {
|
||||
context.Response.StatusCode = 400;
|
||||
return context.Response.WriteAsync("No video file provided.");
|
||||
await context.Response.WriteAsync("No video file provided.");
|
||||
return;
|
||||
}
|
||||
|
||||
// Save the file to a temporary location
|
||||
var jobGuid = Guid.NewGuid();
|
||||
var tempFilePath = Path.GetFullPath(Path.Combine(uploadsPath, jobGuid.ToString("D")+Path.GetExtension(file.FileName)));
|
||||
using (var stream = File.Create(tempFilePath))
|
||||
file.CopyTo(stream);
|
||||
await using (var stream = File.Create(tempFilePath)) file.CopyTo(stream);
|
||||
|
||||
var job = new EncodingJob(jobGuid, tempFilePath);
|
||||
// Create and enqueue the encoding job
|
||||
var job = new EncodingJob(jobGuid, tempFilePath, parsedEncoderType);
|
||||
var encSrv = context.RequestServices.GetService<EncoderService>();
|
||||
if (encSrv != null) encSrv.EnqueueJob(job);
|
||||
else {
|
||||
if (encSrv == null) {
|
||||
context.Response.StatusCode = 500;
|
||||
return context.Response.WriteAsync("Encoder service not available.");
|
||||
await context.Response.WriteAsync("Encoder service not available.");
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
encSrv.EnqueueJob(job);
|
||||
context.Response.StatusCode = 200;
|
||||
return context.Response.WriteAsJsonAsync(new { JobId = jobGuid });
|
||||
}).WithFormOptions(multipartBodyLengthLimit: 1024L*1024L*1024L*64L);
|
||||
await context.Response.WriteAsJsonAsync(new { JobId = jobGuid });
|
||||
}).WithFormOptions(multipartBodyLengthLimit: 1_073_741_824L*64L);
|
||||
|
||||
app.MapGet("status", (context) => {
|
||||
var encSrv = context.RequestServices.GetService<EncoderService>();
|
||||
@@ -77,8 +86,7 @@ app.MapGet("status", (context) => {
|
||||
});
|
||||
|
||||
// Check the status of an encoding job by its ID
|
||||
app.MapGet("status/{jobId:guid}", (HttpContext context, Guid jobId) =>
|
||||
{
|
||||
app.MapGet("status/{jobId:guid}", (HttpContext context, Guid jobId) => {
|
||||
var encSrv = context.RequestServices.GetService<EncoderService>();
|
||||
if (encSrv == null) return Results.InternalServerError();
|
||||
|
||||
@@ -95,8 +103,7 @@ app.MapGet("status/{jobId:guid}", (HttpContext context, Guid jobId) =>
|
||||
});
|
||||
});
|
||||
|
||||
app.MapGet("file/{jobId:guid}", (HttpContext context, Guid jobId) =>
|
||||
{
|
||||
app.MapGet("file/{jobId:guid}", (HttpContext context, Guid jobId) => {
|
||||
var encSrv = context.RequestServices.GetService<EncoderService>();
|
||||
if (encSrv == null) return Results.InternalServerError();
|
||||
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
using System.Runtime.CompilerServices;
|
||||
using Microsoft.Net.Http.Headers;
|
||||
|
||||
namespace Encoder;
|
||||
|
||||
public static class Utils {
|
||||
static Tuple<int, int>[] QPTable = new Tuple<int, int>[] {
|
||||
private static readonly Tuple<int, int>[] QPTable = new Tuple<int, int>[] {
|
||||
new(1280*720, 64),
|
||||
new(1920*1080, 96),
|
||||
new(3840*2160, 128),
|
||||
@@ -9,30 +12,43 @@ public static class Utils {
|
||||
new(8128*4096, 120) //VR8K
|
||||
}.OrderBy(t => t.Item1).ToArray();
|
||||
|
||||
static float lerp(float v0, float v1, float t) {
|
||||
public static float Lerp(float v0, float v1, float t) {
|
||||
return v0 + t * (v1 - v0);
|
||||
}
|
||||
|
||||
static float remap(float value, float from1, float to1, float from2, float to2) {
|
||||
return from2 + (value - from1) * (to2 - from2) / (to1 - from1);
|
||||
}
|
||||
|
||||
public static float Remap(float value, float from1, float to1, float from2, float to2) => from2 + (value - from1) * (to2 - from2) / (to1 - from1);
|
||||
|
||||
public static int ToQPValue(int W, int H) {
|
||||
int pixels = W * H;
|
||||
for (var i = 0; i < QPTable.Length; i++) {
|
||||
var t = QPTable[i];
|
||||
if (pixels <= t.Item1) {
|
||||
if (pixels <= QPTable[i].Item1) {
|
||||
var minQP = QPTable[i - 1].Item2;
|
||||
var maxQP = QPTable[i].Item2;
|
||||
|
||||
var minPixels = QPTable[i - 1].Item1;
|
||||
var maxPixels = QPTable[i].Item1;
|
||||
var tPixels = remap(pixels, minPixels, maxPixels, 0, 1);
|
||||
|
||||
var tPixels = Remap(pixels, minPixels, maxPixels, 0, 1);
|
||||
|
||||
return (int)lerp(minQP, maxQP, tPixels);
|
||||
return (int)Lerp(minQP, maxQP, tPixels);
|
||||
}
|
||||
}
|
||||
// Return the highest QP for anything higher than 8K VR
|
||||
return QPTable.Last().Item2;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static string AbsoluteOrProcessPath(this string path) {
|
||||
return Path.GetFullPath(Path.Combine(Path.GetDirectoryName(Environment.ProcessPath)!, path));
|
||||
}
|
||||
|
||||
public static bool IsMultipartContentType(string contentType)
|
||||
=> !string.IsNullOrEmpty(contentType) && contentType.Contains("multipart/form-data");
|
||||
|
||||
public static string? GetBoundary(string contentType)
|
||||
{
|
||||
var boundary = HeaderUtilities.RemoveQuotes(
|
||||
MediaTypeHeaderValue.Parse(contentType).Boundary
|
||||
).Value;
|
||||
return boundary;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user