技术背景
GB/T28181-2022相对2016版,对图像抓拍有了明确的界定,图像抓拍在视频监控行业非常重要, Android平台GB28181设备接入端,无需实时上传音视频实时数据的情况下,就可以抓图上传到指定的图像存储服务器上。
图像抓拍基本要求如下:
- 源设备向目标设备发送图像抓拍配置命令,需要携带传输路径、会话ID等信息。
- 目标设备完成图像传输后,发送图像抓拍传输完成通知命令,采用IETF RFC3428中的MESSAGE方法实现。
- 图像文件命名规则宜采用“设备编码(20位)、图像编码(2位)、时间编码(17位)、序列码(2位)”的形式
- 图像格式宜使用JPEG,图像分辨率宜采用与主码流相同的分辨率。
需要注意的是,MESSAGE消息头Content-type头域为Content-type:Application/MANSCDP+xml,采用XML封装。设备收到图像抓拍配置命令后,发送配置响应命令,响应命令中包含执行结果信息。
图像抓拍流程如下:
技术实现
大牛直播SDK的SmartGBD已经完成GB28181设备接入侧的图像抓拍。
总体功能设计如下:
- [视频格式]H.264/H.265(Android H.265硬编码);
- [音频格式]G.711 A律、AAC;
- [音量调节]Android平台采集端支持实时音量调节;
- [H.264硬编码]支持H.264特定机型硬编码;
- [H.265硬编码]支持H.265特定机型硬编码;
- [软硬编码参数配置]支持gop间隔、帧率、bit-rate设置;
- [软编码参数配置]支持软编码profile、软编码速度、可变码率设置;
- 支持横屏、竖屏推流;
- Android平台支持后台service推送屏幕(推送屏幕需要5.0+版本);
- 支持纯视频、音视频PS打包传输;
- 支持RTP OVER UDP和RTP OVER TCP被动模式(TCP媒体流传输客户端);
- 支持信令通道网络传输协议TCP/UDP设置;
- 支持注册、注销,支持注册刷新及注册有效期设置;
- 支持设备目录查询应答;
- 支持心跳机制,支持心跳间隔、心跳检测次数设置;
- 支持移动设备位置(MobilePosition)订阅和通知;
- 适用国家标准:GB/T 28181—2016;
- 支持语音广播;
- 支持语音对讲;
- 支持图像抓拍;
- 支持历史视音频文件检索;
- 支持历史视音频文件下载;
- 支持历史视音频文件回放;
- 支持云台控制和预置位查询;
- [实时水印]支持动态文字水印、png水印;
- [镜像]Android平台支持前置摄像头实时镜像功能;
- [实时静音]支持实时静音/取消静音;
- [实时快照]支持实时快照;
- [降噪]支持环境音、手机干扰等引起的噪音降噪处理、自动增益、VAD检测;
- [外部编码前视频数据对接]支持YUV数据对接;
- [外部编码前音频数据对接]支持PCM对接;
- [外部编码后视频数据对接]支持外部H.264数据对接;
- [外部编码后音频数据对接]外部AAC数据对接;
- [扩展录像功能]支持和录像SDK组合使用,录像相关功能。
图像抓拍相关信令处理如下:
/** Author: daniusdk.com*/packagecom.gb.ntsignalling; publicinterfaceGBSIPAgent { voidsetDeviceConfigListener(GBSIPAgentDeviceConfigListenerdeviceConfigListener); /** 通知图像抓拍传输完成*/booleannotifyUploadSnapShotFinished(StringfromUserName, StringfromUserNameAtDomain, StringdeviceID, StringsessionID, java.util.List<String>snapShotList); }
Device配置Listener如下:
packagecom.gb.ntsignalling; publicinterfaceGBSIPAgentDeviceConfigListener { /** 图像抓拍配置*/voidntsOnDeviceSnapShotConfig(Stringfrom_user_name, Stringfrom_user_name_at_domain, Stringsn, Stringdevice_id, SnapShotConfigconfig, List<String>extra_info_list); }
Snapshot配置接口如下:
publicinterfaceSnapShotConfig { intsnap_num(); intinterval(); Stringupload_url(); Stringsession_id(); }
图像抓拍JNI接口设计如下:
publicclassSmartPublisherJniV2 { /*** 截图接口, 支持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*/publicnativeintCaptureImage(longhandle, intcompress_format, intquality, Stringfile_name, Stringuser_data_string); }
Device Snap Shot Listener 核心代码如下:
/** Author: daniusdk.com*/publicclassGBDeviceSnapShotListenerImplimplementsGBSIPAgentDeviceControlListener { publicvoidntsOnDeviceSnapShotConfig(Stringfrom_user_name, finalStringfrom_user_name_at_domain, Stringsn, Stringdevice_id, finalSnapShotConfigconfig, List<String>extra_info_list) { if (null==config) return; handler_.postDelayed(newRunnable() { publicvoidrun() { 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_=newSnapShotGBImpl(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_); } privateStringfrom_user_name_; privateStringfrom_user_name_at_domain_; privateStringsn_; privateStringdevice_id_; privateSnapShotConfigsnap_shot_config_; privateList<String>extra_info_list_; publicRunnableset(Stringfrom_user_name, Stringfrom_user_name_at_domain, Stringsn, Stringdevice_id, SnapShotConfigconfig, 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; returnthis; } }.set(from_user_name, from_user_name_at_domain, sn, device_id, config, extra_info_list), 0); } } publicclassSnapShotGBImplextendsSnapShotImpl { privateList<SnapConfig>config_list_=newLinkedList<>(); publicSnapShotGBImpl(Stringdir, Contextcontext, android.os.Handlerhandler, SmartPublisherJniV2lib_sdk, LibPublisherWrapperpublisher) { super(dir, context, handler, lib_sdk, publisher); } publicbooleanadd_config(GBSIPAgentagent, Stringfrom_user_name, Stringfrom_user_name_at_domain, Stringsn, Stringdevice_id, SnapShotConfigconfig, List<String>extra_info_list) { if (null==agent) returnfalse; if (is_null_or_empty(device_id)) returnfalse; if (null==config) returnfalse; if (config.snap_num() <1) returnfalse; if (config.interval() <1) returnfalse; if (is_null_or_empty(config.session_id())) returnfalse; SnapConfigc=newSnapConfig(agent, from_user_name, from_user_name_at_domain, sn, device_id, config, extra_info_list); config_list_.add(c); returntrue; } publicvoidon_captured_image(longresult, Stringfile_name, longfile_date_time_ms, Stringuser_data) { SnapConfigconfig=find_config(user_data); if (null==config) { super.on_captured_image(result, file_name, file_date_time_ms, user_data); return; } SnapItemitem=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); } publicvoidon_uploaded(booleanis_ok, Stringfile_name, Stringsession_id, Stringgb_name) { SnapConfigconfig=find_config(session_id); if (null==config) { Log.w(TAG, "on_uploaded cannot find config, session_id:"+session_id+", gb_name:"+gb_name); return; } SnapItemitem=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); } } publicvoidon_stop() { this.config_list_.clear(); shutdown(200, TimeUnit.MILLISECONDS); } privatevoidprocess_upload() { android.os.Handlerapp_handler=os_handler(); for(SnapConfigc : config_list_) c.upload_files(app_handler, this); } privatevoidprocess_finished() { List<String>notified_files=null; Iterator<SnapConfig>iterator=config_list_.iterator(); while(iterator.hasNext()) { SnapConfigc=iterator.next(); if (!c.is_can_notify_server()) continue; iterator.remove(); if (null==notified_files) notified_files=newLinkedList<>(); c.notify_server(notified_files); } // 暂时删除这些文件, 根据业务需求随时调整就好if(notified_files!=null&&!notified_files.isEmpty()) execute(newDeleteFilesTask(notified_files)); } privatestaticclassSnapItem { privateintstatus_=INITIALIZATION_STATUS; privatefinalStringdevice_id_; privatefinalintsn_; // 序列码, 40~41privatefinalStringdir_; privateStringfile_name_; } privatestaticclassSnapConfig { privateWeakReference<GBSIPAgent>agent_; privatefinalStringfrom_user_name_; privatefinalStringfrom_user_name_at_domain_; privatefinalStringsn_; privatefinalStringdevice_id_; privatefinalStringsession_id_; privatefinalintsnap_num_; privatefinalStringupload_url_; privatefinalintinterval_sec_; privatefinalList<String>extra_info_list_; privateArrayList<SnapItem>items_; publicfinalStringsession_id() { returnthis.session_id_; } publicvoidupload_files(android.os.Handleros_handler, SnapShotGBImplsnap) { if (null==items_) return; for (SnapItemi : items_) { if (i.is_capture_completion_status()) { i.set_status(SnapItem.UPLOADING_STATUS); BaseUploadTaskupload_task=newMyUploadTask(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"); } } } } publicvoidnotify_server(List<String>notified_files) { ArrayList<String>snap_shot_list=newArrayList(items_.size()); for (SnapItemi : 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; GBSIPAgentagent=agent_.get(); if (null==agent) return; agent.notifyUploadSnapShotFinished(from_user_name_, from_user_name_at_domain_, device_id_, this.session_id(), snap_shot_list); } } privatestaticclassDeleteFilesTaskimplementsRunnable { privateList<String>file_name_list_; publicDeleteFilesTask(List<String>file_name_list) { this.file_name_list_=file_name_list; } publicvoidrun() { if (null==file_name_list_) return; if (file_name_list_.isEmpty()) { file_name_list_=null; return; } for (Stringi : file_name_list_) { try { Filef=newFile(i); if (!f.exists()||!f.isFile() ) continue; if (f.delete()) Log.i(TAG, "delete file:"+i); elseLog.w(TAG, "delete file failed, "+i); } catch(Exceptione) { Log.e(TAG, "DeleteFilesTask.run Exception:", e); } } file_name_list_.clear(); file_name_list_=null; } } publicstaticclassBaseUploadTaskextendsCancellableTask { privatefinalStringupload_url_; privatefinalStringfile_name_; privatefinalStringgb_snap_shot_file_id_; privatefinalStringsession_id_; privatefinalStringgb_name_; privateWeakReference<android.os.Handler>os_handler_; privateWeakReference<SnapShotGBImpl>snap_; publicBaseUploadTask(Stringupload_url, Stringfile_name, Stringgb_snap_shot_file_id, Stringsession_id, Stringgb_name, android.os.Handleros_handler, SnapShotGBImplsnap) { 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_=newWeakReference<>(os_handler); if (snap!=null) this.snap_=newWeakReference<>(snap); } protectedfinalStringupload_url() { returnthis.upload_url_; } protectedfinalStringfile_name() { returnthis.file_name_; } protectedfinalStringgb_snap_shot_file_id() { returnthis.gb_snap_shot_file_id_; } protectedfinalStringsession_id() { returnthis.session_id_; } protectedfinalStringgb_name() { returnthis.gb_name_; } protectedfinalandroid.os.Handleros_handler() { if (os_handler_!=null) returnos_handler_.get(); returnnull; } protectedfinalSnapShotGBImplsnap() { if (snap_!=null) returnsnap_.get(); returnnull; } privatestaticclassResultRunnableimplementsRunnable { privatefinalbooleanresult_; privatefinalStringfile_name_; privatefinalStringsession_id_; privatefinalStringgb_name_; privateWeakReference<SnapShotGBImpl>snap_; publicResultRunnable(booleanresult, Stringfile_name, Stringsession_id, Stringgb_name, SnapShotGBImplsnap) { this.result_=result; this.file_name_=file_name; this.session_id_=session_id; this.gb_name_=gb_name; if (snap!=null) this.snap_=newWeakReference<>(snap); } publicvoidrun(){ if (null==this.snap_) return; SnapShotGBImplsnap=this.snap_.get(); if (null==snap) return; snap.on_uploaded(result_, file_name_, session_id_, gb_name_); } } protectedvoidpost_result(booleanis_ok) { android.os.Handlerhandler=os_handler(); if (null==handler) return; SnapShotGBImplgb_snap=snap(); if (null==gb_snap) return; handler.post(newResultRunnable(is_ok, file_name_,session_id_, gb_name_, gb_snap)); } } }
总结
以上是GB28181图像抓拍大概的流程和设计参考,权当抛砖引玉,Android终端除支持常规的音视频数据接入外,还可以支持移动设备位置(MobilePosition)订阅和通知、图像抓拍、语音广播和语音对讲、历史视音频下载和回放。感兴趣的开发者,可以单独加我微*信 xinsheng120 跟我探讨。