目前我有一个使用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);
}
}
}
我从未使用过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();
}
一旦完成,祈祷接收器将忽略部分帧。如果没有,你需要分析流来确定下一帧在哪里。
要做到这一点,你需要做这样的事情:
- 创建一个
BytesLeftToNextFrame
变量 - 将接收到的数据包标头存储在缓冲区中
- 将"Payload size"位转换为int
- 将
BytesLeftToNextFrame
重置为解析值 - 倒计时,直到下一次你应该读一个标题
最后,当新客户端连接时,在知道下一帧到达之前,不要开始流式传输。
伪代码:
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.
}
我不是流媒体专家,但看起来你应该关闭流,然后所有数据都会被写入
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();
}
我喜欢这个代码,因为它在处理异步编程时显示了一个基本错误
while (true)
{
}
这是一个同步循环,它以尽可能快的速度循环。。它每秒可以执行数千次(取决于可用的软件和硬件资源)
await theSubscriber.WriteAsync( bytes, 0, bytesRec );
这是一个异步命令(如果这还不够清楚的话),在一个不同的线程中执行(而循环表示主线程执行)
现在。。。为了使while循环等待async命令,我们使用await。。。听起来不错(否则while循环将执行数千次,执行无数异步命令)
但是,因为(订阅者的)循环需要模拟地传输所有订阅者的流,它被等待关键字卡住了
这就是为什么重新加载/新订户冻结整个东西(新连接=新订户)
结论:整个for循环应该在Task中。任务需要等待,直到服务器将流发送给所有订阅者。只有这样,它才能继续使用ContinueWith的while循环(这就是它这样调用的原因,对吧?)
所以。。。写入命令需要在没有等待关键字的情况下执行
theSubscriber.WriteAsync
foreach循环应该使用一个任务,该任务在完成