注:此解决方案image控件视频数据流很大时会白屏,后来又用PictureBox来显示视频了。请看我后面的文章。
在没有WPF时,我们用PictureBox控件显示图片和视频流。公司新项目用WPF做,而且WPF已有的框架中添加2.0的PictrueBox兼容性太差,所以,用Image控件来显示视频。
服务端 用的视频数据传到客户端的是没有压缩的bitmap位图文件数,通讯协议是UDP 。在做这个之前,对bitmap位图整体地进行了一次学习。可以看下别人写的位图文件结构 博客,由于传过来的视频数据没有文件头,而Image控件绑定的BitmapImage必须有位图文件头才能EndInit成功,所以在创建文件头上花了不少功夫。
首先前台XAML的Image控件和其Source属性对后台的ShowBackground进行了依赖。
<Grid Background="{TemplateBinding ClientAreaBackground}" >
<Image Stretch="Fill" Source ="{TemplateBinding ShowBackground}"/>
</Grid>
服务端的数据包是一次发送位图的一行数据,如果一个位图的像素高度是240,那一张图片就要发240次数据包。数据包的大小是根据宽度算出来的,一张图片的大小 是长*宽计算出来的。 一张图片240*320的长宽,一秒30帧,一张图片100多KB,一秒钟要处理3MB的数据,CPU使用率30%多,全速的情况下还是很相当耗资源的 。如果降低帧数到一秒3张图片,比较流畅,一样的,视频不流畅了…………
后台代码
/// <summary>
/// Gets/Sets 回显Image控件依赖的图像属性
/// </summary>
public ImageSource ShowBackground
{
get { return (ImageSource)GetValue(ShowBackgroundProperty); }
set
{
SetValue(ShowBackgroundProperty, value);
}
}
public static DependencyProperty ShowBackgroundProperty =
DependencyProperty.Register("ShowBackground", typeof(ImageSource), typeof(VirtualWindow),
new UIPropertyMetadata(new BitmapImage(new Uri("F:\\OpenAll.png", UriKind.Relative)))//给个默认图片
);
public Socket _WinSkt = null;
private BitmapImage _memBmp = new BitmapImage();
public Thread _RdThread;
byte[] _header = new byte[54];//保存bitmap图片的头信息
private delegate void ShowPicHandler(MemoryStream imgstream);
private ShowPicHandler _dShowpic;//显示图片委托
public int _PortNum;
private int _lenstart;//记录包是位图中的哪一行
public int _width;//图片宽度
public int _height;//图片高度
private IPAddress _ipa;
//此方法是启动方法,启动时将IP和端口数据传给服务端在另外一个类里做的,这里只管接收
public void InitPreview()
{
_WinSkt = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.IP);
while (true)
{
_PortNum = new Random().Next(1, 65534);
try
{
IPEndPoint myIP = new IPEndPoint(_ipa, _PortNum);
_WinSkt.Bind(myIP);
_WinSkt.ReceiveBufferSize = 1024 * 768 * 3;
}
catch
{
continue;
}
break;
}
//实例化显示图片委托
_dShowpic = new ShowPicHandler(ShowImage);
//开启线程接收数据
_RdThread = new Thread(new ThreadStart(RdRcvThread));
_RdThread.Start();
}
private void RdRcvThread()
{
byte[] nMsgLenBuf = new byte[65535];
byte[] RgbBuf = new byte[6000]; //两行数据,最大像素点1000 * 3 * 2
int nRev = 0;
//用来依次写入图片包数据
MemoryStream buffstream = new MemoryStream();
//存储一张完成的图片并绘到image
MemoryStream imgstream=new MemoryStream();
#region VIDEO信号
int tmpInt = Convert.ToInt32(_width * 3);//传过去的参数要是4的倍数,这是适应位图数据的一些逻辑,可以不用细究
int i = tmpInt / 4;
int j = 0;
if (tmpInt % 4 != 0) j = (i + 1) * 4;
else j = tmpInt;
int row = 0;
_length = j * _height;//计算一张图片大小
CreateHeader();//创建文件头
buffstream.Write(_header, 0, 54);//将文件头写入内存
while (true)
{
try
{
nRev = _WinSkt.Receive(nMsgLenBuf);
}
catch { return; }
try
{
_lenstart = nMsgLenBuf[0] * 256 + nMsgLenBuf[1];//包的头两个byte是此包所在的图片位置
//丢包的图片数据内存区域没有处理
//丢失的数据包直接不管,找到当前包其所在的位置并写入
buffstream.Position = j * _lenstart + 54;
buffstream.Write(nMsgLenBuf,0,j);
if (_lenstart >= _height - 1)//一张图片传结束了
{
//解决buff被接收线程修改的问题,将buff中的数据复制到图片内存区
imgstream.Position = 0;
imgstream.Write(buffstream.GetBuffer(), 0, _length + 54);
//显示图片
Application.Current.Dispatcher.BeginInvoke(_dShowpic, imgstream);
}
}
catch
{
buffstream.Dispose();
buffstream = new MemoryStream();
buffstream.Write(_header, 0, 54);
}
}
#endregion
}
private void CreateHeader()
{
MemoryStream stream = new MemoryStream(14 + 40); //为头腾出54个长度的空间
byte[] buffer = new byte[13];
buffer[0] = 0x42; //Bitmap固定常数
buffer[1] = 0x4d; //Bitmap固定常数
stream.Write(buffer, 0, 2); //先写入头的前两个字节
//把我们之前获得的数据流的长度转换成字节,
//这个是用来告诉“头”我们的实际图像数据有多大
byte[] bytes = BitConverter.GetBytes(_length + 54);
stream.Write(bytes, 0, 4); //把这个长度写入头中去
buffer[0] = 0; buffer[1] = 0;
buffer[2] = 0; buffer[3] = 0;
stream.Write(buffer, 0, 4); //在写入4个字节长度的数据到头中去
int num2 = 0x36; //Bitmap固定常数
bytes = BitConverter.GetBytes(num2);
stream.Write(bytes, 0, 4); //在写入最后4个字节的长度
tagBITMAPINFOHEADER sInfoHead = new tagBITMAPINFOHEADER();
sInfoHead.biSize = 40; //本结构所占字节数,实际上该结构占用40个字节,但Windows每次还是需要您亲自添上
sInfoHead.biWidth = _width; //Convert.ToInt32(GqyFactVWWidth+1);
sInfoHead.biHeight = -_height; //Convert.ToInt32(GqyFactVWHight);
sInfoHead.biPlanes = 1; //目标设备的平面数,约定必须为1
sInfoHead.biSizeImage = 0;
sInfoHead.biBitCount = 24; //每个像素所需的位数,必须是1(双色)、 4(16色)、8(256色)、24(真彩色)或32(32位真彩)之一
sInfoHead.biCompression = 0; //位图压缩类型,必须是0(不压缩)、1(BI_RLE8压缩类型)或2(BI_RLE4压缩类型)之一
sInfoHead.biXPelsPerMeter = 0; //水平分辨率,每米像素数,一般不用关心,设为0
sInfoHead.biYPelsPerMeter = 0; //垂直分辨率,每米像素数,一般不用关心,设为0
sInfoHead.biClrUsed = 0; //位图实际使用的颜色表中的颜色数,一般不用关心,设为0
sInfoHead.biClrImportant = 0;
byte[] bufffffff = new byte[40];
bufffffff = StructToBytes(sInfoHead);
stream.Write(bufffffff, 0, 40);
_header = stream.GetBuffer(); //将bmp文件头保存为全局变量
}
private void ShowImage(MemoryStream imgstream)
{
try
{
_memBmp = new BitmapImage();
_memBmp.BeginInit();
_memBmp.StreamSource = imgstream;
_memBmp.EndInit();
ShowBackground = _memBmp;//将最终的图片给image控件的资源依赖项属性
}
catch { }
}
private byte[] StructToBytes(object structObj)
{
int size = Marshal.SizeOf(structObj);//得到结构体的大小
byte[] bytes = new byte[size];//创建byte数组
IntPtr structPtr = Marshal.AllocHGlobal(size);//分配结构体大小的内存空间
Marshal.StructureToPtr(structObj, structPtr, false);//将结构体拷到分配好的内存空间
Marshal.Copy(structPtr, bytes, 0, size); //从内存空间拷到byte数组
Marshal.FreeHGlobal(structPtr); //释放内存空间
return bytes;//返回byte数组
}
文件头结构体字节15-54部分。另外一部分在代码中自己创建了,结构体在上面链接的博客中也有详细介绍。
struct tagBITMAPINFOHEADER
{
public uint biSize; //本结构所占字节数,实际上该结构占用40个字节,但Windows每次还是需要您亲自添上
public int biWidth; //位图的宽度,单位为像素
public int biHeight; //位图的高度,单位为像素
public ushort biPlanes; //目标设备的平面数,约定必须为1
public ushort biBitCount;//每个像素所需的位数,必须是1(双色)、 4(16色)、8(256色)、24(真彩色)或32(32位真彩)之一
public uint biCompression; //位图压缩类型,必须是0(不压缩)、1(BI_RLE8压缩类型)或2(BI_RLE4压缩类型)之一
public uint biSizeImage; //位图的大小,以字节为单位,对于BI_RGB必须设置为0,对于压缩文件请参考MSDN
public int biXPelsPerMeter; //水平分辨率,每米像素数,一般不用关心,设为0
public int biYPelsPerMeter; //垂直分辨率,每米像素数,一般不用关心,设为0
public uint biClrUsed;//位图实际使用的颜色表中的颜色数,一般不用关心,设为0
public uint biClrImportant;//位图显示过程中重要的颜色数,一般不用关心,设为0
} //该结构占据40个字节。