I'm using ASP.NET Core and ffmpeg to record a live video stream. When the page receives a get request, the stream should begin recording and saving to a folder using ffmpeg. I want to make it so that when visiting the stop endpoint, the ffmpeg process is closed cleanly.
Unfortunately I'm unable to send a 'q' to stdin after leaving the Get method. Using taskkill requires the use of /F making the ffmpeg process (which is not a window) force quit and not save the video properly, resulting in a corrupt file.
I tried using Process.Kill()
but that results in a corrupt file as well. Also, I tried Process.CloseMainWindow()
which worked, but only when the process is started as a window, and I'm unable to start the process as a window in the server I'm using.
I've include the code I have so far below, so hopefully someone could lead me in the right path.
using System;
...
using Microsoft.Extensions.Logging;
namespace MyApp.Controllers
{
[Route("api/[controller]")]
[Authorize]
public class RecordingController : Controller
{
private readonly ApplicationDbContext _context;
private readonly ILogger<HomeController> _logger;
public RecordingController(ApplicationDbContext context, ILogger<HomeController> logger)
{
_context = context;
_logger = logger;
}
[HttpGet]
public async Task<ActionResult> Get()
{
// Define the os process
var processStartInfo = new ProcessStartInfo()
{
// ffmpeg arguments
Arguments = "-f mjpeg -i \"https://urlofstream.com/video.gci\" -r 5 \"wwwroot/video.mp4\"",
FileName = "ffmpeg.exe",
UseShellExecute = true
};
var p1 = Process.Start(processStartInfo);
// p1.StandardInput.WriteLineAsync("q"); <-- This works here but not in the Stop method
return Ok(p1.Id);
}
// GET: api/Recording/stop
[HttpGet("stop/{pid}")]
public ActionResult Stop(int pid)
{
Process processes = Process.GetProcessById(pid);
processes.StandardInput.WriteLineAsync("q"); // Does not work, is not able to redirect input
return Ok();
}
}
}
I've discovered a solution to this issue which could hopefully help others. The solution is to use a combination of a Singleton, with a ConcurrentDictionary and write to the StandardInput. The only way you can write to standard input of a running process is if you still have access to the started Process handle, and you wont have access to it from within the controller, so you'll need to create a singleton and store the processes in a ConcurrentDictionary (which is great for updated from multiple threads).
First I created a recording manager class. RecordingManager.cs
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
namespace MyApp.Services
{
public class RecordingManager
{
// Key int is the ProcessId. Value process is the running Process
private static ConcurrentDictionary<int, Process> _processes = new ConcurrentDictionary<int, Process>();
public Process Start()
{
// Define the os process
var processStartInfo = new ProcessStartInfo()
{
// ffmpeg arguments
Arguments = "-f mjpeg -i https://urlofstream.com/video.gci -r 5 wwwroot/video.mp4",
FileName = "ffmpeg.exe",
RedirectStandardInput = true, // Must be set to true
UseShellExecute = false // Must be set to false
};
Process p = Process.Start(processStartInfo);
// Add the Process to the Dictionary with Key: Process ID and value as the running process
_processes.TryAdd(p1.Id, p);
return p;
}
private Process GetProcessByPid(int pid)
{
return _processes.FirstOrDefault(p => p.Key == pid).Value;
}
public void Stop(int pid)
{
Process p = GetProcessByPid(pid);
// FFMPEG Expects a q written to the STDIN to stop the process
p.StandardInput.WriteLine("q\n");
_processes.TryRemove(pid, out p);
// Free up resources
p.Close()
}
}
}
Next I add the singleton to my startup.cs class within the ConfigureServices method
services.AddSingleton<RecordingManager>();
Finally, I add the singleton to my controller constructor and call the methods there. RecordingController.cs
namespace MyApp.Controllers
{
[Route("api/[controller]")]
[Authorize]
public class RecordingController : Controller
{
private readonly ApplicationDbContext _context;
private readonly ILogger<HomeController> _logger;
private readonly RecordingManager _recordingManager;
public RecordingController(ApplicationDbContext context, ILogger<HomeController> logger, RecordingManager recordingManager)
{
_context = context;
_logger = logger;
_recordingManager = recordingManager;
}
[HttpGet]
public async Task<ActionResult> Get()
{
Process p = _recordingManager.Start();
return Ok(p.Id); // Wouldn't recommend simply returning an Ok Result here. Check for issues
}
// GET: api/Recording/stop
[HttpGet("stop/{pid}")]
public ActionResult Stop(int pid)
{
_recordingManager.Stop(pid);
return Ok(); // Wouldn't recommend simply returning an Ok Result here. Check for issues
}
}
}