Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 9 additions & 9 deletions MEDIAINFO_NATIVE_JSON.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,12 @@

MediaInfoLib 26.05 的 `Output=JSON` 在当前四 ABI 构建配置中可用, JSON 语法和外层结构稳定, 适合作为未来结构化接口的数据源. 它不能在不改变语义的情况下直接替换 `autojs6-plugin-mediainfo-snapshot-v1`: 原生 JSON 使用机器字段名和原始数值, 可包含嵌套对象及上游诊断字段, 而 v1 使用文本报告中的显示标签, 格式化值和插件生成的 `file` 小节.

因此 v2.1.x 的决定是:
因此当前演进决定是:

1. 保留内部 `MediaInfo.getMIJson()` 与 JNI 安全切换能力, 作为测试和后续 schema 设计的基础.
2. `inform`, `get`, `snapshot` 三个公开行为和 AIDL 均不变; `snapshot-v1` 继续以文本报告解析结果为唯一数据源.
3. 不对原生 JSON 做静默透传, 也不在上游更新时自动改变公开快照.
4. 如后续引入 `snapshot-v2`, 必须采用新的 schema 标识和显式宿主协商, 并由插件维护稳定字段映射, 单位与类型规则.
2. `inform`, `get` 与缺省 `snapshot` 行为及 AIDL 方法签名不变; `snapshot-v1` 继续以文本报告解析结果为唯一数据源.
3. 插件侧提供显式 opt-in 的 `snapshot-v2` 稳定 envelope, 但不对原生根对象做透传, 也不在上游更新时自动替换 v1.
4. 宿主与共享 API 接入前, v2 只作为兼容基础和测试面存在; 完整契约与协同边界见 [`MEDIAINFO_SNAPSHOT_V2.md`](MEDIAINFO_SNAPSHOT_V2.md).

## 官方源码审查

Expand Down Expand Up @@ -72,13 +72,13 @@ MediaInfoLib 26.05 的 `Output=JSON` 在当前四 ABI 构建配置中可用, JSO
| 结构 | section -> array -> string map | 字符串, 属性对象与嵌套 `extra` 可混合 | v1 的 `Map<String, String>` 模型不能无损承载 |
| 诊断字段 | 当前损坏样本只保留四个 General 显示字段 | 另含 `extra.IsTruncated=Yes` | 非稳定诊断信息会重新进入公开契约 |

## 后续 snapshot-v2 门槛
## snapshot-v2 门槛与当前进度

未来如推进 `snapshot-v2`, 至少应满足:
插件侧第一阶段已经满足显式 schema 标识, v1 缺省兼容, 原生 envelope 校验, 多流分组, `@` 属性 / `extra` 分区, schema 缓存隔离和当前 26.05 实机往返测试. 仍需满足的协同与长期门槛包括:

- 使用新的 AIDL/API 能力协商与 `schema` 值, 不复用 v1 名称.
- 明确区分稳定规范字段, 上游扩展字段和显示文本; 不把整个 track 对象无版本透传.
- 在共享 API 中固化 capability / option 常量, 并由宿主显式协商 `schema`; 不复用 v1 名称.
- 继续区分稳定 envelope, 上游扩展字段和显示文本; 不把整个 track 对象无版本透传.
- 为时长, 大小, 码率, 采样率等定义类型与单位, 同时决定是否保留显示值.
- 统一重复流, `@typeorder`, nested `extra` 和二进制 / cover data 的表示及大小上限.
- 评估 `fields` 中常用字段的可选强类型视图, 并定义二进制 / cover data 的表示及大小上限.
- 固定至少前一稳定版与当前稳定版的 JSON fixtures, 并在每次上游更新 Draft PR 中审阅 schema diff.
- 使用现有 MP4, WebM, FLAC, 损坏文件, 多流文件与大文件矩阵回归; 上游升级不得自动合并或发布结构变化.
122 changes: 122 additions & 0 deletions MEDIAINFO_SNAPSHOT_V2.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
# MediaInfo snapshot-v2 契约

## 状态与边界

本文档定义 `autojs6-plugin-mediainfo-snapshot-v2` 的稳定插件契约. M2 协同实现已接入共享 `mediainfo-api`, AutoJs6 宿主及 Node / Rhino 公共 API, 但不会替换 v1: 普通脚本仍默认得到 v1, 只有显式选择并通过能力协商后才得到 v2.

设计遵循三个边界:

