first commit

This commit is contained in:
Sakurai Ryota 2025-02-23 15:10:13 +09:00
commit d591797a96
15 changed files with 465 additions and 0 deletions

3
.gitignore vendored Normal file
View File

@ -0,0 +1,3 @@
bin
obj
revision.info

View File

@ -0,0 +1,67 @@
using Microsoft.AspNetCore.Mvc;
using ReviewProxy.Lib;
namespace ReviewProxy.Controllers;
public class HomeController : Controller
{
public async Task<IActionResult> Index()
{
var inputStream = this.HttpContext.Request.Body;
if (inputStream is null)
{
return BadRequest();
}
Console.WriteLine($"Receive Request: {this.HttpContext.Connection.RemoteIpAddress} {this.HttpContext.Request.ContentLength}");
try
{
string workDir = Path.Combine(Directory.GetCurrentDirectory(), "encoding");
string zipFilePath = Path.Combine(Directory.GetCurrentDirectory(), "encoded.zip");
var encoder = new FileEncoder(inputStream, workDir, zipFilePath);
this.HttpContext.Response.RegisterForDispose(encoder);
await encoder.EncodeAsync().ConfigureAwait(false);
var result = new PhysicalFileResult(zipFilePath, "application/octet-stream");
return result;
}
catch (Exception ex)
{
Console.WriteLine(ex);
return Problem(ex.Message);
}
}
public async Task<IActionResult> Stream()
{
var inputStream = this.HttpContext.Request.Body;
if (inputStream is null)
{
return BadRequest();
}
try
{
string workDir = Path.Combine(Directory.GetCurrentDirectory(), "encoding");
string zipFilePath = Path.Combine(Directory.GetCurrentDirectory(), "encoded.zip");
var encoder = new StreamEncoder(inputStream, workDir, zipFilePath);
await encoder.EncodeAsync().ConfigureAwait(false);
var result = new PhysicalFileResult(zipFilePath, "application/octet-stream");
this.HttpContext.Response.RegisterForDispose(encoder);
return result;
}
catch (Exception ex)
{
Console.WriteLine(ex);
return Problem(ex.Message);
}
}
}

7
Dockerfile Normal file
View File

@ -0,0 +1,7 @@
FROM alpine:latest
WORKDIR /opt
RUN apk add dotnet8-sdk ffmpeg git \
&&

92
Lib/FileEncoder.cs Normal file
View File

@ -0,0 +1,92 @@
using System.Diagnostics;
using ReviewProxy.Lib.IO;
namespace ReviewProxy.Lib;
public class FileEncoder : IEncoder
{
private TempFile tempFile;
private Stream inputStream;
private string workDirectory;
private string outputFilePath;
public FileEncoder(Stream inputStream, string workDirectory, string outputFilePath)
{
this.tempFile = new TempFile();
this.inputStream = inputStream;
this.workDirectory = workDirectory;
this.outputFilePath = outputFilePath;
this.Init();
}
public async ValueTask EncodeAsync()
{
Console.WriteLine($"Using {this.tempFile.FilePath}");
using (var outputStream = this.tempFile.Open())
{
await this.inputStream.CopyToAsync(outputStream).ConfigureAwait(false);
}
using (var proc = this.CreateEncoder())
{
if (proc is null)
{
throw new FileNotFoundException("ffmpeg could't execute.");
}
await proc.WaitForExitAsync().ConfigureAwait(false);
}
this.BundleToZip();
}
private void BundleToZip()
{
using (var archive = new ZipArchiver(this.outputFilePath, this.workDirectory))
{
archive.Compress();
}
}
private void Init()
{
if (!Directory.Exists(workDirectory))
{
Directory.CreateDirectory(workDirectory);
}
}
private Process? CreateEncoder()
{
string tsFilePath = Path.Combine(workDirectory, "a%03d.ts");
string plFilePath = Path.Combine(workDirectory, "playlist.m3u8");
var args = $"-i \"{tempFile.FilePath}\" -c:v libx264 -vf scale=-2:720 -movflags faststart -b:v 1000K -r 24 -c:a aac -b:a 64k -y -pix_fmt yuv420p -crf 23 -f segment -segment_format mpegts -segment_time 5 -segment_list \"{plFilePath}\" \"{tsFilePath}\"";
var startInfo = new ProcessStartInfo()
{
FileName = "ffmpeg",
Arguments = args,
RedirectStandardInput = true,
};
return Process.Start(startInfo);
}
public void Dispose()
{
inputStream.Dispose();
tempFile.Dispose();
if (Directory.Exists(workDirectory))
{
Directory.Delete(workDirectory, true);
}
if (File.Exists(outputFilePath))
{
File.Delete(outputFilePath);
}
}
}

