技术背景
我们在做RTSP|RTMP播放器的时候,有这样的技术诉求,开发者希望同时回调YUV、RGB数据,特别是Unity场景下,YUV数据用于渲染,RGB数据用于做视觉算法分析,拿到的RGB数据,想办法和python通信,发给python做视觉算法处理。
一般来说,如果设备带宽和性能比较好的话,可以直接拉两路流,同时解码回调需要的数据,当然,一般是不建议这么做,特别是4K+分辨率的流,同时解两路,耗费性能,没有必要。
另外一种,可以修改播放器底层逻辑,实现同时回调YUV和RGB数据,但是,我们知道,大多场景,RGB数据做算法分析的话,不一定需要全帧和高分辨率,考虑到算法处理能力,比如,有可能一秒钟只需要处理5-10帧,而且,有的视觉算法对高分辨率数据支持不好(比如分析4K的)。
那么,比较好的方式是,回调YUV数据,然后,RGB数据,提供上层接口,按需转,转过后的RGB数据,发给python或者其他算法就好。
技术实现
基于上述场景,我们做了以下的方案:
1. 设置回调YUV数据:
video_frame_call_back_ = new SP_SDKVideoFrameCallBack(SetVideoFrameCallBackV2); NTSmartPlayerSDK.NT_SP_SetVideoFrameCallBack(player_handle_, (Int32)NT.NTSmartPlayerDefine.NT_SP_E_VIDEO_FRAME_FORMAT.NT_SP_E_VIDEO_FRAME_FROMAT_I420, IntPtr.Zero, video_frame_call_back_);
2. 如果是做YUV到RGB数据转换:
/* * SmartPlayer.cs * Author: https://daniusdk.com * WeChat: xinsheng120 */ rgb_frame.format_ = (int)NT.NTSmartPlayerDefine.NT_SP_E_VIDEO_FRAME_FORMAT.NT_SP_E_VIDEO_FRAME_FORMAT_ARGB; rgb_frame.width_ = video_frame.width_; rgb_frame.height_ = video_frame.height_; rgb_frame.timestamp_ = video_frame.timestamp_; rgb_frame.stride0_ = video_frame.width_ * 4; rgb_frame.stride1_ = 0; rgb_frame.stride2_ = 0; rgb_frame.stride3_ = 0; Int32 argb_size = rgb_frame.stride0_ * rgb_frame.height_; rgb_frame.plane0_ = Marshal.AllocHGlobal(argb_size); NTSmartPlayerSDK.NT_SP_I420ToARGB(video_frame.plane0_, video_frame.stride0_, video_frame.plane1_, video_frame.stride1_, video_frame.plane2_, video_frame.stride2_, rgb_frame.plane0_, rgb_frame.stride0_, video_frame.width_, video_frame.height_);
3. 这里可以看到,我们设计了I420到ARGB的接口:
/* * smart_player_sdk.cs * Author: https://daniusdk.com * WeChat: xinsheng120 */ [DllImport(@"SmartPlayerSDK.dll")] public static extern UInt32 NT_SP_I420ToARGB(IntPtr src_y_plane, Int32 src_y_stride, IntPtr src_u_plane, Int32 src_u_stride, IntPtr src_v_plane, Int32 src_v_stride, IntPtr dst_argb_plane, Int32 dst_argb_stride, Int32 width, Int32 height);
好多算法,对高分辨率数据支持并不友好,于是我们做了分辨率缩放接口,拿到yuv数据后,先做缩放,然后再做yuv到rgb的转换。
/* * SmartPlayer.cs * Author: https://daniusdk.com * WeChat: xinsheng120 */ int scale_width = 1280; int scale_height = 720; int scale_y_stride = scale_width; int scale_u_stride = (scale_width + 1) / 2; int scale_v_stride = scale_u_stride; int scale_y_size = scale_y_stride * scale_height; int scale_u_size = scale_u_stride * ((scale_height + 1) / 2); int scale_v_size = scale_u_size; NT_SP_VideoFrame scale_frame = new NT_SP_VideoFrame(); scale_frame.width_ = scale_width; scale_frame.height_ = scale_height; scale_frame.plane0_ = Marshal.AllocHGlobal(scale_y_size); scale_frame.plane1_ = Marshal.AllocHGlobal(scale_u_size); scale_frame.plane2_ = Marshal.AllocHGlobal(scale_v_size); NTSmartPlayerSDK.NT_SP_I420Scale(video_frame.plane0_, video_frame.stride0_, video_frame.plane1_, video_frame.stride1_, video_frame.plane2_, video_frame.stride2_, video_frame.width_, video_frame.height_, scale_frame.plane0_, scale_y_stride, scale_frame.plane1_, scale_u_stride, scale_frame.plane2_, scale_v_stride, scale_frame.width_, scale_frame.height_, 3); rgb_frame.format_ = (int)NT.NTSmartPlayerDefine.NT_SP_E_VIDEO_FRAME_FORMAT.NT_SP_E_VIDEO_FRAME_FORMAT_ARGB; rgb_frame.width_ = scale_frame.width_; rgb_frame.height_ = scale_frame.height_; rgb_frame.stride0_ = scale_frame.width_ * 4; rgb_frame.stride1_ = 0; rgb_frame.stride2_ = 0; rgb_frame.stride3_ = 0; Int32 argb_size = rgb_frame.stride0_ * rgb_frame.height_; rgb_frame.plane0_ = Marshal.AllocHGlobal(argb_size); NTSmartPlayerSDK.NT_SP_I420ToARGB(scale_frame.plane0_, scale_y_stride, scale_frame.plane1_, scale_u_stride, scale_frame.plane2_, scale_v_stride, rgb_frame.plane0_, rgb_frame.stride0_, scale_frame.width_, scale_frame.height_);
有了rgb数据,下一步,就是如何跟python通信的问题,在此之前,我们有专门写过一篇blog,跟python交互,有多种方式,比如共享内存、通过UDP发送或者写bitmap文件,然后python实时读取就好。以写bitmap为例(Linux demo),开始播放后,video frame数据回调处理如下:
extern "C" void NT_SDK_SDKVideoFrameCallBack(NT_HANDLE handle, NT_PVOID user_data, NT_UINT32 status, const NT_SP_VideoFrame* frame) { if (!frame) return; fprintf(stdout, "OnSDKVideoFrameCallBack handle:%p frame:%p, timestamp:%llu\n", handle, frame, frame->timestamp_); #if NEED_SAVE_BITMAP if (NT_SP_E_VIDEO_FRAME_FORMAT_RGB32 == frame->format_ || NT_SP_E_VIDEO_FRAME_FORMAT_ARGB == frame->format_) { struct timeval tv; if (gettimeofday(&tv, nullptr) != 0) { fprintf(stderr, "save bitmap file call gettimeofday failed"); return; } uint64_t local_time_us = tv.tv_sec*UINT64_C(1000000) + tv.tv_usec; char file_name[128] = { 0 }; sprintf(file_name, "./outbitmaps/%llu.bmp", (unsigned long long)local_time_us); if (!save_bitmap_file(frame->width_, frame->height_, frame->plane0_, frame->stride0_, frame->stride0_*frame->height_, file_name)) fprintf(stderr, "save bitmap file failed, name:%s", file_name); else g_bitmap_file_names_.emplace_back(file_name); while (g_bitmap_file_names_.size() > 32) { remove(g_bitmap_file_names_.front().c_str()); g_bitmap_file_names_.pop_front(); } } #endif // NEED_SAVE_BITMAP }
video frame回调后的数据,直接调用save_bitmap_file()实现bitmap文件写入,python程序,只需要到指定的文件夹下,读取生成的bitmap,然后做算法分析即可。拿到rgb数据,当然也可以通过共享内存或UDP直接发出去。
总结
我们播放RTSP|RTMP流,如果需要同时做渲染和算法分析的话,特别是渲染在上层实现(比如Unity),算法是python这种情况,拉两路流,更耗费带宽和性能,拉一路流,同时回调YUV和RGB数据也可以,但是更灵活的是本文提到的按需转算法期望的RGB数据,然后做算法处理。以上是大概的逻辑实现,感兴趣的开发者,可以单独跟我交流。