Skip to content

Instantly share code, notes, and snippets.

@brianmed
Last active September 30, 2025 00:51
Show Gist options
  • Save brianmed/535b388839f14a5ea9d6ed87d7100f7a to your computer and use it in GitHub Desktop.
Save brianmed/535b388839f14a5ea9d6ed87d7100f7a to your computer and use it in GitHub Desktop.
FFmpegBlast: Proxy and Transcode a Remote File for Viewing in FireFox
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<NoDefaultLaunchSettingsFile>True</NoDefaultLaunchSettingsFile>
</PropertyGroup>
</Project>
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
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;
}
}
<!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