Windows平台如何实现RTSP流二次编码并添加动态水印后推送RTMP或轻量级RTSP服务

news2024/9/19 10:41:53

技术背景

我们在对接RTSP播放器相关的技术诉求的时候,遇到这样的需求,客户做特种设备巡检的,需要把摄像头拍到的RTSP流拉下来,然后添加动态水印后,再生成新的RTSP URL,供平台调用。真个流程需要延迟尽可能的低,分辨率要支持到1080p,并需要把添加过动态水印的数据,保存到本地。

技术实现

在此之前,大牛直播SDK有非常成熟的RTSP播放、轻量级RTSP服务和录像模块,要做的就是,拉取到RTSP流后,把解码后的YUV或RGB回调给上层,上层通过图层的形式,添加动态文字水印(图片水印亦可),然后,投递给轻量级RTSP服务,RTSP服务对外提供个拉流的RTSP URL,无图无真相:

左侧就是我们基于Windows平台C#的播放器的demo,二次开发的,添加了软、硬编码设置(考虑到分辨率比较高,添加支持了硬编码选项设置)、动态水印设置、轻量级RTSP服务、实时录像和RTMP推送。

先说数据回调,本文以回调yuv数据为例:

video_frame_call_back_ = new SP_SDKVideoFrameCallBack(SetVideoFrameCallBack);
NTSmartPlayerSDK.NT_SP_SetVideoFrameCallBack(player_handle_, (Int32)NT.NTSmartPlayerDefine.NT_SP_E_VIDEO_FRAME_FORMAT.NT_SP_E_VIDEO_FRAME_FORMAT_RGB32, IntPtr.Zero, video_frame_call_back_);

回调后的数据,投递到轻量级RTSP服务模块。

public void SetVideoFrameCallBack(IntPtr handle, IntPtr userData, UInt32 status, IntPtr frame)
        {
            if (frame == IntPtr.Zero)
            {
                return;
            }

            NT_SP_VideoFrame video_frame = (NT_SP_VideoFrame)Marshal.PtrToStructure(frame, typeof(NT_SP_VideoFrame));

            if (publisher_wrapper_ != null)
            {
                if (publisher_wrapper_.IsPublisherHandleAvailable())
                {
                    if (publisher_wrapper_.IsPublishing() || publisher_wrapper_.IsRecording() || publisher_wrapper_.IsRTSPPublisherRunning())
                    {
                        //publisher_wrapper_.OnPostRGB32Data(0, video_frame.plane0_, video_frame.width_ * 4 * video_frame.height_, video_frame.stride0_,
                        //            video_frame.width_, video_frame.height_);

                        publisher_wrapper_.OnPostYUVData(0, video_frame.plane0_, video_frame.stride0_, video_frame.plane1_, video_frame.stride1_,
                                    video_frame.plane2_, video_frame.stride2_,
                                    video_frame.width_, video_frame.height_);
                    }
                }
            }
        }

音频由于暂时不要二次处理,直接投递过去,如果需要处理的话,处理后再投递给publisher wrapper:

public void SetAudioPCMFrameCallBack(IntPtr handle, IntPtr user_data,
             UInt32 status, IntPtr data, UInt32 size,
             Int32 sample_rate, Int32 channel, Int32 per_channel_sample_number)
        {
            if (data == IntPtr.Zero || size == 0)
            {
                return;
            }

            if (publisher_wrapper_ != null)
            {
                if (publisher_wrapper_.IsPublisherHandleAvailable())
                {
                    if (publisher_wrapper_.IsPublishing() || publisher_wrapper_.IsRecording() || publisher_wrapper_.IsRTSPPublisherRunning())
                    {
                        publisher_wrapper_.OnPostAudioPCMData(data, size, 0, sample_rate, channel, per_channel_sample_number);
                    }
                }
            }

        }

设置文字水印字体:

        private void btn_set_font_Click(object sender, EventArgs e)
        {
            FontDialog font_dlg = new FontDialog();
            DialogResult result = font_dlg.ShowDialog();

            if (result == DialogResult.OK)
            {
                // 获取用户所选字体
                Font selectedFont = font_dlg.Font;
                btn_set_font.Text = "" + selectedFont.Name + ", " + selectedFont.Size + "pt";

                selected_osd_font_ = new Font(selectedFont.Name, selectedFont.Size, FontStyle.Regular, GraphicsUnit.Point);
            }
        }

