C# EmbedIO 服务器:只有第一个请求正在尝试从 FFMPEG 直播

C# EmbedIO server: Only the first request is working trying to live stream from FFMPEG

我正在尝试构建一个 HTTP 服务器,它将通过 FFMPEG 以 TransportStream 格式传输动态 video/audio。我找到了 EmbedIO,它看起来像是一个轻量级但灵活的基础。

因此,我查看了模块示例并构建了一个非常基本的模块,该模块尚未处理请求 URL 但对任何请求都使用相同的流进行响应,只是为了查看它是否正常工作如预期:

namespace TSserver
{
    using Unosquare.Swan;
    using Unosquare.Swan.Formatters;
    using System;
    using System.Collections.Generic;
    using System.IO;
    using System.Threading;
    using System.Threading.Tasks;
#if NET46
    using System.Net;
#else
    using Unosquare.Net;
    using Unosquare.Labs.EmbedIO;
    using Unosquare.Labs.EmbedIO.Constants;
    using System.Diagnostics;
#endif

    /// <summary>
    /// TSserver Module
    /// </summary>
    public class TSserverModule : WebModuleBase
    {
        /// <summary>
        /// Initializes a new instance of the <see cref="TSserverModule"/> class.
        /// </summary>
        /// <param name="basePath">The base path.</param>
        /// <param name="jsonPath">The json path.</param>
        public TSserverModule()
        {
            AddHandler(ModuleMap.AnyPath, HttpVerbs.Any, HandleRequest);
        }

        /// <summary>
        /// Gets the Module's name
        /// </summary>
        public override string Name => nameof(TSserverModule).Humanize();


        /// <summary>
        /// Handles the request.
        /// </summary>
        /// <param name="context">The context.</param>
        /// <param name="ct">The cancellation token.</param>
        /// <returns></returns>
        private Task<bool> HandleRequest(HttpListenerContext context, CancellationToken ct)
        {
            var path = context.RequestPath();
            var verb = context.RequestVerb();

            System.Net.HttpStatusCode statusCode;
            context.Response.SendChunked = true;
            //context.Response.AddHeader("Last-Modified", File.GetLastWriteTime(filename).ToString("r"));
            context.Response.ContentType = "video/mp2t";

            try
            {
                var ffmpeg = new Process
                {
                    StartInfo = new ProcessStartInfo
                    {
                        FileName = "ffmpeg.exe",
                        Arguments = "-re -loop 1 -i \"./default.png\" -i \"./jeopardy.mp3\" -c:v libx264 -tune stillimage -r 25 -vcodec mpeg2video -profile:v 4 -bf 2 -b:v 4000k -maxrate:v 5000k -acodec mp2 -ac 2 -ab 128k -ar 48000 -f mpegts -mpegts_original_network_id 1 -mpegts_transport_stream_id 1 -mpegts_service_id 1 -mpegts_pmt_start_pid 4096 -streamid 0:289 -streamid 1:337 -metadata service_provider=\"MYCALL\" -metadata service_name=\"My Station ID\" -y pipe:1",
                        UseShellExecute = false,
                        RedirectStandardOutput = true,
                        CreateNoWindow = true
                    }
                };

                ffmpeg.Start();

                FileStream baseStream = ffmpeg.StandardOutput.BaseStream as FileStream;
                int lastRead = 0;
                byte[] buffer = new byte[4096];

                do
                {
                    lastRead = baseStream.Read(buffer, 0, buffer.Length);
                    context.Response.OutputStream.Write(buffer, 0, lastRead);
                    context.Response.OutputStream.Flush();
                } while (lastRead > 0);

                statusCode = System.Net.HttpStatusCode.OK;
            }
            catch (Exception e)
            {
                statusCode = System.Net.HttpStatusCode.InternalServerError;
            }

            context.Response.StatusCode = (int)statusCode;
            context.Response.OutputStream.Flush();
            context.Response.OutputStream.Close();

            return Task.FromResult(true);
        }

    }
}

这确实有效,当我在浏览器中打开连接时,会提供一个 TS 文件供下载,当我通过 VLC 播放器连接时,我看到我的 default.png 文件伴随着 Jeopardy think 音乐 -耶!但是,如果我连接第二个客户端(播放器或浏览器),它将无休止地加载并且不会返回任何内容。即使我关闭之前的连接(中止下载或停止播放),后续连接也不会产生任何响应。我必须停止并重新启动服务器才能再次建立一个连接。

在我看来,我的代码正在阻塞服务器,尽管 运行 在它自己的任务中。我来自 PHP & JavaScript 背景,所以我对 C# 和线程还很陌生。所以这可能很明显......但我希望 EmbedIO 能够处理所有 multitasking/threading 的东西。

仅将 Task<bool> 指定为 return 类型不会在它自己的任务中创建方法 运行。您必须使用 Task.Run(() => ...) 手动 运行 新任务,或者使您的方法 async 然后使用 await 进行异步处理,如下所示。另请注意,必须在发送 headers 之前设置 context.Response.StatusCode

private async Task<bool> HandleRequest(HttpListenerContext context, CancellationToken ct)
{
    var path = context.RequestPath();
    var verb = context.RequestVerb();
    bool headersSent = false;

    try
    {
        var ffmpeg = new Process
        {
            StartInfo = new ProcessStartInfo
            {
                FileName = "ffmpeg.exe",
                Arguments = "-re -i \"./default.png\" -i \"./jeopardy.mp3\" -c:v libx264 -tune stillimage -r 25 -vcodec mpeg2video -profile:v 4 -bf 2 -b:v 4000k -maxrate:v 5000k -acodec mp2 -ac 2 -ab 128k -ar 48000 -f mpegts -mpegts_original_network_id 1 -mpegts_transport_stream_id 1 -mpegts_service_id 1 -mpegts_pmt_start_pid 4096 -streamid 0:289 -streamid 1:337 -metadata service_provider=\"MYCALL\" -metadata service_name=\"My Station ID\" -y pipe:1",
                UseShellExecute = false,
                RedirectStandardOutput = true,
                CreateNoWindow = true
            }
        };

        ffmpeg.Start();

        //StatusCode must be set before headers are sent
        context.Response.StatusCode = (int)System.Net.HttpStatusCode.OK;
        headersSent = true;
        context.Response.SendChunked = true;
        //context.Response.AddHeader("Last-Modified", File.GetLastWriteTime(filename).ToString("r"));
        context.Response.ContentType = "video/mp2t";

        FileStream baseStream = ffmpeg.StandardOutput.BaseStream as FileStream;
        //Copy stream asynchronously, so we will not block current thread and another request can be processed
        await baseStream.CopyToAsync(context.Response.OutputStream, 4096, ct);

        context.Response.OutputStream.Flush();
        context.Response.OutputStream.Close();
    }
    catch (Exception)
    {
        if (!headersSent)//Without this, setting StatusCode would throw exception
        {
            context.Response.StatusCode = (int)System.Net.HttpStatusCode.InternalServerError;
        }
    }

    return true;
}