1. v1 是缺省协议, 现有调用无需修改且输出语义不变.
2. v2 必须由调用方显式选择, 不根据插件版本或 MediaInfoLib 版本自动切换.
3. MediaInfoLib 原生 JSON 只是数据源. 插件拥有 v2 外层契约, 不把上游根对象直接作为公共快照返回.

## Schema 协商

现有 AIDL 已允许 `snapshot(fd, displayName, Bundle options)`, 因此插件侧无需修改 Binder 方法签名. `options` 使用以下值:

| Bundle 键 | 类型 | 值 | 行为 |
| --- | --- | --- | --- |
| `schema` | `String` | 缺失, `null`, 空白或 `autojs6-plugin-mediainfo-snapshot-v1` | 返回 v1 |
| `schema` | `String` | `autojs6-plugin-mediainfo-snapshot-v2` | 返回 v2 |
| `schema` | `String` | 其他值 (包括在标识两侧增加空白) | 以 `IllegalArgumentException` 拒绝, 不猜测或修正版本 |
| `includeInform` | `Boolean` | 缺省 `true` | 决定 `inform` 是否携带文本报告 |
| `includeSections` | `Boolean` | 缺省 `true` | v1 决定 `sections`; v2 为保持现有选项 ABI, 决定 `tracks` |

插件通过 `PluginInfo.capabilities` 广告能力:

| capability 键 | 类型 | 当前值 |
| --- | --- | --- |
| `snapshotSchemas` | `String[]` | v1, v2, 按优先兼容顺序排列 |
| `defaultSnapshotSchema` | `String` | `autojs6-plugin-mediainfo-snapshot-v1` |
| `engineVersion` | `String` | MediaInfoLib `Info_Version` 去除首尾空白后的非空结果; 查询失败时省略 |

共享 API 已通过 `MediainfoOptionKeys`, `MediainfoPluginCapabilityKeys` 与 `MediainfoSnapshotSchemas` 定义这些键名, schema 标识, 缺省策略及稳定的不支持错误前缀; 插件随仓库内 `libs/mediainfo-api.aar` 使用同一份定义. 这些附加 capability 不改变现有 `REQUIRES_HOST_VERSION = 3923`, 因为旧宿主会忽略未知 Bundle 键, 且缺省 snapshot 仍是 v1.

`Info_Parameters` 不放入发现阶段的 capability Bundle. 该值体积大, 应在后续通过专用 AIDL 查询按需取得, 避免每次插件发现都跨 Binder 传输完整参数表.

## v2 Envelope

示例:

```json
{
"schema": "autojs6-plugin-mediainfo-snapshot-v2",
"file": {
"name": "movie.mkv",
"sizeBytes": 4096
},
"engine": {
"name": "MediaInfoLib",
"version": "26.05",
"url": "https://mediaarea.net/MediaInfo"
},
"inform": "",
"tracks": {
"general": [
{
"fields": {
"Format": "Matroska",
"FileSize": 4096
}
}
],
"audio": [
{
"fields": {
"Format": "PCM",
"SamplingRate": 48000
},
"attributes": {
"@typeorder": 1
},
"extra": {
"IsTruncated": "Yes"
}
}
]
}
}
```

外层规则:

| 路径 | 契约 |
| --- | --- |
| `schema` | 始终为完整 v2 标识 |
| `file.name` | 调用方传入的显示名; 无值时为空字符串 |
| `file.sizeBytes` | 插件实际解析的数据源大小, JSON 整数 |
| `engine.name` | 原生 `creatingLibrary.name`, 必须为非空字符串 |
| `engine.version` | 原生 `creatingLibrary.version`, 必须为非空字符串 |
| `engine.url` | 原生值为非空字符串时才出现; 其他 `creatingLibrary` 字段不透传 |
| `inform` | `includeInform=true` 时为文本报告, 否则为空字符串; 键始终存在 |
| `tracks` | `includeSections=true` 时按流类型分组, 否则为空对象; 键始终存在 |

Track 规则:

