此次项目需求是接入海康威视的网络智能摄像头实现实时监控。网上搜罗一番,也有挺多例子的,但是大多数都是通过官方提供的rstp协议地址实现,为了自己记忆,在下打算在这里记录一下,不需要的可以跳过这里哈(这里采用一个叫UMP的插件,当然还有其他Vlc for unity,openCV等都可以实现的)
首先贴一下海康的rtsp协议地址:
rtsp://[username]:[password]@[ip]:[port]/[codec]/[channel]/[subtype]/av_stream
说明:
username: 用户名。例如admin。
password: 密码。例如12345。
ip: 为设备IP。例如 192.0.0.64。
port: 端口号默认为554,若为默认可不填写。
codec:有h264、MPEG-4、mpeg4这几种。
channel: 通道号,起始为1。例如通道1,则为ch1。
subtype: 码流类型,主码流为main,辅码流为sub。
例如,请求海康摄像机通道1的主码流,Url如下
主码流:
rtsp://admin:12345@192.0.0.64:554/h264/ch1/main/av_stream
rtsp://admin:12345@192.0.0.64:554/MPEG-4/ch1/main/av_stream
子码流:
rtsp://admin:12345@192.0.0.64/mpeg4/ch1/sub/av_stream
rtsp://admin:12345@192.0.0.64/h264/ch1/sub/av_stream
然后插件的名字叫UMP 地址我就不贴了,搜一下会有的哈。导入后是这样的
然后随便选一个场景填入你的rtsp地址
然后这是运行的效果
但是本人通过测试后发现延迟~额。。。有一丢丢小高,于是继续搜罗,发现海康的SDK里的函数是可以回调获取视频流的数据的,还提供了一个播放库的SDK,同过此SDK的方法可以将标准的视频码流转换为YV12的格式,相信研究过视频流的对这个格式都不陌生吧,没错,小白我准备在unity中将这个格式的数据实时生成一帧一帧的textur2d,然后,上步骤
首先下载海康最新的SDK包
地址:http://www.hikvision.com/cn/download_more_570.html
记住播放库的也一起下来哦、
前一部分的实现代码其实官方已经提供了各种各样的案例 C#和C++的都有,但是看过代码的同学应该都会知道,在这写Demo中想要播放视频都有一个必不可少的参数就是窗口的句柄Handle,就是一个IntPtr类型的参数,但是untiy中。As you konw ,哪里来的句柄啊,UI都是画出来的嘛,整个unity才是一个窗口,但是SDK中也说了可以给这个参数传空然后给一个回调函数来获取这些视频数据,
记住这个回调函数里是用来启用Play_M4播放库解码的(有点长,还是不贴图片贴代码号了,不要喷我乱(* ̄︶ ̄))
/// <summary>
/// 获取数据流回调函数
/// </summary>
/// <param name="lRealHandle">L real handle.</param>
/// <param name="dwDataType">Dw data type.</param>
/// <param name="pBuffer">P buffer.</param>
/// <param name="dwBufSize">Dw buffer size.</param>
/// <param name="pUser">P user.</param>
public void RealDataCallBack(Int32 lRealHandle, UInt32 dwDataType, IntPtr pBuffer, UInt32 dwBufSize, IntPtr pUser)
{
//下面数据处理建议使用委托的方式
//MyDebugInfo AlarmInfo = new MyDebugInfo(DebugInfo);
switch (dwDataType)
{
case CHCNetSDK.NET_DVR_SYSHEAD: // sys head
if (dwBufSize > 0)
{
if (m_lPort >= 0)
{
return; //同一路码流不需要多次调用开流接口
}
//debugInfo += ("系统头数据:" + pBuffer + "数据长度:" + dwBufSize);
//获取播放句柄 Get the port to play
if (!PlayCtrl.PlayM4_GetPort(ref m_lPort))
{
error_num = PlayCtrl.PlayM4_GetLastError(m_lPort);
debugInfo += ("PlayM4_GetPort failed, error code= " + error_num);
break;
}
//设置流播放模式 Set the stream mode: real-time stream mode
if (!PlayCtrl.PlayM4_SetStreamOpenMode(m_lPort, PlayCtrl.STREAME_REALTIME))
{
error_num = PlayCtrl.PlayM4_GetLastError(m_lPort);
debugInfo += ("Set STREAME_REALTIME mode failed, error code= " + error_num);
}
//打开码流,送入头数据 Open stream
if (!PlayCtrl.PlayM4_OpenStream(m_lPort, pBuffer, dwBufSize, 1024*1024))
{
error_num = PlayCtrl.PlayM4_GetLastError(m_lPort);
debugInfo += ("PlayM4_OpenStream failed, error code= " + error_num);
break;
}
//设置显示缓冲区个数 Set the display buffer number
if (!PlayCtrl.PlayM4_SetDisplayBuf(m_lPort, 15))
{
error_num = PlayCtrl.PlayM4_GetLastError(m_lPort);
debugInfo += ("PlayM4_SetDisplayBuf failed, error code= " + error_num);
}
//设置显示模式 Set the display mode
if (!PlayCtrl.PlayM4_SetOverlayMode(m_lPort, 0, 0)) //play off screen
{
error_num = PlayCtrl.PlayM4_GetLastError(m_lPort);
debugInfo += ("PlayM4_SetOverlayMode failed, error code= " + error_num);
}
//设置解码回调函数,获取解码后音视频原始数据 Set callback function of decoded data
m_fDisplayFun = new PlayCtrl.DECCBFUN(DecCallbackFUN);
if (!PlayCtrl.PlayM4_SetDecCallBack(m_lPort, m_fDisplayFun))
{
debugInfo += ("PlayM4_SetDisplayCallBack fail");
}
//开始解码 Start to play
if (!PlayCtrl.PlayM4_Play(m_lPort, IntPtr.Zero))
{
error_num = PlayCtrl.PlayM4_GetLastError(m_lPort);
debugInfo += ("PlayM4_Play failed, error code= " + error_num);
break;
}
}
break;
case CHCNetSDK.NET_DVR_STREAMDATA: // video stream data
if (dwBufSize > 0 && m_lPort != -1)
{
for (int i = 0; i < 999; i++)
{
//送入码流数据进行解码 Input the stream data to decode
if (!PlayCtrl.PlayM4_InputData(m_lPort, pBuffer, dwBufSize))
{
error_num = PlayCtrl.PlayM4_GetLastError(m_lPort);
debugInfo += ("PlayM4_InputData failed, error code= " + error_num);
Thread.Sleep(10);
}
else
{
break;
}
}
}
break;
default:
if (dwBufSize > 0 && m_lPort != -1)
{
//送入其他数据 Input the other data
for (int i = 0; i < 999; i++)
{
if (!PlayCtrl.PlayM4_InputData(m_lPort, pBuffer, dwBufSize))
{
error_num = PlayCtrl.PlayM4_GetLastError(m_lPort);
debugInfo += ("PlayM4_InputData failed, error code= " + error_num);
Thread.Sleep(10);
}
else
{
break;
}
}
}
break;
}
}
然后在播放库代码中在加一个回调来自己处理数据
看图
这里的数据都是别人官方的播放库函数处理好的每一帧的数据,连帧号都有,可以说已经非常方便了,我们只需要将每一帧的数据拿来然后转换为我们需要的Texutrue2D数据,然后一帧一帧的显示到我们的UI上就可以了,那么重点来了,之前说过播放库拿出来的数据是YV12的格式,而我们需要的Texture2D是rgb(YUV)格式的,这些都是图片数据的存储格式,我实在太懒了,给你们贴个地址,不懂得有兴趣的可以看看O(∩_∩)O https://www.cnblogs.com/samaritan/p/YUV.html
这里转换的话,由于数据量有点大 一帧1280*720的YV12数据大概有2764800,如果自己写硬生生的在数据里复制替换啥的,额,本人试了一下,不到10帧,感觉成GIF图了都,所以网上又搜罗一下(有事没事就搜罗搜罗),看到其他博友测试的五六种方法,有直接硬转的(通过查表法优化过的,其实也没优化多少),然后看到效率比较高的两种,用opencv和ffmpeg实现的,opencv和ffmpeg都试了下,最后选择了ffmpeg,因为ffmpeg的算法效率最好(别人说的,别人说测试了的)
这里如果要用ffmpeg的格式转换算法需要两个库avutil-55.dll和swscale-4.dll
然后呢当然就是调用这两个库,写一个转换方法导出可以供untiy调用的Dll了
下面贴出Dll里的转换函数代码,比较粗糙,大家不要介意.
AVPixelFormat SRC_pixfmt;
AVPixelFormat DST_pixfmt;
AVPicture SRC_frameinfo;
AVPicture DST_frameinfo;
struct SwsContext *img_convert_ctx;
TransformResolution *TP;
FFMPEG_FOR_UNITY_API bool StartConvert_Updated(bool start_or_end,int src_width,int src_height,int dst_width,int dst_height,int src_type,int dst_type)
{
if(start_or_end)
{
SRC_pixfmt = (AVPixelFormat)src_type;//0
DST_pixfmt = (AVPixelFormat)dst_type;//2
int ret=0;
TP = new TransformResolution();
TP->SRC_WIDTH = src_width;
TP->SRC_HEIGHT = src_height;
TP->DST_WIDTH = dst_width;
TP->DST_HEIGHT = dst_height;
ret= av_image_alloc(SRC_frameinfo.data, SRC_frameinfo.linesize,src_width, src_height, SRC_pixfmt, 1);
if (ret< 0) {
//printf( "Could not allocate source image\n");
//strcpy(rst,"Could not allocate source image\n");
return false;
}
ret = av_image_alloc(DST_frameinfo.data, DST_frameinfo.linesize,dst_width, dst_height, DST_pixfmt, 1);
if (ret< 0) {
//printf( "Could not allocate destination image\n");
//strcpy(rst,"Could not allocate destination image\n");
return false;
}
//Init Method 1
img_convert_ctx =sws_alloc_context();
//Show AVOption
//av_opt_show2(img_convert_ctx,stdout,AV_OPT_FLAG_VIDEO_PARAM,NULL);
//Set Value
av_opt_set_int(img_convert_ctx,"sws_flags",SWS_BICUBIC|SWS_PRINT_INFO,NULL);
av_opt_set_int(img_convert_ctx,"srcw",src_width,NULL);
av_opt_set_int(img_convert_ctx,"srch",src_height,NULL);
av_opt_set_int(img_convert_ctx,"src_format",SRC_pixfmt,NULL);
//'0' for MPEG (Y:0-235);'1' for JPEG (Y:0-255)
av_opt_set_int(img_convert_ctx,"src_range",1,NULL);
av_opt_set_int(img_convert_ctx,"dstw",dst_width,NULL);
av_opt_set_int(img_convert_ctx,"dsth",dst_height,NULL);
av_opt_set_int(img_convert_ctx,"dst_format",DST_pixfmt,NULL);
av_opt_set_int(img_convert_ctx,"dst_range",1,NULL);
sws_init_context(img_convert_ctx,NULL,NULL);
return true;
}
else
{
sws_freeContext(img_convert_ctx);
av_freep(&SRC_frameinfo);
av_freep(&DST_frameinfo);
delete TP;
return true;
}
}
StartConvert_Update函数主要做一些初始化,c++里面大家知道的申请内存啊什么的
然后是转化函数
///update
FFMPEG_FOR_UNITY_API bool YV12toRgb_Updated(uint8_t* pDst, uint8_t* pSrc)
{
if(!pDst)
{
//strcpy(rst,"pDst is null");
return false;
}
if(!TP)
{
return false;
}
switch(SRC_pixfmt){
case AV_PIX_FMT_GRAY8:{
memcpy(SRC_frameinfo.data[0],pSrc,TP->SRC_WIDTH*TP->SRC_HEIGHT);
break;
}
case AV_PIX_FMT_YUV420P:{
memcpy(SRC_frameinfo.data[0],pSrc,TP->SRC_WIDTH*TP->SRC_HEIGHT); //Y
memcpy(SRC_frameinfo.data[1],pSrc+TP->SRC_WIDTH*TP->SRC_HEIGHT*5/4,TP->SRC_WIDTH*TP->SRC_HEIGHT/4); //V
memcpy(SRC_frameinfo.data[2],pSrc+TP->SRC_WIDTH*TP->SRC_HEIGHT,TP->SRC_WIDTH*TP->SRC_HEIGHT/4); //U
break;
}
case AV_PIX_FMT_YUV422P:{
memcpy(SRC_frameinfo.data[0],pSrc,TP->SRC_WIDTH*TP->SRC_HEIGHT); //Y
memcpy(SRC_frameinfo.data[1],pSrc+TP->SRC_WIDTH*TP->SRC_HEIGHT,TP->SRC_WIDTH*TP->SRC_HEIGHT/2); //U
memcpy(SRC_frameinfo.data[2],pSrc+TP->SRC_WIDTH*TP->SRC_HEIGHT*3/2,TP->SRC_WIDTH*TP->SRC_HEIGHT/2); //V
break;
}
case AV_PIX_FMT_YUV444P:{
memcpy(SRC_frameinfo.data[0],pSrc,TP->SRC_WIDTH*TP->SRC_HEIGHT); //Y
memcpy(SRC_frameinfo.data[1],pSrc+TP->SRC_WIDTH*TP->SRC_HEIGHT,TP->SRC_WIDTH*TP->SRC_HEIGHT); //U
memcpy(SRC_frameinfo.data[2],pSrc+TP->SRC_WIDTH*TP->SRC_HEIGHT*2,TP->SRC_WIDTH*TP->SRC_HEIGHT); //V
break;
}
case AV_PIX_FMT_YUYV422:{
memcpy(SRC_frameinfo.data[0],pSrc,TP->SRC_WIDTH*TP->SRC_HEIGHT*2); //Packed
break;
}
case AV_PIX_FMT_RGB24:{
memcpy(SRC_frameinfo.data[0],pSrc,TP->SRC_WIDTH*TP->SRC_HEIGHT*3); //Packed
break;
}
default:{
//printf("Not Support Input Pixel Format.\n");
break;
}
}
sws_scale(img_convert_ctx, SRC_frameinfo.data, SRC_frameinfo.linesize, 0, TP->SRC_HEIGHT, DST_frameinfo.data, DST_frameinfo.linesize);
switch(DST_pixfmt){
case AV_PIX_FMT_GRAY8:{
memcpy(pDst,DST_frameinfo.data[0],TP->DST_WIDTH*TP->DST_HEIGHT);
break;
}
case AV_PIX_FMT_YUV420P:{
memcpy(pDst,DST_frameinfo.data[0],TP->DST_WIDTH*TP->DST_HEIGHT); //Y
memcpy(pDst,DST_frameinfo.data[1],TP->DST_WIDTH*TP->DST_HEIGHT/4); //U
memcpy(pDst,DST_frameinfo.data[2],TP->DST_WIDTH*TP->DST_HEIGHT/4); //V
break;
}
case AV_PIX_FMT_YUV422P:{
memcpy(pDst,DST_frameinfo.data[0],TP->DST_WIDTH*TP->DST_HEIGHT); //Y
memcpy(pDst,DST_frameinfo.data[1],TP->DST_WIDTH*TP->DST_HEIGHT/2); //U
memcpy(pDst,DST_frameinfo.data[2],TP->DST_WIDTH*TP->DST_HEIGHT/2); //V
break;
}
case AV_PIX_FMT_YUV444P:{
memcpy(pDst,DST_frameinfo.data[0],TP->DST_WIDTH*TP->DST_HEIGHT); //Y
memcpy(pDst,DST_frameinfo.data[1],TP->DST_WIDTH*TP->DST_HEIGHT); //U
memcpy(pDst,DST_frameinfo.data[2],TP->DST_WIDTH*TP->DST_HEIGHT); //V
break;
}
case AV_PIX_FMT_YUYV422:{
memcpy(pDst,DST_frameinfo.data[0],TP->DST_WIDTH*TP->DST_HEIGHT*2);//Packed
break;
}
case AV_PIX_FMT_RGB24:{
//fwrite(dstFrameInfo.data[0],1,dst_w*dst_h*3,dst_file);
memcpy(pDst,DST_frameinfo.data[0],TP->DST_WIDTH*TP->DST_HEIGHT*3);//Packed
// strcpy(rst,"转换成功!");
return true;
break;
}
default:{
//printf("Not Support Output Pixel Format.\n");
break;
}
}
//strcpy(rst,"转换失败!");
return false;
}
然后unity里的调用
然后调用此函数将之前回调函数里存起来的YV12数据 转换为rgb数据
然后,额,创建Texture2D?,还是贴一下吧,虽然代码很丑。。
最后贴一下运行效果吧(里面其他的功能都很简单,就不多做介绍)
试一下GIF图o(* ̄︶ ̄*)o
共同学习,欢迎留言评论以及给我更好的建议或者实现方案,谢谢大家!
联系客服