知名网站制作,买网站的域名,网络一站式服务平台,wordpress添加表单技术背景
随着移动单兵、智能车载、智慧安防、智能家居、工业仿真、GB28281技术对接等行业的发展#xff0c;现场已经不再限于采集到视频数据编码打包发送或对接到流媒体服务端#xff0c;大多场景对视频水印的要求越来越高#xff0c;从之前的固定位置静态文字水印、png水…技术背景
随着移动单兵、智能车载、智慧安防、智能家居、工业仿真、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封装层实现如果想更灵活的通过图层化设计实现动态水印本文提供的思路开发者可酌情参考。