开发订阅 Provider

July 29, 2026 · View on GitHub

English · 插件开发指南

一个订阅 Provider 提供一份连接表单和五个类型化 Hook。M3UAndroid 负责显示表单、保存 账号、调度刷新、导入频道和恢复播放 Session。

完整示例见 ReferenceExtensionService

五个 Hook

HookSpecSchema基础 capability输入与返回
SubscriptionHookSpecs.Discover4Locale → 一个 Provider Descriptor
SubscriptionHookSpecs.Validate2credential.write提交值 → 宿主认证回执
SubscriptionHookSpecs.Refresh4subscription.read账号与刷新原因 → Source 和完整频道快照
SubscriptionHookSpecs.ResolvePlayback4playback.resolve账号与播放引用 → URL、Header 和可选 Session
SubscriptionHookSpecs.ClosePlayback3playback.resolve账号、播放引用与 Session → 关闭结果

TypedExtensionService 中注册每个 HookSpec,再在 ExtensionManifest 中声明相同的 Hook 和 Schema Version。四个 Hook 会访问服务器,因此使用带 Broker 的处理函数:

init {
    handle(SubscriptionHookSpecs.Discover) { request, _ ->
        discoverProvider(request.localeTag)
    }
    handleResultWithBroker(SubscriptionHookSpecs.Validate) { request, _, broker ->
        validateProvider(request, broker)
    }
    handleResultWithBroker(SubscriptionHookSpecs.Refresh) { request, _, broker ->
        refreshProvider(request, broker)
    }
    handleResultWithBroker(SubscriptionHookSpecs.ResolvePlayback) { request, _, broker ->
        resolvePlayback(request, broker)
    }
    handleResultWithBroker(SubscriptionHookSpecs.ClosePlayback) { request, _, broker ->
        closePlayback(request, broker)
    }
}

Discover 离线运行。其他四个 Hook 声明 network。Broker 请求使用本次提交或已保存的 凭据句柄时,再声明 credential.read

1. 声明 Provider 和表单

Discover 返回一个 SubscriptionProviderDescriptorproviderId 和每个 ProviderKind 在不同版本间保持稳定。按显示顺序列出可选类型,并包含登录需要的全部 字段。只有历史账号仍需识别、但新订阅不应再选择的兼容类型,才设置 userSelectable = false;M3UAndroid 不会在新订阅选项中显示该类型。

private fun discoverProvider(localeTag: String?): SubscriptionProviderDiscoverResult {
    val copy = providerCopy(localeTag)
    return SubscriptionProviderDiscoverResult(
        provider = SubscriptionProviderDescriptor(
            providerId = extensionManifest.id,
            displayName = copy.providerName,
            variants = listOf(
                SubscriptionProviderVariant(
                    kind = ProviderKind("example"),
                    displayName = copy.variantName,
                )
            ),
            settingsSchema = providerSettings(localeTag),
        )
    )
}

Schema 必须包含必填的 base_url 文本字段。密码或 Token 使用 SECRET 字段。普通文本位于 request.settingValues,敏感字段位于 request.credentialHandles

面向用户的名称、Label、说明和选项都应按 request.localeTag 本地化,并在缺失或不支持时 回退插件默认语言。返回自然书写顺序的纯文本,不插入双向控制字符;RTL 隔离由宿主处理。 文本应能独立朗读;ID、URL 与 Handle 不翻译。

2. 认证账号

Validate 通过 broker.authenticate(...) 发送登录请求。需要告诉 Broker:登录响应中的 访问凭据位于哪里,以及 M3UAndroid 应保存哪些服务器或用户 ID 来识别账号。

val response = broker.authenticate(
    BrokerAuthenticationRequest(
        exchange = loginExchange,
        primaryCredentialSource = ResponseValueSource.JsonPointer("/accessToken"),
        opaqueContexts = listOf(
            OpaqueContextCapture(
                key = ProviderAuthenticationContextKeys.ServerId,
                source = ResponseValueSource.JsonPointer("/server_id"),
            ),
            OpaqueContextCapture(
                key = ProviderAuthenticationContextKeys.UserId,
                source = ResponseValueSource.JsonPointer("/user_id"),
            ),
        ),
    )
)

if (response.statusCode !in 200..299) {
    return HookResult.Failure(authenticationError(response.statusCode))
}

return HookResult.Success(
    SubscriptionProviderValidateResult(
        evidence = ProviderValidationEvidence.HostBrokerReceipt(
            receipt = requireNotNull(response.receipt),
        ),
    )
)

返回值只有状态码和回执。M3UAndroid 使用回执创建账号并保存凭据;插件不解析、也不返回 登录响应正文。

请求值、上下文、Capability 和错误处理见 使用宿主网络 Broker

3. 返回完整刷新快照

返回一个 SubscriptionSourceDescriptor 和完整频道快照。Schema 4 的 Source 只有 remoteIdproviderKind,没有 Title。

SubscriptionSourceDescriptor(
    remoteId = request.account.serverId,
    providerKind = request.account.providerKind,
)

每个频道都需要稳定的 remoteIdPlaybackReference。引用中只放稳定 ID。URL、Token 和 Cookie 在 ResolvePlayback 中解析。

M3UAndroid 会用完整快照更新 Provider 数据,同时保留宿主管理的频道本地状态。

4. 解析并关闭播放

ResolvePlayback 返回播放 URL、必要 Header、媒体源 ID 和可选的 PlaybackSessionDescriptor。已保存的凭据和账号信息使用 BrokerValue 引用; M3UAndroid 会在发起请求或打开媒体时解析。

只要返回了 Session,ClosePlayback 就会收到同一个 Descriptor,以及自己的账号作用域 Broker。关闭操作必须幂等。远端 Session 已经关闭时也返回成功。

验收标准

  1. Discover 返回一个 Descriptor,每个类型都能打开表单;
  2. 合法输入完成宿主管理的认证;
  3. 首次刷新导入完整快照并创建账号;
  4. 频道能使用宿主解析的 Header 播放;
  5. 停止播放会关闭远端 Session,重复关闭也成功。

下一步:通过宿主 Broker 发送认证请求,然后 测试插件