我们在Android平台实现GB28181设备接入,把摄像头和麦克风数据,采集过去,用于移动单兵、智能车载、智慧安防、智能家居、工业仿真等行业时,发现大多场景对视频水印的要求越来越高,从之前的固定位置静态文字水印、png水印等慢慢过渡到动态水印需求。
本文,我们要探讨的是,除了常规的时间、经纬度信息获png水印外,如何叠加电量和设备信号状态到视频view。
在Android平台上获取电量信息可以使用以下三种方式:
可以使用Context.getSystemService()方法获取BatteryManager实例,并使用该实例的getIntExtra()方法获取电量信息。具体代码如下:
BatteryManager batteryManager = (BatteryManager) getSystemService(Context.BATTERY_SERVICE);
int batteryLevel = batteryManager.getIntExtra(BatteryManager.EXTRA_LEVEL, -1);
int batteryScale = batteryManager.getIntExtra(BatteryManager.EXTRA_SCALE, -1);
int batteryPlugType = batteryManager.getIntExtra(BatteryManager.EXTRA_PLUGED, -1);
boolean isCharging = batteryManager.getIntExtra(BatteryManager.EXTRA_CHARGING, 0) == 1;
可以使用Context.getSystemService()方法获取PowerManager实例,并使用该实例的isDeviceIdle()方法和isPowerSaveMode()方法获取电量信息。具体代码如下:
PowerManager powerManager = (PowerManager) getSystemService(Context.POWER_SERVICE);
boolean isDeviceIdle = powerManager.isDeviceIdle();
boolean isPowerSaveMode = powerManager.isPowerSaveMode();
可以使用Context.getSystemService()方法获取UsageStatsManager实例,并使用该实例的queryStats()方法获取电量信息。具体代码如下:
UsageStatsManager usageStatsManager = (UsageStatsManager) getSystemService(Context.USAGE_STATS_SERVICE);
long time = System.currentTimeMillis();
UsageStats stats = usageStatsManager.queryStats(UsageStatsManager.INTERVAL_DAILY, time);
long totalTime = stats.getTotalTime();
long screenTime = stats.getScreenTime();
float batteryLevel = (totalTime * 100) / screenTime;
要获取Android设备的信号强度,可以使用TelephonyManager类中的getSignalStrength()方法。以下是一个示例代码片段,演示如何获取设备的信号强度:
TelephonyManager telephonyManager = (TelephonyManager) getSystemService(Context.TELEPHONY_SERVICE);
int signalStrength = telephonyManager.getSignalStrength();
此代码将返回设备的信号强度,单位为dBm(分贝毫瓦)。如果设备没有电话卡,则返回值为int最小值(0)。
请注意,要使用TelephonyManager类,您需要在AndroidManifest.xml文件中添加以下权限:
此权限允许您的应用程序访问设备的电话状态和信息。
叠加设备电量和设备实时信号状态,实际上,调用的是我们动态文字水印,通过生成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;
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;
}
最终投递接口设计如下,接口不再赘述,几乎你期望的针对图像的处理,都已覆盖:
/**
* 投递层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;
@Override
public 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;
}
实时文字水印可以通过控制显示还是隐藏:
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);
}
}
如需移除图层,也可以调用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;
}
}
Android平台获取设备电量信息和设备实时信号状态,然后叠加到视频view,投递出去,比如执法记录仪之类场景下,非常有价值,可以让GB28181平台侧,获取到更多需要的信息,扩展性极强。