动态设置文字水印:

        private async void btn_text_osd_Click(object sender, EventArgs e)
        {
            string format = "yyyy-MM-dd HH:mm:ss.fff";

            StringBuilder sb = new StringBuilder();
            sb.Append("施工单位:上海视沃信息科技有限公司(daniusdk.com)");
            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 bmp = GenerateBitmap(str);

            int index = 1;
            int x = 0;
            int y = 200;
            UpdateLayerRegion(index, x, y, bmp);

            await Task.Delay(30);
            UpdateARGBBitmap(index, bmp);
        }

如果需要硬编码:

            if (btn_check_video_hardware_encoder_.Checked)
            {
                is_hw_encoder = true;
            }

            Int32 cur_sel_encoder_id = 0;
            Int32 cur_sel_gpu = 0;

            if (is_hw_encoder)
            {
                int cur_sel_hw = combobox_video_encoders_.SelectedIndex;
                if (cur_sel_hw >= 0)
                {
                    cur_sel_encoder_id = Convert.ToInt32(combobox_video_encoders_.SelectedValue);
                    cur_sel_gpu = -1;

                    int cur_sel_hw_dev = combobox_video_hardware_encoder_devices_.SelectedIndex;
                    if (cur_sel_hw_dev >= 0)
                    {
                        cur_sel_gpu = Convert.ToInt32(combobox_video_hardware_encoder_devices_.SelectedValue);
                    }
                }
                else
                {
                    is_hw_encoder = false;
                }
            }

            if (!is_hw_encoder)
            {
                if ((int)NTCommonMediaDefine.NT_MEDIA_CODEC_ID.NT_MEDIA_CODEC_ID_H264 == cur_video_codec_id)
                {
                    cur_sel_encoder_id = btn_check_openh264_encoder_.Checked ? 1 : 0;
                }
            }

            publisher_wrapper_.SetVideoEncoder((int)(is_hw_encoder ? 1 : 0), (int)cur_sel_encoder_id, (uint)cur_video_codec_id, (int)cur_sel_gpu);

            publisher_wrapper_.SetVideoQualityV2(publisher_wrapper_.CalVideoQuality(width_, height_, is_h264_encoder));

            publisher_wrapper_.SetVideoBitRate(publisher_wrapper_.CalBitRate(video_fps_, width_, height_));

            publisher_wrapper_.SetVideoMaxBitRate(publisher_wrapper_.CalMaxKBitRate(video_fps_, width_, height_, false));

            publisher_wrapper_.SetVideoKeyFrameInterval(key_frame_interval_);

            if (is_h264_encoder)
            {
                publisher_wrapper_.SetVideoEncoderProfile(1);
            }

            publisher_wrapper_.SetVideoEncoderSpeed(publisher_wrapper_.CalVideoEncoderSpeed(width_, height_, is_h264_encoder));

启动停止RTSP服务:

        private void btn_rtsp_service_Click(object sender, EventArgs e)
        {
            if(publisher_wrapper_.IsRTSPSerivceRunning())
            {
                publisher_wrapper_.StopRtspService();
                btn_rtsp_service.Text = "启动RTSP服务";
                btn_rtsp_stream.Enabled = false;
            }
            else
            {
                if(publisher_wrapper_.StartRtspService())
                {
                    btn_rtsp_service.Text = "停止RTSP服务";
                    btn_rtsp_stream.Enabled = true;
                }
            }
        }

