? GB28181-2022 新增图像抓拍功能, 这个功能很有用, 无需在设备实时点播的情况下,就可以抓图上传到指定的图像存储服务器上。如果配置合适的抓拍间隔,JPEG也选择适当的压缩参数,相比实时音视频回传更省流量,设备功耗也低。
? GB28181图像抓拍分为三步,一是下发图像抓拍配置命令给设备,? 二是设备抓图并上传图像,三是设备通知服务器抓拍图像已完成传输。
? 图像抓拍配置走的是有应答设备控制流程,配置信息采用SIP MESSAGE方法携带,Content-type是:Application/MANSCDP+xml, 命令协议还是MANSCDP协议,采用XML封装。设备收到图像抓拍配置命令后,需要发送配置响应命令,响应命令中包含执行结果信息。
? 抓拍配置命令包含: 连拍张数(最多10张,最小1张), 单张抓拍间隔(单位秒,最短间隔是1秒), 抓拍图对象上传路径,会话ID。例如:
<SnapShotConfig>
<SnapNum>7</SnapNum>
<Interval>2</Interval>
<UploadURL>http://192.168.0.7:8070/snap</UploadURL>
<SessionID>97593257101100111123900141597596</SessionID>
</SnapShotConfig>
? 设备收到抓拍配置命令后,按抓拍间隔时间和连拍张数来执行抓图, 抓拍的图像格式推荐为JPEG有损压缩格式(多数安卓设备也支持JPEG硬编码), 抓拍图像分辨率使用主码流分辨率, 图像文件名规则:设备ID(长度20)+ "02" + 日期时间(YYYYMMDDhhmmssSSS)+ 抓拍序号(长度为2) + ".jpeg"。抓拍到单张图像后建议立即上传,推荐用https/http传输图像.
? 单次配置抓拍的所有图像上传完成后,需发送Message消息通知SIP服务器图像传输已完成, 通知消息中的SessionID应与抓拍配置命令中的SessionID一致。例如:
<SessionID>97593257101100111123900141597596</SessionID>
<SnapShotList>
<SnapShotFileID>34020000001380000037022023122814385675501.jpeg</SnapShotFileID>
<SnapShotFileID>34020000001380000037022023122814385868002.jpeg</SnapShotFileID>
<SnapShotFileID>34020000001380000037022023122814390073803.jpeg</SnapShotFileID>
<SnapShotFileID>34020000001380000037022023122814390262604.jpeg</SnapShotFileID>
<SnapShotFileID>34020000001380000037022023122814390471205.jpeg</SnapShotFileID>
<SnapShotFileID>34020000001380000037022023122814390686506.jpeg</SnapShotFileID>
<SnapShotFileID>34020000001380000037022023122814390869907.jpeg</SnapShotFileID>
</SnapShotList>
? 相关实现代码:
/*
* Copyright (C) 1130758427@qq.com. All rights reserved.
*/
/**
* 部分信令接口
*/
package com.gb.ntsignalling;
public interface GBSIPAgent {
void setDeviceConfigListener(GBSIPAgentDeviceConfigListener deviceConfigListener);
/*
* 通知图像抓拍传输完成
*/
boolean notifyUploadSnapShotFinished(String fromUserName, String fromUserNameAtDomain, String deviceID, String sessionID, java.util.List<String> snapShotList);
}
/*
* Device Config Listener
*/
package com.gb.ntsignalling;
public interface GBSIPAgentDeviceConfigListener {
/*
* 图像抓拍配置
*/
void ntsOnDeviceSnapShotConfig(String from_user_name, String from_user_name_at_domain,
String sn, String device_id, SnapShotConfig config,
List<String> extra_info_list);
}
package com.gb.ntsignalling;
public interface SnapShotConfig {
int snap_num();
int interval();
String upload_url();
String session_id();
}
/**
* 抓拍JNI接口
*/
public class SmartPublisherJniV2 {
/**
* 截图接口, 支持JPEG和PNG两种格式
* @param compress_format: 压缩格式, 0:JPEG格式, 1:PNG格式, 其他返回错误
* @param quality: 取值范围:[0, 100], 值越大图像质量越好, 仅对JPEG格式有效, 若是PNG格式,请填100
* @param file_name: 图像文件名, 例如:/dirxxx/test20231113100739.jpeg, /dirxxx/test20231113100739.png
* @param user_data_string: 用户自定义字符串
* @return {0} if successful
*/
public native int CaptureImage(long handle, int compress_format, int quality, String file_name, String user_data_string);
}
/**
* Device Snap Shot Listener 部分实现代码
*/
public class GBDeviceSnapShotListenerImpl implements GBSIPAgentDeviceControlListener {
@Override
public void ntsOnDeviceSnapShotConfig(String from_user_name, final String from_user_name_at_domain,
String sn, String device_id, final SnapShotConfig config,
List<String> extra_info_list) {
if (null == config)
return;
handler_.postDelayed(new Runnable() {
@Override
public void run() {
Log.i(TAG, "ntsOnDeviceSnapShotConfig device_id:" + device_id_ + " session_id:" + config.session_id()
+ ", snap_num:" + config.snap_num() + ", interval:" + config.interval() + ", upload_url:" + config.upload_url());
if (null == gb28181_agent_)
return;
if (null == snap_shot_impl_) {
snap_shot_impl_ = new SnapShotGBImpl(image_path_, context_, handler_, lib_publisher_jni, snap_shot_publisher_);
snap_shot_impl_.start();
}
snap_shot_impl_.add_config(gb28181_agent_, from_user_name_, from_user_name_at_domain_, sn_,
device_id_, snap_shot_config_, extra_info_list_);
}
private String from_user_name_;
private String from_user_name_at_domain_;
private String sn_;
private String device_id_;
private SnapShotConfig snap_shot_config_;
private List<String> extra_info_list_;
public Runnable set(String from_user_name, String from_user_name_at_domain,
String sn, String device_id, SnapShotConfig config,
List<String> extra_info_list) {
this.from_user_name_ = from_user_name;
this.from_user_name_at_domain_ = from_user_name_at_domain;
this.sn_ = sn;
this.device_id_ = device_id;
this.snap_shot_config_ = config;
this.extra_info_list_ = extra_info_list;
return this;
}
}.set(from_user_name, from_user_name_at_domain, sn, device_id, config, extra_info_list), 0);
}
}
public class SnapShotGBImpl extends SnapShotImpl {
private List<SnapConfig> config_list_ = new LinkedList<>();
public SnapShotGBImpl(String dir, Context context, android.os.Handler handler,
SmartPublisherJniV2 lib_sdk, LibPublisherWrapper publisher) {
super(dir, context, handler, lib_sdk, publisher);
}
public boolean add_config(GBSIPAgent agent, String from_user_name, String from_user_name_at_domain, String sn,
String device_id, SnapShotConfig config, List<String> extra_info_list) {
if (null == agent)
return false;
if (is_null_or_empty(device_id))
return false;
if (null == config)
return false;
if (config.snap_num() < 1)
return false;
if (config.interval() < 1)
return false;
if (is_null_or_empty(config.session_id()))
return false;
SnapConfig c = new SnapConfig(agent, from_user_name, from_user_name_at_domain, sn, device_id, config, extra_info_list);
config_list_.add(c);
return true;
}
public void on_captured_image(long result, String file_name, long file_date_time_ms, String user_data) {
SnapConfig config = find_config(user_data);
if (null == config) {
super.on_captured_image(result, file_name, file_date_time_ms, user_data);
return;
}
SnapItem item = config.find_capturing_item(file_name);
if (null == item) {
super.on_captured_image(result, file_name, file_date_time_ms, user_data);
return;
}
if (result != 0) {
item.set_status(SnapItem.ERROR_STATUS);
item.set_error_info("capture failed");
Log.e(TAG, "capture failed, file:" + file_name + ", session_id:" + user_data);
return;
}
item.set_status(SnapItem.CAPTURE_COMPLETION_STATUS);
}
public void on_uploaded(boolean is_ok, String file_name, String session_id, String gb_name) {
SnapConfig config = find_config(session_id);
if (null == config) {
Log.w(TAG, "on_uploaded cannot find config, session_id:" + session_id + ", gb_name:" + gb_name);
return;
}
SnapItem item = config.find_uploading_item(gb_name);
if (null == item) {
Log.w(TAG, "on_uploaded cannot find item, session_id:" + session_id + ", gb_name:" + gb_name);
return;
}
if (is_ok) {
item.set_status(SnapItem.UPLOAD_COMPLETION_STATUS);
Log.i(TAG, "on_uploaded ok, session_id:" + session_id + ", file:" + file_name);
}else {
item.set_status(SnapItem.ERROR_STATUS);
item.set_error_info("upload failed");
Log.e(TAG, "on_uploaded failed, session_id:" + session_id + ", file:" + file_name);
}
}
@Override
public void on_stop() {
this.config_list_.clear();
shutdown(200, TimeUnit.MILLISECONDS);
}
private void process_upload() {
android.os.Handler app_handler = os_handler();
for(SnapConfig c : config_list_)
c.upload_files(app_handler, this);
}
private void process_finished() {
List<String> notified_files = null;
Iterator<SnapConfig> iterator = config_list_.iterator();
while(iterator.hasNext()) {
SnapConfig c = iterator.next();
if (!c.is_can_notify_server())
continue;
iterator.remove();
if (null == notified_files)
notified_files = new LinkedList<>();
c.notify_server(notified_files);
}
// 暂时删除这些文件, 根据业务需求随时调整就好
if(notified_files != null && !notified_files.isEmpty())
execute(new DeleteFilesTask(notified_files));
}
private static class SnapItem {
private int status_ = INITIALIZATION_STATUS;
private final String device_id_;
private final int sn_; // 序列码, 40~41
private final String dir_;
private String file_name_;
}
private static class SnapConfig {
private WeakReference<GBSIPAgent> agent_;
private final String from_user_name_;
private final String from_user_name_at_domain_;
private final String sn_;
private final String device_id_;
private final String session_id_;
private final int snap_num_;
private final String upload_url_;
private final int interval_sec_;
private final List<String> extra_info_list_;
private ArrayList<SnapItem> items_;
public final String session_id() {
return this.session_id_;
}
public void upload_files(android.os.Handler os_handler, SnapShotGBImpl snap) {
if (null == items_)
return;
for (SnapItem i : items_) {
if (i.is_capture_completion_status()) {
i.set_status(SnapItem.UPLOADING_STATUS);
BaseUploadTask upload_task = new MyUploadTask(upload_url_, i.file_name(), i.gb_snap_shot_file_id(),
session_id(), i.gb_name(), os_handler, snap);
if (!snap.submit(upload_task) ) {
i.set_status(SnapItem.ERROR_STATUS);
i.set_error_info("submit upload task failed");
}
}
}
}
public void notify_server(List<String> notified_files) {
ArrayList<String> snap_shot_list = new ArrayList(items_.size());
for (SnapItem i : items_) {
if (SnapItem.UPLOAD_COMPLETION_STATUS == i.status())
snap_shot_list.add(i.gb_snap_shot_file_id());
if (notified_files != null)
notified_files.add(i.file_name());
}
if (null == agent_)
return;
GBSIPAgent agent = agent_.get();
if (null == agent)
return;
agent.notifyUploadSnapShotFinished(from_user_name_, from_user_name_at_domain_, device_id_, this.session_id(), snap_shot_list);
}
}
private static class DeleteFilesTask implements Runnable {
private List<String> file_name_list_;
public DeleteFilesTask(List<String> file_name_list) {
this.file_name_list_ = file_name_list;
}
@Override
public void run() {
if (null == file_name_list_)
return;
if (file_name_list_.isEmpty()) {
file_name_list_ = null;
return;
}
for (String i : file_name_list_) {
try {
File f = new File(i);
if (!f.exists()||!f.isFile() )
continue;
if (f.delete())
Log.i(TAG, "delete file:" + i);
else
Log.w(TAG, "delete file failed, " + i);
}
catch(Exception e) {
Log.e(TAG, "DeleteFilesTask.run Exception:", e);
}
}
file_name_list_.clear();
file_name_list_ = null;
}
}
public static class BaseUploadTask extends CancellableTask {
private final String upload_url_;
private final String file_name_;
private final String gb_snap_shot_file_id_;
private final String session_id_;
private final String gb_name_;
private WeakReference<android.os.Handler> os_handler_;
private WeakReference<SnapShotGBImpl> snap_;
public BaseUploadTask(String upload_url, String file_name, String gb_snap_shot_file_id,
String session_id, String gb_name, android.os.Handler os_handler, SnapShotGBImpl snap) {
this.upload_url_ = upload_url;
this.file_name_ = file_name;
this.gb_snap_shot_file_id_ = gb_snap_shot_file_id;
this.session_id_ = session_id;
this.gb_name_ = gb_name;
if (os_handler !=null)
this.os_handler_ = new WeakReference<>(os_handler);
if (snap != null)
this.snap_ = new WeakReference<>(snap);
}
protected final String upload_url() {
return this.upload_url_;
}
protected final String file_name() {
return this.file_name_;
}
protected final String gb_snap_shot_file_id() {
return this.gb_snap_shot_file_id_;
}
protected final String session_id() {
return this.session_id_;
}
protected final String gb_name() { return this.gb_name_; }
protected final android.os.Handler os_handler() {
if (os_handler_ != null)
return os_handler_.get();
return null;
}
protected final SnapShotGBImpl snap() {
if (snap_ != null)
return snap_.get();
return null;
}
private static class ResultRunnable implements Runnable {
private final boolean result_;
private final String file_name_;
private final String session_id_;
private final String gb_name_;
private WeakReference<SnapShotGBImpl> snap_;
public ResultRunnable(boolean result, String file_name, String session_id,
String gb_name, SnapShotGBImpl snap) {
this.result_ = result;
this.file_name_ = file_name;
this.session_id_ = session_id;
this.gb_name_ = gb_name;
if (snap != null)
this.snap_ = new WeakReference<>(snap);
}
@Override
public void run(){
if (null == this.snap_)
return;
SnapShotGBImpl snap = this.snap_.get();
if (null == snap)
return;
snap.on_uploaded(result_, file_name_, session_id_, gb_name_);
}
}
protected void post_result(boolean is_ok) {
android.os.Handler handler = os_handler();
if (null == handler)
return;
SnapShotGBImpl gb_snap = snap();
if (null == gb_snap)
return;
handler.post(new ResultRunnable(is_ok, file_name_,session_id_, gb_name_, gb_snap));
}
}
}
? 考虑到GB28181服务端实现非常多, 每家的上传图像方式可能有所不同,有些用https上传,有些用ftp上传,上传图像这块代码只能对接具体服务器时再做调整(我的代码只要增加一个新的上传实现类就好), 整个图像抓拍功能有些用Java实现有些用C++完成, 整体代码较多,上面只给出部分接口定义和实现代码, 如有不清楚的地方请联系qq: 1130758427,?微信:ldxevt。