一个共享演示项目有个可预见的失败模式:每个贡献者都往里加一个视觉上很惊艳的效果,但没人愿意删掉任何东西。 美颜滤镜之后,有人加了背景分割。接着又有人加了 GAN 效果、3D 虚拟形象、贴纸图层。每一个拉取请求单独看都能跑通,合在一起的应用却跑不动了。 问题不在于团队缺少 AI 功能,而在于对一个效果说“不”,听起来总不如把所有东西堆上去更有野心——尤其是在黑客松或社区共建的场景里。但一个可靠的美颜 AR 体验,需要的是一份明确的渲染预算,而不是乐观情绪。 在本教程中,我们将构建一个应用级的运行时预算仲裁器。 腾讯 RTC 的美颜 AR 能力概览里,列出了实时美颜滤镜、美妆、贴纸、虚拟背景、虚拟形象、手势识别以及图像或视频增强等能力。其低端设备性能优化实践指南建议让效果适配设备档位,包括在必要时禁用开销较大的分割或 3D/GAN 效果,并控制分辨率和帧率。 下面的控制器把这份建议转化为确定性的应用行为,而不去凭空发明一套通用的设备基准。 看这样一串用户操作: 1. 用户选择了分割背景。 2. 背景变可见。 3. 用户选择了 GAN 人像模式。 4. 应用在启用 GAN 之前先移除了分割。 5. 设备压力持续升高。 6. 设备被重新归类为受限。 7. GAN 被移除。 8. 只有在用户事先同意该降级方案时,基础增强才会被启用。 这串操作里其实藏着三种不同的事实。把它们压缩进一个 currentEffect 变量,会产出误导性的界面。比如应用可能显示“已选择 GAN”,而资源还在加载,或者策略早已拒绝了它。 我们会把这些事实分开保存。 本教程使用五个由应用自己定义的模式。这些是产品策略层面的命名,不是腾讯 RTC 的 API 名称。适配层需要把它们映射到目标平台支持的 SDK 操作上。 关键规则是:一个模式代表一种精确的呈现方式,而不是又一个可以追加的图层。选择 ganPortrait 会替换掉 segmentedBackground,而不是悄悄和它叠加在一起。 这套策略刻意把贴纸和美妆的组合排除在第一版之外。只有在你的目标设备上实测过之后,再考虑加入组合。“SDK 能表达它”和“我们的会话负担得起它”是两句不同的话。 mkdir beauty-budget-arbiter cd beauty-budget-arbiter npm init -y npm install -D typescript vitest @types/node mkdir src 替换 package.json: { "name": "beauty-budget-arbiter", "private": true, "type": "module", "scripts": { "test": "vitest run", "test:watch": "vitest" }, "devDependencies": { "@types/node": "latest", "typescript": "latest", "vitest": "latest" } } 添加 tsconfig.json: { "compilerOptions": { "target": "ES2022", "module": "NodeNext", "moduleResolution": "NodeNext", "strict": true, "noUncheckedIndexedAccess": true, "outDir": "dist" }, "include": ["src"] } 实现仲裁器 创建 src/arbiter.ts: export type BeautyMode = | "off" | "basic" | "ganPortrait" | "segmentedBackground" | "avatar3d"; export type DeviceCapability = "constrained" | "standard"; export type Phase = "active" | "applying" | "blocked" | "failed"; export interface Selection { mode: BeautyMode; // This is separate consent. Selecting GAN does not automatically mean // that the user accepts a different appearance when GAN is unavailable. allowBasicFallback: boolean; } export interface BeautyPort { /** * Apply exactly this mode, removing any mutually exclusive previous mode. * Reject if the operation cannot be completed. */ applyMode(mode: BeautyMode): Promise; } export interface ArbiterSnapshot { capability: DeviceCapability; requested: BeautyMode; desired: BeautyMode; visible: BeautyMode; phase: Phase; notice: string | null; } type Admission = | { kind: "admitted"; target: BeautyMode; notice: string | null } | { kind: "blocked"; notice: string }; export function isIntensive(mode: BeautyMode): boolean { return ( mode === "ganPortrait" || mode === "segmentedBackground" || mode === "avatar3d" ); } export function admit( selection: Selection, capability: DeviceCapability, ): Admission { if (capability === "standard" || !isIntensive(selection.mode)) { return { kind: "admitted", target: selection.mode, notice: null }; } if (selection.allowBasicFallback) { return { kind: "admitted", target: "basic", notice: `${selection.mode} is unavailable in the current device profile; basic mode was selected.`, }; } return { kind: "blocked", notice: `${selection.mode} is unavailable in the current device profile and no fallback was approved.`, }; } export class BeautyBudgetArbiter { private requested: BeautyMode = "off"; private desired: BeautyMode = "off"; private visible: BeautyMode = "off"; private phase: Phase = "active"; private notice: string | null = null; private lastSelection: Selection = { mode: "off", allowBasicFallback: false, }; // All renderer mutations are serialized through this queue. private tail: Promise = Promise.resolve(); constructor( private capability: DeviceCapability, private readonly port: BeautyPort, ) {} select(selection: Selection): void { this.lastSelection = selection; this.requested = selection.mode; const decision = admit(selection, this.capabili

打开网易新闻 查看精彩图片