6
Lib/IEncoder.cs Normal file
View File

@ -0,0 +1,6 @@
namespace ReviewProxy.Lib;
public interface IEncoder : IDisposable
{
public ValueTask EncodeAsync();
}

58
Lib/IO/TempFile.cs Normal file
View File

@ -0,0 +1,58 @@
namespace ReviewProxy.Lib.IO;
public class TempFile : IDisposable
{
public string FilePath { get; }
private FileStream? stream;
public TempFile()
{
// 一時フォルダが提供されている場合、一時フォルダからファイルを取得する
string tmpPath = Path.GetTempPath();
if (!Directory.Exists(tmpPath))
{
FilePath = Path.GetTempFileName();
}
else
{
FilePath = Path.Combine(tmpPath, $"{Guid.NewGuid().ToString()}.tmp");
}
stream = null;
}
private TempFile(string filePath)
{
FilePath = filePath;
stream = null;
}
/// <summary>
/// 異なる拡張子で新しい一時ファイルを作成します。
/// </summary>
/// <param name="extension"></param>
/// <returns></returns>
public TempFile ChangeFileExtension(string extension)
{
return new TempFile(Path.ChangeExtension(FilePath, extension));
}
public FileStream Open()
{
if (stream is null)
{
stream = new FileStream(FilePath, FileMode.OpenOrCreate);
}
return stream;
}
public void Dispose()
{
stream?.Dispose();
if (File.Exists(FilePath))
{
File.Delete(FilePath);
}
}
}

83
Lib/StreamEncoder.cs Normal file
View File

@ -0,0 +1,83 @@
using System.Diagnostics;
namespace ReviewProxy.Lib;
public class StreamEncoder : IEncoder
{
private Stream stream;
private string workDirectory;
private string outputFilePath;
public StreamEncoder(Stream inputStream, string workDirectory, string outputFilePath)
{
this.stream = inputStream;
this.workDirectory = workDirectory;
this.outputFilePath = outputFilePath;
this.Init();
}
public async ValueTask EncodeAsync()
{
using (var proc = this.CreateEncoder())
{
if (proc is null)
{
throw new FileNotFoundException("ffmpeg could't execute.");
}
Console.WriteLine($"CopyToAsync");
await this.stream.CopyToAsync(proc.StandardInput.BaseStream).ConfigureAwait(false);
Console.WriteLine($"Close");
proc.StandardInput.BaseStream.Close();
await proc.WaitForExitAsync().ConfigureAwait(false);
}
this.BundleToZip();
}
private void BundleToZip()
{
using (var archive = new ZipArchiver(this.outputFilePath, this.workDirectory))
{
archive.Compress();
}
}
private void Init()
{
if (!Directory.Exists(workDirectory))
{
Directory.CreateDirectory(workDirectory);
}
}
private Process? CreateEncoder()
{
string tsFilePath = Path.Combine(workDirectory, "a%03d.ts");
string plFilePath = Path.Combine(workDirectory, "playlist.m3u8");
var args = $"-i pipe:0 -c:v libx264 -vf scale=-2:720 -movflags faststart -b:v 1000K -r 24 -c:a aac -b:a 64k -y -pix_fmt yuv420p -crf 23 -f segment -segment_format mpegts -segment_time 5 -segment_list \"{plFilePath}\" \"{tsFilePath}\"";
var startInfo = new ProcessStartInfo()
{
FileName = "ffmpeg",
Arguments = args,
RedirectStandardInput = true,
};
return Process.Start(startInfo);
}
public void Dispose()
{
if (Directory.Exists(workDirectory))
{
Directory.Delete(workDirectory, true);
}
if (File.Exists(outputFilePath))
{
File.Delete(outputFilePath);
}
}
}