发布RTSP流:

        private void btn_rtsp_stream_Click(object sender, EventArgs e)
        {
            if (publisher_wrapper_.IsRTSPPublisherRunning())
            {
                publisher_wrapper_.StopRtspStream();
                btn_rtsp_stream.Text = "发布RTSP流";
                btn_get_rtsp_session_numbers.Enabled = false;
                btn_rtsp_service.Enabled = true;
            }
            else
            {
                if (!publisher_wrapper_.IsPublisherHandleAvailable())
                {
                    if (!OpenPublisherHandle())
                    {
                        return;
                    }
                }

                if (publisher_wrapper_.GetPublisherHandleCount() < 1)
                {
                    SetCommonOptionToPublisherSDK();
                }

                if (!publisher_wrapper_.StartRtspStream())
                {
                    MessageBox.Show("调用StartRtspStream失败..");
                    return;
                }

                btn_rtsp_stream.Text = "停止RTSP流";
                btn_get_rtsp_session_numbers.Enabled = true;
                btn_rtsp_service.Enabled = false;
            }
        }

获取RTSP会话数:

        private void btn_get_rtsp_session_numbers_Click(object sender, EventArgs e)
        {
            if (publisher_wrapper_.IsRTSPPublisherRunning())
            {
               int session_numbers = publisher_wrapper_.GetRtspSessionNumbers();

               MessageBox.Show(session_numbers.ToString(), "当前RTSP连接会话数");
            }
        }

本地录像:

        private void btn_start_recorder_Click(object sender, EventArgs e)
        {
            if (!publisher_wrapper_.IsPublisherHandleAvailable())
            {
                if (!OpenPublisherHandle())
                {
                    return;
                }
            }

            if (publisher_wrapper_.GetPublisherHandleCount() < 1)
            {
                SetCommonOptionToPublisherSDK();
            }

            if (!publisher_wrapper_.StartRecorder())
            {
                MessageBox.Show("调用StartRecorder失败..");
                return;
            }

            btn_start_recorder.Enabled = false;
            btn_stop_recorder.Enabled = true;
        }

        private void btn_stop_recorder_Click(object sender, EventArgs e)
        {
            if (!publisher_wrapper_.IsPublisherHandleAvailable())
                return;

            if (publisher_wrapper_.IsRecording())
            {
                publisher_wrapper_.StopRecorder();

                btn_start_recorder.Enabled = true;
                btn_stop_recorder.Enabled = false;
            }
        }

暂停录像、恢复录像:

        private void btn_pause_recorder_Click(object sender, EventArgs e)
        {
            if (!publisher_wrapper_.IsPublisherHandleAvailable())
            {
                return;
            }

            String btn_pause_rec_text = btn_pause_recorder.Text;

            if ("暂停录像" == btn_pause_rec_text)
            {
                UInt32 ret = publisher_wrapper_.PauseRecorder(true);

                if ((UInt32)NT.NTSmartPublisherDefine.NT_PB_E_ERROR_CODE.NT_ERC_PB_NEED_RETRY == ret)
                {
                    MessageBox.Show("暂停录像失败, 请重新尝试!");
                    return;
                }
                else if (NTBaseCodeDefine.NT_ERC_OK == ret)
                {
                    btn_pause_recorder.Text = "恢复录像";
                }
            }
            else
            {
                UInt32 ret = publisher_wrapper_.PauseRecorder(false);
                if ((UInt32)NT.NTSmartPublisherDefine.NT_PB_E_ERROR_CODE.NT_ERC_PB_NEED_RETRY == ret)
                {
                    MessageBox.Show("恢复录像失败, 请重新尝试!");
                    return;
                }
                else if (NTBaseCodeDefine.NT_ERC_OK == ret)
                {
                    btn_pause_recorder.Text = "暂停录像";
                }
            }
        }

推送RTMP:

        private void btn_publish_rtmp_Click(object sender, EventArgs e)
        {
            if (!publisher_wrapper_.IsPublisherHandleAvailable())
            {
                if (!OpenPublisherHandle())
                {
                    return;
                }
            }

            if (publisher_wrapper_.GetPublisherHandleCount() < 1)
            {
                SetCommonOptionToPublisherSDK();
            }

            String url = "rtmp://192.168.0.108:1935/hls/stream1";

            if (url.Length < 8)
            {
                publisher_wrapper_.Close();

                MessageBox.Show("请输入推送地址");
                return;
            }

            if (!publisher_wrapper_.StartPublisher(url))
            {
                MessageBox.Show("调用StartPublisher失败..");
                return;
            }

            btn_publish_rtmp.Enabled = false;
            btn_stop_publish_rtmp.Enabled = true;
        }

        private void btn_stop_publish_rtmp_Click(object sender, EventArgs e)
        {
            if (!publisher_wrapper_.IsPublisherHandleAvailable())
                return;

            if (publisher_wrapper_.IsPublishing())
            {
                publisher_wrapper_.StopPublisher();

                btn_publish_rtmp.Enabled = true;
                btn_stop_publish_rtmp.Enabled = false;
            }
        }

