最近用wpf做一个树莓派机器人的综合控制端,需要解析机器人摄像头的视频流,树莓派是用mjpg-streamer调用并搭建了视频流服务。
客户端解析mjpg-streamer视频帧的原理是:建立Http长连接,每次接收1024长度的数据,数据流中包含数据头信息和紧跟在数据头信息后的图像帧数据,所以需要先定位数据头信息,头信息中包含图像帧数据的长度,然后从头信息的结尾开始解析图像帧数据,第一次读取的数据中不一定包含完整的图像帧数据,需要从第二次读取的数据中继续解析未完整的数据。
原理参考:https://blog.csdn.net/qingkongyeyue/article/details/52824165
以下为具体代码:
private static readonly string DefaultUserAgent = "Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.2; SV1; .NET CLR 1.1.4322; .NET CLR 2.0.50727)";
private byte[] HeaderStartBuffer = new byte[20]
{
45, 45, 98, 111, 117, 110, 100, 97, 114, 121,
100, 111, 110, 111, 116, 99, 114, 111, 115, 115
};
private byte[] HeaderEndBuffer = new byte[4] { 13, 10, 13, 10 };
public void GetLivingVedio(string requestUrl, Action<byte[]> setFrame)
{
Task.Run(delegate
{
if (string.IsNullOrEmpty(requestUrl))
{
MessageBox.Show("GetLivingVedio url 为空!");
return;
}
try
{
HttpWebRequest request = WebRequest.Create(requestUrl) as HttpWebRequest; ;
//在使用curl做POST的时候, 当要POST的数据大于1024字节的时候, curl并不会直接就发起POST请求, 而是会分为俩步,
//1.发送一个请求, 包含一个Expect: 100 -continue, 询问Server使用愿意接受数据
//2.接收到Server返回的100 - continue应答以后, 才把数据POST给Server
//并不是所有的Server都会正确应答100 -continue, 比如lighttpd, 就会返回417 “Expectation Failed”, 则会造成逻辑出错,,
request.ServicePoint.Expect100Continue = false;
//默认代理
request.UserAgent = DefaultUserAgent;
//ContentType
//request.ContentType = "application/json;charset=utf-8";
request.Accept = "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8";
request.KeepAlive = true;
request.Method = "Get";
request.Timeout = (int)(30 * 1000f); // to ensure no timeout
using (Stream stream = request.GetResponse().GetResponseStream())
{
BinaryReader reader = new BinaryReader(new BufferedStream(stream), new System.Text.ASCIIEncoding());
int headerStart = 0;
int frameStart = 0;
int frameLength = 0;
byte[] frameBuffer = null;
int remainDataInNext = 0;
while (!StopLivingVedio)
{
try
{
byte[] buffer = reader.ReadBytes(1024);
if (remainDataInNext > 0)
{
int locationBuffStart = frameBuffer.Length - remainDataInNext;
if (buffer.Length > remainDataInNext)
{
Array.Copy(buffer, 0, frameBuffer, locationBuffStart, remainDataInNext);
}
else
{
Array.Copy(buffer, 0, frameBuffer, locationBuffStart, buffer.Length);
}
remainDataInNext = remainDataInNext - buffer.Length < 0 ? 0 : remainDataInNext - buffer.Length;
if (remainDataInNext == 0)
{
//Console.WriteLine("frame is ok! {0}", DateTime.Now.ToString("HH:mm:ss.fff"));
if (setFrame != null)
setFrame(frameBuffer);
}
}
else if (LocateHeader(buffer, ref headerStart, ref frameStart, ref frameLength))
{
frameBuffer = new byte[frameLength];
Array.Copy(buffer, frameStart, frameBuffer, 0, buffer.Length - frameStart);
remainDataInNext = frameLength - (buffer.Length - frameStart);
}
}
catch (Exception ex)
{
Console.WriteLine("GetLivingVedio's error:{0}", ex);
}
}
}
}
catch (Exception ex)
{
Console.WriteLine("GetLivingVedio's error:{0}", ex.ToString());
}
Console.WriteLine("exit real time preview");
});
}
/// <summary>
/// 定位buffer中的头信息
/// </summary>
/// <param name="buffer"></param>
/// <param name="headerstart"></param>
/// <param name="framestart"></param>
/// <param name="framelength"></param>
/// <returns></returns>
public bool LocateHeader(byte[] buffer, ref int headerstart, ref int framestart, ref int framelength)
{
bool searchHeaderStartResult = false;
bool searchHeaderEndResult = false;
if (buffer != null && buffer.Length > 0)
{
for (int i = 0; i < buffer.Length; i++)
{
//比较消息头
for (int j = 0; j < this.HeaderStartBuffer.Length; j++)
{
//相等时继续比较,不跳出循环
if ((i + j) < buffer.Length && buffer[i + j] == this.HeaderStartBuffer[j])
{
if (j == 19)
{
headerstart = i;
searchHeaderStartResult = true;
i += 20;
}
}
else
{
break;
}
}
//比较消息结尾
for (int j = 0; j < this.HeaderEndBuffer.Length; j++)
{
//相等时继续比较,不跳出循环
if ((i + j) < buffer.Length && buffer[i + j] == this.HeaderEndBuffer[j])
{
if (j == 3)
{
framestart = i + 4;
searchHeaderEndResult = true;
}
}
else
{
break;
}
}
if (searchHeaderStartResult && searchHeaderEndResult)
{
if (headerstart < framestart)
{
byte[] headerBuffer = new byte[framestart - headerstart];
Array.Copy(buffer, headerstart, headerBuffer, 0, headerBuffer.Length);
string headerString = Encoding.UTF8.GetString(headerBuffer);
int lengthStartIndex = headerString.IndexOf("Content-Length:") + 15;
int lengthEndIndex = headerString.IndexOf("X-Timestamp");
string lengthString = headerString.Substring(lengthStartIndex, lengthEndIndex - lengthStartIndex);
framelength = int.Parse(lengthString.Trim());
}
break;
}
}
}
return searchHeaderStartResult && searchHeaderEndResult;
}
wpf调用:
private void Window_Loaded(object sender, RoutedEventArgs e)
{
string videoUrl = "http://192.168.1.1:8080/?action=stream";
this.ViewModel.GetLivingVedio(videoUrl, displayVedioFrame);
}
private void displayVedioFrame(byte[] frameBuffer)
{
this.Dispatcher.Invoke(delegate
{
BitmapImage bmp = null;
try
{
bmp = new BitmapImage();
bmp.BeginInit();
bmp.StreamSource = new MemoryStream(frameBuffer);
bmp.EndInit();
imgLiving.Source = bmp;
}
catch (Exception ex)
{
Console.WriteLine("displayVedioFrame's error:{0}", ex);
}
});
}