# Dramivio 开发技术路线

2026-10-02 · 配合PRD v3.3与已确认设计稿 · 本次交付技术规格，未开发真实App/服务

## 1. 执行结论

App使用原生视频引擎：iOS AVPlayer、Android ExoPlayer，通过react-native-video接入，RN负责操作层。Web独立使用HTML video；支持原生HLS时优先原生，否则检测MSE能力再动态加载hls.js。Web不接App长按手势，不把RN播放器代码打进Web包。

登录采用Better Auth官方Email OTP和官方Expo客户端集成，不自写验证码、会话或令牌系统。保留邮箱单入口；框架负责身份，本项目只接邮件发送、协议记录、用户资料和业务权限。每日额度、邀请有效观看、奖励账本属于产品规则，不能假设登录框架会代做。

初期只保持一个API进程、PostgreSQL和一个Outbox worker；需要跨实例限流时增加Valkey。API用Hono接Better Auth；数据库先用官方支持的PostgreSQL连接方式，不为认证再搭一套ORM或SSO平台。[Hono官方接入示例](https://hono.dev/examples/better-auth)。

管理后台补充采用Payload开源核心，复用表单、资源管理、管理员认证、访问控制和草稿版本；Hono负责业务API。详细职责与B00接入关口见[后端复用方案](dramivio-backend-reuse.md)。

## 2. 组件及修改边界

| 模块 | 采用 | 本项目负责 | 验证后才能锁定 |
|---|---|---|---|
| App工程 | RN；新项目建议Expo development build/prebuild | 原生构建、导航、界面与业务适配 | RN/Expo/原生依赖兼容矩阵 |
| 原生播放 | react-native-video稳定6.x候选 | PlayerAdapter、控件、授权与生命周期 | Android/iOS真机播放、手势、离线asset |
| 长按手势 | react-native-gesture-handler与工程兼容版本 | 临时倍速意图、控件区域隔离 | 不混用2.x/3.x API；真机取消事件 |
| Web播放 | HTML video + 按需hls.js | 轻控制层、错误恢复、响应式布局 | Safari原生/Chrome MSE两条路径 |
| 邮箱登录 | Better Auth + 官方Email OTP | SMTP接线、协议、额外业务限流 | RN和Web登录/退出/重启/多实例 |
| RN会话 | @better-auth/expo + expo-secure-store | 业务请求复用官方cookie获取能力 | 不在AsyncStorage存会话秘密 |
| 邀请归因 | better-auth-referral社区插件候选 | 绑定确认、资格审核和额度结算 | OTP新建用户适配、幂等和源审查 |
| 离线 | Media3 / AVAssetDownloadURLSession | 小型平台桥接、队列、许可与账号隔离 | 与在线引擎共用可播放asset |

[react-native-video官方说明](https://docs.thewidlarzgroup.com/react-native-video/docs/v6/intro/)确认iOS/Android原生引擎。[当前发布页](https://github.com/TheWidlarzGroup/react-native-video/releases)列v6.19.3为稳定候选，v7仍有预发布版本；本项目不默认追Beta。所有依赖记录精确版本和许可证，再运行兼容PoC，不使用latest作为锁版本策略。

新项目的Expo路线使用本地development build，不要求付费EAS，不用Expo Go验证自定义播放器/下载模块；参见[Expo development build](https://docs.expo.dev/develop/development-builds/introduction/)与[播放器Expo说明](https://docs.thewidlarzgroup.com/react-native-video/docs/v6/other/expo/)。已有bare RN项目则保持原工程，只评估加入必要Expo模块，不能为登录整体迁移。

不fork认证/解码内核；不同时接两套登录；不默认引入全功能播放器UI包。现有设计的选集、追剧和额度控件放在薄业务层。手势库只负责识别，不自写长按计时器。

## 3. App播放器结构与状态

```text
PlayerScreen
  ├─ PlaybackController（授权、状态、来源、请求代次）
  ├─ NativePlayerAdapter（react-native-video）
  ├─ GestureLayer（App专用）
  ├─ Controls / EpisodeSheet / RateSheet
  ├─ ProgressReporter（本地存储、10秒上报、revision）
  └─ Telemetry（首帧、缓冲、错误、内存）
```

只有一个活跃原生播放器。列表只放封面；切集复用当前实例并改变稳定source，控件/进度更新不能重新挂载播放器。source对象、headers、缓冲配置保持稳定引用，只有源/授权确实变化才替换；不能把currentTime当组件key。

基础状态：idle→authorizing→loading→ready→playing；旁支paused/buffering/seeking/ended/error/disposed。播放意图、前台状态和媒体状态分开：ready不等于用户同意播放；buffer结束也不代表应自动恢复被用户暂停的内容。

每次切集生成loadGeneration；异步授权、首帧、seek、错误及重试均带代次。旧请求abort；无法abort的原生事件也检查所属源/代次，迟到事件不能覆盖新剧名、位置或倍速。原生库没有天然代次时由adapter按当前绑定源及加载状态隔离，无法区分的旧事件忽略，不按回调到达先后猜测归属。

页面失焦/后台、来电、耳机拔出或音频焦点丢失：保存可得进度、取消长按、暂停。回来不擅自开声，按之前的用户意图和前台状态恢复。媒体末尾先记完成，再检查下一集授权；最后一集停下，不跨剧。

## 4. 首帧、缓冲与画面连续性

### 4.1 加载顺序

1. 点击后立即用已有封面铺满播放器，显示当前集信息；不等授权才绘制页面。
2. 可并行读取目录元数据和服务器进度，但必须取得当前集play grant后才请求可播放媒体。不得为提速跳过账本提交。
3. 授权成功后创建稳定source并加载。若需续播，等待load可接受seek后跳到合法位置；在续播位置可显示画面前保留封面，不先闪出0秒画面。
4. `onLoad`用于元数据就绪；`onReadyForDisplay`用于UI隐藏封面的起点。首帧时钟单独记录，不把封面出现或onLoad当视频首帧。[事件定义](https://docs.thewidlarzgroup.com/react-native-video/docs/v6/component/events/)。
5. 首帧确认后撤下封面，保留画面上方控件。加载超过300ms再显示小加载提示，避免短暂请求也闪圈；缓冲超过500ms才显示提示。

这些延迟是本项目交互起点，不是库默认值。若选定Android实现的ready回调只代表可播状态，TTFF报告须标“ready代理指标”；严格首个渲染帧用原生首帧观察或录屏核对。恢复seek也需要在目标位置确认显示，不能把seek命令发出当作目标画面完成。

### 4.2 媒体与网络策略

- 兼容主路径HLS + H.264/AAC；可控授权源提供360/540/720级别，具体码率经样本确认。首版不强制HEVC、1080p或低延迟直播配置。
- 可控点播源可试2–4秒分片、分片边界关键帧对齐；支持的源再验证CMAF。播放器不能修复上游编码/时间戳问题，记录异常源并隔离。
- 默认自动码率，不在未知移动网络直接锁720p；按实际可用档位显示画质。自动档使用吞吐与缓冲反馈，避免业务层每秒强行切档。
- 媒体域名保持少量且固定，减少重复DNS/TLS；CDN正确设置Content-Type/CORS/范围请求及签名过期。私有媒体缓存遵守授权边界，缓存键不能错误复用不同用户凭证；日志不记录完整签名URL。
- 短期凭证续期和失败重取都沿用既有解锁身份；401/403先分类是会话、到期还是内容撤权，不无限换线。恢复后仍从可验证进度继续。
- 首版预取只包含下一集封面/元数据。不要提前下载下一集manifest或片段、创建第二个解码器、提前扣下一集次数。以后要媒体预取，必须单独设计授权及账本规则再启用。

### 4.3 缓冲参数：实验起点

| 端 | 起点 | 调整依据 |
|---|---|---|
| Android | source.bufferConfig：minBufferMs=8000、maxBufferMs=20000、bufferForPlaybackMs=1000、bufferForPlaybackAfterRebufferMs=2000、backBufferDurationMs=0 | 首帧、重缓冲、内存；1×/2×及弱网分别对照 |
| iOS | automaticallyWaitsToMinimizeStalling=true；preferredForwardBufferDuration先比较默认0与10秒 | AVPlayer会自主决策，不能把preferred当严格上限；不为快首帧全局关闭等待 |
| Android渲染 | 全屏静态播放优先测surfaceView | 叠层/变换确实不兼容再测textureView；DRM另查安全surface，不能通用替换 |
| 通用 | progressUpdateInterval起点250ms，状态小组件局部更新 | 这只是UI采样，不是每250ms全页render或网络上报 |

参数接口见[官方props](https://docs.thewidlarzgroup.com/react-native-video/docs/v6/component/props/)。上表为本项目建议，需要依赖版本与真机验证；不是通用最优值。Android优先验证封面覆盖+shutter设置解决闪黑；不同内容切换不能裸露上一集最后一帧。不要只把黑底设透明就宣布首帧变快。

### 4.4 恢复与资源限制

普通网络失败最多2次自动重试，退避起点0.5秒/1.5秒并加少量抖动；成功播放后重置计数。长期buffer超过8秒提示重试/换线。解码、格式、内容撤权不走无限网络重试。

同剪辑等价源可换线，保留解锁和映射进度；不等价则明确版本变化，从合法位置重新选择。离开播放页要停止媒体请求并释放原生资源、事件、手势和定时器；不能仅让组件不可见。

## 5. App长按临时倍速

只在正片画面空白操作区域识别：不覆盖右侧按钮、底部进度条、选集/倍速弹层、系统手势边缘。初始长按阈值350ms、移动容差约16pt，均需真机验证。暂停、加载、广告、后台和无权内容不激活；单指，第二指加入则取消。

| 情况 | 行为 |
|---|---|
| 普通点击/短按 | 原有显示控件/播放暂停逻辑，不触发倍速 |
| 成功长按 | 临时2×；原生确认后显示“2× 倍速播放”/“Playing at 2×” |
| 原本1.5× | 长按2×，松手恢复1.5× |
| 原本2× | 保持2×，不发重复设置、不进一步提到4× |
| 松手/触摸取消/滑走 | 幂等取消临时状态，恢复当前手动选择的baseRate |
| 切集/切线/打开弹层/后台/来电 | 同样取消；新内容不能继承临时长按 |
| 2×失败或不支持 | 回到baseRate；简短提示，不伪显示已经加速 |
| 原本暂停 | 不因长按自动启动或产生声音 |

控制层只维护baseRate与holdActive：effectiveRate=holdActive且正片可播放 ? 2 : baseRate。原生倍速通过单一adapter设置；手势不直接另写一套rate。用户打开倍速选择时先取消hold，再改baseRate；迟到的release不得把新选择恢复成旧值。临时倍速不持久化，不进入下一集偏好。

使用工程已验证的[Gesture Handler](https://github.com/software-mansion/react-native-gesture-handler)；回调API按锁定版本使用，不把2.x示例抄到3.x。取消、失败和生命周期离开都需要同一restore操作，不能只处理成功onEnd。无需为这个提示增加复杂动画。无障碍用户仍可从倍速按钮操作。

长按不增加每日消耗；仍按同集解锁一次。邀请只累计可信前台墙钟与未重复内容区间，2×不把观看时间乘2；短集资格沿用PRD的最大资格倍速公式。

## 6. Web性能与适配

### 6.1 独立而简洁

同一React/TypeScript Web工程即可，播放业务契约与App共享，播放渲染和手势分开。首页不导入播放器和hls.js，用户进入播放路由且需要MSE时再加载；hls.js及RN原生依赖不进入首页主包。

使用`<video playsinline preload="metadata">`与自有轻控件。浏览器原生HLS能力检测→原生src；否则`Hls.isSupported()`→hls.js；都不支持时给出可播放格式的合法后备或下载App，不能假称已支持HLS。[hls.js官方能力说明](https://github.com/video-dev/hls.js)。preload只是浏览器提示；浏览器或hls加载可能产生实际请求，因此直到有grant才附src/loadSource，不能靠metadata设置防止额度绕过。

播放调用检查`video.play()`的Promise。用户点击后的异步授权可能使浏览器不再认可最初点击：被拒绝时保留画面和播放按钮，请用户再点，不重复扣次，也不循环调用play。[浏览器play规则](https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/play)。

### 6.2 优化起点

- Web只保留倍速菜单、选集、拖动、全屏、追剧和下载引导；不注册长按倍速、竖滑切集或App触摸层。保留浏览器正常滚动/选择/辅助操作，不全局preventDefault。
- hls.js候选起点：maxBufferLength=15、maxMaxBufferLength=30、backBufferLength=0、capLevelToPlayerSize=true；按设备与网络测内存和卡顿，不开启点播无需求的低延迟直播参数。尺寸/比特率策略允许画质被网络降档，不假定15秒等于15秒墙钟，2×要专门测试。[配置接口](https://hlsjs.video-dev.org/api-docs/hls.js.hlsconfig)。
- video画面与控制层尺寸稳定，移动浏览器地址栏伸缩时不重建播放器；宽屏桌面侧栏选集，窄屏底部弹层。横屏和iOS全屏需实际测，不强制套统一CSS假全屏。
- 离开路由调用hls.destroy()并移除事件、定时器；video暂停、移除src并按需load释放。页面隐藏暂停，回来不擅自开声。
- 浏览器首帧可用requestVideoFrameCallback作观察；不支持则退到playing/loadeddata并标代理指标。后台时回调不可靠，不能据此算墙钟观看。[帧回调定义](https://developer.mozilla.org/en-US/docs/Web/API/HTMLVideoElement/requestVideoFrameCallback)。
- 封面有width/height/aspect-ratio防布局跳动，首屏两张优先，其余lazy；列表每次10部cursor追加。先测普通双列列表，累计超过约100部有明显DOM/内存压力再按需虚拟化，不先装重型列表框架。
- 支持320/390/768/1280宽度；触控区不因英文长词缩到不可点。Web缓存按钮永远下载引导，不能复用App下载API。

## 7. 框架登录的具体接入

### 7.1 配置起点

Better Auth官网当前文档显示1.7.7；锁定时核对发布及Expo插件兼容版本，下面为待编译验证的配置片段，不是已运行代码。

```ts
// 保留框架的OTP/会话实现，只配置业务要求。
emailOTP({
  otpLength: 6,
  expiresIn: 600,
  allowedAttempts: 5,
  storeOTP: 'hashed',
  resendStrategy: 'rotate',
  disableSignUp: false,
  changeEmail: { enabled: true, verifyCurrentEmail: true },
  sendVerificationOTP: sendThroughSmtp,
});
// auth层建议 session.freshAge = 300；数据库采用官方PostgreSQL接入。
```

插件本身提供自动新用户注册、验证码验证与上述配置；实现前逐项对照[官方Email OTP](https://better-auth.com/docs/plugins/email-otp)。不要保留默认明文OTP存储；不新增自己的验证码表或随机算法。SMTP适配失败要明确可重试，不吞异常假称发送成功。邮箱投递和验证码端点分别限流，验证码不进日志/分析事件。

Web使用同源`/api/auth`和框架Cookie会话，生产HTTPS与明确trustedOrigins；不配置通配CORS+credentials、不关闭CSRF。App用官方Expo客户端与SecureStore，业务请求按官方getCookie方式携带已管理的会话。[Expo官方集成](https://better-auth.com/docs/integrations/expo)。App本地缓存的session只用于界面恢复，服务器每次关键动作仍验证会话与emailVerified。

### 7.2 仅保留必要的业务接线

1. 协议同意的版本/时间/账号关联，账号创建后幂等记录；未同意时不能获得新账号业务权限。
2. 邮箱/设备/IP共享限流预算，重发不能重置累计失败；部署多API实例也共用存储。框架默认限流不代表已满足本产品跨维度规则。[框架限流文档](https://better-auth.com/docs/concepts/rate-limit)。
3. 业务Profile与auth user唯一对应，邮箱不是用户主键。重试profile初始化不重送额度；邀请码失败不破坏正常身份登录。
4. 登录后的pendingAction只保存白名单业务意图（追剧/查询/续播/缓存确认），不保存任意跳转URL；执行前再校验权限且幂等，取消返回原页面。
5. 会话撤销复用框架；同IP只提高新奖励审核，不整体踢出老用户。用户明细遵守最小暴露。
6. 换邮箱优先官方changeEmail及verifyCurrentEmail。注销用框架新鲜会话/删除钩子，加最小业务终态和Outbox清理。

注销复验不发明官方未支持的OTP type。界面仍可要求邮箱OTP重新登录，服务端验证是当前账号、本次敏感意图之后的新会话，并用一次性action token绑定账号/动作/短期nonce。此token不是新认证系统；只约束“验证后允许注销”这一动作，登录证明不能直接拿去执行换邮箱或别的敏感操作。敏感意图nonce和创建时间由服务端生成；签发证明核对同一账号在该意图之后取得的OTP新会话，不接受客户端fresh标志。证明消费与terminal提交须同事务，或以稳定operation_id绑定幂等结果；清理失败查询并继续原操作，不重新消费或重建账户。beforeDelete钩子必须检查该证明，直接调框架删除路由也不得绕过；幂等终态→撤授权/奖励/许可→Outbox清理→框架身份清理，失败可恢复且不重建账户。[官方新鲜会话](https://better-auth.com/docs/concepts/session-management)、[删除用户与钩子](https://better-auth.com/docs/concepts/users-accounts)。

### 7.3 邀请的诚实边界

[better-auth-referral](https://github.com/marinedotsh/better-auth-referral)是社区候选，不把它称作已验证的成熟奖励系统。只先验证验证码自动注册时的归因、一次绑定、手输邀请码。通过才复用关系层；发奖仍由业务账本驱动。失败时提交PoC证据与选择，不擅自重写一整套营销平台，也不能用演示成功冒充插件已兼容。

## 8. 接口与状态必须先约定

| 契约 | 最少约定 |
|---|---|
| PlayerAdapter | load、seek、setBaseRate、setTemporaryRate/restore、pause、dispose；ready/buffer/error/progress事件带当前代次 |
| play grant | canonical episode、edition、unlock、session、source version、签名过期、可用线路、端能力、policy version；不含上游秘密 |
| 进度提交 | session、sequence、position、history_epoch、revision；服务端取用户，不信任传入user_id |
| 有效观看 | 前台单会话、去重内容区间、真实墙钟；广告和buffer暂停不累计 |
| 额度读取 | 当前端基础上限/已用、App奖励、下一重置时间、会员状态；跨端不加成60 |
| 下载确认 | 实际选中集合、版权许可预览、设备、到期、已解锁/新增费用、空间估计 |

错误必须区分NETWORK、AUTH_EXPIRED、QUOTA_EXHAUSTED、CONTENT_UNAVAILABLE、DECODE_UNSUPPORTED、LICENSE_EXPIRED、CONCURRENT_SESSION。播放器错误不是统一“请登录”。适配器事件不可直接发奖励；同集重试/换线不重扣。正式接口统一PRD数据字典，不在各端再创一套身份或权益。

## 9. 分阶段测试与证据

功能按`dramivio-model-task-cards.md`逐卡完成。认证用真实框架+测试SMTP捕获工具，授权/账本用真实PostgreSQL集成测试；Mock只能用于失败注入或纯控件，不能作为框架兼容/并发/离线的通过证据。

- 纯状态单测：倍速恢复、事件代次、缓存选择/费用、查询草稿恢复、进度冲突；采用现有测试工具，避免为了测试再换整套工程。
- 集成：Auth+DB+邮箱捕获、grant事务/幂等、进度CAS、邀请结算、删除/迟到事件。并发测试保留数据库事实与唯一约束结果。
- 浏览器：Chromium自动化主流程；真实Safari/macOS与iPhone Safari单独测原生HLS、自动播放、全屏；不能拿Playwright WebKit冒充真Safari验收。
- App：至少一台目标最低系统iPhone、一台低内存Android、一台中档Android；Release/profile构建，不拿模拟器Debug帧率作为上线性能。
- 离线：真机下载→断网→系统强退→重启→播放；权限到期、退出/换账号、低空间、Wi-Fi切换、版本恢复。不能用在线播放缓存代替。

### 性能实验矩阵

受控片：30/60/180秒，360/540/720档，相同编码与源。正常网候选8Mbps/RTT60ms/0丢包；弱网候选1.5Mbps/RTT150ms/1%丢包；另测10秒断网、Wi-Fi↔蜂窝、后台/前台。每端配置对照至少50次启动，标冷/热缓存、1×/2×、是否恢复seek；样本用于工程对照，不声称生产总体分位数已可靠。

| 指标 | 初始目标/断言 |
|---|---|
| 封面占位 | 点击后约100ms内有稳定可见页面，无空白等待授权 |
| TTFF | 正常网受控样本目标P50≤1.5s、P95≤3s，包含grant；另列纯媒体加载耗时 |
| 卡顿比 | 正常网候选≤1%；定义为有效播放期间缓冲墙钟/播放意图墙钟，排除手动暂停/seek/首次加载 |
| 连续切集 | 50次不崩溃/重复声音，最后10次内存无持续阶梯增长；回到空闲后对照基线，阈值按设备预算固定 |
| 长按 | 100次成功/取消混合测试均恢复正确baseRate；无一次把下一集/广告留在临时2× |
| 授权正确 | 没有首帧提前于grant/账本提交；无次数时不能取新媒体；失败/重试不重复扣 |
| Web首页 | 网络记录不下载hls.js/视频分片；核心包独立记录压缩大小、执行耗时和长任务 |
| Web兼容 | 320–1280不水平溢出；触屏正常滚动；拒绝play后可手动恢复且不重扣 |

达不到目标先定位grant、CDN、manifest、片段、解码或渲染哪一步慢，再改一个变量对照；不能只增大buffer或默认预载多播放器。弱网优先正确降档/恢复，不卡死、不误扣，不承诺固定首帧时间。

每张卡交付：锁版本、变更列表、测试命令、实际结果、真机/浏览器/网络、脱敏日志/录屏、已知失败、回滚步骤。无设备或无真实媒体时写“未验证”，不能写通过；兼容失败只阻塞依赖它的卡，继续可独立完成的模块。

## 10. 对本次设计的增量

App新增临时2×状态与松手恢复；Web保留原倍速菜单，没有长按功能。RN状态层与Web包分开；UI只显示简短2×反馈和必要错误，不把缓冲参数或认证实现说明放进产品页面。

本文件由开发模型先阅读，再接单张任务卡。当前交付是路线，不代表上表性能、框架或安全测试已执行。

进度上传的异步任务固定绑定user_id、session_id、history_epoch；退出/换号后不得用当前账户替换旧任务身份。App端分类沿用PRD的端证明等级；只开源且没有可信平台证明时，安装密钥/请求头可被复制，必须记录仿冒App残余风险并施加共享设备预算、账号限流及成本保护，不能宣称绝对阻止Web伪装。

端证明验收必须列出iOS、带Google服务Android、不带Google服务Android的能力矩阵。签名挑战和安装钥匙仅提供连续性；无Google服务的低风险合法App仍按PRD提供50集，不能一律降到Web10集。可完整模拟握手的攻击者仍可能仿冒，按成本/带宽限流、共享预算与审核控制；媒体访问始终需要合法grant。

性能采样：基线与弱网各选择代表组合，每组至少50次启动；其余片长/清晰度/冷热/倍速/seek组合记录功能样本数量，不把全部组合混成一个P95。D04固定每设备内存基线、工具及阈值，D19不得改统计定义掩盖回归。错误重试预算只有稳定播放一段时间后才重置，不能因播放几帧就无限循环重试。
