为什么你的json_decode返回null?深度限制的隐藏坑点大曝光

第一章:为什么你的json_decode返回null?深度限制的隐藏坑点大曝光

在PHP开发中,json_decode() 是处理JSON数据的核心函数。然而,许多开发者常遇到其返回 null 的诡异现象,即便JSON字符串看似合法。其中一个鲜为人知的原因是“嵌套深度限制”。

理解PHP的JSON解码深度限制

PHP默认对JSON字符串的嵌套层级设置了上限(通常为512层)。当数据结构超过该限制时,json_decode() 会解析失败并返回 null,而非抛出异常。 例如,以下深度嵌套的JSON将触发此问题:

// 构造深度嵌套的JSON(示例仅展示结构)
$deepJson = '{"data":' . str_repeat('{"child":', 600) . '1' . str_repeat('}', 600) . '}';
$result = json_decode($deepJson);

var_dump($result); // 输出: NULL

如何检测和规避该问题

首先应使用 json_last_error() 检查错误类型:
  • JSON_ERROR_DEPTH:表示超出最大堆栈深度
  • JSON_ERROR_SYNTAX:语法错误
可通过调整数据结构或预处理降低嵌套层级。若无法避免,需在前端或服务端协商简化结构。

常见错误与对应码值对照表

错误常量含义
JSON_ERROR_NONE无错误
JSON_ERROR_DEPTH超出最大堆栈深度
JSON_ERROR_SYNTAX语法错误
确保在调用 json_decode() 后始终验证结果并检查错误状态,以快速定位此类隐蔽问题。

第二章:深入理解JSON解码的深度机制

2.1 PHP中json_decode的递归解析原理

PHP 的 `json_decode` 函数在处理嵌套 JSON 数据时,采用递归方式逐层解析结构。当传入一个包含对象或数组的 JSON 字符串时,解析器会深度优先遍历每个节点,将子结构再次交由内部解析函数处理。
递归解析过程
解析器首先判断当前值类型(字符串、数字、对象、数组等),若遇到对象或数组,则创建对应 PHP 结构,并对其中每个元素递归调用解析函数。

$json = '{"data": {"users": [{"id": 1, "name": "Alice"}]}}';
$result = json_decode($json, true);
// $result['data']['users'][0]['name'] === "Alice"
上述代码中,`json_decode` 先解析顶层键 `data`,发现其为对象后递归解析其子结构,直至叶子节点。参数 `true` 表示将对象转换为关联数组。
关键参数说明
  • assoc:设为 true 时,JSON 对象转为数组,便于递归访问;
  • depth:限制递归最大层级,默认为 512,防止栈溢出。

2.2 默认深度限制的底层实现分析

在大多数递归数据结构处理中,系统会设置默认深度限制以防止栈溢出。该机制通常通过维护一个运行时计数器实现。
核心控制逻辑
func traverse(node *Node, depth int) error {
    if depth > MaxDepth {
        return ErrMaxDepthExceeded // 超出最大深度返回错误
    }
    // 继续遍历子节点
    for _, child := range node.Children {
        traverse(child, depth+1)
    }
    return nil
}
上述代码中,depth 参数记录当前递归层级,每次进入子节点时加1。当其值超过预设的 MaxDepth(如1000),立即终止执行。
默认值配置策略
  • 硬编码方式:直接在源码中定义常量,适用于稳定场景;
  • 运行时配置:通过环境变量或配置文件动态调整,提升灵活性。

2.3 深度超限导致null的触发条件实验

在JavaScript引擎中,对象嵌套深度超过调用栈限制时可能引发异常或返回null。为验证该行为,设计递归构造测试用例。
实验代码实现

function createDeepObject(depth) {
  let obj = { data: "value" };
  for (let i = 0; i < depth; i++) {
    obj = { nested: obj };
    // 每层包装一个新对象
  }
  return obj;
}
// 调用 createDeepObject(100000) 观察行为
上述代码通过循环逐层封装对象,模拟深度嵌套。当depth过大时,V8引擎在序列化或访问时可能抛出"Maximum call stack size exceeded"或返回null。
触发条件分析
  • 调用栈深度通常限制在10,000~100,000层,因引擎而异
  • null出现在JSON.stringify等操作中,表示无法安全处理
  • 垃圾回收机制可能提前释放深层引用

2.4 不同PHP版本对嵌套深度的处理差异

PHP在不同版本中对嵌套结构的解析深度存在显著差异,尤其体现在数组和对象的递归处理上。早期版本如PHP 5.6默认最大嵌套深度为100,超过后会触发致命错误。
配置参数变化
从PHP 7.0起,max_execution_depth被移除,转而由Zend引擎统一管理调用栈,实际限制更依赖系统栈大小。
版本对比表
PHP版本默认最大嵌套深度行为说明
5.6100超出时报“Maximum function nesting level”
7.0+不限(受内存和栈限制)更依赖底层资源,不再硬编码限制
// 示例:深度嵌套数组生成
function buildDeepArray($depth) {
    $array = [];
    for ($i = 0; $i < $depth; $i++) {
        $array = [$array];
    }
    return $array;
}
// 在PHP 5.6中,buildDeepArray(101)将触发错误
该函数在低版本中易触达限制,高版本则更多受限于内存与系统栈大小,体现底层机制优化。

2.5 如何通过调试手段定位深度问题

在复杂系统中,深度问题往往表现为偶发性崩溃、性能退化或数据不一致。传统的日志打印难以覆盖多线程、异步调用等场景,需结合多种调试手段进行精准定位。
使用断点与条件调试
在关键路径设置条件断点,可有效减少干扰信息。例如,在 Go 中使用 Delve 调试器:

// 在满足特定用户ID时中断
(dlv) break main.go:123 if userId == "debug-user"
该命令仅在 userId 匹配指定值时触发中断,避免全量停顿,提升排查效率。
核心转储与事后分析
当生产环境发生崩溃,可通过生成 core dump 结合 gdb 进行回溯:
  • 启用核心转储:ulimit -c unlimited
  • 使用 gdb binary core 文件分析调用栈
  • 提取线程状态与寄存器信息
分布式追踪集成
引入 OpenTelemetry 可视化请求链路,快速定位延迟瓶颈。表格对比常见工具能力:
工具采样精度跨服务支持
Jaeger
Zipkin

第三章:实战中的深度限制陷阱案例

3.1 API响应嵌套过深导致解析失败

在实际开发中,API返回数据层级过深会导致客户端解析困难,甚至引发内存溢出或解析异常。
典型嵌套结构示例
{
  "data": {
    "user": {
      "profile": {
        "address": {
          "city": "Beijing"
        }
      }
    }
  }
}
上述结构需通过 res.data.user.profile.address.city 访问目标字段,极易因中间层级为空导致运行时错误。
解决方案对比
方案优点缺点
扁平化响应易于访问语义弱化
可选链操作符安全读取仅语言层防护
使用 ?. 操作符可缓解问题:
const city = res?.data?.user?.profile?.address?.city;
但仍建议后端优化结构,避免深度嵌套。

3.2 配置文件层级过多引发的静默错误

在微服务架构中,配置文件常通过多层继承(如 application.yml、application-dev.yml、bootstrap.yml)实现环境差异化。然而,层级过深易导致属性覆盖混乱,某些配置项被意外屏蔽或替换,系统仍正常启动却行为异常。
典型问题场景
  • 高优先级配置未生效,因低层级文件存在同名但不同值的键
  • 环境变量与配置中心参数冲突,日志无明确告警
  • 默认配置被空值覆盖,引发空指针异常
代码示例:Spring Boot 多环境配置
# application.yml
server:
  port: 8080
database:
  url: localhost:5432

# application-prod.yml
database:
  url: prod-db:5432
  username: admin
上述结构中,若 application.yml 缺失 password 字段,生产环境将使用 null 值,可能静默失败连接池初始化。
规避策略
建立配置审计流程,结合 CI 阶段校验工具扫描冗余与缺失项,确保关键参数显式声明。

3.3 第三方库输出超出默认深度限制

在使用某些第三方库进行数据序列化时,常因嵌套层级过深触发默认深度限制,导致输出被截断或抛出异常。
常见触发场景
此类问题多见于结构复杂的配置对象、递归数据模型或依赖注入树的调试输出。例如,在 Go 的 spew 库中,默认最大深度为 10 层。

import "github.com/davecgh/go-spew/spew"

spew.Config{Depth: 20}.Dump(complexStruct)
上述代码通过设置 Depth 参数将打印深度扩展至 20,避免中途截断。参数说明: - Depth:控制结构体嵌套的最大展开层级; - 默认值为 10,适用于大多数简单对象; - 超出后以 "(values beyond depth limit)" 替代。
解决方案对比
  • 调整库配置中的深度阈值
  • 实现自定义格式化器规避递归
  • 预处理数据结构扁平化输出

第四章:规避与解决方案详解

4.1 调整json_decode最大深度参数实践

在处理嵌套较深的JSON数据时,PHP默认的`json_decode`最大深度限制(1024)可能不足以解析复杂结构,导致返回`null`并触发警告。
调整最大深度参数
可通过第三个参数指定递归深度,突破默认限制:

$json = '{"data": {"level1": {"level2": {"value": "test"}}}}';
$result = json_decode($json, true, 128); // 设置最大深度为128
if ($result === null) {
    echo '解析失败:' . json_last_error_msg();
}
参数说明:第三个参数为整数类型,表示解码过程中的最大嵌套层级。超出该值将返回null
常见错误与应对策略
  • 未设置足够深度导致解析中断
  • 过度提高深度影响性能或引发栈溢出
  • 建议根据实际数据结构预估合理值,并结合json_last_error()进行错误排查

