GB/T28181-2022之图像抓拍规范解读和设计实现

​技术背景

GB/T28181-2022相对2016版,对图像抓拍有了明确的界定,图像抓拍在视频监控行业非常重要, Android平台GB28181设备接入端,无需实时上传音视频实时数据的情况下,就可以抓图上传到指定的图像存储服务器上。

图像抓拍基本要求如下:

  1. 源设备向目标设备发送图像抓拍配置命令,需要携带传输路径、会话ID等信息。
  2. 目标设备完成图像传输后,发送图像抓拍传输完成通知命令,采用IETF RFC3428中的MESSAGE方法实现。
  3. 图像文件命名规则宜采用"设备编码(20位)、图像编码(2位)、时间编码(17位)、序列码(2位)"的形式
  4. 图像格式宜使用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组合使用,录像相关功能。

图像抓拍相关信令处理如下:

arduino 复制代码
/*
 * Author: daniusdk.com
 */
 
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配置Listener如下:

arduino 复制代码
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);
}

Snapshot配置接口如下:

csharp 复制代码
public interface SnapShotConfig {
    int snap_num();
    int interval();
    String upload_url();
    String session_id();
}

图像抓拍JNI接口设计如下:

java 复制代码
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 核心代码如下:

ini 复制代码
/*
 * Author: daniusdk.com
 */
 
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图像抓拍大概的流程和设计参考,权当抛砖引玉,Android终端除支持常规的音视频数据接入外,还可以支持移动设备位置(MobilePosition)订阅和通知、图像抓拍、语音广播和语音对讲、历史视音频下载和回放。感兴趣的开发者,可以单独跟我探讨。

相关推荐
dvlinker4 小时前
【音视频开发】使用支持硬件加速的D3D11绘图遇到的绘图失败与绘图崩溃问题的记录与总结
音视频开发·c/c++·视频播放·d3d11·d3d11绘图模式
aqi003 天前
FFmpeg开发笔记(五十八)把32位采样的MP3转换为16位的PCM音频
ffmpeg·音视频·直播·流媒体
音视频牛哥5 天前
Android平台GB28181实时回传流程和技术实现
音视频开发·视频编码·直播
音视频牛哥7 天前
RTMP、RTSP直播播放器的低延迟设计探讨
音视频开发·视频编码·直播
音视频牛哥11 天前
电脑共享同屏的几种方法分享
音视频开发·视频编码·直播
aqi0013 天前
FFmpeg开发笔记(五十四)使用EasyPusher实现移动端的RTSP直播
android·ffmpeg·音视频·直播·流媒体
aqi0014 天前
FFmpeg开发笔记(五十三)移动端的国产直播录制工具EasyPusher
android·ffmpeg·音视频·直播·流媒体
加油吧x青年15 天前
Web端开启直播技术方案分享
前端·webrtc·直播
aqi001 个月前
FFmpeg开发笔记(五十二)移动端的国产视频播放器GSYVideoPlayer
android·ffmpeg·音视频·直播·流媒体