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
|
POST {{Encoder_HostAddress}}/encode
|
||||||
Content-Type: multipart/form-data; boundary=WebAppBoundary
|
Content-Type: multipart/form-data; boundary=WebAppBoundary
|
||||||
|
|
||||||
|
--WebAppBoundary
|
||||||
|
Content-Disposition: form-data; name="encoder"
|
||||||
|
Content-Type: text/plain
|
||||||
|
|
||||||
|
HEVC
|
||||||
--WebAppBoundary
|
--WebAppBoundary
|
||||||
Content-Disposition: form-data; name="video"; filename="testVid8k.mp4"
|
Content-Disposition: form-data; name="video"; filename="testVid8k.mp4"
|
||||||
|
Content-Type: video/mp4
|
||||||
|
|
||||||
< ../EncodingSampleTest/testVid8k.mp4
|
< ../EncodingSampleTest/testVid8k.mp4
|
||||||
--WebAppBoundary--
|
--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
|
POST {{Encoder_HostAddress}}/encode
|
||||||
Content-Type: multipart/form-data; boundary=WebAppBoundary
|
Content-Type: multipart/form-data; boundary=WebAppBoundary
|
||||||
|
|
||||||
|
|||||||
@@ -14,8 +14,8 @@ public class EncoderService : BackgroundService, IEncoderService {
|
|||||||
public EncoderService(ILogger<EncoderService> logger, IOptions<FFmpegOptions> ffmpegOptions) {
|
public EncoderService(ILogger<EncoderService> logger, IOptions<FFmpegOptions> ffmpegOptions) {
|
||||||
Logger = logger;
|
Logger = logger;
|
||||||
options = ffmpegOptions.Value;
|
options = ffmpegOptions.Value;
|
||||||
options.FfmpegPath = Path.GetFullPath(Path.Combine(Path.GetDirectoryName(Environment.ProcessPath), options.FfmpegPath));
|
options.FfmpegPath = options.FfmpegPath.AbsoluteOrProcessPath();
|
||||||
options.TemporaryFilesPath = Path.GetFullPath(Path.Combine(Path.GetDirectoryName(Environment.ProcessPath), options.TemporaryFilesPath));
|
options.TemporaryFilesPath = options.TemporaryFilesPath.AbsoluteOrProcessPath();
|
||||||
Directory.CreateDirectory(options.TemporaryFilesPath); // Ensure the temporary files directory exists
|
Directory.CreateDirectory(options.TemporaryFilesPath); // Ensure the temporary files directory exists
|
||||||
JobQueue = new();
|
JobQueue = new();
|
||||||
|
|
||||||
@@ -63,7 +63,7 @@ public class EncoderService : BackgroundService, IEncoderService {
|
|||||||
|
|
||||||
void ProcessJob(EncodingJob job) {
|
void ProcessJob(EncodingJob job) {
|
||||||
job.Status = JobStatus.InProgress;
|
job.Status = JobStatus.InProgress;
|
||||||
var file = job.OrigFilePath;
|
var file = job.FilePath;
|
||||||
var mediaInfo = FFProbe.Analyse(file);
|
var mediaInfo = FFProbe.Analyse(file);
|
||||||
if (mediaInfo.PrimaryVideoStream == null) {
|
if (mediaInfo.PrimaryVideoStream == null) {
|
||||||
job.Status = JobStatus.Failed;
|
job.Status = JobStatus.Failed;
|
||||||
@@ -71,7 +71,7 @@ public class EncoderService : BackgroundService, IEncoderService {
|
|||||||
}
|
}
|
||||||
var W = mediaInfo.PrimaryVideoStream.Width;
|
var W = mediaInfo.PrimaryVideoStream.Width;
|
||||||
var H = mediaInfo.PrimaryVideoStream.Height;
|
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);
|
int qp = Utils.ToQPValue(W, H);
|
||||||
var status = FFMpegArguments.FromFileInput(file, true, args => args.WithHardwareAcceleration())
|
var status = FFMpegArguments.FromFileInput(file, true, args => args.WithHardwareAcceleration())
|
||||||
.OutputToFile(outputPath, true, args => args
|
.OutputToFile(outputPath, true, args => args
|
||||||
@@ -99,8 +99,11 @@ public class EncoderService : BackgroundService, IEncoderService {
|
|||||||
if(status) {
|
if(status) {
|
||||||
job.Status = JobStatus.Completed;
|
job.Status = JobStatus.Completed;
|
||||||
job.EncodedFilePath = outputPath;
|
job.EncodedFilePath = outputPath;
|
||||||
|
File.Delete(file); // Clean up original file
|
||||||
} else {
|
} else {
|
||||||
job.Status = JobStatus.Failed;
|
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.CompletedAt = DateTime.Now;
|
||||||
job.Progress = 1.0f;
|
job.Progress = 1.0f;
|
||||||
|
|||||||
@@ -1,9 +1,31 @@
|
|||||||
namespace Encoder;
|
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 JobStatus Status { get; set; } = JobStatus.Pending;
|
||||||
public DateTime CreatedAt { get; init; } = DateTime.Now;
|
public DateTime CreatedAt { get; init; } = DateTime.Now;
|
||||||
public DateTime? CompletedAt { get; set; } = null;
|
public DateTime? CompletedAt { get; set; } = null;
|
||||||
|
public string FilePath { get; init; }
|
||||||
|
public EncoderType RequestedEncoding { get; init; }
|
||||||
public string EncodedFilePath { get; set; } = string.Empty;
|
public string EncodedFilePath { get; set; } = string.Empty;
|
||||||
public float Progress { get; set; } = 0.0f;
|
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();
|
builder.Services.AddLogging();
|
||||||
|
|
||||||
string uploadsPath = builder.Configuration.GetSection("UploadsPath").Get<string>() ?? "./Uploads";
|
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
|
Directory.CreateDirectory(uploadsPath); // Ensure the uploads directory exists
|
||||||
|
|
||||||
builder.Services.Configure<FFmpegOptions>(builder.Configuration.GetSection(FFmpegOptions.SectionName));
|
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 a video file as multipart form data and schedule it for encoding
|
||||||
// Get video encoding settings from query parameters
|
// Get video encoding settings from query parameters
|
||||||
// Returns the ID of the job handling the encoding
|
// Returns the ID of the job handling the encoding
|
||||||
app.MapPost("encode", context =>
|
app.MapPost("encode", async context => {
|
||||||
{
|
|
||||||
// Disable request size limit
|
// Disable request size limit
|
||||||
context.Features.Get<IHttpMaxRequestBodySizeFeature>()?.MaxRequestBodySize = null;
|
context.Features.Get<IHttpMaxRequestBodySizeFeature>()?.MaxRequestBodySize = null;
|
||||||
|
|
||||||
var request = context.Request;
|
var request = context.Request;
|
||||||
if (!request.HasFormContentType) {
|
if (!request.HasFormContentType) {
|
||||||
context.Response.StatusCode = 400;
|
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 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) {
|
if (file == null) {
|
||||||
context.Response.StatusCode = 400;
|
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
|
// Save the file to a temporary location
|
||||||
var jobGuid = Guid.NewGuid();
|
var jobGuid = Guid.NewGuid();
|
||||||
var tempFilePath = Path.GetFullPath(Path.Combine(uploadsPath, jobGuid.ToString("D")+Path.GetExtension(file.FileName)));
|
var tempFilePath = Path.GetFullPath(Path.Combine(uploadsPath, jobGuid.ToString("D")+Path.GetExtension(file.FileName)));
|
||||||
using (var stream = File.Create(tempFilePath))
|
await using (var stream = File.Create(tempFilePath)) file.CopyTo(stream);
|
||||||
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>();
|
var encSrv = context.RequestServices.GetService<EncoderService>();
|
||||||
if (encSrv != null) encSrv.EnqueueJob(job);
|
if (encSrv == null) {
|
||||||
else {
|
|
||||||
context.Response.StatusCode = 500;
|
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;
|
context.Response.StatusCode = 200;
|
||||||
return context.Response.WriteAsJsonAsync(new { JobId = jobGuid });
|
await context.Response.WriteAsJsonAsync(new { JobId = jobGuid });
|
||||||
}).WithFormOptions(multipartBodyLengthLimit: 1024L*1024L*1024L*64L);
|
}).WithFormOptions(multipartBodyLengthLimit: 1_073_741_824L*64L);
|
||||||
|
|
||||||
app.MapGet("status", (context) => {
|
app.MapGet("status", (context) => {
|
||||||
var encSrv = context.RequestServices.GetService<EncoderService>();
|
var encSrv = context.RequestServices.GetService<EncoderService>();
|
||||||
@@ -77,8 +86,7 @@ app.MapGet("status", (context) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Check the status of an encoding job by its ID
|
// 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>();
|
var encSrv = context.RequestServices.GetService<EncoderService>();
|
||||||
if (encSrv == null) return Results.InternalServerError();
|
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>();
|
var encSrv = context.RequestServices.GetService<EncoderService>();
|
||||||
if (encSrv == null) return Results.InternalServerError();
|
if (encSrv == null) return Results.InternalServerError();
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
|
using System.Runtime.CompilerServices;
|
||||||
|
using Microsoft.Net.Http.Headers;
|
||||||
|
|
||||||
namespace Encoder;
|
namespace Encoder;
|
||||||
|
|
||||||
public static class Utils {
|
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(1280*720, 64),
|
||||||
new(1920*1080, 96),
|
new(1920*1080, 96),
|
||||||
new(3840*2160, 128),
|
new(3840*2160, 128),
|
||||||
@@ -9,30 +12,43 @@ public static class Utils {
|
|||||||
new(8128*4096, 120) //VR8K
|
new(8128*4096, 120) //VR8K
|
||||||
}.OrderBy(t => t.Item1).ToArray();
|
}.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);
|
return v0 + t * (v1 - v0);
|
||||||
}
|
}
|
||||||
|
|
||||||
static float remap(float value, float from1, float to1, float from2, float to2) {
|
public static float Remap(float value, float from1, float to1, float from2, float to2) => from2 + (value - from1) * (to2 - from2) / (to1 - from1);
|
||||||
return from2 + (value - from1) * (to2 - from2) / (to1 - from1);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static int ToQPValue(int W, int H) {
|
public static int ToQPValue(int W, int H) {
|
||||||
int pixels = W * H;
|
int pixels = W * H;
|
||||||
for (var i = 0; i < QPTable.Length; i++) {
|
for (var i = 0; i < QPTable.Length; i++) {
|
||||||
var t = QPTable[i];
|
if (pixels <= QPTable[i].Item1) {
|
||||||
if (pixels <= t.Item1) {
|
|
||||||
var minQP = QPTable[i - 1].Item2;
|
var minQP = QPTable[i - 1].Item2;
|
||||||
var maxQP = QPTable[i].Item2;
|
var maxQP = QPTable[i].Item2;
|
||||||
|
|
||||||
var minPixels = QPTable[i - 1].Item1;
|
var minPixels = QPTable[i - 1].Item1;
|
||||||
var maxPixels = QPTable[i].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 the highest QP for anything higher than 8K VR
|
||||||
return QPTable.Last().Item2;
|
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