图层设计,目前设计两个图层,一个是原始YUV底层,另外一个是文字水印图层,如果需要动态去除文字水印,只要index为1的图层,enable设置为0即可。

            NTSmartPublisherSDK.NT_PB_ClearLayersConfig(publisher_handle_, 0,
                            0, IntPtr.Zero);

            if (video_option_ == (uint)NTSmartPublisherDefine.NT_PB_E_VIDEO_OPTION.NT_PB_E_VIDEO_OPTION_LAYER)
            {
                NT_PB_ExternalVideoFrameLayerConfig external_layer_c0 = new NT_PB_ExternalVideoFrameLayerConfig();

                external_layer_c0.base_.type_ = (Int32)NTSmartPublisherDefine.NT_PB_E_LAYER_TYPE.NT_PB_E_LAYER_TYPE_EXTERNAL_VIDEO_FRAME;
                external_layer_c0.base_.index_ = 0;
                external_layer_c0.base_.enable_ = 1;
                external_layer_c0.base_.region_.x_ = 0;
                external_layer_c0.base_.region_.y_ = 0;
                external_layer_c0.base_.region_.width_ = video_width_;
                external_layer_c0.base_.region_.height_ = video_height_;

                external_layer_c0.base_.offset_ = Marshal.OffsetOf(external_layer_c0.GetType(), "base_").ToInt32();
                external_layer_c0.base_.cb_size_ = (uint)Marshal.SizeOf(external_layer_c0);

                IntPtr external_layer_conf0 = Marshal.AllocHGlobal(Marshal.SizeOf(external_layer_c0));

                Marshal.StructureToPtr(external_layer_c0, external_layer_conf0, true);

                UInt32 external_r0 = NTSmartPublisherSDK.NT_PB_AddLayerConfig(publisher_handle_, 0,
                                external_layer_conf0, (int)NTSmartPublisherDefine.NT_PB_E_LAYER_TYPE.NT_PB_E_LAYER_TYPE_EXTERNAL_VIDEO_FRAME,
                                0, IntPtr.Zero);

                Marshal.FreeHGlobal(external_layer_conf0);

                //OSD水印层
                NT_PB_ExternalVideoFrameLayerConfig external_layer_c1 = new NT_PB_ExternalVideoFrameLayerConfig();

                external_layer_c1.base_.type_ = (Int32)NTSmartPublisherDefine.NT_PB_E_LAYER_TYPE.NT_PB_E_LAYER_TYPE_EXTERNAL_VIDEO_FRAME;
                external_layer_c1.base_.index_ = 1;
                external_layer_c1.base_.enable_ = 1;
                external_layer_c1.base_.region_.x_ = 0;
                external_layer_c1.base_.region_.y_ = 200;
                external_layer_c1.base_.region_.width_ = 200;
                external_layer_c1.base_.region_.height_ = 200;

                external_layer_c1.base_.offset_ = Marshal.OffsetOf(external_layer_c1.GetType(), "base_").ToInt32();
                external_layer_c1.base_.cb_size_ = (uint)Marshal.SizeOf(external_layer_c1);

                IntPtr external_layer_conf = Marshal.AllocHGlobal(Marshal.SizeOf(external_layer_c1));

                Marshal.StructureToPtr(external_layer_c1, external_layer_conf, true);

                UInt32 external_r1 = NTSmartPublisherSDK.NT_PB_AddLayerConfig(publisher_handle_, 0,
                                external_layer_conf, (int)NTSmartPublisherDefine.NT_PB_E_LAYER_TYPE.NT_PB_E_LAYER_TYPE_EXTERNAL_VIDEO_FRAME,
                                0, IntPtr.Zero);

                Marshal.FreeHGlobal(external_layer_conf);
                //end
            }

总结

