# 原生离线下载接入

本轮为 O00 的原生队列与缓存实现，尚未开放 O01 消费者下载入口。网站不写视频文件；以下缓存写入的是手机 App 沙盒。原生队列中的账号、assetId 和有效期属于任务数据，不能代替源方离线许可。

## 已接入

Android 复用 react-native-video 6.19.2 所用的 Media3 1.8.0：DownloadManager、DefaultDownloadIndex、SimpleCache、DownloadHelper 和 DownloadService。没有自写下载器。MP4 直接进入下载队列，HLS 由成熟内核准备轨道与下载分段。队列、进度和缓存索引由 Media3 持久化；并发下载为 1。

播放器接收 `dramivio-offline://asset/<UUID>`，原生层再次检查当前账号、有效期和完成状态，然后使用禁止上游回源的 CacheDataSource。已有媒体请求隔离实现仍负责 Android 下载请求的 cookie、网站凭据、DNS、TLS 和每次重定向检查。任务快照不暴露来源 URL 或头。

每个账号使用独立原生缓存目录，每个已签发 asset 使用其现有 UUID 区分缓存键。同一个视频网址可以用于两个任务，删除一个任务不会删除另一个的内容。未完成文件没有播放 URI。切换账号暂停其他账号任务并隐藏其内容；显式 clearOwner 才删除文件和队列。

删除等待 Media3 索引移除，并验证实际目录已不存在。同一账号的并发清理共用一个任务和结果，避免旧轮询删除重新创建的目录。取消后的旧准备回调先比较当前任务，再操作任务表，不能移除同 ID 的新任务。账号清理进行中禁止重新配置同一账号，避免新任务写进即将删除的目录；该状态使用已有原生偏好存储，失败或中断后保留，重试完成后清除。

JS 通过可导入函数 `createNativeOffline()` 调用原生 Promise 方法：configureOwner、enqueue、list、pause、resume、remove、clearOwner、playback。请求和返回的账号归属须相符；有效期使用毫秒；只接受来源 Referer/User-Agent；旧 App 没有原生模块时明确失败，不能把操作当作成功。SDK 改动需要重新构建 App，不能只发 OTA。现有 Expo runtimeVersion 已升为 3；正式安装包与 OTA 未发布。

## iOS

MP4 使用 URLSessionDownloadTask，HLS 使用 AVAssetDownloadURLSession。原生 manifest 原子保存账号、asset、系统任务映射和状态；完成后检查真实本地资产可播，HLS 还检查 isPlayableOffline。播放器解析相同 asset 别名并检查注册的本地路径，不能由 JS 指定任意离线目录文件。

本机仅有 CommandLineTools/macOS SDK，没有 Xcode/iPhoneOS SDK。已检查共享原生代码及本地 Foundation/AVFoundation 夹具，不能据此声称完整 iOS 构建、真机下载或后台恢复通过。

Apple 后台 URLSession 自动跟随重定向，不调用前置重定向回调；最终 URL 和 metrics 检查不能撤销已经发生的请求。HLS 所有分段、密钥的 cookie 隔离和 DNS 目的地址还需实际设备网络夹具。离线 asset 使用系统下载，不另起自写播放器规避这些限制。[Apple 后台重定向行为](https://developer.apple.com/documentation/foundation/urlsessiontaskdelegate/urlsession(_:task:willperformhttpredirection:newrequest:completionhandler:))。

## 验证

| 项目 | 当前证据 | 证据范围 |
| --- | --- | --- |
| App | 210/210 测试，类型检查通过 | JS、React、官方认证客户端既有回归与新增适配层 |
| Android | 15/15 原生集成测试通过 | 实际 Media3 队列、SQLite 索引、文件缓存；仅媒体网络替换为合成夹具 |
| Android 媒体请求 | 既有真实 HTTPS JVM 夹具通过 | 实际 OkHttp 来源/网关隔离、HLS 子请求、重定向、DNS、TLS；不等同真机播放 |
| iOS | 77 项本地真实 SDK 夹具通过 | 账号/来源规则、真实合成 MP4、持久记录、路径与删除；不是 iOS 后台执行 |

Android 覆盖实际新构建合并清单中的服务注册；MP4 与 HLS 分段/AES 密钥缓存；播放读取零上游请求；冷启动索引与缓存恢复；暂停继续；换账号中断；打开缓存后换账号/过期拒绝读取；文件与目录删除；清理等待期间禁止同账号重新入队；同网址两资产删除隔离；并发账号清理合并；取消后的旧准备回调不阻断替代任务；IPv6 本地开发账号兼容。AES 夹具是合成测试密钥，不是 DRM 离线许可证。Android 的 ready 表示 Media3 下载字节完成；独立 HTML200 负夹具证明这不等同解码可播，真实来源能力、解码与受保护媒体仍须按 O00 验证。

测试可通过普通函数调用：

```js
const { runOfflineNativeTests } = require('./tests/offline-native.cjs');
const result = runOfflineNativeTests({ javaHome, androidHome, logPath });
```

遵循 [Robolectric 官方配置](https://robolectric.org/getting-started/)；下载与离线读取采用 [Media3 官方下载组件](https://developer.android.com/media/media3/exoplayer/downloading-media)。安装的 1.8.0 API 已实际编译，不照搬其他版本示例。

## 下一阶段

先复用现有额度事务、标准渠道与实际媒体能力，完成离线 preview/order/asset 签发和账号设备绑定，再接详情页多选、确认、队列及缓存列表。签发必须使用可离线的真实来源能力；受保护媒体需要源方持久许可，不能把在线播放地址包装成许可证。

尚未完成：实际源方离线许可与设备证明；O01 后端签发及额度联动；消费者 UI 与账号生命周期对原生队列的接线；iOS 宿主 AppDelegate 后台事件转发；完整 Android/iOS 安装和真机飞行模式、seek、后台/强退、空间不足、字幕与系统逐出验收。下载失败后的转发与解码能力须沿用按需路线单独验证。消费者入口继续返回 offline_authorization_required，避免旧 viewer URL 被当作授权缓存。

本轮未提交或推送仓库，未发布真实 API、App 或 OTA。在线更新仅为静态文档与已有交互设计。