4.2 分层解析大型JSON结构的设计模式

在处理大型JSON数据时,分层解析能有效降低内存占用并提升解析效率。通过构建层级访问路径,仅解析必要字段,避免全量加载。
分层解析核心策略
  • 惰性解析:仅在访问具体字段时触发解析
  • 路径索引:使用JSON Pointer定位深层节点
  • 结构分离:将元数据与主体内容解耦处理
代码实现示例

type JSONLayer struct {
    raw []byte
    cache map[string]interface{}
}

func (j *JSONLayer) Get(path string) interface{} {
    if val, ok := j.cache[path]; ok {
        return val
    }
    // 使用gjson按路径提取
    result := gjson.Get(string(j.raw), path)
    j.cache[path] = result.Value()
    return result.Value()
}
该结构通过缓存机制避免重复解析,Get 方法接收 JSON Pointer 路径(如 "user.profile.name"),利用 gjson 库实现精准提取,显著减少CPU和内存开销。

4.3 使用流式处理器处理超深JSON数据

在处理嵌套层级极深的JSON数据时,传统解析方式容易导致内存溢出。流式处理器通过边读取边解析的方式,显著降低内存占用。
流式解析优势
  • 逐事件驱动,无需加载完整文档
  • 适用于GB级JSON文件处理
  • 支持实时数据管道接入
Go语言实现示例
decoder := json.NewDecoder(file)
for {
    token, err := decoder.Token()
    if err == io.EOF { break }
    // 处理对象开始、键值对、数组等事件
    processToken(token)
}
该代码利用json.Decoder按需读取token,避免全量加载。每次调用Token()仅解析下一个JSON元素,适合处理深度嵌套结构。
性能对比
方法内存占用适用场景
标准解析小型JSON
流式处理超深/大体积JSON

4.4 构建健壮JSON解析封装类的最佳实践

在处理复杂的 JSON 数据时,直接使用原生解析方法容易导致空指针、类型转换异常等问题。构建一个健壮的封装类可显著提升代码的可维护性与容错能力。
统一错误处理机制
封装类应集中处理解析异常,避免散落在各处的 try-catch 块。通过预判字段存在性和类型一致性,提前拦截潜在问题。
链式调用设计
采用链式 API 提升可读性,例如:

JsonParser.parse(json)
    .require("user")
    .expectString("name")
    .expectInt("age");
该设计确保每一步操作都进行校验,任一环节失败即抛出结构化错误信息。
  • 支持默认值回退机制
  • 内置类型自动转换(如字符串转数字)
  • 提供调试模式输出解析路径

第五章:总结与建议

性能优化的实际路径
在高并发系统中,数据库连接池的合理配置至关重要。以 Go 语言为例,可通过以下方式设置最大空闲连接和生命周期控制:
db.SetMaxOpenConns(50)
db.SetMaxIdleConns(10)
db.SetConnMaxLifetime(30 * time.Minute)
此类配置可有效避免因连接泄漏导致的服务雪崩。
监控与告警机制建设
建立完善的可观测性体系是保障系统稳定的核心。推荐使用 Prometheus + Grafana 组合进行指标采集与可视化展示。关键监控项应包括:
  • 请求延迟 P99 小于 200ms
  • 错误率持续高于 1% 触发告警
  • 服务实例 CPU 使用率超过 80%
  • GC 停顿时间大于 50ms