RTSP拉流二次编码,整体逻辑不复杂,就是把数据回调后,二次处理,我们推送端设计的是图层的形式,所以,回调后的数据,直接作为第0层,文字水印作为第一层,如果需要图片水印,图片水印作为第三层即可。RTSP拉流二次编码,如果做到客户端尽量无感知,需要尽可能的压缩整体处理的延迟,确保从数据采集,到二次处理,到再次播放出来毫秒级,满足绝大多数场景下的技术需求。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1238753.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

LL(1)语法分析程序设计与实现

制作一个简单的C语言词法分析程序_用c语言编写词法分析程序-CSDN博客文章浏览阅读322次。C语言的程序中&#xff0c;有很单词多符号和保留字。一些单词符号还有对应的左线性文法。所以我们需要先做出一个单词字符表&#xff0c;给出对应的识别码&#xff0c;然后跟据对应的表格…

FlinkCDC实现主数据与各业务系统数据的一致性(瀚高、TIDB)

文章末尾附有flinkcdc对应瀚高数据库flink-cdc-connector代码下载地址 1、业务需求 目前项目有主数据系统和N个业务系统,为保障“一数一源”,各业务系统表涉及到主数据系统的字段都需用主数据系统表中的字段进行实时覆盖,这里以某个业务系统的一张表举例说明:业务系统表Ta…

社群乱象,社群玩法正解

社群乱象&#xff0c;社群玩法正解 越来越不喜欢混社群了&#xff0c;从原有的好几百社群&#xff0c;精简到剩两三个。就这两三个&#xff0c;也貌似奄奄一息&#xff0c;命不久矣的感觉。 现在的社群&#xff0c;妥妥的已经被各路妖魔鬼怪给玩坏了。童话觉得这里面还有不少…

vue3 iconify 图标几种使用 并加载本地 svg 图标

iconify iconify 与 iconify/vue 使用 下载 pnpm add iconify/vue -D使用 import { Icon } from "iconify/vue";<template><Icon icon"mdi-light:home" style"color: red; font-size: 43px" /><Icon icon"mdi:home-flo…

Spring Boot 邮件发送(五种类型的邮件)

邮件协议&#xff1a; SMTP、POP3、IMAP SMTP 协议全称为 Simple Mail Transfer Protocol&#xff0c;译作简单邮件传输协议&#xff0c;它定义了邮件客户端软件与 SMTP 服务器之间&#xff0c;以及 SMTP 服务器与 SMTP 服务器之间的通信规则。 用户先将邮件投递到腾讯的 SMT…

.nvmrc 文件使用详解

文章目录 1. 前言2. .nvmrc 是什么3. 创建 .nvmrc 文件4. 使用 .nvmrc 文件5. 终端自动切换版本 1. 前言 当开发多个项目时&#xff0c;每个项目运行环境要求的 node 版本不一样&#xff0c;那么我们就需要给每个项目指定 node 版本&#xff0c;也就是通过终端执行 nvm install…

虚拟摇杆OnJoystickMove未被调用,角色不移动

更改interaction type 为 event notification

Windows权限维持方法论

Windows权限维持方法论 1.注册表自启动2.组策略设置脚本启动3.计划任务4.服务自启动5.dll劫持6.直接上远程控制木马 1.注册表自启动 通过修改注册表自启动键值&#xff0c;添加一个木马程序路径&#xff0c;实现开机自启动 常用的注册表启动键&#xff1a; # Run键 HKEY_CU…

数据结构-栈的实现

1.栈的概念及结构 栈&#xff1a;一种特殊的线性表&#xff0c;其只允许在固定的一端进行插入和删除元素操作。进行数据插入和删除操作的一端称为栈顶&#xff0c;另一端称为栈底。栈中的数据元素遵守后进先出LIFO&#xff08;Last In First Out&#xff09;的原则。 压栈&…

2023年中国语言大模型行业发展趋势分析:预计未来行业将迎来高速增长[图]

自然语言处理&#xff08;NLP&#xff09;大模型是一种利用深度学习技术来理解、解释和生成人类语言的高参数模型。语言大模型通过编码解码的方式模仿人类处理语言的过程从而达到进行自然语言文本输出的能力。 语言大模型主要组成部分 资料来源&#xff1a;共研产业咨询&#…

【grep】从html表格中快速定位某个数据

文章目录 1 背景2 参考知识2.1 grep2.2 HTML基础语言标签 3 解决方案 1 背景 在html中是一堆表格、图片、文字什么的&#xff0c;想从表格中提取关键词为“GJC”后对应的数字&#xff0c;怎么办呢&#xff1f; 逐个打开html文件&#xff0c;“ctrlF”搜一下&#xff0c;然后复…

数字化文旅系统,让景区营销变得更加简单!

随着互联网的普及和信息技术的不断发展&#xff0c;越来越多的消费者开始通过互联网来获取旅游信息、预订旅游产品和服务。因此&#xff0c;文旅行业需要紧跟时代步伐&#xff0c;借助数字化技术来提高服务质量和效率&#xff0c;满足消费者对于便捷、个性化的需求。 1. 强大功…

每日OJ题_算法_双指针_力扣202. 快乐数

力扣202. 快乐数 202. 快乐数 - 力扣&#xff08;LeetCode&#xff09; 难度 简单 编写一个算法来判断一个数 n 是不是快乐数。 「快乐数」 定义为&#xff1a; 对于一个正整数&#xff0c;每一次将该数替换为它每个位置上的数字的平方和。然后重复这个过程直到这个数变为…

基于MS16F3211芯片的触摸控制灯的状态变化和亮度控制总结版(11.22)

1.任务需求 基于MS16F3211芯片实现功能一个按键通过长按可以控制当前处于亮状态的灯的亮度&#xff0c;当灯从最亮达到最暗时&#xff0c;所用时为3s。现有三盏颜色分别为红绿蓝的灯&#xff0c;在处于关机状态时红灯亮&#xff0c;处于开机状态时红灯灭。点按第一次仅绿灯亮&…

气膜体育馆:低碳环保体育新潮流

在追求健康生活的今天&#xff0c;体育运动的重要性无法忽视。为了满足人民日益增长的体育需求&#xff0c;气膜体育馆应运而生&#xff0c;成为体育场馆领域的一次革命性创新。这种新型体育馆解决了传统体育场馆建设中面临的审批难、周期长、门槛高等问题&#xff0c;为我们的…

网络安全之渗透测试入门准备

渗透测试入门所需知识 操作系统基础&#xff1a;Windows&#xff0c;Linux 网络基础&#xff1a;基础协议与简单原理 编程语言&#xff1a;PHP&#xff0c;python web安全基础 渗透测试入门 渗透测试学习&#xff1a; 1.工具环境准备&#xff1a;①VMware安装及使用&#xff1b…

在Jupyter Lab中使用多个环境,及魔法命令简介

一、Jupyter Lab使用conda虚拟环境 1、给虚拟环境添加 ipykernel 方法一: 创建环境时直接添加ipykernel 方法&#xff1a;conda create -n 【虚拟环境名称】python3.8 ipykernel实例如下&#xff1a; conda create -n tensorflow_cpu python3.8 ipykernel 方法二&#xff…

7-34 通讯录的录入与显示

方法1 import java.util.Scanner;class PTA34 {public static void main (String [] args) {Scanner sc new Scanner(System.in);String s sc.nextLine();int nInteger.parseInt(s);addressbook[] aanew addressbook[n];for (int i 0; i < n; i) {String addressline sc…

动态规划:2304. 网格中的最小路径代价

2304. 网格中的最小路径代价 给你一个下标从 0 开始的整数矩阵 grid &#xff0c;矩阵大小为 m x n &#xff0c;由从 0 到 m * n - 1 的不同整数组成。你可以在此矩阵中&#xff0c;从一个单元格移动到 下一行 的任何其他单元格。如果你位于单元格 (x, y) &#xff0c;且满足…

metersphere 创建场景, 自动动态变换参数值,实现接口自动化测试。

创建场景 创建产品变量 添加数值 添加后点击确定 点击右下角 号 点击 循环控制器 写循环 创建/导入接口 选择运行环境&#xff0c; 没有的话新建一个 需要点击引用环境 保存后点击 调试 成功做到每次请求的参数是列表里的 10