- 原生 `media.track[]` 的 `@type` 使用 `Locale.US` 转为小写并作为 `tracks` 的分组键. 分组键必须匹配 `[a-z][a-z0-9_-]*`.
- 同类流保持原生数组顺序. 数组下标就是从 0 开始的 `streamNumber`; v1 的 `audio #1` / `audio #2` 显示标题不再承担寻址语义.
- `@type` 只用于分组, 不进入 track 对象.
- 其他以 `@` 开头的原生键原名放入 `attributes`; 没有属性时省略该对象.
- 原生 `extra` 必须是对象并独立放入 `extra`; 没有 `extra` 时省略. 诊断字段由上游定义, 消费方不得假定固定集合.
- 其余原生 track 成员原名放入 `fields`. 字符串, 数字, 布尔值, `null`, 数组和对象保留 JSON 类型, 不转为 v1 的显示文本或 camelCase.

只有 v2 的 envelope, 分组方式与分区规则属于插件稳定契约. `fields`, `attributes` 和 `extra` 内的具体键、值与可用性仍是 MediaInfoLib 上游扩展面; 升级原生库时必须审阅差异, 但不应把新增字段误判为 v2 envelope 破坏.

## 兼容与错误行为

- 未指定 `schema` 的旧调用继续走原有文本 Inform 解析路径, 包括 v1 的 `fileName`, `sizeBytes`, `inform` 与 `sections`.
- v1 与 v2 使用包含 schema 标识的不同缓存键, 相同文件和选项不会串用结果.
- v2 原生 JSON 为空时, regular FD 直读仍按现有策略回退到插件私有副本; 私有副本仍无法解析时返回空结果.
- 原生 JSON envelope 缺少 `creatingLibrary`, `media.track`, 必需字符串或合法 track 类型时显式失败, 不生成看似成功但结构不完整的 v2.
- 解析, FD 回退, 30 秒取消和超时边界沿用现有服务机制.

## 协同接入状态

- [x] 共享 API 通过 `MediainfoOptionKeys` / capability keys / `MediainfoSnapshotSchemas` 统一本页常量, 并将新 AAR 同步回插件.
- [x] 宿主发现插件时读取 `snapshotSchemas`; 只有 capability 明确包含 v2 才发送 v2 schema, 对旧插件保留缺省 v1 路径.
- [x] Node / Rhino API 提供显式 schema 选择与 `capabilities()`, 默认保持 v1, 不支持显式 v2 时返回稳定错误.
- [x] 双引擎类型定义, API 文档与示例已同步; v2 的 `fields`, `attributes` 与 `extra` 均保留为动态 JSON 扩展面.
- [x] 插件侧多轨, 多字幕, MP4, WebM, FLAC + cover, 损坏文件及大文件矩阵保持通过; QV710AF65F 进一步通过真实 AIDL, Rhino 生产引擎, Node 直连 / compat 门面及真实 Android Provider 的跨仓库端到端验证.
- [ ] `Info_Parameters` 继续延后. 如后续决定公开, 新增按需查询方法与响应大小测试, 不扩张发现 Bundle.

因此 Roadmap 已勾选 “快照 schema v2 (协同项)” 与小体积的 “引擎版本透出”; `Info_Parameters`, 直接 `get` 流序号, 流计数与 InfoKind 仍保持独立未完成条目.
13 changes: 8 additions & 5 deletions ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
|---|---|---|
| M0 | 基线能力 (v1.0.0) | 已完成 |
| M1 | 文档与工程化 | 进行中 |
| M2 | 读取能力增强 | 规划中 |
| M2 | 读取能力增强 | 进行中 |
| M3 | 性能与大文件 | 已完成 |
| M4 | 健壮性与诊断 | 进行中 |
| M5 | 原生库演进 (v2.0.0+) | 进行中 |
Expand Down Expand Up @@ -67,19 +67,22 @@

******

## M2 读取能力增强 - 规划中
## M2 读取能力增强 - 进行中

******

说明: JNI 封装 `MediaInfo.kt` 已具备 `streamNum`, `InfoKind`, `countGet`, `getMIOption` 等完整能力; AIDL 与插件服务已可接收 `streamNumber`, 但当前宿主 Node / Rhino API 仍固定查询同类流第 1 条, InfoKind 也固定为参数值文本. 本里程碑以 "接线已有能力" 为主, 原生层无需改动.

当前进度: snapshot-v2 已完成插件, 共享 API, AutoJs6 宿主, Rhino / Node 公共入口, 类型声明与文档的协同接入. 缺省及空白 schema 仍返回 v1, 只有精确请求且插件明确广告 v2 时才启用 v2, 未知或两侧带空白的 schema 显式拒绝. v2 以 MediaInfoLib 原生 JSON 为数据源, 将同类流按数组分组并隔离动态字段, 属性与诊断扩展; `Info_Version` 已通过 capability 按需暴露, `Info_Parameters` 继续延后. (落点: `MEDIAINFO_SNAPSHOT_V2.md`, `MediainfoSnapshot.kt`, `PluginRuntimeInfo.kt`, `libs/mediainfo-api.aar`, AutoJs6 `MediainfoPluginHost.kt` / `Mediainfo.kt` / `NodeBridgeProtocol.kt`)