微服务拆分实践参考
某电商平台在用户量突破百万后,将单体架构重构为微服务。以下是核心模块拆分前后的对比数据:
指标拆分前拆分后
平均响应时间480ms160ms
部署频率每周1次每日多次
故障恢复时间30分钟+5分钟内
该案例表明,合理的服务边界划分能显著提升系统可维护性与弹性能力。
onvif_passthrough:/****************************************************************************** * Copyright (c) 2018-2018 TP-Link Systems Inc. * * Filename: imaging.c * Version: 1.0 * Description: Imaging 请求中,与透传相关的指令处理接口 * Author: yexuelin<yexuelin@tp-link.com.cn> * Date: 2019-03-14 ******************************************************************************/ #include <stdio.h> #include "onvif_passthrough.h" #include "libds.h" #include "soap_auth.h" #include "soap_pack.h" #include "soap_timg.h" #include "soap_parse.h" #include "soap_tptz.h" #define MAX_INTEGER_STR_LEN 32 /*最大整数字符串长度*/ #define IMAGE_STR "image" #define COMMON_STR "common" #define START_FOCUS_STR "start_focus" #define STOP_FOCUS_STR "stop_focus" #define START_IRIS_STR "start_iris" #define VELOCITY_STR "velocity" #define TIMG_IMG_SETTING "timg:ImagingSettings" #define TIMG_GET_IMG_SETTINGS "timg:GetImagingSettings" #define TIMG_SET_IMG_SETTINGS "timg:SetImagingSettings" #define TIMG_GET_SETTING_RSP "timg:GetImagingSettingsResponse" #define TIMG_SET_SETTING_RSP "timg:SetImagingSettingsResponse" #define TIMG_MOVE "timg:Move" #define TIMG_STOP "timg:Stop" #define TIMG_MOVE_RSP "timg:MoveResponse" #define TIMG_STOP_RSP "timg:StopResponse" /* 功能:构建一个获取图像设置的JSON请求对象。 输入:SOAP_CONTEXT指针(包含请求信息),输出参数json_req(用于返回构建的JSON对象),模块名和节名。 输出:返回OK或ERROR。 过程: a. 进行鉴权(检查是否有操作员权限)。 b. 创建JSON对象,添加方法为"get",并添加指定的模块和节。 c. 将构建的JSON对象通过json_req返回。*/ LOCAL S32 get_json_settings(SOAP_CONTEXT *soap, JSON_OBJPTR *json_req, char *module_name, char *sec_name) { JSON_OBJPTR json_data = NULL; JSON_OBJPTR json_sec = NULL; if (soap == NULL || json_req == NULL) { ONVIF_WARN("soap == NULL."); return ERROR; } /* 需要鉴权 */ if (OK != soap_usernametoken_auth(soap, UM_OPERATOR)) { ONVIF_ERROR("Auth failed\n"); soap_fault(soap, "SOAP-ENV:Sender", "ter:NotAuthorized", NULL, "Authority failure"); soap->error = 400; return ERROR; } json_data = jso_new_obj(); json_sec = jso_new_obj(); if (json_data == NULL || json_sec == NULL) { ONVIF_WARN("jso_new_obj error."); jso_free_obj(json_data); jso_free_obj(json_sec); return ERROR; } jso_add_string(json_data, DS_METHOD_STR, METHOD_GET_STR); jso_obj_add(json_data, module_name, json_sec); jso_add_string(json_sec, DS_SECTION_STR, sec_name); *json_req = json_data; return OK; } /*------------------------- 获取图像参数 ----------------------------------*/ /* 功能:生成获取图像设置的SOAP响应(XML格式)。 输入:xml_buf(输出缓冲区),data(未使用)。 输出:返回OK或ERROR。 过程: a. 从设备服务(ds_read)读取当前的图像通用设置(亮度、饱和度、对比度、锐度)和模块规格(判断是否支持光圈优先)。 b. 开始构建XML响应,按照ONVIF标准格式输出各个图像参数。 c. 如果设备支持光圈优先(iris_first),则输出曝光模式(手动、自动或光圈优先)和当前的光圈值。 d. 如果设备支持变焦(通过tptz_get_local_capability判断),则输出聚焦相关的参数(自动对焦模式、默认速度、近限和远限)。*/ LOCAL S32 soap_out_timg_get_settings_rsp(ONVIF_BUF *xml_buf, void *data) { IMAGE_COMMON image_common; MODULE_SPEC module_spec; struct onvif_ptz_capability *ptz_capability = NULL; const char *tag_str[] = {TT_BRIGHTNESS_STR, TT_COLORSATURATION_STR, TT_CONTRAST_STR, TT_SHARPNESS_STR}; U8 *value[] = {&image_common.luma, &image_common.saturation, &image_common.contrast, &image_common.sharpness}; U32 i; if (NULL == xml_buf) { ONVIF_WARN("Input NULL."); return ERROR; } memset(&image_common, 0, sizeof(IMAGE_COMMON)); if (0 == ds_read(IMAGE_COMMON_PATH, &image_common, sizeof(IMAGE_COMMON))) { ONVIF_ERROR("ds_read %s fail.", IMAGE_COMMON_PATH); return ERROR; } memset(&module_spec, 0, sizeof(MODULE_SPEC)); if (0 == ds_read(MODULE_SPEC_PATH, &module_spec, sizeof(MODULE_SPEC))) { ONVIF_ERROR("ds_read %s fail.", MODULE_SPEC_PATH); return ERROR; } SOAP_IF_FAIL_RET(soap_element_begin_out(xml_buf, TIMG_GET_SETTING_RSP, NULL)); SOAP_IF_FAIL_RET(soap_element_begin_out(xml_buf, TIMG_IMG_SETTING, NULL)); for (i = 0; i < sizeof(tag_str) / sizeof(char*); ++i) { SOAP_IF_FAIL_RET(soap_element_int_type(xml_buf, tag_str[i], *value[i], NULL)); } if (module_spec.iris_first[0] == '1') { /* tt:Exposure begin */ SOAP_IF_FAIL_RET(soap_element_begin_out(xml_buf, "tt:Exposure", NULL)); /* tt:Mode */ switch (image_common.exp_type) { case EXP_TYPE_MANUAL: SOAP_IF_FAIL_RET(soap_element(xml_buf, "tt:Mode", "MANUAL", NULL)); break; case EXP_TYPE_AUTO: SOAP_IF_FAIL_RET(soap_element(xml_buf, "tt:Mode", "AUTO", NULL)); break; case EXP_TYPE_IRIS_FIRST: SOAP_IF_FAIL_RET(soap_element(xml_buf, "tt:Mode", "IRIS_FIRST", NULL)); break; default: ONVIF_WARN("Unsupported expore mode."); return ERROR; } /* tt:Iris */ SOAP_IF_FAIL_RET(soap_element_int_type(xml_buf, "tt:Iris", image_common.iris_level, NULL)); /* tt:Exposure end */ SOAP_IF_FAIL_RET(soap_element_end_out(xml_buf, "tt:Exposure")); } if (tptz_get_local_capability(&ptz_capability) && ptz_capability->zoom_valid) { /*FOCUS begin */ SOAP_IF_FAIL_RET(soap_element_begin_out(xml_buf, "tt:Focus", NULL)); SOAP_IF_FAIL_RET(soap_element(xml_buf, "tt:AutoFocusModes", "AUTO", NULL)); SOAP_IF_FAIL_RET(soap_element(xml_buf, "tt:AutoFocusModes", "MANUAL", NULL)); SOAP_IF_FAIL_RET(soap_element_begin_out(xml_buf, "tt:DefaultSpeed", NULL)); SOAP_IF_FAIL_RET(soap_element_int_type(xml_buf, "tt:Min", 1, NULL)); SOAP_IF_FAIL_RET(soap_element_int_type(xml_buf, "tt:Max", 1, NULL)); SOAP_IF_FAIL_RET(soap_element_end_out(xml_buf, "tt:DefaultSpeed")); SOAP_IF_FAIL_RET(soap_element_begin_out(xml_buf, "tt:NearLimit", NULL)); SOAP_IF_FAIL_RET(soap_element_int_type(xml_buf, "tt:Min", 10, NULL)); SOAP_IF_FAIL_RET(soap_element_int_type(xml_buf, "tt:Max", 2000, NULL)); SOAP_IF_FAIL_RET(soap_element_end_out(xml_buf, "tt:NearLimit")); SOAP_IF_FAIL_RET(soap_element_begin_out(xml_buf, "tt:FarLimit", NULL)); SOAP_IF_FAIL_RET(soap_element_int_type(xml_buf, "tt:Min", 0, NULL)); SOAP_IF_FAIL_RET(soap_element_int_type(xml_buf, "tt:Max", 0, NULL)); SOAP_IF_FAIL_RET(soap_element_end_out(xml_buf, "tt:FarLimit")); SOAP_IF_FAIL_RET(soap_element_end_out(xml_buf, "tt:Focus")); /*FOCUS end */ } SOAP_IF_FAIL_RET(soap_element_end_out(xml_buf, TIMG_IMG_SETTING)); SOAP_IF_FAIL_RET(soap_element_end_out(xml_buf, TIMG_GET_SETTING_RSP)); return OK; } /* 功能:将获取图像设置的SOAP请求(XML)转换为内部JSON请求对象。 输入:SOAP_CONTEXT指针,输出参数json_req(返回构建的JSON对象)。 输出:返回OK或ERROR。 过程: a. 鉴权。 b. 解析SOAP请求中的VideoSourceToken(视频源令牌),检查是否为"raw_vs1"(目前只支持这个)。 c. 调用get_json_settings函数构建JSON请求。*/ LOCAL S32 timg_get_settings_xml_to_json(SOAP_CONTEXT *soap, JSON_OBJPTR *json_req) { S32 request_len = 0; S32 ch = 0; char *xml_start = NULL; char *xml_str = NULL; char **p = NULL; char charbuf[LEN_INFO] = {0}; if (soap == NULL || json_req == NULL) { ONVIF_TRACE("soap == NULL."); return ERROR; } ONVIF_TRACE("timg:GetImagingSettings"); /* 需确认是否支持 */ if (OK != soap_usernametoken_auth(soap, UM_OPERATOR)) { ONVIF_TRACE("Auth failed\n"); soap_fault(soap, "SOAP-ENV:Sender", "ter:NotAuthorized", NULL, "Authority failure"); soap->error = 400; return ERROR; } /* 分析 GetVideoEncoderConfigurationOptions 请求的具体内容 */ if (soap->request_begin != NULL && soap->request_end != NULL) { request_len = soap->request_end - soap->request_begin; if (request_len < 0) { ONVIF_TRACE("request_len < 0."); return ERROR; } xml_str = xml_start = soap->request_begin; p = (char **)&xml_str; } else { ONVIF_TRACE("no content"); return ERROR; } while (((*p) - xml_start) < request_len) { if ((ch = soap_get_tag(xml_start, request_len, p, charbuf, sizeof(charbuf))) == EOF) { return ERROR; } if (charbuf[0] == '/') { continue; } if (TRUE == soap_match_tag(charbuf, "VideoSourceToken")) { if (OK != soap_parse_element_value(xml_start, request_len, p, ch, charbuf, sizeof(charbuf))) { return ERROR; } ONVIF_TRACE("VideoSourceToken: %s", charbuf); if (strcmp(charbuf, "raw_vs1")) { SOAP_IF_FAIL_RET(soap_fault(soap, "SOAP-ENV:Sender", "ter:InvalidArgVal", "ter:SettingsInvalid", "error data")); soap->error = SOAP_FAULT; return ERROR; } continue; } } return get_json_settings(soap, json_req, IMAGE_STR, COMMON_STR); } /* 功能:将内部处理后的JSON响应转换为SOAP响应(XML)。 输入:SOAP_CONTEXT指针,json_rsp(内部处理返回JSON响应)。 输出:返回OK或ERROR。 过程:调用soap_generate_xml函数,使用soap_out_timg_get_settings_rsp函数生成XML。*/ LOCAL S32 timg_get_settings_json_to_xml(SOAP_CONTEXT *soap, JSON_OBJPTR json_rsp) { if (soap == NULL) { ONVIF_WARN("soap == NULL."); return ERROR; } return soap_generate_xml((p_out_fun)(soap_out_timg_get_settings_rsp), soap, NULL); } /*------------------------- 配置图像参数 ----------------------------------*/ /* 功能:生成设置图像设置的SOAP响应(XML)。 输入:xml_buf(输出缓冲区),data(未使用)。 输出:返回OK或ERROR。 过程:生成一个空的timg:SetImagingSettingsResponse标签。*/ LOCAL S32 soap_out_set_timg_rsp(ONVIF_BUF *xml_buf, void *data) { if (NULL == xml_buf) { ONVIF_WARN("Input NULL."); return ERROR; } SOAP_IF_FAIL_RET(soap_element_begin_out(xml_buf, TIMG_SET_SETTING_RSP, NULL)); SOAP_IF_FAIL_RET(soap_element_end_out(xml_buf, TIMG_SET_SETTING_RSP)); return OK; } #if 0 /*------------------------- 开始聚焦 ----------------------------------*/ LOCAL S32 soap_out_timg_move_rsp(ONVIF_BUF *xml_buf, void *data) { if (NULL == xml_buf) { ONVIF_WARN("Input NULL."); return ERROR; } /* timg:MoveResponse */ SOAP_IF_FAIL_RET(soap_element(xml_buf, TIMG_MOVE_RSP, NULL, NULL)); return OK; } /*------------------------- 停止聚焦 ----------------------------------*/ LOCAL S32 soap_out_timg_stop_rsp(ONVIF_BUF *xml_buf, void *data) { if (NULL == xml_buf) { ONVIF_WARN("Input NULL."); return ERROR; } /* timg:StopResponse */ SOAP_IF_FAIL_RET(soap_element(xml_buf, TIMG_STOP_RSP, NULL, NULL)); return OK; } #endif LOCAL S32 timg_set_imaging_xml_to_json(SOAP_CONTEXT *soap, JSON_OBJPTR *json_req) { S32 request_len = 0; char *xml_start = NULL; char *xml_str = NULL; char **p = NULL; S32 ch = 0; char charbuf[LEN_INFO * 2] = {0}; JSON_OBJPTR json_data = NULL; JSON_OBJPTR json_sec = NULL; JSON_OBJPTR json_seg = NULL; const char *tag_str[] = {BRIGHTNESS_STR, COLORSATURATION_STR, CONTRAST_STR, SHARPNESS_STR}; const char *jso_str[] = {LUMA_JSO_STR, SATURATION_JSO_STR, CONTRAST_JSO_STR, SHARPNESS_JSO_STR}; U32 min[] = {LUMA_MIN, SATURATION_MIN, CONTRAST_MIN, SHARPNESS_MIN}; U32 max[] = {LUMA_MAX, SATURATION_MAX, CONTRAST_MAX, SHARPNESS_MAX}; U32 i; JSON_OBJPTR json_data_iris = NULL; JSON_OBJPTR json_sec_iris = NULL; JSON_OBJPTR json_seg_iris = NULL; int iris_to_set = 0; IMAGE_COMMON image_common; MODULE_SPEC module_spec; JSON_OBJ *rsp_obj = NULL; if (soap == NULL || json_req == NULL) { ONVIF_WARN("soap == NULL."); return ERROR; } /* 需要鉴权 */ if (OK != soap_usernametoken_auth(soap, UM_OPERATOR)) { ONVIF_WARN("Auth failed"); soap_fault(soap, "SOAP-ENV:Sender", "ter:NotAuthorized", NULL, "Authority failure"); soap->error = 400; return ERROR; } /* 分析SetImage请求的具体内容 */ if (soap->request_begin != NULL && soap->request_end != NULL) { request_len = soap->request_end - soap->request_begin; if (request_len < 0) { ONVIF_WARN("Invalid request"); return ERROR; } xml_str = soap->request_begin; xml_start = soap->request_begin; p = (char **)&xml_str; } else { ONVIF_ERROR("soap request content is NULL."); return ERROR; } json_data = jso_new_obj(); json_sec = jso_new_obj(); json_seg = jso_new_obj(); if (NULL == json_data || NULL == json_sec || NULL == json_seg) { ONVIF_WARN("jso_new_obj error."); goto err_out; } while (((*p) - xml_start) < request_len) { if ((ch = soap_get_tag(xml_start, request_len, p, charbuf, sizeof(charbuf))) == EOF) { goto err_out; } if (charbuf[0] == '/') { continue; } if (TRUE == soap_match_tag(charbuf, "VideoSourceToken")) { if (OK != soap_parse_element_value(xml_start, request_len, p, ch, charbuf, sizeof(charbuf))) { goto err_out; } ONVIF_TRACE("VideoSourceToken: %s", charbuf); if (strcmp(charbuf, "raw_vs1")) { SOAP_IF_FAIL_RET(soap_fault(soap, "SOAP-ENV:Sender", "ter:InvalidArgVal", "ter:SettingsInvalid", "error data")); soap->error = SOAP_FAULT; goto err_out; } continue; } for (i = 0; i < sizeof(tag_str) / sizeof(char*); ++i) { if (TRUE != soap_match_tag(charbuf, tag_str[i])) { continue; } if (OK != soap_parse_element_value(xml_start, request_len, p, ch, charbuf, sizeof(charbuf)) || 0 != jso_add_int(json_seg, jso_str[i], atoi(charbuf))) { goto err_out; } if((atoi(charbuf) < min[i]) || (atoi(charbuf) > max[i])) { SOAP_IF_FAIL_RET(soap_fault(soap, "SOAP-ENV:Sender", "ter:InvalidArgVal", "ter:SettingsInvalid", "error data")); soap->error = SOAP_FAULT; goto err_out; } break; } if (TRUE == soap_match_tag(charbuf, "Iris")) { if (OK != soap_parse_element_value(xml_start, request_len, p, ch, charbuf, sizeof(charbuf))) { goto err_out; } ONVIF_TRACE("Iris: %s", charbuf); iris_to_set = atoi(charbuf); continue; } } jso_obj_add(json_sec, COMMON_STR, json_seg); jso_obj_add(json_data, IMAGE_STR, json_sec); jso_add_string(json_data, DS_METHOD_STR, METHOD_SET_STR); *json_req = json_data; memset(&module_spec, 0, sizeof(MODULE_SPEC)); if (0 == ds_read(MODULE_SPEC_PATH, &module_spec, sizeof(MODULE_SPEC))) { ONVIF_ERROR("ds_read %s fail.", MODULE_SPEC_PATH); goto err_out; } if (module_spec.iris_first[0] == '1') { memset(&image_common, 0, sizeof(IMAGE_COMMON)); if (0 == ds_read(IMAGE_COMMON_PATH, &image_common, sizeof(IMAGE_COMMON))) { ONVIF_ERROR("ds_read %s fail.", IMAGE_COMMON_PATH); goto err_out; } json_data_iris = jso_new_obj(); json_sec_iris = jso_new_obj(); json_seg_iris = jso_new_obj(); if (NULL == json_data_iris || NULL == json_sec_iris || NULL == json_seg_iris) { ONVIF_WARN("jso_new_obj error."); goto err_out; } if (iris_to_set > image_common.iris_level) { jso_add_string(json_seg_iris, VELOCITY_STR, "0.1"); } else if (iris_to_set < image_common.iris_level) { jso_add_string(json_seg_iris, VELOCITY_STR, "-0.1"); } else { ONVIF_WARN("iris_to_set is the same as DUT."); goto err_out; } jso_obj_add(json_sec_iris, START_IRIS_STR, json_seg_iris); jso_obj_add(json_data_iris, IMAGE_STR, json_sec_iris); jso_add_string(json_data_iris, DS_METHOD_STR, METHOD_DO_STR); if (OK != onvif_passthrough(soap, json_data_iris, &rsp_obj)) { ONVIF_WARN("passthrough error."); goto err_out; } jso_free_obj(json_data_iris); jso_free_obj(rsp_obj); } return OK; err_out: jso_free_obj(json_data); jso_free_obj(json_sec); jso_free_obj(json_seg); jso_free_obj(json_data_iris); jso_free_obj(json_sec_iris); jso_free_obj(json_seg_iris); jso_free_obj(rsp_obj); return ERROR; } LOCAL S32 timg_set_imaging_json_to_xml(SOAP_CONTEXT *soap, JSON_OBJPTR json_rsp) { if (soap == NULL) { ONVIF_WARN("soap == NULL."); return ERROR; } return soap_generate_xml((p_out_fun)(soap_out_set_timg_rsp), soap, NULL); } #if 0 LOCAL S32 timg_move_xml_to_json(SOAP_CONTEXT *soap, JSON_OBJPTR *json_req) { S32 request_len = 0; char *xml_start = NULL; char *xml_str = NULL; char **p = NULL; S32 ch = 0; char charbuf[LEN_INFO] = {0}; char vs_token[LEN_INFO] = {0}; JSON_OBJPTR json_data = NULL; JSON_OBJPTR json_sec = NULL; JSON_OBJPTR json_seg = NULL; if (soap == NULL || json_req == NULL) { ONVIF_WARN("soap == NULL."); return ERROR; } /* 判断是否支持ptz和zoom,不支持直接返回 */ if (!is_ptz_support() || !is_ptz_3D_zoom_support()) { ONVIF_WARN("not support ptz or zoom."); return ERROR; } /* 需要鉴权 */ if (OK != soap_usernametoken_auth(soap, UM_OPERATOR)) { ONVIF_WARN("Auth failed"); soap_fault(soap, "SOAP-ENV:Sender", "ter:NotAuthorized", NULL, "Authority failure"); soap->error = 400; return ERROR; } /* 需要分析 timg:Move请求的具体内容 */ if (soap->request_begin != NULL && soap->request_end != NULL) { request_len = soap->request_end - soap->request_begin; if (request_len < 0) { ONVIF_TRACE("request_len < 0."); return ERROR; } xml_str = xml_start = soap->request_begin; p = (char **)&xml_str; } else { ONVIF_ERROR("soap request content is NULL."); return ERROR; } json_data = jso_new_obj(); json_sec = jso_new_obj(); json_seg = jso_new_obj(); if (NULL == json_data || NULL == json_sec || NULL == json_seg) { ONVIF_WARN("jso_new_obj error."); goto err_out; } while (((*p) - xml_start) < request_len) { if ((ch = soap_get_tag(xml_start, request_len, p, charbuf, sizeof(charbuf))) == EOF) { ONVIF_ERROR("soap_get_tag failed"); goto err_out; } if (charbuf[0] == '/') { continue; } if (TRUE == soap_match_tag(charbuf, "VideoSourceToken")) { if (OK != soap_parse_element_value(xml_start, request_len, p, ch, charbuf, sizeof(charbuf))) { goto err_out; } ONVIF_TRACE("VideoSourceToken: %s", charbuf); snprintf(vs_token, LEN_INFO, "%s", charbuf); continue; } if (TRUE == soap_match_tag(charbuf, "Speed")) { if (OK != soap_parse_element_value(xml_start, request_len, p, ch, charbuf, sizeof(charbuf)) || 0 != jso_add_string(json_seg, VELOCITY_STR, charbuf)) { goto err_out; } ONVIF_TRACE("Speed: %s", charbuf); } } if (0 != strcmp("raw_vs1", vs_token)) { soap->error = SOAP_FAULT; goto err_out; } jso_obj_add(json_sec, START_FOCUS_STR, json_seg); jso_obj_add(json_data, IMAGE_STR, json_sec); jso_add_string(json_data, DS_METHOD_STR, METHOD_DO_STR); *json_req = json_data; return OK; err_out: jso_free_obj(json_data); jso_free_obj(json_sec); jso_free_obj(json_seg); return ERROR; } LOCAL S32 timg_move_json_to_xml(SOAP_CONTEXT *soap, JSON_OBJPTR json_rsp) { if (soap == NULL) { ONVIF_WARN("soap == NULL."); return ERROR; } return soap_generate_xml((p_out_fun)(soap_out_timg_move_rsp), soap, NULL); } LOCAL S32 timg_stop_xml_to_json(SOAP_CONTEXT *soap, JSON_OBJPTR *json_req) { S32 request_len = 0; char *xml_start = NULL; char *xml_str = NULL; char **p = NULL; S32 ch = 0; char charbuf[LEN_INFO] = {0}; char vs_token[LEN_INFO] = {0}; JSON_OBJPTR json_data = NULL; JSON_OBJPTR json_sec = NULL; JSON_OBJPTR json_seg = NULL; if (soap == NULL || json_req == NULL) { ONVIF_WARN("soap == NULL."); return ERROR; } /* 判断是否支持ptz和zoom,不支持直接返回 */ if (!is_ptz_support() || !is_ptz_3D_zoom_support()) { ONVIF_WARN("not support ptz or zoom."); return ERROR; } /* 需要鉴权 */ if (OK != soap_usernametoken_auth(soap, UM_OPERATOR)) { ONVIF_WARN("Auth failed"); soap_fault(soap, "SOAP-ENV:Sender", "ter:NotAuthorized", NULL, "Authority failure"); soap->error = 400; return ERROR; } /* 需要分析 timg:Move请求的具体内容 */ if (soap->request_begin != NULL && soap->request_end != NULL) { request_len = soap->request_end - soap->request_begin; if (request_len < 0) { ONVIF_TRACE("request_len < 0."); return ERROR; } xml_str = xml_start = soap->request_begin; p = (char **)&xml_str; } else { ONVIF_ERROR("soap request content is NULL."); return ERROR; } json_data = jso_new_obj(); json_sec = jso_new_obj(); json_seg = jso_new_obj(); if (NULL == json_data || NULL == json_sec || NULL == json_seg) { ONVIF_WARN("jso_new_obj error."); goto err_out; } while (((*p) - xml_start) < request_len) { if ((ch = soap_get_tag(xml_start, request_len, p, charbuf, sizeof(charbuf))) == EOF) { ONVIF_ERROR("soap_get_tag failed"); goto err_out; } if (charbuf[0] == '/') { continue; } if (TRUE == soap_match_tag(charbuf, "VideoSourceToken")) { if (OK != soap_parse_element_value(xml_start, request_len, p, ch, charbuf, sizeof(charbuf))) { goto err_out; } ONVIF_TRACE("VideoSourceToken: %s", charbuf); snprintf(vs_token, LEN_INFO, "%s", charbuf); break; } } if (0 != strcmp("raw_vs1", vs_token)) { soap->error = SOAP_FAULT; goto err_out; } jso_obj_add(json_sec, STOP_FOCUS_STR, json_seg); jso_obj_add(json_data, IMAGE_STR, json_sec); jso_add_string(json_data, DS_METHOD_STR, METHOD_DO_STR); *json_req = json_data; return OK; err_out: jso_free_obj(json_data); jso_free_obj(json_sec); jso_free_obj(json_seg); return ERROR; } LOCAL S32 timg_stop_json_to_xml(SOAP_CONTEXT *soap, JSON_OBJPTR json_rsp) { if (soap == NULL) { ONVIF_WARN("soap == NULL."); return ERROR; } return soap_generate_xml((p_out_fun)(soap_out_timg_stop_rsp), soap, NULL); } #endif void imaging_passthrough_init() { onvif_passthrough_handle_add(TIMG_GET_IMG_SETTINGS, timg_get_settings_xml_to_json, timg_get_settings_json_to_xml); onvif_passthrough_handle_add(TIMG_SET_IMG_SETTINGS, timg_set_imaging_xml_to_json, timg_set_imaging_json_to_xml); /* 调焦相关,目前不支持 */ #if 0 onvif_passthrough_handle_add(TIMG_MOVE, timg_move_xml_to_json, timg_move_json_to_xml); onvif_passthrough_handle_add(TIMG_STOP, timg_stop_xml_to_json, timg_stop_json_to_xml); #endif } /****************************************************************************** 模块流程: 系统初始化时调用imaging_passthrough_init,将本模块的处理函数注册到全局透传路由表。 当收到SOAP请求时,透传框架(onvif_serve_passthrough)会匹配请求标签(如timg:GetImagingSettings)。 匹配成功后,调用注册的xml_to_json函数(如timg_get_settings_xml_to_json)将SOAP请求转换为内部JSON请求。 调用onvif_passthrough函数(核心业务处理)执行JSON指令,得到JSON响应。 调用注册的json_to_xml函数(如timg_get_settings_json_to_xml)将JSON响应转换为SOAP响应。 将SOAP响应返回给客户端。 对外接口: 本模块对外提供的唯一接口是imaging_passthrough_init,它被系统初始化函数调用,用于注册本模块的处理函数。 ******************************************************************************/ /****************************************************************************** * Copyright (c) 2018-2018 TP-Link Systems Inc. * * Filename: md_active_cells.c * Version: 1.0 * Description: 移动侦测区域计算的相关接口头文件 * Author: dengpeng<dengpeng@tp-link.com.cn> * Date: 2019-03-15 ******************************************************************************/ #include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <math.h> #include "onvif_passthrough.h" #include "packbits.h" #include <json/json.h> #include <json/json_object_private.h> #include "md_active_cells.h" /**************************************************************************** * Function : json_parse * Description: 从json对象解析section子对象 * Input : json_recv : json对象 mod_name : 模块名称 sub_mod_name : section名称 * Output : N/A * Return : 解析结果 ****************************************************************************/ json_object* json_parse(json_object* json_recv, char* mod_name, char* sub_mod_name) { json_object* json_data = NULL; if (json_recv == NULL || mod_name == NULL || sub_mod_name == NULL) { return NULL; } /* !!!json_object_object_get返回的指针不能手动释放!!! */ json_data = json_object_object_get(json_recv, mod_name); if (NULL == json_data) { ONVIF_ERROR("get %s mod error", mod_name); return NULL; } json_data = json_object_object_get(json_data, sub_mod_name); if (NULL == json_data) { ONVIF_ERROR("get %s subMode error", sub_mod_name); return NULL; } return json_data; } #define UP_GET_CELL_IDX(total, cell_cnt, v) \ ({ \ int i = 0; \ int idx = 1; \ float gap = 0.0; \ for (i = (cell_cnt); i > 0; i--) \ { \ gap = (((float)total) * (i - 1)) / (cell_cnt); \ if ((v) > gap || fabs((v) - gap) <= 1e-5) \ { \ idx = i; \ break; \ } \ } \ idx; \ }) #define DOWN_GET_CELL_IDX(total, cell_cnt, v) \ ({ \ int i = 0; \ int idx = cell_cnt; \ float gap = 0.0; \ for (i = 1; i <= (cell_cnt); i++) \ { \ gap = (((float)total) * i) / (cell_cnt); \ if ((v) < gap || fabs((v) - gap) <= 1e-5) \ { \ idx = i; \ break; \ } \ } \ idx; \ }) /**************************************************************************** * Function : md_get_active_cells * Description: 读取移动侦测区域信息,映射到22*18的网格中,将映射后的 * 的网格使用packbits和base64算法编码 * Input : SLP协议定义的区域信息 * Output : N/A * Return : base64编码的区域信息 ****************************************************************************/ char *md_get_active_cells_alloc(json_object* region_info) { int i = 0; int c = 0; int r = 0; int region_cnt = 0; int x = 0; int y = 0; int height = 0; int width = 0; int left_col = 0; int right_col = 0; int top_row = 0; int bottom_row = 0; int byte_idx = 0; int bit_idx = 0; int packbit_len = 0; json_object* jso_opt = NULL; json_object** module_obj = NULL; unsigned char bit_arr[(CELL_LAYOUT_ROWS * CELL_LAYOUT_COLS + BITS_IN_BYTE) / BITS_IN_BYTE] = {0}; unsigned char packbit_arr[((CELL_LAYOUT_ROWS * CELL_LAYOUT_COLS + BITS_IN_BYTE) * 2) / BITS_IN_BYTE] = {0}; char *base64_buf = NULL; if (NULL == region_info) { ONVIF_ERROR("NULL == region_info"); return NULL; } memset(bit_arr, 0, sizeof(bit_arr)); region_cnt = jso_array_length(region_info); for (i = 0; i < region_cnt; i++) { jso_opt = jso_array_get_idx(region_info, i); /* 取region_info_i下一级的内容 */ module_obj = NULL; module_obj = jso_next_sub_obj(jso_opt, NULL, NULL); if (NULL == module_obj) { ONVIF_ERROR("NULL == module_obj"); return NULL; } jso_opt = *module_obj; jso_obj_get_int(jso_opt, "x_coor", &x); jso_obj_get_int(jso_opt, "y_coor", &y); jso_obj_get_int(jso_opt, "height", &height); jso_obj_get_int(jso_opt, "width", &width); left_col = UP_GET_CELL_IDX(SLP_REGION_MAX_WIDTH, CELL_LAYOUT_COLS, x); right_col = DOWN_GET_CELL_IDX(SLP_REGION_MAX_WIDTH, CELL_LAYOUT_COLS, x + width); top_row = UP_GET_CELL_IDX(SLP_REGION_MAX_HEIGHT, CELL_LAYOUT_ROWS, y); bottom_row = DOWN_GET_CELL_IDX(SLP_REGION_MAX_HEIGHT, CELL_LAYOUT_ROWS, y + height); for (r = top_row; r <= bottom_row; r++) { for (c = left_col; c <= right_col; c++) { byte_idx = ((r - 1) * CELL_LAYOUT_COLS + c - 1) / BITS_IN_BYTE; bit_idx = BITS_IN_BYTE - 1 - ((r - 1) * CELL_LAYOUT_COLS + c - 1) % BITS_IN_BYTE; bit_arr[byte_idx] |= (0x1<<bit_idx); } } } packbit_len = tiff6_PackBits(bit_arr, sizeof(bit_arr) / sizeof(bit_arr[0]), packbit_arr); onvif_base64_encode_alloc((char *)packbit_arr, packbit_len, &base64_buf); return base64_buf; } /**************************************************************************** * Function : md_get_active_cells_alloc_no_json * Description: 通过ds_read读取移动侦测区域信息,映射到22*18的网格中,将映射后的 * 的网格使用packbits和base64算法编码 * Input : SLP协议定义的区域信息 * Output : N/A * Return : base64编码的区域信息 ****************************************************************************/ char *md_get_active_cells_alloc_no_json() { int i = 0; int c = 0; int r = 0; int x = 0; int y = 0; int height = 0; int width = 0; int left_col = 0; int right_col = 0; int top_row = 0; int bottom_row = 0; int byte_idx = 0; int bit_idx = 0; int packbit_len = 0; DEVICE_INFO device_info; char path[DATA_PATH_SIZE] = {0}; MOTION_DETECT_REGION_INFO region = {0}; unsigned char bit_arr[(CELL_LAYOUT_ROWS * CELL_LAYOUT_COLS + BITS_IN_BYTE) / BITS_IN_BYTE] = {0}; unsigned char packbit_arr[((CELL_LAYOUT_ROWS * CELL_LAYOUT_COLS + BITS_IN_BYTE) * 2) / BITS_IN_BYTE] = {0}; char *base64_buf = NULL; if (0 == ds_read(DEVICE_INFO_PATH, (U8 *)&device_info, sizeof(DEVICE_INFO))) { ONVIF_DEBUG("read DEVICE_INFO fail."); return NULL; } for (i = 0; i < device_info.md_reg_num; i++) { snprintf(path, DATA_PATH_SIZE, "/motion_detection/%s%d", MOTION_DETECT_REGION_PREFIX, i + 1); if (0 == ds_read((const char*)path, &region, sizeof(MOTION_DETECT_REGION_INFO))) { ONVIF_TRACE("read %s fail.", path); continue; } x = region.x_coor; y = region.y_coor; height = region.height; width = region.width; left_col = UP_GET_CELL_IDX(SLP_REGION_MAX_WIDTH, CELL_LAYOUT_COLS, x); right_col = DOWN_GET_CELL_IDX(SLP_REGION_MAX_WIDTH, CELL_LAYOUT_COLS, x + width); top_row = UP_GET_CELL_IDX(SLP_REGION_MAX_HEIGHT, CELL_LAYOUT_ROWS, y); bottom_row = DOWN_GET_CELL_IDX(SLP_REGION_MAX_HEIGHT, CELL_LAYOUT_ROWS, y + height); for (r = top_row; r <= bottom_row; r++) { for (c = left_col; c <= right_col; c++) { byte_idx = ((r - 1) * CELL_LAYOUT_COLS + c - 1) / BITS_IN_BYTE; bit_idx = BITS_IN_BYTE - 1 - ((r - 1) * CELL_LAYOUT_COLS + c - 1) % BITS_IN_BYTE; bit_arr[byte_idx] |= (0x1<<bit_idx); } } } packbit_len = tiff6_PackBits(bit_arr, sizeof(bit_arr) / sizeof(bit_arr[0]), packbit_arr); onvif_base64_encode_alloc((char *)packbit_arr, packbit_len, &base64_buf); return base64_buf; } /**************************************************************************** * Function : md_parse_active_cells_alloc * Description: 将onvif标准格式的区域信息转换为SLP私有协议的json串 * 其中,该函数会申请内存用于存储json命令,需要调用者释放该内存 * Input : active_cells: onvif标准定义的的区域信息(base64Binary) * Output : json_set_region: SLP协议设置区域的json串命令 * Return : 0-success, -1-error ****************************************************************************/ int md_parse_active_cells_alloc(const char *active_cells, char **json_set_region) { int ret = -1; int found; int byte_len = 0; int region_cnt = 0; int r, c, byte_idx, bit_idx, mask; // 遍历整个bitmask int sub_r, sub_c, sub_byte_idx, sub_bit_idx, sub_mask; // 从新矩阵左上角开始遍历bitmask int top_left_row, top_left_col, right_col, bottom_row; // 最大矩形区域的左上角及右下角 int top_left_x, top_left_y, right_bottom_x, right_bottom_y; // 矩形映射为SLP协议的坐标值 size_t inlen = 0; size_t packbit_len = 0; float cell_width = (float)SLP_REGION_MAX_WIDTH / CELL_LAYOUT_COLS; float cell_height = (float)SLP_REGION_MAX_HEIGHT / CELL_LAYOUT_ROWS; char *packbit_buf = NULL; unsigned char byte_buf[(CELL_LAYOUT_ROWS * CELL_LAYOUT_COLS + BITS_IN_BYTE) / BITS_IN_BYTE] = {0}; unsigned char cell_visited[(CELL_LAYOUT_ROWS * CELL_LAYOUT_COLS + BITS_IN_BYTE) / BITS_IN_BYTE] = {0}; json_object *json_root = NULL; json_object *json_module = NULL; json_object *json_action = NULL; json_object *json_param = NULL; json_object *json_region = NULL; int array_len = (CELL_LAYOUT_ROWS * CELL_LAYOUT_COLS + BITS_IN_BYTE) / BITS_IN_BYTE; char *cmd = NULL; if (!active_cells || !json_set_region) { ret = -1; goto exit; } inlen = strlen(active_cells); memset(byte_buf, 0, sizeof(byte_buf)); memset(cell_visited, 0, sizeof(cell_visited)); json_root = jso_new_obj(); if (NULL == json_root) { ONVIF_ERROR("NULL == json_root"); ret = -1; goto exit; } jso_add_string(json_root, "method", "do"); json_module = jso_new_obj(); if (NULL == json_module) { ONVIF_ERROR("NULL == json_module"); ret = -1; goto free_json_obj; } jso_obj_add(json_root, "motion_detection", json_module); json_action = jso_new_obj(); if (NULL == json_action) { ONVIF_ERROR("NULL == json_action"); ret = -1; goto free_json_obj; } jso_obj_add(json_module, "add_md_regions", json_action); json_param = jso_new_array();//注意,创建数组不能用jso_new_obj,在添加元素时会失败 if (NULL == json_param) { ONVIF_ERROR(""); ret = -1; goto free_json_obj; } jso_obj_add(json_action, "region_info", json_param); ret = onvif_base64_decode_alloc(active_cells, inlen, &packbit_buf, &packbit_len); if (ret == FALSE || (ret == TRUE && !packbit_buf)) { ONVIF_ERROR("ret[%d], packbuf[%p]", ret, packbit_buf); ret = -1; goto free_json_obj; } byte_len = tiff6_unPackBits(packbit_buf, packbit_len, byte_buf, array_len); if (byte_len < 0) { ret = -1; goto free_packbit_buf; } // 遍历整个bitmask for (r = 1; r <= CELL_LAYOUT_ROWS; r++) { if (region_cnt >= SLP_REGION_MAX_NUM) { break; } for (c = 1; c <= CELL_LAYOUT_COLS; c++) { if (region_cnt >= SLP_REGION_MAX_NUM) { break; } byte_idx = ((r - 1) * CELL_LAYOUT_COLS + c - 1) / BITS_IN_BYTE; bit_idx = BITS_IN_BYTE - 1 - ((r - 1) * CELL_LAYOUT_COLS + c - 1) % BITS_IN_BYTE; mask = 0x1<<bit_idx; if (cell_visited[byte_idx] & mask) { continue; } // find a new region if (byte_buf[byte_idx] & mask) { top_left_row = r; top_left_col = c; bottom_row = top_left_row; right_col = CELL_LAYOUT_COLS; found = 0; // 从新矩阵左上角开始遍历bitmask for (sub_r = top_left_row; sub_r <= CELL_LAYOUT_ROWS; sub_r++) { // 找到最大矩形 if (found) { break; } for (sub_c = top_left_col; sub_c <= right_col; sub_c++) { sub_byte_idx = ((sub_r - 1) * CELL_LAYOUT_COLS + sub_c - 1) / BITS_IN_BYTE; sub_bit_idx = BITS_IN_BYTE - 1 - ((sub_r - 1) * CELL_LAYOUT_COLS + sub_c - 1) % BITS_IN_BYTE; sub_mask = 0x1<<sub_bit_idx; // blank cell if (!(byte_buf[sub_byte_idx] & sub_mask)) { // 第一列出现空cell,已找到最大矩形 if (sub_c == top_left_col) { found = 1; bottom_row = sub_r - 1; } else { right_col = (sub_c - 1) < right_col ? (sub_c - 1) : right_col; } break; } } } // 到达最底部第一列都没有空的cell if (!found) { bottom_row = CELL_LAYOUT_ROWS; } if (region_cnt < SLP_REGION_MAX_NUM) { // 设置visited mask for (sub_r = top_left_row; sub_r <= bottom_row; sub_r++) { for (sub_c = top_left_col; sub_c <= right_col; sub_c++) { sub_byte_idx = ((sub_r - 1) * CELL_LAYOUT_COLS + sub_c - 1) / BITS_IN_BYTE; sub_bit_idx = BITS_IN_BYTE - 1 - ((sub_r - 1) * CELL_LAYOUT_COLS + sub_c - 1) % BITS_IN_BYTE; sub_mask = 0x1<<sub_bit_idx; cell_visited[sub_byte_idx] |= sub_mask; } } top_left_x = (int)ceilf(cell_width * (top_left_col - 1)); top_left_y = (int)ceilf(cell_height * (top_left_row - 1)); right_bottom_x = (int)floorf(cell_width * right_col); right_bottom_y = (int)floorf(cell_height * bottom_row); json_region = jso_new_obj(); if (NULL == json_region) { ONVIF_ERROR("NULL == json_region"); ret = -1; goto free_packbit_buf; } jso_add_string_from_int(json_region, "x_coor", top_left_x); jso_add_string_from_int(json_region, "y_coor", top_left_y); jso_add_string_from_int(json_region, "width", right_bottom_x - top_left_x); jso_add_string_from_int(json_region, "height", right_bottom_y - top_left_y); jso_array_add(json_param, json_region); region_cnt++; } } } } cmd = (char *)json_object_to_json_string(json_root); *json_set_region = (char *)malloc((strlen(cmd) + 1) * sizeof(char)); if (!*json_set_region) { ONVIF_TRACE("Malloc fail"); ret = -1; goto free_packbit_buf; } strcpy(*json_set_region, cmd); ret = 0; free_packbit_buf: if (packbit_buf) { free(packbit_buf); packbit_buf = NULL; } free_json_obj: jso_free_obj(json_root); exit: return ret; } /* 输入:设置移动侦测区域的字符串(可以是base64编码的,也可以是slp协议的json字符串), * 返回:slp协议的json对象指针,失败返回NULL */ json_object * parse_modify_rules_value(char *md_active_cells) { MOTION_DETECT motion_detect; int md_on = FALSE; int region_cnt = 0; char *json_set_region_str = NULL; json_object *json_md_set = NULL; json_object *json_sub_module = NULL; json_object *json_region = NULL; if (NULL == md_active_cells) { ONVIF_ERROR("ptr is NULL"); return NULL; } memset(&motion_detect, 0, sizeof(MOTION_DETECT)); if (0 == ds_read(MOTION_DETECT_PATH, &motion_detect, sizeof(MOTION_DETECT))) { ONVIF_ERROR("ds_read return 0"); goto error_out; } md_on = motion_detect.enabled; // md_active_cells是slp接口的json字符串 if (md_active_cells) { /* '{' 是非base64编码字符,且合法的json串中都会包含该符号 * 如果activeCell的值包括'{',则其不是base64编码,按私有协议处理 * 如果activeCell的值不包括'{',则按照onvif标准解析 */ // 如果md_active_cells 是json格式的字符串串,则解析该串并构造slp接口json对象 if (strstr(md_active_cells, "{")) { json_md_set = jso_from_string(md_active_cells); if (NULL == json_md_set) { ONVIF_TRACE("Error parsing json"); goto error_out; } if (FALSE == md_on) { ONVIF_TRACE("Motion_detect.enable is FALSE"); return json_md_set; } json_sub_module = jso_obj_get(json_md_set, "motion_detection"); if (NULL == json_sub_module) { ONVIF_TRACE("Error parsing json"); goto error_out; } json_sub_module = jso_obj_get(json_sub_module, "add_md_regions"); if (NULL == json_sub_module) { ONVIF_TRACE("Error parsing json"); goto error_out; } json_sub_module = jso_obj_get(json_sub_module, "region_info"); if (NULL == json_sub_module) { ONVIF_TRACE("Error parsing json"); goto error_out; } /* 获取数组元素个数:即区域个数 */ region_cnt = jso_array_length(json_sub_module); if (region_cnt == 0) { // 如果region_info的值为空,则构造默认的值 json_region = jso_new_obj();//jsonRegion不是数组,是数组的元素 if (NULL == json_region) { ONVIF_TRACE("Ptr is NULL"); goto error_out; } jso_add_string_from_int(json_region, "x_coor", 0); jso_add_string_from_int(json_region, "y_coor", 0); jso_add_string_from_int(json_region, "width", SLP_REGION_MAX_WIDTH); jso_add_string_from_int(json_region, "height", SLP_REGION_MAX_HEIGHT); jso_array_add(json_sub_module, json_region); return json_md_set; } else/* 区域个数不为0,直接返回 */ { return json_md_set; } } else/* 如果md_active_cells不是json串,而是base64Binary的字符串 */ { /* 根据onvif标准定义的的区域信息(base64Binary)-md_active_cells */ /* 获取SLP协议设置区域的json串命令-json_set_region_str */ if (0 != md_parse_active_cells_alloc(md_active_cells, &json_set_region_str)) { ONVIF_TRACE("Decode base64 error"); goto error_out; } json_md_set = jso_from_string(json_set_region_str); if (NULL == json_md_set) { ONVIF_TRACE("Error parsing json"); goto error_out; } return json_md_set; } } error_out: jso_free_obj(json_md_set); ONVIF_FREE(json_set_region_str); return NULL; } /****************************************************************************** * Copyright (c) 2018-2018 TP-Link Systems Inc. * * Filename: onvif_passthrough.c * Version: 1.0 * Description: 将配置参数写入内存及FLASH的相关接口 * Author: liyijie<liyijie@tp-link.com.cn> * Date: 2019-02-01 ******************************************************************************/ #include <stdio.h> #include <stdlib.h> #include <unistd.h> #include "onvif_passthrough.h" #include "soap_parse.h" #include "libds.h" #define PASSTHROUGH_TAG_NUM 128 /*最大支持的透传标签数量*/ typedef struct _PASSTHROUGH_HANDLE { char tag[LEN_TAG]; /*标签名(如"tds:GetDeviceInformation")*/ S32 (*xml_to_json)(SOAP_CONTEXT *soap, JSON_OBJ **req_obj); /*XML转JSON函数指针*/ S32 (*json_to_xml)(SOAP_CONTEXT *soap, JSON_OBJ *rsp_obj); /*JSON转XML函数指针*/ }PASSTHROUGH_HANDLE; LOCAL PASSTHROUGH_HANDLE g_passthrough_handle[PASSTHROUGH_TAG_NUM]; /*全局透传处理函数表*/ /****************************************************************************** * 函数名称: onvif_passthrough() * 函数描述: 执行json指令 * 输 入: params_obj -- 输入的json指令 * 输 出: rsp_obj -- 应答json object * 返 回 值: ERROR/OK ******************************************************************************/ S32 onvif_passthrough(SOAP_CONTEXT *soap, JSON_OBJ *req_obj, JSON_OBJ **rsp_obj) { DS_HANDLE_CONTEXT context; if (NULL == req_obj || rsp_obj == NULL) { ONVIF_TRACE("Params_obj is NULL."); return ERROR; } memset(&context, 0, sizeof(DS_HANDLE_CONTEXT)); /* 运行该透传接口。*/ ONVIF_TRACE("request:%s\n", json_object_get_string(req_obj)); context.req_obj = req_obj; if (OK != ds_parse(&context)) { ONVIF_TRACE("Parse method failed."); return ERROR; } context.group_mask = ROOT_MASK; ds_handle(&context); ONVIF_TRACE("Ds_handle OK."); *rsp_obj = context.res_obj; ONVIF_TRACE("response:%s\n", json_object_get_string(*rsp_obj)); return OK; } /****************************************************************************** * 函数名称: onvif_serve_passthrough() * 函数描述: onvif透传请求处理入口 * 输 入: soap -- soap结构体地址 * 输 出: N/A * 返 回 值: PASSTHROUGH_RET * 流 程:标签路由匹配:根据SOAP请求中的标签(如tds:GetDeviceInformation)查找对应的转换函数。 XML->JSON转换:调用注册的xml_to_json函数解析SOAP请求体,生成JSON请求对象。 业务处理:调用onvif_passthrough()执行实际业务逻辑(通过设备服务库)。 JSON->XML转换:将业务返回JSON对象转换成SOAP响应XML。 资源清理:释放临时创建的JSON对象。 ******************************************************************************/ PASSTHROUGH_RET onvif_serve_passthrough(SOAP_CONTEXT *soap) { U32 index = 0; PASSTHROUGH_RET ret = PASSTHROUGH_OK; JSON_OBJ *req_obj = NULL; JSON_OBJ *rsp_obj = NULL; if (soap == NULL || soap->tag[0] == '\0') { return PASSTHROUGH_ERROR; } for (index = 0; index < PASSTHROUGH_TAG_NUM; index++) { if (NULL == g_passthrough_handle[index].xml_to_json || NULL == g_passthrough_handle[index].json_to_xml) { ret = PASSTHROUGH_NOT_MATCH; break; } if (soap_match_tag(soap->tag, g_passthrough_handle[index].tag)) { if (OK != g_passthrough_handle[index].xml_to_json(soap, &req_obj) || OK != onvif_passthrough(soap, req_obj, &rsp_obj) || OK != g_passthrough_handle[index].json_to_xml(soap, rsp_obj)) { ret = PASSTHROUGH_SOAP_FAULT; break; } break; } } if (index >= PASSTHROUGH_TAG_NUM) { ret = PASSTHROUGH_NOT_MATCH; } jso_free_obj(req_obj); jso_free_obj(rsp_obj); return ret; } /****************************************************************************** * 函数名称: soap_tag_handle_add() * 函数描述: 注册onvif透传请求处理函数 * 输 入: tag -- 请求tag * handle -- 处理函数 * 输 出: N/A * 返 回 值: ERROR/OK ******************************************************************************/ void onvif_passthrough_handle_add(char *tag, void *xml_to_json, void *json_to_xml) { U32 index = 0; if ((NULL == tag) || (strlen(tag) >= 64) || (NULL == xml_to_json) || (NULL == json_to_xml)) { ONVIF_ERROR("soap handle add error, invalid arg."); return; } while ((index < PASSTHROUGH_TAG_NUM) && (NULL != g_passthrough_handle[index].xml_to_json) && (NULL != g_passthrough_handle[index].json_to_xml)) { index++; } if (index >= PASSTHROUGH_TAG_NUM) { ONVIF_ERROR("soap handle add error, max support %d tags.", PASSTHROUGH_TAG_NUM); return; } snprintf(g_passthrough_handle[index].tag, sizeof(g_passthrough_handle[index].tag), "%s", tag); g_passthrough_handle[index].xml_to_json = xml_to_json; g_passthrough_handle[index].json_to_xml = json_to_xml; return; } /****************************************************************************** * 函数名称: onvif_passthrough_handle_init() * 函数描述: 初始化onvif透传请求处理函数 * 输 入: N/A * 输 出: N/A * 返 回 值: ERROR/OK ******************************************************************************/ void onvif_passthrough_handle_init() { U32 index = 0; for (index = 0; index < PASSTHROUGH_TAG_NUM; index++) { memset(g_passthrough_handle[index].tag, 0, sizeof(g_passthrough_handle[index].tag)); g_passthrough_handle[index].xml_to_json = NULL; g_passthrough_handle[index].json_to_xml = NULL; } tds_passthrough_init(); imaging_passthrough_init(); tan_passthrough_init(); trt_passthrough_init(); tptz_passthrough_init(); tr2_passthrough_init(); return; } /* 标签路由匹配:当SOAP请求到来时,根据请求中的标签(如"tds:GetDeviceInformation")在全局处理函数表g_passthrough_handle中查找对应的处理函数。 XML->JSON转换:如果找到匹配的标签,调用注册的xml_to_json函数,将SOAP请求XML转换为JSON对象。 业务处理:调用onvif_passthrough()函数,它进一步调用ds_parse()和ds_handle()来执行实际的业务逻辑,并将结果返回JSON对象。 JSON->XML转换:然后调用注册的json_to_xml函数,将JSON响应对象转换回SOAP响应XML。 资源清理:释放临时创建的JSON对象,避免内存泄漏。 *//****************************************************************************** * Copyright (c) 2015-2018 TP-Link Systems Inc. * * 文件名称: packbits.c * 版 本: 1.0 * 摘 要: compress/decompress image with packbits * 作 者: wupimin <wupimin@tp-link.com.cn> * 创建时间: 2018-02-23 ******************************************************************************/ #include <stdio.h> #include <string.h> #include "packbits.h" static signed char* Pack_init(unsigned char Pack[], unsigned char Byte); static signed char* Pack_byte(signed char* Count, unsigned char Byte); static unsigned char* End_byte(signed char* Count); static int unPack_count(char Pack[], int count); int tiff6_PackBits(unsigned char array[], int count, unsigned char Pack[]) { int i = 0; signed char* Count = Pack_init(Pack, array[i]); i++; for (; i < count; i++) { Count = Pack_byte(Count, array[i]); } unsigned char* End = End_byte(Count); *End = '\0'; return (End - Pack); } int tiff6_unPackBits(char Pack[], int count, unsigned char array[], int array_len) { if (!Pack) { return -1; } if (!array) { return unPack_count(Pack, count); } int nRes = 0; signed char* Count = (signed char*)Pack; while ((char*)Count < (Pack+count)) { int c = *Count; if (c<0) { int n = (1-c); if (nRes + n <= array_len) { memset(&(array[nRes]), Count[1], n); nRes += n; } else { return -1; } } else { int n = (1+c); if (nRes + n <= array_len) { memcpy(&(array[nRes]), &Count[1], n); nRes += n; } else { return -1; } } Count = (signed char*)End_byte(Count); } return nRes; } int unPack_count(char Pack[], int count) { int nRes = 0; signed char* Count = (signed char*)Pack; while ((char*)Count < (Pack+count)) { int c = *Count; if (c<0) { nRes += (1-c); } else { nRes += (1+c); } Count = (signed char*)End_byte(Count); } return nRes; } signed char* Pack_init( unsigned char Pack[], unsigned char Byte ) { signed char* Cnt = (signed char*)Pack; *Cnt = 0; Pack[1] = Byte; return Cnt; } unsigned char* End_byte( signed char* Count ) { unsigned char* Pack = (unsigned char*)(Count+1); signed char c = *Count; if (c >0) { Pack = &(Pack[c+1]); } else { Pack = &(Pack[1]); } return Pack; } signed char* Pack_byte( signed char* Count, unsigned char Byte ) { signed char c = *Count; unsigned char* End = End_byte(Count); if (127 == c || c == -127) { return Pack_init(End, Byte); } else { unsigned char* Pack = End-1; if (*(Pack) == Byte) { if (c >0) { (*Count) = c-1; Count = Pack_byte(Pack_init(Pack, Byte), Byte); } else { (*Count)--; } } else { if (c >= 0) { *End = Byte; (*Count)++; } else { Count = Pack_init(End, Byte); } } } return Count; }
09-04
def read_dark_calibration_data(file_path): """ 读取暗场校准文件并处理其中的图像数据 对应C++中的暗场校准流程 """ allData = {} s_file_path = Path(file_path) # 2. 检查文件是否存在 (对应 QFileInfo::exists()) if not s_file_path.exists(): print("Dark calibration file does not exist!") return False try: # 3. 以二进制模式打开文件 (对应 fopen) with open(s_file_path, 'rb') as p_file: # 4. 读取配置信息 (对应 fread pCfgDt) # 假设 JYW_10M 为 10MB,即 10 * 1024 * 1024 字节 JYW_10M = 10 * 1024 * 1024 p_cfg_dt = p_file.read(JYW_10M) # 检查是否读取到足够的数据 if len(p_cfg_dt) != JYW_10M: print("Error: Error reading dark file configuration section") return False # 5. 解析配置信息 (对应 QString::fromLocal8Bit 和 JYWData::fromJsonStr) # 假设配置信息是JSON格式,存储在文件开头,可能并非全部 JYW_10M 都是有效配置数据 # 需要找到实际的配置数据结束位置(例如,遇到空字符或有效的JSON结束) try: # 尝试找到第一个空字符,其后的数据可能是二进制图像数据 null_pos = p_cfg_dt.find(b'\0') if null_pos != -1: config_json_str = p_cfg_dt[:null_pos].decode('utf-8') # 或根据实际编码调整 else: # 如果没有空字符,可能整个块都是配置数据(但不太可能) config_json_str = p_cfg_dt.decode('utf-8') # 需确认编码 config_data = json.loads(config_json_str) except (UnicodeDecodeError, json.JSONDecodeError) as e: print(f"Error parsing configuration data: {e}") return False # 6. 提取相机暗场数据和位置数据数组 (对应 JYWData 的查找操作) # 以下键名需根据实际的 JYW_CB_FD_* 定义调整 camera_dark_data = config_data.get("cameralDarkData", {}) pos_data_array = camera_dark_data.get("posData", []) file_name = camera_dark_data.get("fileName", "unknown") # 模拟开始更新暗场校准数据 # factory_calibration().StartUpdateDarkCalibrationData() # 在Python中,这可能对应一个类的实例方法 # 例如:calibration_system.start_update_dark_calibration_data() # 8. 遍历位置数据数组,读取每幅图像 for pos_data in pos_data_array: start_pos = pos_data.get("startPos", 0) # 数据起始位置 byte_size = pos_data.get("byteSize", 0) # 数据大小 exposure_time = pos_data.get("exposureTime", 0.0) # 曝光时间 width = pos_data.get("width", 0) # 图像宽 height = pos_data.get("height", 0) # 图像高 if byte_size == 0 or width == 0 or height == 0: print("Error: Invalid image parameters in position data") continue # 移动文件指针到图像数据开始位置 (对应 fseek) p_file.seek(start_pos) # 读取图像数据 (对应 fread pBuffer) image_data = p_file.read(byte_size) if len(image_data) != byte_size: print("Error: Error reading image data from dark file") continue # 9. 将数据转换为OpenCV格式 (对应 cv::Mat 构造) # 注意: CV_16UC1 对应 numpy.uint16, 单通道 # 使用 np.frombuffer 避免复制数据,提高效率 np_img_data = np.frombuffer(image_data, dtype=np.uint16).reshape(height, width) np_img_data_f = np.array(np_img_data, dtype=np.float32) # 转为浮型用于线性插值 print(f"exptime:{exposure_time}, max:{np.max(np_img_data)}, mean:{np.mean(np_img_data)}") allData[exposure_time] = np_img_data_f # 10. 添加暗场校准数据 (对应 factoryCalibration()->AddDarkCalibrationData) # 这里调用你的Python校准模块的相应函数 # add_dark_calibration_data(np_img_data, exposure_time) # 11. (可选) 保存图像用于调试 (对应 cv::imwrite) # 12. 完成更新 (对应 factoryCalibration()->FinishUpdateDarkCalibrationData) # result = finish_update_dark_calibration_data() result = 0 # 假设成功 if result == 0: print("Read dark file successfully!") else: print("Error: Read dark file failed, image null or LV too high!") except IOError as e: print(f"Error opening/reading dark calibration file: {e}") return allData
最新发布
11-06
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符  | 博主筛选后可见
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值