展示型网站企业网站建设多语言网站建设幻境
2026/2/21 23:08:30 网站建设 项目流程
展示型网站企业网站建设,多语言网站建设幻境,触屏端网站开发,永久免费企业网站申请前言#xff1a;为什么我们需要“二次编码”#xff1f; 在安防监控、教育直播或庭审录像等场景中#xff0c;我们往往不满足于仅仅把视频“拉下来看”。我们经常面临以下高阶需求#xff1a; 版权与取证#xff1a;需要在原始视频流上叠加实时的“当前时间”或“执法记录…前言为什么我们需要“二次编码”在安防监控、教育直播或庭审录像等场景中我们往往不满足于仅仅把视频“拉下来看”。我们经常面临以下高阶需求版权与取证需要在原始视频流上叠加实时的“当前时间”或“执法记录仪ID”且必须烧录在画面中Hardcode防止被篡改。品牌露出在直播转推过程中添加频道Logo或动态滚屏文字。多流合一将摄像头画面与本地桌面、或AI算法分析出的边框结果合成后生成新的流推送到服务器。本文将结合SmartMediakit大牛直播SDK详细拆解如何在Windows平台实现一个全能中间件”它既是播放器拉流解码又是渲染引擎GDI绘制水印更是推流器二次编码推RTMP本地录像。核心架构设计我们的目标是打造一个闭环的视频处理管道Pipeline[RTSP/RTMP源] ⬇️ (拉流) [SmartPlayer播放器] - 解码 - [RGB32/I420数据回调] ⬇️ [GDI 水印渲染引擎] - 生成ARGB水印Bitmap ⬇️ [SmartPublisher推流器] - 图层混合 (视频层 水印层) ⬇️ (编码) [RTMP推流] [本地MP4录像]基于您提供的源码我们将重点分析三个核心模块数据回调桥接、GDI动态水印渲染、以及多图层推流配置。一、 播放器端获取“纯净”的RGB数据首先我们需要配置播放器使其不直接上屏渲染或在渲染的同时将解码后的原始数据抛出来。在CSmartPlayerDlg::OnBnClickedButtonPlay中我们通过SetVideoFrameCallBack设置回调// 1. 初始化SDK player_api_.SetVideoSizeCallBack(player_handle_, GetSafeHwnd(), SP_SDKVideoSizeHandle); // 2. 设置回调格式为 RGB32方便后续GDI处理虽然I420效率更高但RGB处理水印更方便 // 如果需要高性能建议使用I420但此处演示RGB32的通用性 player_api_.SetVideoFrameCallBack(player_handle_, NT_SP_E_VIDEO_FRAME_FORMAT_RGB32, this, SM_SDKVideoFrameHandleV2); // 3. 启动播放 if (NT_ERC_OK ! player_api_.StartPlay(player_handle_)) { AfxMessageBox(_T(播放器失败!)); return; }关键回调函数OnVideoFrameHandle这是连接“播放”与“推流”的桥梁。每当播放器解出一帧画面就会调用此函数。我们在这里将数据“喂”给推流端void CSmartPlayerDlg::OnVideoFrameHandle(NT_HANDLE handle, NT_UINT32 status, const NT_SP_VideoFrame* frame) { if (nullptr frame) return; // 加锁保护防止推流端正在析构或停止 std::unique_lockstd::recursive_mutex lock(push_handle_mutex_); if (!is_pushing_ !is_push_recording_ !is_push_previewing_) return; if (GetPushHandle() nullptr) return; // 组装 NT_PB_Image 数据结构准备投递给推流SDK NT_PB_Image image; memset(image, 0, sizeof(image)); image.width_ frame-width_; image.height_ frame-height_; if (NT_SP_E_VIDEO_FRAME_FORMAT_RGB32 frame-format_) { image.format_ NT_PB_E_IMAGE_FORMAT_RGB32; image.plane_[0] frame-plane0_; image.stride_[0] frame-stride0_; image.plane_size_[0] frame-stride0_ * frame-height_; } else if (NT_SP_E_VIDEO_FRAME_FROMAT_I420 frame-format_) { // 处理I420格式... (代码略原理同上) image.format_ NT_PB_E_IMAGE_FORMAT_I420; // ...赋值plane 0, 1, 2 } else { return; } // 核心步骤将从播放器拿到的视频帧作为“第0层”视频底层投递给推流器 // index_ 0 表示这是最底层的视频画面 int index_ 0; push_api_.PostLayerImage(push_handle_, 0, index_, image, 0, NULL); }二、 渲染引擎GDI 实现动态时间水印静态图片水印很容易直接加载PNG即可但动态时间水印如“2026-01-21 14:00:01”需要实时生成图片。为此我们封装了一个NTWatermarkRenderer类源自nt_watermark_renderer.cpp利用 Windows GDI 绘制文字并转换为 ARGB 数据。1. GDI 初始化与字体缓存频繁创建字体对象极其消耗性能因此我们采用缓存策略void NTWatermarkRenderer::SetTextWatermarkFont(const LOGFONT lf, COLORREF color) { // 如果字体参数变了才重新创建 Gdiplus::Font否则复用 if (memcmp(cached_lf_, lf, sizeof(LOGFONT)) ! 0 || cached_color_ ! color || !has_cached_font_) { cached_lf_ lf; cached_color_ color; cached_font_.reset(CreateGdiplusFontFromLOGFONT(lf)); // 将LOGFONT转为Gdiplus::Font has_cached_font_ (cached_font_ ! nullptr); } }2. 核心渲染逻辑文字转ARGB Bitmap这个函数是动态水印的核心它动态计算文字宽绘制并返回内存块。std::shared_ptrnt_watermark_argb_image NTWatermarkRenderer::RenderTextWatermark( int width, int height, const std::wstring text) { if (!is_gdiplus_initialized_ || width 0 || height 0) return nullptr; // 如果未传入文本自动生成当前系统时间字符串 std::wstring w_text text.empty() ? MakeCurrentTimeString() : text; Gdiplus::Font* font has_cached_font_ ? cached_font_.get() : nullptr; // ... 防御性代码如果字体为空使用默认字体 ... // 1. 测量文字大小防止显示不全 Gdiplus::Bitmap measure_bmp(1, 1, PixelFormat32bppARGB); Gdiplus::Graphics g_measure(measure_bmp); Gdiplus::RectF boundingBox; g_measure.MeasureString(w_text.c_str(), -1, font, Gdiplus::PointF(0, 0), boundingBox); int final_w max(width, (int)boundingBox.Width 20); int final_h max(height, (int)boundingBox.Height 10); final_w (final_w 3) ~3; // 4字节对齐优化 // 2. 创建画布 Gdiplus::Bitmap bitmap(final_w, final_h, PixelFormat32bppARGB); Gdiplus::Graphics g(bitmap); // 3. 设置高质量渲染参数抗锯齿 g.Clear(Gdiplus::Color(0, 0, 0, 0)); // 背景完全透明 g.SetTextRenderingHint(Gdiplus::TextRenderingHintClearTypeGridFit); g.SetSmoothingMode(Gdiplus::SmoothingModeAntiAlias); // 4. 绘制文字 Gdiplus::SolidBrush brush(Gdiplus::Color(255, GetRValue(cached_color_), GetGValue(cached_color_), GetBValue(cached_color_))); Gdiplus::RectF layoutRect(5.0f, 0, (Gdiplus::REAL)final_w - 5.0f, (Gdiplus::REAL)final_h); // 垂直居中绘制 Gdiplus::StringFormat format; format.SetLineAlignment(Gdiplus::StringAlignmentCenter); g.DrawString(w_text.c_str(), -1, font, layoutRect, format, brush); // 5. 锁定位图数据拷贝出来返回 Gdiplus::BitmapData data; if (bitmap.LockBits(nullptr, Gdiplus::ImageLockModeRead, PixelFormat32bppARGB, data) Gdiplus::Ok) { auto res std::make_sharednt_watermark_argb_image(data.Width, data.Height); res-stride_ data.Stride; res-data_.reset(new NT_BYTE[data.Stride * data.Height]); memcpy(res-data_.get(), data.Scan0, data.Stride * data.Height); bitmap.UnlockBits(data); return res; } return nullptr; }三、 推流端多图层叠加与编码有了视频源和水印源接下来就是“组装”。SmartPublisher SDK 提供了强大的图层Layer概念。1. 配置图层结构在开始推流前我们需要定义图层顺序。通常顺序是外部视频(底层) - 图片水印(中层) - 文字水印(顶层)。代码位于CSmartPlayerDlg::SetLayersConfigbool CSmartPlayerDlg::SetLayersConfig() { auto push_handle GetPushHandle(); if (push_handle nullptr) return false; layer_conf_wrappers_.clear(); int index 0; AddExternalVideoFrameLayer(index); AddImageWatermarkLayer(index); AddTextWatermarkLayer(index); // 将配置应用到SDK std::vectorconst NT_PB_LayerBaseConfig* base_confs; for (const auto wrapper : layer_conf_wrappers_) { base_confs.push_back(wrapper-getBase()); } if (!base_confs.empty()) { // 核心调用一次性设置所有图层参数 auto ret push_api_.SetLayersConfig(push_handle, 0, base_confs.data(), base_confs.size(), 0, nullptr); return (NT_ERC_OK ret); } return true; }2. 定时刷新文字水印为了让时间动起来我们需要一个定时器Timer每隔几百毫秒生成一个新的时间Bitmap投递给推流端。在CSmartPlayerDlg::OnTimer中void CSmartPlayerDlg::OnTimer(UINT_PTR nIDEvent) { if (nIDEvent TEXT_WATERMARK_REFRESH_TIMER_ID) { // 只有在推流且启用水印时才刷新 if (IsPusherRunning() is_text_watermark_enabled_ text_watermark_layer_index_ 0) { UpdateTextWatermarkLayer(); } return; } // ... 其他Timer处理 }UpdateTextWatermarkLayer的实现void CSmartPlayerDlg::UpdateTextWatermarkLayer() { auto push_handle GetPushHandle(); if (!push_handle || text_watermark_layer_index_ 0 || !watermark_renderer_) return; // 1. 生成当前时间字符串 std::wstring time_str MakeWatermarkTimeStr(); // 2. 调用渲染器生成 ARGB Bitmap watermark_renderer_-SetTextWatermarkFont(text_watermark_lf_, text_watermark_color_); auto watermark watermark_renderer_-RenderTextWatermark( text_watermark_region_.width_, text_watermark_region_.height_, time_str); if (watermark) { // 3. 将新生成的水印图片投递到指定的 Layer Index NT_PB_Image image; memset(image, 0, sizeof(image)); image.format_ NT_PB_E_IMAGE_FORMAT_ARGB; // 注意格式是ARGB image.width_ watermark-width_; image.height_ watermark-height_; image.plane_[0] watermark-data_.get(); image.stride_[0] watermark-stride_; image.plane_size_[0] watermark-stride_ * watermark-height_; // index: 0 (reserved), layer_index: text_watermark_layer_index_ push_api_.PostLayerImage(push_handle, 0, text_watermark_layer_index_, image, 0, NULL); } }四、 开启推流与录像最后一步就是启动推流。这里我们不仅推送到RTMP服务器还利用SDK的并发能力同时录制到本地MP4。bool CSmartPlayerDlg::StartPush(const std::string url) { // ... 前置检查与Handle打开 ... // 1. 设置通用的编码参数H.264, 码率, 帧率等 if (publisher_handle_count_ 1) { SetCommonOptionToPublisherSDK(); } // 2. 设置RTMP URL if (NT_ERC_OK ! push_api_.SetURL(push_handle, url.c_str(), NULL)) return false; // 3. 启动推流 if (NT_ERC_OK ! push_api_.StartPublisher(push_handle, NULL)) return false; // ... 状态更新 ... return true; } // 独立的录像控制 void CSmartPlayerDlg::OnBnClickedButtonPushRec() { // ... // 设置录像文件名规则自动追加时间 NT_PB_RecorderFileNameRuler rec_name_ruler { 0 }; rec_name_ruler.file_name_prefix_ push_rec; rec_name_ruler.append_date_ 1; rec_name_ruler.append_time_ 1; push_api_.SetRecorderFileNameRuler(push_handle_, rec_name_ruler); // 启动本地录像不影响RTMP推流二者共享编码数据效率极高 push_api_.StartRecorder(push_handle_, NULL); }总结与开发建议通过上述步骤我们实现了一个功能完备的“视频流处理工作站”。技术要点总结数据源利用 SmartPlayer 的SetVideoFrameCallBack获取原始RGB/YUV数据这是二次处理的基础。动态水印GDI 是 Windows 下处理文字渲染的最佳伴侣但要注意LockBits的性能和内存对齐。使用 Timer 动态刷新 PostLayerImage 实现了时间戳跳动。图层管理SmartPublisher 的图层设计非常灵活将视频、图片、文字分层处理SDK内部会自动进行 Alpha 混合极大地简化了开发者的工作量。编码效率虽然我们用了 RGB 回调方便 GDI但在推流 SDK 内部它会高效地转换颜色空间并进行 H.264/H.265 编码且支持硬编码保证了低 CPU 占用。避坑指南线程安全播放器的回调是在 SDK 的内部线程而 UI 操作如点击按钮停止推流在主线程。务必像代码中那样使用std::recursive_mutex锁住 Handle防止在回调过程中 Handle 被释放引发 Crash。GDI 性能不要在每一帧视频回调里都去 RenderTextWatermark那样 CPU 会炸。通常 1 秒刷新 2-4 次文字水印就足够流畅了。内存泄露GDI 的 Bitmap 和 SDK 的回调数据都要注意生命周期管理代码中使用了std::shared_ptr和std::unique_ptr是很好的实践。希望这篇博文能帮助你在 Windows 音视频开发中少走弯路。如果有关于大牛直播 SDK 的具体配置问题欢迎留言交流 CSDN官方博客音视频牛哥-CSDN博客

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询