Last active
September 30, 2025 00:51
-
-
Save brianmed/535b388839f14a5ea9d6ed87d7100f7a to your computer and use it in GitHub Desktop.
FFmpegBlast: Proxy and Transcode a Remote File for Viewing in FireFox
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <Project Sdk="Microsoft.NET.Sdk.Web"> | |
| <PropertyGroup> | |
| <TargetFramework>net9.0</TargetFramework> | |
| <Nullable>enable</Nullable> | |
| <ImplicitUsings>enable</ImplicitUsings> | |
| <NoDefaultLaunchSettingsFile>True</NoDefaultLaunchSettingsFile> | |
| </PropertyGroup> | |
| </Project> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| using System.Diagnostics; | |
| using System.Runtime.InteropServices; | |
| using Microsoft.AspNetCore.Mvc; | |
| using Microsoft.Extensions.FileProviders; | |
| // dotnet run -- --urls='http://*:5050' | |
| // http://localhost:5050/video.html?outputFormat=mp4&sourceUri=http://localhost:8080/Sintel.2010.720p.mkv | |
| // http://localhost:5050/video.html?outputFormat=webm&sourceUri=file:///Path/Sintel.2010.720p.mkv | |
| namespace FFmpegBlast; | |
| static partial class NativeMethods | |
| { | |
| [StructLayout(LayoutKind.Sequential)] | |
| public struct pollfd | |
| { | |
| public int fd; // File descriptor | |
| public short events; // Events to look for | |
| public short revents; // Events that occurred | |
| } | |
| [DllImport("libc", SetLastError = true)] | |
| public static extern int poll( | |
| [In, Out] pollfd[] fds, | |
| uint nfds, | |
| int timeout | |
| ); | |
| public const short POLLIN = 0x0001; | |
| } | |
| public class Program | |
| { | |
| public static async Task Main(string[] args) | |
| { | |
| if (Environment.GetEnvironmentVariable("ASPNETCORE_SHUTDOWNTIMEOUTSECONDS") == null) | |
| { | |
| Environment.SetEnvironmentVariable("ASPNETCORE_SHUTDOWNTIMEOUTSECONDS", "5"); | |
| } | |
| WebApplicationBuilder builder = WebApplication.CreateBuilder(args); | |
| WebApplication app = builder.Build(); | |
| app.UseStaticFiles(new StaticFileOptions | |
| { | |
| FileProvider = new PhysicalFileProvider(Directory.GetCurrentDirectory()), | |
| RequestPath = "" | |
| }); | |
| app.MapGet("/", () => "Hello World!"); | |
| app.MapGet("/transcode/{outputFormat}", async (HttpContext httpContext, [FromQuery]string sourceUri, [FromRoute]string outputFormat = "mp4") => | |
| { | |
| httpContext.Response.ContentType = outputFormat == "mp4" | |
| ? "video/mp4" | |
| : "video/webm"; | |
| Process proc = new Process(); | |
| proc.EnableRaisingEvents = false; | |
| string sourceContainer = Path.GetExtension(sourceUri) == ".mkv" | |
| ? "matroska" | |
| : "mp4"; | |
| proc.StartInfo.FileName = @"ffmpeg"; | |
| proc.StartInfo.Arguments = outputFormat == "mp4" | |
| ? $"-f {sourceContainer} -i - -map_metadata -1 -c:v h264 -c:a aac -b:a 96k -ac 2 -sn -map_chapters -1 -movflags +empty_moov -f mp4 -" | |
| : $"-f {sourceContainer} -i - -map_metadata -1 -c:v libsvtav1 -c:a libopus -b:a 96k -ac 2 -sn -map_chapters -1 -f webm -"; | |
| proc.StartInfo.UseShellExecute = false; | |
| proc.StartInfo.RedirectStandardInput = true; | |
| proc.StartInfo.RedirectStandardOutput = true; | |
| proc.StartInfo.RedirectStandardError = true; | |
| proc.ErrorDataReceived += (sender, args) => | |
| { | |
| // Console.WriteLine(args.Data); | |
| }; | |
| proc.Start(); | |
| proc.BeginErrorReadLine(); | |
| using StreamVideo streamVideo = await StreamVideo.CreateAsync(sourceUri); | |
| using BinaryReader br = new(streamVideo.Stream); | |
| byte[] buffer = new byte[32768]; | |
| byte[] stdoutBuffer = new byte[32768]; | |
| Stopwatch timebox = new(); | |
| while (await br.BaseStream.ReadAsync(buffer, 0, buffer.Length) is int readed && readed > 0) | |
| { | |
| await proc.StandardInput.BaseStream.WriteAsync(buffer, 0, readed); | |
| await proc.StandardInput.BaseStream.FlushAsync(); | |
| await Program.PushToBrowser(proc, httpContext, stdoutBuffer); | |
| } | |
| proc.StandardInput.BaseStream.Close(); | |
| timebox.Start(); | |
| while (!proc.HasExited && timebox.Elapsed <= TimeSpan.FromSeconds(30)) | |
| { | |
| await Program.PushToBrowser(proc, httpContext, stdoutBuffer); | |
| } | |
| if (!proc.HasExited) | |
| { | |
| proc.Kill(); | |
| } | |
| await httpContext.Response.CompleteAsync(); | |
| }); | |
| await app.RunAsync(); | |
| } | |
| public static async Task PushToBrowser(Process proc, HttpContext httpContext, byte[] stdoutBuffer) | |
| { | |
| System.IO.Pipes.PipeStream output = (System.IO.Pipes.PipeStream)(proc.StandardOutput.BaseStream); | |
| if (!output.SafePipeHandle.IsInvalid) | |
| { | |
| bool success = false; | |
| output.SafePipeHandle.DangerousAddRef(ref success); | |
| if (success) | |
| { | |
| try | |
| { | |
| IntPtr rawHandle = output.SafePipeHandle.DangerousGetHandle(); | |
| NativeMethods.pollfd[] fds = new NativeMethods.pollfd[1]; | |
| fds[0].fd = rawHandle.ToInt32(); | |
| fds[0].events = NativeMethods.POLLIN; | |
| int result = 0; | |
| do | |
| { | |
| result = NativeMethods.poll(fds, 1, 30); | |
| if (result > 0) | |
| { | |
| if ((fds[0].revents & NativeMethods.POLLIN) != 0) | |
| { | |
| await proc.StandardOutput.BaseStream.FlushAsync(); | |
| int readed2 = await proc.StandardOutput.BaseStream.ReadAsync(stdoutBuffer, 0, stdoutBuffer.Length); | |
| if (readed2 == 0) | |
| { | |
| // Hope this means the transcode is done | |
| break; | |
| } | |
| // Console.WriteLine(readed2); | |
| Memory<byte> slice = new Memory<byte>(stdoutBuffer, start: 0, length: readed2); | |
| await httpContext.Response.BodyWriter.WriteAsync(slice); | |
| await httpContext.Response.Body.FlushAsync(); | |
| } | |
| } | |
| else if (result == 0) | |
| { | |
| // Console.WriteLine("timeout"); | |
| } | |
| } while (result > 0); | |
| } | |
| catch (Exception ex) | |
| { | |
| Console.WriteLine(ex); | |
| } | |
| finally | |
| { | |
| output.SafePipeHandle.DangerousRelease(); | |
| } | |
| } | |
| } | |
| } | |
| } | |
| // https://stackoverflow.com/questions/45227568/net-process-redirect-stdin-and-stdout-without-causing-deadlock | |
| // https://stackoverflow.com/questions/48562948/reading-http-response-as-it-comes-in |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| namespace FFmpegBlast; | |
| public class StreamVideo : IDisposable | |
| { | |
| public Stream Stream | |
| { | |
| get | |
| { | |
| return this.FileStream ?? this.HttpStream; | |
| } | |
| } | |
| private string SourceUri { get; init; } | |
| private HttpClient HttpClient = null; | |
| private Stream HttpStream = null; | |
| private FileStream FileStream = null; | |
| protected StreamVideo(string sourceUri) | |
| { | |
| this.SourceUri = sourceUri; | |
| } | |
| public void Dispose() | |
| { | |
| this.HttpStream?.Dispose(); | |
| this.HttpClient?.Dispose(); | |
| this.FileStream?.Dispose(); | |
| this.HttpStream = null; | |
| this.HttpClient = null; | |
| this.FileStream = null; | |
| } | |
| private async Task OpenAsync() | |
| { | |
| if (this.SourceUri.StartsWith("file://")) | |
| { | |
| this.FileStream = File.OpenRead(this.SourceUri.Substring("file://".Length)); | |
| } | |
| else | |
| { | |
| this.HttpClient = new HttpClient(); | |
| this.HttpClient.Timeout = TimeSpan.FromMilliseconds(Timeout.Infinite); | |
| this.HttpStream = await this.HttpClient.GetStreamAsync(this.SourceUri); | |
| } | |
| } | |
| public static async Task<StreamVideo> CreateAsync(string sourceUri) | |
| { | |
| StreamVideo streamVideo = new(sourceUri); | |
| await streamVideo.OpenAsync(); | |
| return streamVideo; | |
| } | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <!DOCTYPE html> | |
| <html> | |
| <body> | |
| <style> | |
| body | |
| { | |
| margin: 0px; | |
| } | |
| video | |
| { | |
| height: 100%; | |
| position: absolute; | |
| top: 0; | |
| left: 0; | |
| width: 100%; | |
| } | |
| </style> | |
| <video id="video" preload="none" controls></video> | |
| <script> | |
| var urlParams; | |
| (function () | |
| { | |
| let match, | |
| pl = /\+/g, // Regex for replacing addition symbol with a space | |
| search = /([^&=]+)=?([^&]*)/g, | |
| decode = function (s) { return decodeURIComponent(s.replace(pl, " ")); }, | |
| query = window.location.search.substring(1); | |
| urlParams = {}; | |
| while (match = search.exec(query)) | |
| { | |
| urlParams[decode(match[1])] = decode(match[2]); | |
| } | |
| })(); | |
| let video = document.getElementById("video"); | |
| let outputFormat = urlParams["outputFormat"] | |
| ? urlParams["outputFormat"] | |
| : "mp4"; | |
| video.src = `/transcode/${outputFormat}?sourceUri=${urlParams["sourceUri"]}`; | |
| </script> | |
| </body> | |
| </html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment