Android平台音视频RTMP推送|GB28181对接之动态水印设计

技术背景

随着移动单兵、智能车载、智慧安防、智能家居、工业仿真、GB28281技术对接等行业的发展,现场已经不再限于采集到视频数据编码打包发送或对接到流媒体服务端,大多场景对视频水印的要求越来越高,从之前的固定位置静态文字水印、png水印等慢慢过渡到动态水印需求。

本文以Android平台采集摄像头数据为例,通过类似于PhotoShop图层的形式,添加不同图层,编码实现动态水印的效果。

废话不多说,先上个效果图,Android采集端获取到摄像头数据后,分别展示了实时时间水印、文字水印、png水印、文字水印二,所有水印均支持动态设置,可满足传统行业如实时时间戳叠加、动态经纬度设定、png logo等场景的水印设定需求。

技术实现

摄像头数据采集,不再赘述,获取到前后摄像头的数据数据后(具体参见onPreviewFrame()处理),通过PostLayerImageNV21ByteArray()把数据投递到jni层。

int w = videoWidth, h = videoHeight;int y_stride = videoWidth, uv_stride = videoWidth;int y_offset = 0, uv_offset = videoWidth * videoHeight;int is_vertical_flip = 0, is_horizontal_flip = 0;int rotation_degree = 0;// 镜像只用在前置摄像头场景下if (is_mirror && FRONT == currentCameraType) {// 竖屏, (垂直翻转->顺时旋转270度)等价于(顺时旋转旋转270度->水平翻转)if (PORTRAIT == currentOrigentation)is_vertical_flip = 1;elseis_horizontal_flip = 1;}if (PORTRAIT == currentOrigentation) {if (BACK == currentCameraType)rotation_degree = 90;elserotation_degree = 270;} else if (LANDSCAPE_LEFT_HOME_KEY == currentOrigentation) {rotation_degree = 180;}int scale_w = 0, scale_h = 0, scale_filter_mode = 0;// 缩放测试++/*if (w >= 1280 && h >= 720) {scale_w = align((int)(w * 0.8 + 0.5), 2);scale_h = align((int)(h * 0.8 + 0.5), 2);} else {scale_w = align((int)(w * 1.5 + 0.5), 2);scale_h = align((int)(h * 1.5 + 0.5), 2);}if(scale_w >0 && scale_h >0) {scale_filter_mode = 3;Log.i(TAG, "onPreviewFrame w:" + w + ", h:" + h + " s_w:" + scale_w + ", s_h:" + scale_h);}*/// 缩放测试---libPublisher.PostLayerImageNV21ByteArray(publisherHandle, 0, 0, 0,data, y_offset, y_stride, data, uv_offset, uv_stride, w, h,is_vertical_flip, is_horizontal_flip, scale_w, scale_h, scale_filter_mode, rotation_degree);

大家可能好奇PostLayerImageNV21ByteBuffer()和PostLayerImageNV21ByteArray()设计,接口参数很强大,和我们之前针对camera2的接口一样,几乎是万能接口,拿到的原始数据,不仅可以做水平、垂直翻转,还可以缩放处理。

/*** 投递层NV21图像** @param index: 层索引, 必须大于等于0** @param left: 层叠加的左上角坐标, 对于第0层的话传0** @param top: 层叠加的左上角坐标, 对于第0层的话传0** @param y_plane: y平面图像数据** @param y_offset: 图像偏移, 这个主要目的是用来做clip的,一般传0** @param y_row_stride: stride information** @param uv_plane: uv平面图像数据** @param uv_offset: 图像偏移, 这个主要目的是用来做clip的,一般传0** @param uv_row_stride: stride information** @param width: width, 必须大于1, 且必须是偶数** @param height: height, 必须大于1, 且必须是偶数** @param  is_vertical_flip: 是否垂直翻转, 0不翻转, 1翻转** @param  is_horizontal_flip:是否水平翻转, 0不翻转, 1翻转** @param  scale_width: 缩放宽,必须是偶数, 0或负数不缩放** @param  scale_height: 缩放高, 必须是偶数, 0或负数不缩放** @param  scale_filter_mode: 缩放质量, 传0使用默认速度,可选等级范围是:[1,3],值越大缩放质量越好, 但速度越慢** @param  rotation_degree: 顺时针旋转, 必须是0, 90, 180, 270, 注意:旋转是在缩放, 垂直/水品反转之后再做, 请留意顺序** @return {0} if successful*/public native int PostLayerImageNV21ByteBuffer(long handle, int index, int left, int top,ByteBuffer y_plane, int y_offset, int y_row_stride,ByteBuffer uv_plane, int uv_offset, int uv_row_stride,int width, int height, int is_vertical_flip,  int is_horizontal_flip,int scale_width,  int scale_height, int scale_filter_mode,int rotation_degree);/*** 投递层NV21图像, 详细说明请参考PostLayerImageNV21ByteBuffer** @return {0} if successful*/public native int PostLayerImageNV21ByteArray(long handle, int index, int left, int top,byte[] y_plane, int y_offset, int y_row_stride,byte[] uv_plane, int uv_offset, int uv_row_stride,int width, int height, int is_vertical_flip,  int is_horizontal_flip,int scale_width,  int scale_height, int scale_filter_mode,int rotation_degree);

动态时间水印

动态时间水印其实就是文字水印的扩展,通过生成TextBitmap,然后从bitmap里面拷贝获取到text_timestamp_buffer_,通过我们设计的PostLayerImageRGBA8888ByteBuffer()投递到jni层。

private int postTimestampLayer(int index, int left, int top) {Bitmap text_bitmap = makeTextBitmap(makeTimestampString(), getFontSize(),Color.argb(255, 0, 0, 0), true, Color.argb(255, 255, 255, 255),true);if (null == text_bitmap)return 0;if ( text_timestamp_buffer_ != null) {text_timestamp_buffer_.rewind();if ( text_timestamp_buffer_.remaining() < text_bitmap.getByteCount())text_timestamp_buffer_ = null;}if (null == text_timestamp_buffer_ )text_timestamp_buffer_ = ByteBuffer.allocateDirect(text_bitmap.getByteCount());text_bitmap.copyPixelsToBuffer(text_timestamp_buffer_);int scale_w = 0, scale_h = 0, scale_filter_mode = 0;//scale_w = align((int)(bitmapWidth*1.5 + 0.5), 2);//scale_h = align((int)(bitmapHeight*1.5 + 0.5),2);//scale_filter_mode = 3;/*if ( scale_w > 0 && scale_h > 0)Log.i(TAG, "postTextLayer scale_w:" + scale_w + ", scale_h:" + scale_h + " w:" + bitmapWidth + ", h:" + bitmapHeight) ;*/libPublisher.PostLayerImageRGBA8888ByteBuffer(handle_, index, left, top, text_timestamp_buffer_, 0,text_bitmap.getRowBytes(), text_bitmap.getWidth(), text_bitmap.getHeight(),0, 0, scale_w, scale_h, scale_filter_mode,0);int ret = scale_h > 0? scale_h : text_bitmap.getHeight();text_bitmap.recycle();return ret;
}

文字水印

文字水印不再赘述,主要注意的是文字的大小、颜色、位置。

private int postText1Layer(int index, int left, int top) {Bitmap text_bitmap = makeTextBitmap("文本水印一", getFontSize()+8,Color.argb(255, 200, 250, 0),false, 0,false);if (null == text_bitmap)return 0;ByteBuffer buffer = ByteBuffer.allocateDirect(text_bitmap.getByteCount());text_bitmap.copyPixelsToBuffer(buffer);libPublisher.PostLayerImageRGBA8888ByteBuffer(handle_, index, left, top, buffer, 0,text_bitmap.getRowBytes(), text_bitmap.getWidth(), text_bitmap.getHeight(),0, 0, 0, 0, 0,0);int ret = text_bitmap.getHeight();text_bitmap.recycle();return ret;
}

png水印

png水印,除了常规的位置需要注意之外,还涉及到logo水印的大小问题,为此,我们添加了缩放效果,可以缩放后,再贴到图层,确保以更合适的比例展示在图层期望位置。

private int postPictureLayer(int index, int left, int top) {Bitmap bitmap = getAssetsBitmap();if (null == bitmap) {Log.e(TAG, "postPitcureLayer getAssetsBitmap is null");return 0;}if (bitmap.getConfig() != Bitmap.Config.ARGB_8888) {Log.e(TAG, "postPitcureLayer config is not ARGB_8888, config:" + Bitmap.Config.ARGB_8888);return 0;}ByteBuffer buffer = ByteBuffer.allocateDirect(bitmap.getByteCount());bitmap.copyPixelsToBuffer(buffer);final int w = bitmap.getWidth();final int h = bitmap.getHeight();if ( w < 2 || h < 2 )return 0;int scale_w = 0, scale_h = 0, scale_filter_mode = 0;final float r_w = width_ - left; // 有可能负数final float r_h = height_ - top; // 有可能负数if (w > r_w || h > r_h) {float s_w = w;float s_h = h;// 0.85的10次方是0.19687, 缩放到0.2倍差不多了for ( int i = 0; i < 10; ++i)  {s_w *= 0.85f;s_h *= 0.85f;if (s_w < r_w && s_h < r_h )break;}if (s_w > r_w || s_h > r_h)return 0;// 如果小于16就算了,太小看也看不见if (s_w < 16.0f || s_h < 16.0f)return  0;scale_w = align((int)(s_w + 0.5f), 2);scale_h = align( (int)(s_h + 0.5f), 2);scale_filter_mode = 3;}/*if ( scale_w > 0 && scale_h > 0)Log.i(TAG, "postTextLayer scale_w:" + scale_w + ", scale_h:" + scale_h + " w:" + w + ", h:" + h) ; */libPublisher.PostLayerImageRGBA8888ByteBuffer(handle_, index, left, top, buffer, 0, bitmap.getRowBytes(), w, h,0, 0, scale_w, scale_h, scale_filter_mode,0);int ret = scale_h > 0 ? scale_h : bitmap.getHeight();bitmap.recycle();return ret;
}

以上几种水印,最终投递接口设计如下,接口不再赘述,几乎你期望的针对图像的处理,都已覆盖:

/*** 投递层RGBA8888图像,如果不需要Aplpha通道的话, 请使用RGBX8888接口, 效率高** @param index: 层索引, 必须大于等于0, 注意:如果index是0的话,将忽略Alpha通道** @param left: 层叠加的左上角坐标, 对于第0层的话传0** @param top: 层叠加的左上角坐标, 对于第0层的话传0** @param rgba_plane: rgba 图像数据** @param offset: 图像偏移, 这个主要目的是用来做clip的, 一般传0** @param row_stride: stride information** @param width: width, 必须大于1, 如果是奇数, 将减1** @param height: height, 必须大于1, 如果是奇数, 将减1** @param  is_vertical_flip: 是否垂直翻转, 0不翻转, 1翻转** @param  is_horizontal_flip:是否水平翻转, 0不翻转, 1翻转** @param  scale_width: 缩放宽,必须是偶数, 0或负数不缩放** @param  scale_height: 缩放高, 必须是偶数, 0或负数不缩放** @param  scale_filter_mode: 缩放质量, 传0使用默认速度,可选等级范围是:[1,3],值越大缩放质量越好, 但速度越慢** @param  rotation_degree: 顺时针旋转, 必须是0, 90, 180, 270, 注意:旋转是在缩放, 垂直/水品反转之后再做, 请留意顺序** @return {0} if successful*/public native int PostLayerImageRGBA8888ByteBuffer(long handle, int index, int left, int top,ByteBuffer rgba_plane, int offset, int row_stride, int width, int height,int is_vertical_flip,  int is_horizontal_flip,int scale_width,  int scale_height, int scale_filter_mode,int rotation_degree);

以上水印的显示控制,我们通过LayerPostThread封装处理:

/** LayerPostThread实现动态水印封装* Author: https://daniusdk.com*/
class LayerPostThread extends Thread
{private final int update_interval = 400; // 400 毫秒private volatile boolean is_exit_ = false;private long handle_ = 0;private int width_  = 0;private int height_ = 0;private volatile boolean is_text_ = false;private volatile boolean is_picture_ = false;private volatile boolean clear_flag_ = false;private final int timestamp_index_ = 1;private final int text1_index_ = 2;private final int text2_index_ = 3;private final int picture_index_ = 4;private final int rectangle_index_ = 5;ByteBuffer text_timestamp_buffer_ = null;ByteBuffer rectangle_buffer_ = null;@Overridepublic void run() {text_timestamp_buffer_ = null;rectangle_buffer_ = null;if (0 == handle_)return;boolean is_posted_pitcure = false;boolean is_posted_text1 = false;boolean is_posted_text2 = false;int rectangle_aplha = 0;while(!is_exit_) {long t = SystemClock.elapsedRealtime();if (clear_flag_) {clear_flag_ = false;is_posted_pitcure = false;is_posted_text1 = false;is_posted_text2 = false;if (!is_text_ || !is_picture_) {rectangle_aplha = 0;libPublisher.RemoveLayer(handle_, rectangle_index_);}}int cur_h = 8;int ret = 0;if (!is_exit_ && is_text_) {ret = postTimestampLayer(timestamp_index_, 0, cur_h);if ( ret > 0 )cur_h = align(cur_h + ret + 2, 2);}if(!is_exit_&& is_text_&&!is_posted_text1) {cur_h += 6;ret = postText1Layer(text1_index_, 0, cur_h);if ( ret > 0 ) {is_posted_text1 = true;cur_h = align(cur_h + ret + 2, 2);}}if (!is_exit_ && is_picture_ && !is_posted_pitcure) {ret = postPictureLayer(picture_index_, 0, cur_h);if ( ret > 0 ) {is_posted_pitcure = true;cur_h = align(cur_h + ret + 2, 2);}}if(!is_exit_&& is_text_&&!is_posted_text2) {postText2Layer(text2_index_);is_posted_text2 = true;}// 这个是演示一个矩形, 不需要可以屏蔽掉if (!is_exit_ && is_text_ && is_picture_) {postRGBRectangle(rectangle_index_, rectangle_aplha);rectangle_aplha += 8;if (rectangle_aplha > 255)rectangle_aplha = 0;}waitSleep((int)(SystemClock.elapsedRealtime() - t));}text_timestamp_buffer_ = null;rectangle_buffer_ = null;}

我们把水印分两类:一类系文字、一类png logo水印,可以通过控制显示还是隐藏:

public void enableText(boolean is_text) {is_text_ = is_text;clear_flag_ = true;if (handle_ != 0) {libPublisher.EnableLayer(handle_, timestamp_index_, is_text_?1:0);libPublisher.EnableLayer(handle_, text1_index_, is_text_?1:0);libPublisher.EnableLayer(handle_, text2_index_, is_text_?1:0);}}public void enablePicture(boolean is_picture) {is_picture_ = is_picture;clear_flag_ = true;if (handle_ != 0) {libPublisher.EnableLayer(handle_, picture_index_, is_picture_?1:0);}}

如需移除图层,也可以调用RemoveLayer()接口,具体设计如下:

/*** 启用或者停用视频层, 这个接口必须在StartXXX之后调用.** @param index: 层索引, 必须大于0, 注意第0层不能停用** @param  is_enable: 是否启用, 0停用, 1启用** @return {0} if successful*/public native int EnableLayer(long handle, int index, int is_enable);/*** 移除视频层, 这个接口必须在StartXXX之后调用.** @param index: 层索引, 必须大于0, 注意第0层不能移除** @return {0} if successful*/public native int RemoveLayer(long handle, int index);

针对启动水印类型等外层封装:

private LayerPostThread layer_post_thread_ = null;private void startLayerPostThread() {if (3 == video_opt_) {if (null == layer_post_thread_) {layer_post_thread_ = new LayerPostThread();layer_post_thread_.startPost(publisherHandle, videoWidth, videoHeight, currentOrigentation, isHasTextWatermark(), isHasPictureWatermark());}}}private void stopLayerPostThread() {if (layer_post_thread_ != null) {layer_post_thread_.stopPost();layer_post_thread_ = null;}}

总结

随着传统行业对视频数据实时水印要求越来越高,动态水印设计是大势所趋,水印设计有多种实现模式,比如早期我们针对静态水印的处理,直接通过jni封装层实现,如果想更灵活的通过图层化设计实现动态水印,本文提供的思路,开发者可酌情参考。

 

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

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

相关文章

探究C++11智能指针之std::unique_ptr

背景 谈起C&#xff0c;它被公认为最难学的编程语言之一&#xff0c;不仅语法知识点广泛&#xff0c;细节内容之多&#xff0c;学习难度和学习周期也长&#xff0c;导致好多新入行的开发者对C“敬而远之”&#xff0c;甚至“从入门到放弃”。自C11开始&#xff0c;好多C程序员…

C++17新特性之try_emplace与insert_or_assign

由于std::map中&#xff0c;元素的key是唯一的&#xff0c;我们经常遇到这样的场景&#xff0c;向map中插入元素时&#xff0c;先检测map指定的key是否存在&#xff0c;不存在时才做插入操作&#xff0c;如果存在&#xff0c;直接取出来使用&#xff0c;或者key不存在时&#x…

Unity环境下实现Camera高帧率RTMP推送

Unity下RTMP直播背景方面不再赘述&#xff0c;今天主要讨论的是&#xff0c;Unity环境下&#xff0c;如何实现Camera高帧率RTMP推送&#xff0c;这里提到的高帧率&#xff0c;不再局限于常规环境下的30帧&#xff0c;以VR头显为例&#xff0c;更高的帧率&#xff08;比如50帧&a…

如何在Android平台GB28181接入终端实现语音广播和语音对讲

技术背景 在之前的blog&#xff0c;我们以Android平台国标接入终端为例&#xff0c;分别介绍了一些常规的功能&#xff0c;比如REGISTER、CATALOG、INVITE、Keepalive、SUBSCRIBE、NOTIFY等常规操作&#xff0c;今天主要介绍下语音广播和语音对讲这部分。 GB28181平台广播和对…

Android GB28181设备接入端语音广播和语音对讲技术实现探究

上篇文章提到Android端GB28181接入端的语音广播和语音对讲的实现&#xff0c;从spec角度大概介绍了下流程和简单的接口设计&#xff0c;好多开发者私信我&#xff0c;希望展开说一下。其实这块难度不大&#xff0c;只是广播和对讲涉及到双向实现&#xff0c;如果之前没有相关的…

Android native层实现MediaCodec编码H264/HEVC

Android平台在上层实现mediacodec的编码&#xff0c;资料泛滥&#xff0c;已经不再是难事&#xff0c;今天给大家介绍下&#xff0c;如何在Android native层实现MediaCodec编码H264/HEVC&#xff0c;网上千篇一律的接口说明&#xff0c;这里不再赘述&#xff0c;本文主要介绍下…

GB/T 28181联网系统通信协议结构和技术实现

技术回顾 在本文开头&#xff0c;我们先一起回顾下GB/T28181联网系统通信协议结构&#xff1a; 联网系统在进行视音频传输及控制时应建立两个传输通道&#xff1a;会话通道和媒体流通道。 会话通道用于在设备之间建立会话并传输系统控制命令&#xff1b;媒体流通道用于传输视…

Android平台GB28181设备接入端对接编码前后音视频源类型浅析

前言 今天主要对Android平台GB28181设备接入模块支持的接入数据类型&#xff0c;做个简单的汇总&#xff1a; 编码前数据&#xff08;目前支持的有YV12/NV21/NV12/I420/RGB24/RGBA32/RGB565等数据类型&#xff09;&#xff0c;其中&#xff0c;Android平台前后摄像头数据&…

C++学习之-析构函数必须为虚函数吗?

今天讨论个比较有意思的话题&#xff1a;析构函数是不是必须要为虚函数&#xff1f; 先说答案&#xff1a; 析构函数可以是虚函数&#xff0c;也可以不是虚函数。 再说原因&#xff1a; 析构函数为虚函数的情况&#xff1a;继承 当父类指针释放子类对象时&#xff0c;如果…

如何实现RTMP或RTSP播放端回调YUV/RGB数据?

今天某乎收到个问题推荐&#xff0c;如何实现RTSP回调YUV数据&#xff0c;用于二次处理&#xff1f; 正好前些年我们做RTSP和RTMP直播播放的时候&#xff0c;实现过相关的需求&#xff0c;本文就以Android为例&#xff0c;大概说说具体实现吧。 先说回调yuv或rgb这块意义吧&a…

GB28181控制、传输流程和协议接口之注册|注销和技术实现

注册和注销基本要求 SIP客户端、网关、SIP设备、联网系统等 SIP代理(SIP UA)使用IETFRFC3261中定义的方法 15 GB/T28181—2016Register进行注册和注销。 注册和注销时应进行认证&#xff0c;认证方式应支持数字摘要认证方式&#xff0c;高安全级别的宜支持数字证书的认证方式&…

GB28181状态信息报送解读及Android端国标设备接入技术实现

今天主要聊聊GB/T28181状态信息报送这块&#xff0c;先回顾下协议规范相关细节&#xff0c;然后再针对代码实现&#xff0c;做个简单的说明。 状态消息报送基本要求 当源设备(包括网关、SIP设备、SIP客户端或联网系统)发现工作异常时,应立即向本 SIP监控域 的SIP服务器发送状…

GB28181设备接入端如何实现校时?

在探讨这个问题之前&#xff0c;我们先看看GB/T28181-2016官方文档怎么说的&#xff0c;9.10.1章节校时基本要求提到&#xff1a; 联网内设备支持基于SIP方式或 NTP方式的网络校时功能&#xff0c;标准时间为北京时间。 SIP方式校时见本节具体描述&#xff1b;NTP(见IETFRFC2…

如何在Unity下采集音视频实现轻量级RTSP服务(类似于IPC)

好多开发者在做虚拟仿真、VR教育等场景的时候&#xff0c;遇到个问题&#xff0c;想把头显里面的画面在内网环境下低延迟的同步出来&#xff0c;又不想单独部署流媒体服务器。为此&#xff0c;我们在Unity下&#xff0c;添加了轻量级RTSP服务模块&#xff0c;通过头显端启动个轻…

【Datawhale 大模型基础】第十一章 环境影响

第十一章 环境影响 This blog is based on datawhale files and a paper. The initial consideration revolves around the potential positive or negative direct impact on the environment. Other transformative technological advancements, like the metaverse, are li…

Android GB28181接入端实时位置订阅和上报之-如何获取当前经纬度

我们在做Android平台GB28181的时候&#xff0c;其中实时位置(MobilePosition)订阅和上报这块&#xff0c;涉及到实时经纬度的获取&#xff0c;特别是执法记录、车载系统的那个等场景&#xff0c;几乎就是标配。 今天主要是分享一段实时获取位置的代码&#xff1a; /** Camera…

如何实现Android平台GB28181设备对接Camera2数据

技术背景 在写如何实现Android平台GB28181设备对接Camera2数据说明之前&#xff0c;我在前两年的blog就有针对camera2的RTMP直播推送模块做过技术分享&#xff1a; 在Google 推出Android 5.0的时候, Android Camera API 版本升级到了API2(android.hardware.camera2), 之前使用…

Android平台GB28181设备接入端本地SIP端口被占用或屏蔽怎么办?

好多开发者或厂商&#xff0c;对Android平台GB28181接入模块的定位&#xff0c;大多是IPC国标流程打通模拟&#xff0c;基于这个目的&#xff0c;很难按照标准SPEC规范实现Android平台GB28181设备接入&#xff0c;我们在跟第三方国标平台厂商对接时发现&#xff0c;部分公司&am…

Android平台GB28181设备接入端如何实现本地录像?

实现Android平台GB28181设备接入的时候&#xff0c;有个功能点不可避免&#xff0c;那就是本地录像&#xff0c;实际上&#xff0c;在实现GB28181设备接入模块之前&#xff0c;我们前些年做RTMP推送和轻量级RTSP服务的时候&#xff0c;早已经实现了本地录像功能。 本地录像功能…

国网B接口注册(REGISTER)接口描述和消息示例

技术背景 电网视频监控系统是智能电网的一个重要组成部分&#xff0c;广泛应用于电网的建设、生产、运行、经营等方面。由于视频监控系统在不同的建设时期选用了不同的技术和不同厂家的产品&#xff0c;导致了标准不统一、技术路线不一致。目前国家电网公司智能电网建设&#…