commit d591797a9692813e61afb9d98a82f55d0534801b Author: Sakurai Date: Sun Feb 23 15:10:13 2025 +0900 first commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..011b122 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +bin +obj +revision.info diff --git a/Controllers/HomeController.cs b/Controllers/HomeController.cs new file mode 100644 index 0000000..5d41fae --- /dev/null +++ b/Controllers/HomeController.cs @@ -0,0 +1,67 @@ +using Microsoft.AspNetCore.Mvc; +using ReviewProxy.Lib; + +namespace ReviewProxy.Controllers; + +public class HomeController : Controller +{ + public async Task 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 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); + } + } +} diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..bf70bcb --- /dev/null +++ b/Dockerfile @@ -0,0 +1,7 @@ +FROM alpine:latest + +WORKDIR /opt +RUN apk add dotnet8-sdk ffmpeg git \ + && + + diff --git a/Lib/FileEncoder.cs b/Lib/FileEncoder.cs new file mode 100644 index 0000000..7f27575 --- /dev/null +++ b/Lib/FileEncoder.cs @@ -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); + } + } +} diff --git a/Lib/IEncoder.cs b/Lib/IEncoder.cs new file mode 100644 index 0000000..1564d0c --- /dev/null +++ b/Lib/IEncoder.cs @@ -0,0 +1,6 @@ +namespace ReviewProxy.Lib; + +public interface IEncoder : IDisposable +{ + public ValueTask EncodeAsync(); +} diff --git a/Lib/IO/TempFile.cs b/Lib/IO/TempFile.cs new file mode 100644 index 0000000..fa0fb6e --- /dev/null +++ b/Lib/IO/TempFile.cs @@ -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; + } + + /// + /// 異なる拡張子で新しい一時ファイルを作成します。 + /// + /// + /// + 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); + } + } +} diff --git a/Lib/StreamEncoder.cs b/Lib/StreamEncoder.cs new file mode 100644 index 0000000..840a9bf --- /dev/null +++ b/Lib/StreamEncoder.cs @@ -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); + } + } +} diff --git a/Lib/ZipArchiver.cs b/Lib/ZipArchiver.cs new file mode 100644 index 0000000..5b8e11f --- /dev/null +++ b/Lib/ZipArchiver.cs @@ -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); + } + } +} diff --git a/Program.cs b/Program.cs new file mode 100644 index 0000000..21ef5ac --- /dev/null +++ b/Program.cs @@ -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(); diff --git a/Properties/launchSettings.json b/Properties/launchSettings.json new file mode 100644 index 0000000..9e6750a --- /dev/null +++ b/Properties/launchSettings.json @@ -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" + } + } + } +} diff --git a/README.md b/README.md new file mode 100644 index 0000000..b2b99b0 --- /dev/null +++ b/README.md @@ -0,0 +1,12 @@ + +# ReviewProxy + +### ReviewProxy + +#### 概要 + +下記機能を提供するHTTPサーバになります。 +1. ZIP形式でのファイルアップロード +1. ファイルの取得 +1. ストリーム形式でのエンコード + diff --git a/ReviewProxy.csproj b/ReviewProxy.csproj new file mode 100644 index 0000000..1b28a01 --- /dev/null +++ b/ReviewProxy.csproj @@ -0,0 +1,9 @@ + + + + net8.0 + enable + enable + + + diff --git a/ReviewProxy.sln b/ReviewProxy.sln new file mode 100644 index 0000000..d2c1270 --- /dev/null +++ b/ReviewProxy.sln @@ -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 diff --git a/appsettings.Development.json b/appsettings.Development.json new file mode 100644 index 0000000..0c208ae --- /dev/null +++ b/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/appsettings.json b/appsettings.json new file mode 100644 index 0000000..02439e9 --- /dev/null +++ b/appsettings.json @@ -0,0 +1,10 @@ +{ + "Urls": "http://*:5000", + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*" +}