- [ ] `get` 支持流序号 (协同项): AIDL 层以选项或新方法携带 `streamNumber`, Node / Rhino API 同步透出, 使脚本可查询第 2 条及之后的音轨 / 字幕. (落点: `MediaInfo.kt` 的 `get(filename, streamKind, streamNum, parameter)`, `MediainfoPluginService.kt`)
- [ ] 流计数查询 (协同项): 透出 `countGet`, 返回某流类型的流数量, 配合流序号实现多流遍历. (落点: `MediaInfo.kt` 的 `countGet`)
- [ ] InfoKind 扩展查询 (协同项): 支持 `MEASURE` / `INFO` / `NAME_TEXT` 等信息种类, 获取参数单位, 说明与本地化名称. (落点: `MediaInfo.kt` 的 `InfoKind` 枚举)
- [ ] 引擎信息透出: 经 `getMIOption` 提供 `Info_Version` / `Info_Parameters` 等库级信息, 便于脚本诊断与参数发现. (落点: `MediaInfo.kt` 的 `getMIOption`)
- [ ] 快照 schema v2 (协同项): 规范化多流 sections 表示 (以数组序号取代 `audio #1` 式小节名), 明确字段命名规则与 schema 版本协商方式, 保持对 v1 消费方的兼容期.
- [x] 引擎版本透出 (协同项): 插件发现 capability 以小体积 `Info_Version` 提供 `engineVersion`, 共享 API 定义稳定键名, Rhino `mediainfo.capabilities()` 与 Node `require("mediainfo").capabilities()` 均可查询; 查询失败时安全省略版本字段. (落点: `MediaInfo.kt` 的 `getMIOption`, `PluginRuntimeInfo.kt`, `MEDIAINFO_SNAPSHOT_V2.md`, `libs/mediainfo-api.aar`, AutoJs6 `MediainfoPluginHost.kt` / `Mediainfo.kt` / `NodeBridgeProtocol.kt`)
- [ ] `Info_Parameters` 按需查询 (协同项, 继续延后): 大体积参数表不进入发现阶段的 capability Bundle; 如后续需要公开, 使用专用 AIDL / 宿主脚本入口并补充响应大小与 Binder 边界测试. (落点: `MediaInfo.kt` 的 `getMIOption`, `MEDIAINFO_SNAPSHOT_V2.md`)
- [x] 快照 schema v2 (协同项): 规范化多流表示 (以数组下标取代 `audio #1` 式小节名), 明确字段分区, 命名边界与 schema 版本协商方式, 保持 v1 为默认协议. 插件契约, opt-in 实现, 缓存隔离, 共享 API 常量, 宿主能力协商, Rhino / Node 公共入口, 类型声明, 文档与 ARM64 实体机端到端验收均已完成. (落点: `MEDIAINFO_SNAPSHOT_V2.md`, `MediainfoSnapshot.kt`, `MediainfoPluginService.kt`, `MediaInfoSnapshotV2ContractTest.kt`, `libs/mediainfo-api.aar`, AutoJs6 `MediainfoSnapshotSchemas.kt` / `MediainfoPluginHost.kt` / `MediainfoSnapshotRhinoInstrumentationTest.kt` / `NodeMediaBridgeInstrumentationTest.kt`)

验收条件: 双引擎示例脚本可读取多音轨样本的第 2 条流及其单位信息; `.readme` / `plugin_instruction` 文档同步更新; 涉及 AIDL 变更的条目与宿主版本要求 (`REQUIRES_HOST_VERSION`) 联动更新.
验收进度: snapshot-v2 子项已在 QV710AF65F 上通过插件真实 AIDL, Rhino 生产引擎, Node 直连 / compat 门面与真实 Android Provider 验证, 双引擎类型声明及文档已同步; 本里程碑整体仍等待直接 `get` 流序号, 流计数与 InfoKind 单位信息. 涉及未来 AIDL 变更的条目仍须与宿主版本要求 (`REQUIRES_HOST_VERSION`) 联动更新.

