需求:
使用WPF开发应用程序来预览多个摄像头视频数据。
设计思路:
1)通过FFMPEG获取摄像头视频数据。
2)通过SDL来显示视频图像。
技术问题点:
SDL只能在窗体显示,要么弹出新窗体,要么在程序UI相关控件(比如PictureBox控件,也可以是其他Static类控件)显示。显然,获取一个视频设备弹出一个新窗体,不符合需求。那只能把SDL显示窗体嵌入到UI控件里。
要想把SDL嵌入UI控件,就必须要把该UI控件的句柄传给SDL的SDL_CreateWindowFrom()函数。
但是,WPF的UI控件没有句柄这概念,需要在WPF应用程序嵌入Win Form的控件。先创建WindowsFormsHost控件,就可以插入Win Form控件了,代码示例:
<WindowsFormsHost Canvas.Left="10" Canvas.Top="70" Width="230" Height="180" Background="Black">
<wf:PictureBox />
</WindowsFormsHost>
但是,生成的效果如下:
我们发现,在滚动时WindowsFormsHost控件始终都是置顶显示,查询类似问题,答复是“wpf嵌套Win Form组件必定置顶,无解,wpf控件无句柄”。
那显然此需求不能使用SDL来显示视频图像,只好想办法直接使用WPF的Image控件。
新的问题来了,要在Image控件显示,就需要把视频数据转换成Image控件能识别的BitmapImage。
网上有Bitmap转换BitmapImage的方法(参考:http://blog.sina.com.cn/s/blog_6e6941e10100n2yn.html),其代码如下:
public static BitmapImage BitmapToBitmapImage(Bitmap bitmap)
{
Bitmap bitmapSource = new Bitmap(bitmap.Width, bitmap.Height);
bitmapSource.MakeTransparent();
int i, j;
for (i = 0; i < bitmap.Width; i++)
for (j = 0; j < bitmap.Height; j++)
{
Color pixelColor = bitmap.GetPixel(i, j);
Color newColor = Color.FromArgb(pixelColor.R, pixelColor.G, pixelColor.B);
if (newColor == Color.FromArgb(255, 0, 0, 0))
{
newColor = Color.FromArgb(255, 255, 255, 255);
}
bitmapSource.SetPixel(i, j, newColor);
}
MemoryStream ms = new MemoryStream();
bitmapSource.Save(ms, System.Drawing.Imaging.ImageFormat.Bmp);
BitmapImage bitmapImage = new BitmapImage();
bitmapImage.BeginInit();
bitmapImage.StreamSource = new MemoryStream(ms.ToArray());
bitmapImage.EndInit();
return bitmapImage;
}
但是,我们从FFMPEG直接得到的视频数据是某种格式的数据,比如AV_PIX_FMT_RGB24格式。我们没有必要把AV_PIX_FMT_RGB24格式数据先转换到Bitmap,直接转换到BitmapImage即可,其代码如下:
/// <summary>
/// 格式为AV_PIX_FMT_RGB24的视频数据,转换成WPF Image控件所能识别的BitmapImage类的数据
///
/// </summary>
/// <param name="data">AV_PIX_FMT_RGB24的视频数据</param>
/// <param name="imgWidth">显示图像宽度</param>
/// <param name="imgHeight">显示图像高度</param>
/// <returns></returns>
public static BitmapImage VideoDataToBitmapImage(byte []data, int imgWidth, int imgHeight)
{
Bitmap bitmapSource = new Bitmap(imgWidth, imgHeight);
bitmapSource.MakeTransparent();
int i, j;
#if true
for (i = 0; i < imgWidth; i++)
for (j = 0; j < imgHeight; j++)
{
Color newColor = Color.FromArgb(
data[j * imgWidth * 3 + i * 3 + 0],
data[j * imgWidth * 3 + i * 3 + 1],
data[j * imgWidth * 3 + i * 3 + 2]);
if (newColor == Color.FromArgb(255, 0, 0, 0))
{
newColor = Color.FromArgb(255, 255, 255, 255);
}
bitmapSource.SetPixel(i, j, newColor);
}
#else
for (i = 0; i < imgHeight; i++)
for (j = 0; j < imgWidth; j++)
{
Color newColor = Color.FromArgb(
data[i * imgWidth * 3 + j * 3 + 0],
data[i * imgWidth * 3 + j * 3 + 1],
data[i * imgWidth * 3 + j * 3 + 2]);
if (newColor == Color.FromArgb(255, 0, 0, 0))
{
newColor = Color.FromArgb(255, 255, 255, 255);
}
bitmapSource.SetPixel(j, i, newColor);
}
#endif
MemoryStream ms = new MemoryStream();
bitmapSource.Save(ms, System.Drawing.Imaging.ImageFormat.Bmp);
BitmapImage bitmapImage = new BitmapImage();
bitmapImage.BeginInit();
bitmapImage.StreamSource = new MemoryStream(ms.ToArray());
bitmapImage.EndInit();
return bitmapImage;
}
上述代码,我调试了两天,才能正常显示图像,差点开始怀疑人生……上述函数重点是如何给每个像素点设置颜色,即处理SetPixel()函数。直到意识到这一点才得到正确的算法:我参考的代码的填充流程是一列一列的填充,但是获取的视频数据是一行一行的顺序。