技术背景
我们在做Windows平台RTMP推送、轻量级RTSP服务的时候,遇到过这样的技术需求,除了常规的png图片水印外,开发者希望能在桌面或摄像头上,叠加上实时时间和位置信息,并保存到图像里。
技术实现
本文以大牛直播SDK的摄像头采集+动态文字水印为例,谈谈如何实现的,简单来说,这块分两步,第一步,如何从文字里面获取到rgb数据,第二步,如何吧rgb数据叠加到摄像头上?
废话不多说,先上图,选中采集摄像头和摄像头添加文字水印,如果需要默认打开动态文字水印,直接打开即可,如需关闭,随时可以关闭或二次打开:
本文以启动个轻量级RTSP服务为例,效果如下,可以清楚的看到右侧播放端,显示实时更新的文字信息(更新间隔,可以自行设置):
打开摄像头添加文字水印:
private void btn_text_osd_Click(object sender, EventArgs e) { if (btn_text_osd.Text.Equals("打开动态文字水印")) { if (!timer_clock_.Enabled) { timer_clock_.Enabled = true; } enable_layer(get_text_layer_index(), true); btn_text_osd.Text = "关闭动态文字水印"; is_enable_text_watermarker_ = true; } else { if (!is_enable_publish_random_userdata_) { timer_clock_.Enabled = false; } enable_layer(get_text_layer_index(), false); btn_text_osd.Text = "打开动态文字水印"; is_enable_text_watermarker_ = false; } }
添加文字水印图层:
private async void AddWaterMarkerLayer() { Bitmap bitmap = null; try { string format = "yyyy-MM-dd HH:mm:ss.fff"; StringBuilder sb = new StringBuilder(); sb.Append("施工单位:上海视沃信息科技有限公司"); sb.Append("\r\n"); sb.Append("施工时间:"); sb.Append(DateTime.Now.DayOfWeek.ToString()); sb.Append(" "); sb.Append(DateTime.Now.ToString(format)); sb.Append("\r\n"); sb.Append("当前位置:上海市"); string str = sb.ToString(); bitmap = GenerateBitmap(str); } catch (Exception) { return; } if (null == bitmap) return; int x = 0; int y = 200; UpdateLayerRegion(get_text_layer_index(), x, y, bitmap); enable_layer(get_text_layer_index(), true); await Task.Delay(30); post_argb8888_layer_image(get_text_layer_index(), bitmap); }
投递图层数据:
/* * SmartPublisherDemoDlg.cs * Author: daniusdk.com */ public void post_argb8888_layer_image(int index, Bitmap bitmap) { if (index < 0 || null == bitmap) return; if (!is_running()) return; Rectangle rect = new Rectangle(0, 0, bitmap.Width, bitmap.Height); System.Drawing.Imaging.BitmapData bmp_data = bitmap.LockBits(rect, System.Drawing.Imaging.ImageLockMode.ReadOnly, bitmap.PixelFormat); IntPtr ptr = bmp_data.Scan0; int stride = bmp_data.Stride; if (stride < 0) stride = -stride; NT_PB_Image pb_image = new NT_PB_Image(); pb_image.format_ = (int)NT.NTSmartPublisherDefine.NT_PB_E_IMAGE_FORMAT.NT_PB_E_IMAGE_FORMAT_ARGB; pb_image.width_ = bmp_data.Width; pb_image.height_ = bmp_data.Height; pb_image.timestamp_ = 0; pb_image.cb_size_ = (UInt32)Marshal.SizeOf(pb_image); pb_image.stride_ = new Int32[NTSmartPublisherDefine.NT_PB_IMAGE_MAX_PLANE_NUM]; pb_image.stride_[0] = stride; pb_image.plane_size_ = new Int32[NTSmartPublisherDefine.NT_PB_IMAGE_MAX_PLANE_NUM]; pb_image.plane_size_[0] = pb_image.stride_[0] * bmp_data.Height; pb_image.plane_ = new IntPtr[NTSmartPublisherDefine.NT_PB_IMAGE_MAX_PLANE_NUM]; pb_image.plane_[0] = ptr; for (int i = 1; i < NTSmartPublisherDefine.NT_PB_IMAGE_MAX_PLANE_NUM; ++i) { pb_image.stride_[i] = 0; pb_image.plane_size_[i] = 0; pb_image.plane_[i] = IntPtr.Zero; } IntPtr image_ptr = Marshal.AllocHGlobal(Marshal.SizeOf(pb_image)); Marshal.StructureToPtr(pb_image, image_ptr, false); if (is_running()) { if (shared_lock_.TryEnterReadLock(enter_read_lock_timeout_ms_)) { try { if (is_running()) NTSmartPublisherSDK.NT_PB_PostLayerImage(publisher_handle_, 0, index, image_ptr, 0, IntPtr.Zero); } finally { shared_lock_.ExitReadLock(); } } } Marshal.FreeHGlobal(image_ptr); bitmap.UnlockBits(bmp_data); }
再说如何实现两个图层:
if (btn_check_camera_input_.Checked && btn_add_osd_to_camera_.Checked) { if (-1 != cur_sel_camera_index_ && -1 != cur_sel_camera_resolutions_index_ && -1 != cur_sel_camera_frame_rate_index_) { NT_PB_CameraLayerConfigV2 camera_layer_c0 = new NT_PB_CameraLayerConfigV2(); CameraInfo camera = cameras_[cur_sel_camera_index_]; NT_PB_VideoCaptureCapability cap = camera.capabilities_[cur_sel_camera_resolutions_index_]; camera_layer_c0.device_unique_id_utf8_ = camera.id_; type = (Int32)NTSmartPublisherDefine.NT_PB_E_LAYER_TYPE.NT_PB_E_LAYER_TYPE_CAMERA; fill_layer_base(camera_layer_c0, out camera_layer_c0.base_, type, index, true, 0, 0, cap.width_, cap.height_); camera_layer_index_ = camera_layer_c0.base_.index_; if (btn_check_flip_horizontal_camera_.Checked) { camera_layer_c0.is_flip_horizontal_ = 1; } else { camera_layer_c0.is_flip_horizontal_ = 0; } if (btn_check_flip_vertical_camera_.Checked) { camera_layer_c0.is_flip_vertical_ = 1; } else { camera_layer_c0.is_flip_vertical_ = 0; } // 这种叠加模式下不要旋转,否则变形厉害, 要么就定好一个角度,调整宽高,但不要动态旋转 camera_layer_c0.rotate_degress_ = 0; if (add_layer_config(camera_layer_c0, type)) index++; //第二层:文字水印 //叠加的文本层 NT_PB_ExternalVideoFrameLayerConfig text_layer = new NT_PB_ExternalVideoFrameLayerConfig(); type = (Int32)NTSmartPublisherDefine.NT_PB_E_LAYER_TYPE.NT_PB_E_LAYER_TYPE_EXTERNAL_VIDEO_FRAME; fill_layer_base(text_layer, out text_layer.base_, type, index, true, 0, 0, 64, 64); if (add_layer_config(text_layer, type)) text_layer_index_ = index++; } NTSmartPublisherSDK.NT_PB_SetFrameRate(publisher_handle_, (uint)(cur_sel_camera_frame_rate_index_ + 1)); }
如果需要更新图层位置:
public bool update_layer_region(int index, int x, int y, int w, int h) { if (index < 1) return false; if (is_empty_handle()) return false; NT_PB_RectRegion region = new NT_PB_RectRegion(); region.x_ = x; region.y_ = y; region.width_ = w; region.height_ = h; return NTBaseCodeDefine.NT_ERC_OK == NTSmartPublisherSDK.NT_PB_UpdateLayerRegion(publisher_handle_, 0, index, ref region); }
动态打开关闭图层:
public bool enable_layer(int index, bool enable) { if (index < 1) return false; if (is_empty_handle()) return false; return NTBaseCodeDefine.NT_ERC_OK == NTSmartPublisherSDK.NT_PB_EnableLayer(publisher_handle_, 0, index, enable ? 1 : 0); }
总结
Windows平台添加动态文字水印,首先确保从文字拿到rgb数据,然后,设置两个图层,摄像头或者屏幕数据,作为底层,上层添加文字图层,如果需要实时更新,有个定制器,刷新即可,感兴趣的开发者,可以单独和我交流。