******

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
package io.github.supermonster003.autojs6.plugin.mediainfo

import androidx.test.ext.junit.runners.AndroidJUnit4
import org.autojs.plugin.mediainfo.api.MediainfoSnapshotSchemas
import org.json.JSONObject
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.junit.Test
import org.junit.runner.RunWith

@RunWith(AndroidJUnit4::class)
class MediaInfoSnapshotV2ContractTest {

@Test
fun groupsRepeatedTracksAndPreservesUpstreamValueTypes() {
val snapshot = JSONObject(
MediaInfoSnapshotV2.build(
fileName = "two-tracks.mka",
sizeBytes = 4_096,
inform = "General\nFormat : Matroska",
includeInform = true,
includeTracks = true,
nativeJson = nativeFixture,
),
)

assertEquals(MediainfoSnapshotSchemas.V2, snapshot.getString("schema"))
assertEquals("two-tracks.mka", snapshot.getJSONObject("file").getString("name"))
assertEquals(4_096, snapshot.getJSONObject("file").getLong("sizeBytes"))
assertEquals("General\nFormat : Matroska", snapshot.getString("inform"))

val engine = snapshot.getJSONObject("engine")
assertEquals("MediaInfoLib", engine.getString("name"))
assertEquals("26.05", engine.getString("version"))
assertEquals("https://mediaarea.net/MediaInfo", engine.getString("url"))
assertFalse("Unspecified native library keys leaked into the stable engine", engine.has("build"))

val tracks = snapshot.getJSONObject("tracks")
assertEquals(1, tracks.getJSONArray("general").length())
val audio = tracks.getJSONArray("audio")
assertEquals(2, audio.length())

val firstAudio = audio.getJSONObject(0)
val fields = firstAudio.getJSONObject("fields")
assertEquals("PCM", fields.getString("Format"))
assertEquals(8_000, fields.getInt("SamplingRate"))
assertTrue(fields.getBoolean("Default"))
assertFalse("The grouping discriminator leaked into fields", fields.has("@type"))
assertEquals(1, firstAudio.getJSONObject("attributes").getInt("@typeorder"))
assertEquals("Yes", firstAudio.getJSONObject("extra").getString("IsTruncated"))

assertEquals("AAC", audio.getJSONObject(1).getJSONObject("fields").getString("Format"))
}

@Test
fun payloadFlagsKeepTheEnvelopeAndOmitOptionalContent() {
val snapshot = JSONObject(
MediaInfoSnapshotV2.build(
fileName = "minimal.mka",
sizeBytes = 10,
inform = "must not leak",
includeInform = false,
includeTracks = false,
nativeJson = nativeFixture,
),
)

assertEquals("", snapshot.getString("inform"))
assertEquals(0, snapshot.getJSONObject("tracks").length())
assertEquals("MediaInfoLib", snapshot.getJSONObject("engine").getString("name"))
}

@Test
fun rejectsNativeShapesThatCannotSatisfyTheContract() {
val malformedFixtures = listOf(
"""
{
"creatingLibrary": { "name": "MediaInfoLib", "version": "26.05" },
"media": {}
}
""".trimIndent(),
"""
{
"creatingLibrary": { "name": "MediaInfoLib", "version": "26.05" },
"media": {
"track": [
{ "@type": "Audio Track", "Format": "PCM" }
]
}
}
""".trimIndent(),
)

malformedFixtures.forEach { nativeJson ->
val failure = runCatching {
MediaInfoSnapshotV2.build(
fileName = "invalid.media",
sizeBytes = 0,
inform = "",
includeInform = false,
includeTracks = false,
nativeJson = nativeJson,
)
}.exceptionOrNull()
assertTrue("Malformed native JSON was accepted: $nativeJson", failure != null)
}
}

private val nativeFixture = """
{
"creatingLibrary": {
"name": "MediaInfoLib",
"version": "26.05",
"url": "https://mediaarea.net/MediaInfo",
"build": "fixture-only"
},
"media": {
"@ref": "two-tracks.mka",
"track": [
{
"@type": "General",
"Format": "Matroska",
"FileSize": 4096
},
{
"@type": "Audio",
"@typeorder": 1,
"Format": "PCM",
"SamplingRate": 8000,
"Default": true,
"extra": {
"IsTruncated": "Yes"
}
},
{
"@type": "Audio",
"@typeorder": 2,
"Format": "AAC"
}
]
}
}
""".trimIndent()
}
Loading