Skip to content

Commit 26ab065

Browse files
Merge branch 'feature/mediainfo-snapshot-v2-contract' (#3)
2 parents a2bd717 + 9bc4ca9 commit 26ab065

20 files changed

Lines changed: 1684 additions & 36 deletions

File tree

.python/run_real_media_validation.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -221,15 +221,18 @@ def parser() -> argparse.ArgumentParser:
221221
result = argparse.ArgumentParser(description=__doc__)
222222
result.add_argument("--serial", required=True, help="Exact adb serial; an emulator is required by default")
223223
result.add_argument("--sample", action="append", required=True, help="Real media path; repeat for each file")
224-
result.add_argument("--output", help="Optional metadata-only JSON output path")
224+
result.add_argument("--output", help="Optional JSON output path")
225225
result.add_argument("--overwrite", action="store_true")
226226
result.add_argument("--skip-build", action="store_true")
227227
result.add_argument("--allow-physical-device", action="store_true")
228228
result.add_argument("--allow-large-transfer", action="store_true", help="Allow aggregate samples over 2 GiB")
229229
result.add_argument(
230230
"--capture-details",
231231
action="store_true",
232-
help="Include full reports, parsed sections, and fixed technical field queries in the JSON output",
232+
help=(
233+
"Include full reports, parsed sections, native MediaInfo JSON, and fixed technical field "
234+
"queries in the JSON output"
235+
),
233236
)
234237
result.add_argument(
235238
"--update-existing-package",

MEDIAINFO_NATIVE_JSON.md

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
# MediaInfoLib 原生 JSON 评估
2+
3+
## 结论
4+
5+
MediaInfoLib 26.05 的 `Output=JSON` 在当前四 ABI 构建配置中可用, JSON 语法和外层结构稳定, 适合作为未来结构化接口的数据源. 它不能在不改变语义的情况下直接替换 `autojs6-plugin-mediainfo-snapshot-v1`: 原生 JSON 使用机器字段名和原始数值, 可包含嵌套对象及上游诊断字段, 而 v1 使用文本报告中的显示标签, 格式化值和插件生成的 `file` 小节.
6+
7+
因此当前演进决定是:
8+
9+
1. 保留内部 `MediaInfo.getMIJson()` 与 JNI 安全切换能力, 作为测试和后续 schema 设计的基础.
10+
2. `inform`, `get` 与缺省 `snapshot` 行为及 AIDL 方法签名不变; `snapshot-v1` 继续以文本报告解析结果为唯一数据源.
11+
3. 插件侧提供显式 opt-in 的 `snapshot-v2` 稳定 envelope, 但不对原生根对象做透传, 也不在上游更新时自动替换 v1.
12+
4. 宿主与共享 API 接入前, v2 只作为兼容基础和测试面存在; 完整契约与协同边界见 [`MEDIAINFO_SNAPSHOT_V2.md`](MEDIAINFO_SNAPSHOT_V2.md).
13+
14+
## 官方源码审查
15+
16+
当前固定源码提供完整 JSON 能力:
17+
18+
- `Source/MediaInfo/Setup.h` 在未设置 `MEDIAINFO_EXPORT_NO``MEDIAINFO_JSON_NO` 时定义 `MEDIAINFO_JSON_YES`; 当前 Android CMake profile 未关闭该功能.
19+
- `MediaInfo_Config::Option("Output", value)` 将值写入进程级 `Inform` 配置, 并非 `MediaInfo` 实例私有选项.
20+
- `MediaInfo_Internal::Inform()` 为 JSON 生成 `creatingLibrary``media` 外层对象; 每条流放在 `media.track[]`, 流类型通过 `@type` 表示.
21+
- track 字段由当前解析结果的 `Info_Name` 动态生成. 官方源码没有独立的 JSON schema 版本, `creatingLibrary.version` 只能标识生成该结果的引擎版本.
22+
23+
对 v24.12, v25.04, v25.10, v26.01 与 v26.05 五个稳定标签逐一检查后, `creatingLibrary` / `media` / `track` 外层节点保持一致; 下面的结论只把这一 envelope 视为观察到的兼容性, 不把动态 track 字段提升为官方承诺.
24+
25+
相邻稳定版的源码差异也证明字段层不能视为固定契约. 下表统计 JSON / XML 共用的 `MediaInfo_Inform.cpp`, `OutputHelpers.cpp` 及文本字段资源; 即使外层 envelope 保持一致, 生成规则, 显示资源和解析器可用字段仍随版本演进.
26+
27+
| 官方稳定版区间 | 涉及文件 | 增加 | 删除 | `MediaInfo_Inform.cpp` 变化 | 默认字段语言变化 |
28+
| --- | ---: | ---: | ---: | ---: | ---: |
29+
| v24.12..v25.04 | 8 | 86 | 12 | +59 / -8 | +2 / -0 |
30+
| v25.04..v25.10 | 10 | 157 | 60 | +69 / -23 | +22 / -0 |
31+
| v25.10..v26.01 | 3 | 131 | 6 | +127 / -6 | +3 / -0 |
32+
| v26.01..v26.05 | 5 | 20 | 4 | +2 / -2 | 0 |
33+
34+
这些数字用于判断上游字段层的演进性质, 不表示每项差异都会影响任意给定媒体文件.
35+
36+
## JNI 隔离策略
37+
38+
`Output` 是进程级全局配置, 所以不能简单地在并行 Binder 调用中执行 `Option("Output", "JSON")` 后直接 `Inform()`. 本地桥接层采用以下顺序:
39+
40+
1. 获取专用互斥锁.
41+
2. 读取并保存 `Output_Get`.
42+
3. 临时切换为 `JSON` 或显式 `Text`.
43+
4. 生成 `Inform()`.
44+
5. 通过 RAII 恢复原输出格式及文本 `Complete` 状态, 包括异常路径.
45+
6. 释放锁.
46+
47+
`getMediaInfoOption()` 也使用同一把锁, 防止其他本地选项访问与临时输出格式交错. JSON 解析被取消时返回空字符串, 不返回混入取消说明的无效 JSON. JNI 仍只导出 `JNI_OnLoad`, 新方法通过 `RegisterNatives` 注册, 未扩大 ELF 导出面.
48+
49+
## API 31 ARM64 实测
50+
51+
2026-09-01 在 Sony XQ-AT72 (`QV710AF65F`, Android API 31, `arm64-v8a`) 上使用 MediaInfoLib 26.05 验证三个正常样本和一个损坏样本. 每项原生 JSON 都能由 Android `JSONObject` 解析, `creatingLibrary.name``MediaInfoLib`, track 类型与有效媒体的 v1 核心小节一致.
52+
53+
| 样本 | 原生 track 与字段数 | snapshot-v1 小节与字段数 | 原生 JSON 冷调用 | 文本 Inform 冷调用 |
54+
| --- | --- | --- | ---: | ---: |
55+
| MP4 | General 21 / Video 41 / Audio 24 | file 1 / general 10 / video 29 / audio 19 | 26.820 ms | 101.987 ms |
56+
| WebM | General 16 / Video 48 | file 1 / general 8 / video 28 | 34.629 ms | 32.541 ms |
57+
| FLAC + cover | General 27 / Audio 17 / Image 10 | file 1 / general 24 / audio 13 / image 10 | 221.928 ms | 233.456 ms |
58+
| 损坏 MP4 | General 11 | file 1 / general 4 | 21.784 ms | 22.009 ms |
59+
60+
两列时间都包含一次独立的完整文件解析, 只用于发现明显回归, 不能解释为 JSON 序列化本身比文本更快或更慢. 完整原始记录位于 [`benchmark/results/2026-09-01-api31-arm64-v8a-native-json-evaluation.json`](benchmark/results/2026-09-01-api31-arm64-v8a-native-json-evaluation.json), 仓库规范化 LF 字节的 SHA-256 为 `97ea9ebfe7f7498df46b37ff80e7336b7152a0e52aef9576bded2a7665279085`.
61+
62+
同机仪器测试还并发重复执行 8 次 JSON 读取与 8 次文本读取. 所有 JSON 均可解析, 所有文本报告仍以 `File` 开头并包含 `Audio`; JSON 输出模式没有泄漏到公开文本路径. 测试结束后四个 staged 样本, 插件测试包及插件开发包均已从设备删除.
63+
64+
## 与 snapshot-v1 的不可等价项
65+
66+
| 维度 | snapshot-v1 | MediaInfoLib 原生 JSON | 影响 |
67+
| --- | --- | --- | --- |
68+
| 文件小节 | 插件前置 `File` / `Complete name` 并解析为 `file.completeName` | 路径位于 `media.@ref`, 不存在 File track | 直接替换会删除公开字段和小节 |
69+
| 字段名 | 从显示标签转 camelCase, 例如 `Codec ID` -> `codecId` | 机器名, 例如 `CodecID`, `Part_Position_Total` | 机械 camelCase 无法保持现有名称与别名语义 |
70+
|| 面向人的格式化文本, 包含单位与组合说明 | 常为无单位原始值, 例如字节数, 秒数, Hz | 相同键会产生不同值语义 |
71+
| 字段集合 | 只包含文本 Inform 当前显示的字段 | 包含更多内部字段, 例如 count, header / data size | 小版本更新可增加或删除公开数据 |
72+
| 结构 | section -> array -> string map | 字符串, 属性对象与嵌套 `extra` 可混合 | v1 的 `Map<String, String>` 模型不能无损承载 |
73+
| 诊断字段 | 当前损坏样本只保留四个 General 显示字段 | 另含 `extra.IsTruncated=Yes` | 非稳定诊断信息会重新进入公开契约 |
74+
75+
## snapshot-v2 门槛与当前进度
76+
77+
插件侧第一阶段已经满足显式 schema 标识, v1 缺省兼容, 原生 envelope 校验, 多流分组, `@` 属性 / `extra` 分区, schema 缓存隔离和当前 26.05 实机往返测试. 仍需满足的协同与长期门槛包括:
78+
79+
- 在共享 API 中固化 capability / option 常量, 并由宿主显式协商 `schema`; 不复用 v1 名称.
80+
- 继续区分稳定 envelope, 上游扩展字段和显示文本; 不把整个 track 对象无版本透传.
81+
- 为时长, 大小, 码率, 采样率等定义类型与单位, 同时决定是否保留显示值.
82+
- 评估 `fields` 中常用字段的可选强类型视图, 并定义二进制 / cover data 的表示及大小上限.
83+
- 固定至少前一稳定版与当前稳定版的 JSON fixtures, 并在每次上游更新 Draft PR 中审阅 schema diff.
84+
- 使用现有 MP4, WebM, FLAC, 损坏文件, 多流文件与大文件矩阵回归; 上游升级不得自动合并或发布结构变化.

MEDIAINFO_SNAPSHOT_V2.md

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
# MediaInfo snapshot-v2 契约
2+
3+
## 状态与边界
4+
5+
本文档定义 `autojs6-plugin-mediainfo-snapshot-v2` 的稳定插件契约. M2 协同实现已接入共享 `mediainfo-api`, AutoJs6 宿主及 Node / Rhino 公共 API, 但不会替换 v1: 普通脚本仍默认得到 v1, 只有显式选择并通过能力协商后才得到 v2.
6+
7+
设计遵循三个边界:
8+
9+
1. v1 是缺省协议, 现有调用无需修改且输出语义不变.
10+
2. v2 必须由调用方显式选择, 不根据插件版本或 MediaInfoLib 版本自动切换.
11+
3. MediaInfoLib 原生 JSON 只是数据源. 插件拥有 v2 外层契约, 不把上游根对象直接作为公共快照返回.
12+
13+
## Schema 协商
14+
15+
现有 AIDL 已允许 `snapshot(fd, displayName, Bundle options)`, 因此插件侧无需修改 Binder 方法签名. `options` 使用以下值:
16+
17+
| Bundle 键 | 类型 || 行为 |
18+
| --- | --- | --- | --- |
19+
| `schema` | `String` | 缺失, `null`, 空白或 `autojs6-plugin-mediainfo-snapshot-v1` | 返回 v1 |
20+
| `schema` | `String` | `autojs6-plugin-mediainfo-snapshot-v2` | 返回 v2 |
21+
| `schema` | `String` | 其他值 (包括在标识两侧增加空白) |`IllegalArgumentException` 拒绝, 不猜测或修正版本 |
22+
| `includeInform` | `Boolean` | 缺省 `true` | 决定 `inform` 是否携带文本报告 |
23+
| `includeSections` | `Boolean` | 缺省 `true` | v1 决定 `sections`; v2 为保持现有选项 ABI, 决定 `tracks` |
24+
25+
插件通过 `PluginInfo.capabilities` 广告能力:
26+
27+
| capability 键 | 类型 | 当前值 |
28+
| --- | --- | --- |
29+
| `snapshotSchemas` | `String[]` | v1, v2, 按优先兼容顺序排列 |
30+
| `defaultSnapshotSchema` | `String` | `autojs6-plugin-mediainfo-snapshot-v1` |
31+
| `engineVersion` | `String` | MediaInfoLib `Info_Version` 去除首尾空白后的非空结果; 查询失败时省略 |
32+
33+
共享 API 已通过 `MediainfoOptionKeys`, `MediainfoPluginCapabilityKeys``MediainfoSnapshotSchemas` 定义这些键名, schema 标识, 缺省策略及稳定的不支持错误前缀; 插件随仓库内 `libs/mediainfo-api.aar` 使用同一份定义. 这些附加 capability 不改变现有 `REQUIRES_HOST_VERSION = 3923`, 因为旧宿主会忽略未知 Bundle 键, 且缺省 snapshot 仍是 v1.
34+
35+
`Info_Parameters` 不放入发现阶段的 capability Bundle. 该值体积大, 应在后续通过专用 AIDL 查询按需取得, 避免每次插件发现都跨 Binder 传输完整参数表.
36+
37+
## v2 Envelope
38+
39+
示例:
40+
41+
```json
42+
{
43+
"schema": "autojs6-plugin-mediainfo-snapshot-v2",
44+
"file": {
45+
"name": "movie.mkv",
46+
"sizeBytes": 4096
47+
},
48+
"engine": {
49+
"name": "MediaInfoLib",
50+
"version": "26.05",
51+
"url": "https://mediaarea.net/MediaInfo"
52+
},
53+
"inform": "",
54+
"tracks": {
55+
"general": [
56+
{
57+
"fields": {
58+
"Format": "Matroska",
59+
"FileSize": 4096
60+
}
61+
}
62+
],
63+
"audio": [
64+
{
65+
"fields": {
66+
"Format": "PCM",
67+
"SamplingRate": 48000
68+
},
69+
"attributes": {
70+
"@typeorder": 1
71+
},
72+
"extra": {
73+
"IsTruncated": "Yes"
74+
}
75+
}
76+
]
77+
}
78+
}
79+
```
80+
81+
外层规则:
82+
83+
| 路径 | 契约 |
84+
| --- | --- |
85+
| `schema` | 始终为完整 v2 标识 |
86+
| `file.name` | 调用方传入的显示名; 无值时为空字符串 |
87+
| `file.sizeBytes` | 插件实际解析的数据源大小, JSON 整数 |
88+
| `engine.name` | 原生 `creatingLibrary.name`, 必须为非空字符串 |
89+
| `engine.version` | 原生 `creatingLibrary.version`, 必须为非空字符串 |
90+
| `engine.url` | 原生值为非空字符串时才出现; 其他 `creatingLibrary` 字段不透传 |
91+
| `inform` | `includeInform=true` 时为文本报告, 否则为空字符串; 键始终存在 |
92+
| `tracks` | `includeSections=true` 时按流类型分组, 否则为空对象; 键始终存在 |
93+
94+
Track 规则:
95+
96+
- 原生 `media.track[]``@type` 使用 `Locale.US` 转为小写并作为 `tracks` 的分组键. 分组键必须匹配 `[a-z][a-z0-9_-]*`.
97+
- 同类流保持原生数组顺序. 数组下标就是从 0 开始的 `streamNumber`; v1 的 `audio #1` / `audio #2` 显示标题不再承担寻址语义.
98+
- `@type` 只用于分组, 不进入 track 对象.
99+
- 其他以 `@` 开头的原生键原名放入 `attributes`; 没有属性时省略该对象.
100+
- 原生 `extra` 必须是对象并独立放入 `extra`; 没有 `extra` 时省略. 诊断字段由上游定义, 消费方不得假定固定集合.
101+
- 其余原生 track 成员原名放入 `fields`. 字符串, 数字, 布尔值, `null`, 数组和对象保留 JSON 类型, 不转为 v1 的显示文本或 camelCase.
102+
103+
只有 v2 的 envelope, 分组方式与分区规则属于插件稳定契约. `fields`, `attributes``extra` 内的具体键、值与可用性仍是 MediaInfoLib 上游扩展面; 升级原生库时必须审阅差异, 但不应把新增字段误判为 v2 envelope 破坏.
104+
105+
## 兼容与错误行为
106+
107+
- 未指定 `schema` 的旧调用继续走原有文本 Inform 解析路径, 包括 v1 的 `fileName`, `sizeBytes`, `inform``sections`.
108+
- v1 与 v2 使用包含 schema 标识的不同缓存键, 相同文件和选项不会串用结果.
109+
- v2 原生 JSON 为空时, regular FD 直读仍按现有策略回退到插件私有副本; 私有副本仍无法解析时返回空结果.
110+
- 原生 JSON envelope 缺少 `creatingLibrary`, `media.track`, 必需字符串或合法 track 类型时显式失败, 不生成看似成功但结构不完整的 v2.
111+
- 解析, FD 回退, 30 秒取消和超时边界沿用现有服务机制.
112+
113+
## 协同接入状态
114+
115+
- [x] 共享 API 通过 `MediainfoOptionKeys` / capability keys / `MediainfoSnapshotSchemas` 统一本页常量, 并将新 AAR 同步回插件.
116+
- [x] 宿主发现插件时读取 `snapshotSchemas`; 只有 capability 明确包含 v2 才发送 v2 schema, 对旧插件保留缺省 v1 路径.
117+
- [x] Node / Rhino API 提供显式 schema 选择与 `capabilities()`, 默认保持 v1, 不支持显式 v2 时返回稳定错误.
118+
- [x] 双引擎类型定义, API 文档与示例已同步; v2 的 `fields`, `attributes``extra` 均保留为动态 JSON 扩展面.
119+
- [x] 插件侧多轨, 多字幕, MP4, WebM, FLAC + cover, 损坏文件及大文件矩阵保持通过; QV710AF65F 进一步通过真实 AIDL, Rhino 生产引擎, Node 直连 / compat 门面及真实 Android Provider 的跨仓库端到端验证.
120+
- [ ] `Info_Parameters` 继续延后. 如后续决定公开, 新增按需查询方法与响应大小测试, 不扩张发现 Bundle.
121+
122+
因此 Roadmap 已勾选 “快照 schema v2 (协同项)” 与小体积的 “引擎版本透出”; `Info_Parameters`, 直接 `get` 流序号, 流计数与 InfoKind 仍保持独立未完成条目.

0 commit comments

Comments
 (0)