Search code examples
asp.netasp.net-coreffmpegvideo-streamingrecording

FFMPEG recording in ASP.NET Core mvc application


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();
        }
    }
}



Solution

  • 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
            }
        }
    }