31
Lib/ZipArchiver.cs Normal file
View File

@ -0,0 +1,31 @@
using System.IO.Compression;
namespace ReviewProxy.Lib;
public class ZipArchiver : IDisposable
{
private FileStream fileStream;
private ZipArchive archive;
private DirectoryInfo dirInfo;
public ZipArchiver(string outputFilePath, string inputDirectoryPath)
{
this.fileStream = new FileStream(outputFilePath, FileMode.CreateNew);
this.archive = new ZipArchive(this.fileStream, ZipArchiveMode.Create);
this.dirInfo = new DirectoryInfo(inputDirectoryPath);
}
public void Dispose()
{
this.archive.Dispose();
this.fileStream.Dispose();
}
public void Compress()
{
foreach (FileInfo file in this.dirInfo.EnumerateFiles())
{
this.archive.CreateEntryFromFile(file.FullName, file.Name);
}
}
}

16
Program.cs Normal file
View File

@ -0,0 +1,16 @@
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllers();
builder.WebHost.UseKestrel(options => {
options.Limits.MaxRequestBodySize = long.MaxValue;
});
var app = builder.Build();
app.UseRouting();
app.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}");
app.Run();

View File

@ -0,0 +1,38 @@
{
"$schema": "http://json.schemastore.org/launchsettings.json",
"iisSettings": {
"windowsAuthentication": false,
"anonymousAuthentication": true,
"iisExpress": {
"applicationUrl": "http://localhost:50685",
"sslPort": 44333
}
},
"profiles": {
"http": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"applicationUrl": "http://localhost:5193",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"https": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"applicationUrl": "https://localhost:7055;http://localhost:5193",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"IIS Express": {
"commandName": "IISExpress",
"launchBrowser": true,
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
}
}
}

12
README.md Normal file
View File

@ -0,0 +1,12 @@
# ReviewProxy
### ReviewProxy
#### 概要
下記機能を提供するHTTPサーバになります。
1. ZIP形式でのファイルアップロード
1. ファイルの取得
1. ストリーム形式でのエンコード

9
ReviewProxy.csproj Normal file
View File

@ -0,0 +1,9 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
</Project>

25
ReviewProxy.sln Normal file
View File

@ -0,0 +1,25 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.5.002.0
MinimumVisualStudioVersion = 10.0.40219.1
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ReviewProxy", "ReviewProxy.csproj", "{4EA5FAC3-870B-4B7B-9DCF-70AE0E3D2864}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{4EA5FAC3-870B-4B7B-9DCF-70AE0E3D2864}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{4EA5FAC3-870B-4B7B-9DCF-70AE0E3D2864}.Debug|Any CPU.Build.0 = Debug|Any CPU
{4EA5FAC3-870B-4B7B-9DCF-70AE0E3D2864}.Release|Any CPU.ActiveCfg = Release|Any CPU
{4EA5FAC3-870B-4B7B-9DCF-70AE0E3D2864}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {981AB5AE-964E-4191-AAD9-05D0B0220A6C}
EndGlobalSection
EndGlobal

View File

@ -0,0 +1,8 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
}
}

10
appsettings.json Normal file
View File

@ -0,0 +1,10 @@
{
"Urls": "http://*:5000",
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*"
}