使用C# WebApi进行FLV直播流传输

16

目前我使用WebAPI实现了一个工作中的直播流。通过直接从ffmpeg接收flv流并使用PushStreamContent将其直接发送到客户端,这在页面已经打开时工作得非常完美。问题是当我打开另一个页面或刷新此页面时,您无法再查看该流(流仍然可以正常发送到客户端)。我认为这是由于流开始时缺少某些内容,但我不确定该怎么做。任何指针都将非常感激。

客户端读取流的代码

public class VideosController : ApiController
{
    public HttpResponseMessage Get()
    {
        var response = Request.CreateResponse();
        response.Content = new PushStreamContent(WriteToStream, new MediaTypeHeaderValue("video/x-flv"));

        return response;
    }

    private async Task WriteToStream( Stream arg1, HttpContent arg2, TransportContext arg3 )
    {
        //I think metadata needs to be written here but not sure how
        Startup.AddSubscriber( arg1 );
        await Task.Yield();
    }
}

接收流并将其发送给客户端的代码

while (true)
{
    bytes = new byte[8024000];
    int bytesRec = handler.Receive(bytes);

    foreach (var subscriber in Startup.Subscribers.ToList())
    {
        var theSubscriber = subscriber;
        try
        {
            await theSubscriber.WriteAsync( bytes, 0, bytesRec );
        }
        catch
        {
            Startup.Subscribers.Remove(theSubscriber);
        }
    }
}
3个回答

2

我从未使用过FLV或仔细研究过视频格式

大多数文件格式都是有结构的,尤其是视频格式。它们包含帧(即根据压缩格式完整或部分的屏幕截图)。

如果您能够在开始向新订阅者进行流媒体传输时命中特定帧,则非常幸运。因此,当他们开始接收流时,不能将格式识别为帧是局部的。

您可以在维基百科文章中了解更多关于FLV帧的信息。这很可能是您的问题所在。

一种简单的尝试是尝试保存首个订阅者连接时从流媒体服务器接收到的初始标题。

例如:

static byte _header = new byte[9]; //signature, version, flags, headerSize

public void YourStreamMethod()
{
    int bytesRec = handler.Receive(bytes);
    if (!_headerIsStored)
    {
        //store header
        Buffer.BlockCopy(bytes, 0, _header, 0, 9);
        _headerIsStored = true;
    }
}

...这使您可以将标题发送到下一个连接的订阅者:

private async Task WriteToStream( Stream arg1, HttpContent arg2, TransportContext arg3 )
{
    // send the FLV header
    arg1.Write(_header, 0, 9);

    Startup.AddSubscriber( arg1 );
    await Task.Yield();
}

一旦完成,希望接收者忽略部分帧。如果没有忽略,则需要分析流以确定下一帧的位置。
要做到这一点,您需要像以下方式操作:
1.创建一个“BytesLeftToNextFrame”变量。 2.将接收到的数据包头存储在缓冲区中。 3.将“有效载荷大小”位转换为整数。 4.将“BytesLeftToNextFrame”重设为解析后的值。 5.倒计时,直到下次读取标头的时间。
最后,当一个新的客户端连接时,请确保在下一帧到达之前不要开始传输。
伪代码:
var bytesLeftToNextFrame = 0;
while (true)
{
    bytes = new byte[8024000];
    int bytesRec = handler.Receive(bytes);

    foreach (var subscriber in Startup.Subscribers.ToList())
    {
        var theSubscriber = subscriber;
        try
        {
            if (subscriber.IsNew && bytesLeftToNextFrame < bytesRec)
            {
                //start from the index where the new frame starts
                await theSubscriber.WriteAsync( bytes, bytesLeftToNextFrame, bytesRec - bytesLeftToNextFrame);
                subscriber.IsNew = false;
            }
            else
            {
                //send everything, since we've already in streaming mode
                await theSubscriber.WriteAsync( bytes, 0, bytesRec );
            }
        }
        catch
        {
            Startup.Subscribers.Remove(theSubscriber);
        }
    }

    //TODO: check if the current frame is done
    // then parse the next header and reset the counter.
}

找到了一个简单的FLV元数据阅读器:http://johndyer.name/flash-flv-meta-reader-in-net-c/ - jgauffin

1

我不是流媒体方面的专家,但看起来你应该关闭流,这样所有数据才会被写入。

await theSubscriber.WriteAsync( bytes, 0, bytesRec );

就像WebAPI StreamContent vs PushStreamContent中提到的那样。

{
     // After save we close the stream to signal that we are done writing.
     xDoc.Save(stream);
     stream.Close();
}

问题在于这是一个实时流,我希望当新人连接时保持它的实时性。 - Jake Rote

-2

我喜欢这段代码,因为它展示了在处理异步编程时的一个基本错误。

while (true)
{

}

这是一个同步循环,尽可能快地循环自身...每秒可以执行数千次(取决于可用的软件和硬件资源)

await theSubscriber.WriteAsync( bytes, 0, bytesRec );

这是一个异步命令(如果不够清楚的话),它在一个不同的线程中执行(while循环代表主线程执行)。

现在...为了使while循环等待异步命令,我们使用await...听起来不错(否则while循环将执行数千次,执行无数个异步命令)。

但是,由于订阅者的循环需要同时传输所有订阅者的流,所以它被await关键字卡住了。

这就是为什么重新加载/新订阅者会冻结整个过程(新连接=新订阅者)。

结论:整个for循环应该在一个任务中。任务需要等待服务器向所有订阅者发送流。只有在这之后,它才应该继续使用ContinueWith进入while循环(这就是为什么它被称为这样的原因吧?)。

所以...写命令需要在没有await关键字的情况下执行。

theSubscriber.WriteAsync

foreach循环应该使用一个任务,在完成后继续执行while循环


同步循环?你在说什么?它只是一个循环而已。重要的是你在其中做了什么。他在循环中使用了 await,因此它是完全有效的。 - jgauffin

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接