A GAN-powered beauty effect can look convincing in a product demo and still be the wrong default for a real session.
The uncomfortable part is not whether the effect is “AI.” It is deciding what the application should do when appearance processing, segmentation, rendering, and video compete for a limited device budget. If the answer is simply “enable everything and hope,” lower-capability devices pay the price.
This is also where writing less integration code can create more engineering responsibility. The durable skill is not producing another effect toggle. It is defining consent, capability, fallback, and verification rules that remain understandable when the renderer fails.
In this tutorial, we will build an application-owned Beauty AR controller that:
- does not process appearance effects before consent;
- treats GAN, segmentation, and other expensive effects as optional capabilities;
- selects a profile from measured device evidence;
- downgrades after sustained frame pressure instead of reacting to one noisy sample;
- refuses to auto-upgrade during an active session;
- ignores stale asynchronous callbacks;
- disables effects if even the safest profile cannot be applied.
Tencent RTC Beauty AR supports scenarios including real-time beauty filters, makeup, stickers, virtual backgrounds, avatars, gesture recognition, and image or video enhancement. The official overview is the appropriate starting point for checking the features available to your integration:
https://trtc.io/document/beauty-ar-overview
For device-tier and degradation guidance, Tencent RTC’s low-end optimization guide recommends adapting the configuration to device capability, using performance-oriented modes, controlling resolution and frame rate, and disabling expensive segmentation or 3D/GAN effects where necessary:
https://trtc.io/document/66968
The code below is deliberately an application policy, not a replacement for the official platform-specific integration instructions.
The visual contract comes before the renderer
A useful Beauty AR contract separates three decisions that are often collapsed into one checkbox:
- May the application alter or process the user’s appearance? This is a consent decision.
- Which effects does the user want? This is a preference decision.
- Which effects can the current device sustain? This is an operational decision.
A user selecting “Full” should not force a constrained device to run every effect. It means the application may use the richest profile that its capability policy currently permits.
We will use these states:
awaiting-consent
|
+-- denied --------------------------> off
|
+-- granted --> probing --> applying --> running
| |
| +-- sustained pressure
| |
+-- failure v
applying safer profile
|
+--> degraded
|
+--> failed --> off
The distinction between degraded and failed matters. Degraded means the session is still providing an intentionally reduced visual experience. Failed means no approved profile could be applied safely, so the effects are off.
Create the reproducible project
Use Node.js with TypeScript for the policy and tests:
mkdir beauty-budget
cd beauty-budget
npm init -y
npm install --save-dev typescript tsx @types/node
mkdir src test
Add these scripts to package.json:
{
"scripts": {
"test": "tsx --test test/**/*.test.ts"
}
}
The policy can be tested without a camera, a live room, or a specific Beauty AR SDK method. That is intentional: renderer callbacks should provide evidence to the policy, not contain the policy themselves.
Represent visual cost as profiles
Create src/policy.ts:
export type Tier = "constrained" | "balanced" | "capable";
export type Preference = "off" | "auto" | "basic" | "full";
export type Phase =
| "awaiting-consent"
| "probing"
| "applying"
| "running"
| "degraded"
| "off"
| "failed";
export type Feature =
| "beauty"
| "makeup"
| "stickers"
| "segmentation"
| "gan"
| "avatar";
export interface RenderProfile {
id: string;
mode: "performance" | "quality";
inputHeight: number;
targetFps: number;
features: Feature[];
}
const tierProfiles: Record<Tier, RenderProfile> = {
constrained: {
id: "constrained-v1",
mode: "performance",
inputHeight: 480,
targetFps: 15,
features: ["beauty"]
},
balanced: {
id: "balanced-v1",
mode: "performance",
inputHeight: 720,
targetFps: 24,
features: ["beauty", "makeup", "stickers"]
},
capable: {
id: "capable-v1",
mode: "quality",
inputHeight: 720,
targetFps: 30,
features: [
"beauty",
"makeup",
"stickers",
"segmentation",
"gan",
"avatar"
]
}
};
function profileFor(tier: Tier, preference: Preference): RenderProfile {
const base = tierProfiles[tier];
if (preference === "basic") {
return {
...base,
id: `${base.id}-basic`,
features: base.features.filter(feature => feature === "beauty")
};
}
return { ...base, features: [...base.features] };
}
export interface State {
phase: Phase;
consent: "unknown" | "granted" | "denied";
preference: Preference;
tier?: Tier;
profile?: RenderProfile;
revision: number;
badWindows: number;
hasDegraded: boolean;
error?: string;
}
export type Event =
| { type: "CONSENT_GRANTED" }
| { type: "CONSENT_DENIED" }
| { type: "PROBE_COMPLETED"; tier: Tier }
| { type: "PREFERENCE_CHANGED"; preference: Preference }
| { type: "FRAME_WINDOW"; observedFps: number }
| { type: "PROFILE_APPLIED"; revision: number }
| { type: "PROFILE_FAILED"; revision: number; error: string };
export type Command =
| { type: "APPLY_PROFILE"; revision: number; profile: RenderProfile }
| { type: "DISABLE_EFFECTS" };
export function initialState(): State {
return {
phase: "awaiting-consent",
consent: "unknown",
preference: "auto",
revision: 0,
badWindows: 0,
hasDegraded: false
};
}
function lowerTier(tier: Tier): Tier | undefined {
if (tier === "capable") return "balanced";
if (tier === "balanced") return "constrained";
return undefined;
}
function startApply(
state: State,
tier: Tier,
degraded: boolean
): [State, Command[]] {
const revision = state.revision + 1;
const profile = profileFor(tier, state.preference);
return [
{
...state,
phase: "applying",
tier,
profile,
revision,
badWindows: 0,
hasDegraded: state.hasDegraded || degraded,
error: undefined
},
[{ type: "APPLY_PROFILE", revision, profile }]
];
}
export function reduce(state: State, event: Event): [State, Command[]] {
switch (event.type) {
case "CONSENT_GRANTED":
return [
{ ...state, consent: "granted", phase: "probing", error: undefined },
[]
];
case "CONSENT_DENIED":
return [
{
...state,
consent: "denied",
preference: "off",
phase: "off",
profile: undefined,
revision: state.revision + 1,
badWindows: 0
},
[{ type: "DISABLE_EFFECTS" }]
];
case "PROBE_COMPLETED":
if (state.consent !== "granted" || state.preference === "off") {
return [state, []];
}
return startApply(state, event.tier, false);
case "PREFERENCE_CHANGED": {
if (event.preference === "off") {
return [
{
...state,
preference: "off",
phase: "off",
profile: undefined,
revision: state.revision + 1,
badWindows: 0
},
[{ type: "DISABLE_EFFECTS" }]
];
}
const next = { ...state, preference: event.preference };
if (next.consent !== "granted") return [next, []];
if (!next.tier) return [{ ...next, phase: "probing" }, []];
return startApply(next, next.tier, false);
}
case "FRAME_WINDOW": {
if (
(state.phase !== "running" && state.phase !== "degraded") ||
!state.profile ||
!state.tier
) {
return [state, []];
}
// An application-owned starting threshold, not a product benchmark.
const belowBudget = event.observedFps < state.profile.targetFps * 0.8;
const badWindows = belowBudget ? state.badWindows + 1 : 0;
if (badWindows < 3) {
return [{ ...state, badWindows }, []];
}
const saferTier = lowerTier(state.tier);
if (!saferTier) {
return [
{
...state,
phase: "failed",
profile: undefined,
badWindows: 0,
error: "Frame budget was not sustained on the safest profile"
},
[{ type: "DISABLE_EFFECTS" }]
];
}
return startApply({ ...state, badWindows: 0 }, saferTier, true);
}
case "PROFILE_APPLIED":
// The user may have turned effects off while an apply was in flight.
if (event.revision !== state.revision || state.phase !== "applying") {
return [state, []];
}
return [
{
...state,
phase: state.hasDegraded ? "degraded" : "running",
badWindows: 0
},
[]
];
case "PROFILE_FAILED": {
if (event.revision !== state.revision || state.phase !== "applying") {
return [state, []];
}
const saferTier = state.tier ? lowerTier(state.tier) : undefined;
if (saferTier) {
return startApply(
{ ...state, error: event.error },
saferTier,
true
);
}
return [
{
...state,
phase: "failed",
profile: undefined,
error: event.error
},
[{ type: "DISABLE_EFFECTS" }]
];
}
}
}
The numeric profile values and the 80% threshold are example application defaults, not Tencent RTC performance guarantees. Calibrate them using measurements from the devices your application actually supports.
The more important property is the ordering:
capable -> balanced -> constrained -> effects off
GAN and segmentation disappear before basic beauty processing does. The user still gets a valid session rather than an all-or-nothing renderer.
Connect the commands to Beauty AR
Keep platform-specific SDK calls behind a narrow adapter:
import type { Command, Event, RenderProfile } from "./policy.js";
export interface BeautyRenderer {
apply(profile: RenderProfile): Promise<void>;
disable(): Promise<void>;
}
export async function execute(
command: Command,
renderer: BeautyRenderer,
dispatch: (event: Event) => void
): Promise<void> {
if (command.type === "DISABLE_EFFECTS") {
await renderer.disable();
return;
}
try {
await renderer.apply(command.profile);
dispatch({
type: "PROFILE_APPLIED",
revision: command.revision
});
} catch (error) {
dispatch({
type: "PROFILE_FAILED",
revision: command.revision,
error: error instanceof Error ? error.message : "Unknown renderer error"
});
}
}
BeautyRenderer.apply is an application-defined port, not a Tencent RTC API name. Its concrete implementation should map the selected profile to the documented Beauty AR configuration for your target platform.
That adapter is also the right place to normalize errors. A production implementation should distinguish at least:
- an unsupported effect;
- an unavailable asset;
- a renderer or graphics-context failure;
- invalid product configuration;
- cancellation caused by the user turning effects off.
A permanent configuration error should normally stop immediately rather than trying every performance tier. The generic sample falls back because it cannot know the platform-specific error taxonomy.
Device classification should use evidence, not branding
Do not infer “capable” solely from a device model or user-agent string. Two nominally identical devices can differ because of thermal state, background load, browser behavior, camera configuration, or power settings.
A practical probe can combine:
- platform capability checks available to your client;
- a short local render warm-up;
- observed render frame rate;
- renderer initialization success;
- whether required effects or assets can be loaded.
Run the probe only after consent, and avoid publishing or retaining camera frames merely to classify the device. The policy needs a tier result, not the images used to produce it.
Use a conservative tier when measurement is missing. “Unknown” should not silently mean “capable.”
There is another measurement trap: poor remote video can come from the network, encoder, decoder, or receiver. Do not downgrade local Beauty AR solely because a remote participant reports low frame rate. Feed this policy measurements attributable to the local rendering path.
Verify the behavior before connecting a camera
Create test/policy.test.ts:
import test from "node:test";
import assert from "node:assert/strict";
import { initialState, reduce, type State } from "../src/policy.js";
function grantAndProbe(tier: "constrained" | "balanced" | "capable") {
let state = initialState();
[state] = reduce(state, { type: "CONSENT_GRANTED" });
const [applying, commands] = reduce(state, {
type: "PROBE_COMPLETED",
tier
});
return { state: applying, commands };
}
test("does not apply a profile before consent", () => {
const [state, commands] = reduce(initialState(), {
type: "PROBE_COMPLETED",
tier: "capable"
});
assert.equal(state.phase, "awaiting-consent");
assert.equal(commands.length, 0);
});
test("a capable device may receive the GAN profile", () => {
const { state, commands } = grantAndProbe("capable");
assert.equal(state.phase, "applying");
assert.equal(state.profile?.features.includes("gan"), true);
assert.equal(commands[0]?.type, "APPLY_PROFILE");
});
test("three bad windows cause one downgrade", () => {
let { state } = grantAndProbe("capable");
[state] = reduce(state, {
type: "PROFILE_APPLIED",
revision: state.revision
});
let commands = [] as ReturnType<typeof reduce>[1];
for (let i = 0; i < 3; i++) {
[state, commands] = reduce(state, {
type: "FRAME_WINDOW",
observedFps: 10
});
}
assert.equal(state.tier, "balanced");
assert.equal(state.phase, "applying");
assert.equal(state.hasDegraded, true);
assert.equal(commands[0]?.type, "APPLY_PROFILE");
assert.equal(state.profile?.features.includes("gan"), false);
});
test("an apply failure tries a safer tier", () => {
let { state } = grantAndProbe("balanced");
const failedRevision = state.revision;
const [next, commands] = reduce(state, {
type: "PROFILE_FAILED",
revision: failedRevision,
error: "renderer initialization failed"
});
assert.equal(next.tier, "constrained");
assert.equal(next.phase, "applying");
assert.equal(commands[0]?.type, "APPLY_PROFILE");
});
test("a late success cannot resurrect effects after user turns them off", () => {
let state: State = grantAndProbe("capable").state;
const oldRevision = state.revision;
[state] = reduce(state, {
type: "PREFERENCE_CHANGED",
preference: "off"
});
[state] = reduce(state, {
type: "PROFILE_APPLIED",
revision: oldRevision
});
assert.equal(state.phase, "off");
assert.equal(state.profile, undefined);
});
Run the suite:
npm test
These tests verify the policy’s invariants rather than whether one machine can render a particular effect.
Why the controller only downgrades automatically
An obvious extension is to upgrade again as soon as frame rate recovers. That usually creates a new problem: oscillation.
A device may briefly recover after an expensive effect is disabled. If the application immediately restores the effect, frame pressure returns, causing another downgrade. The user sees repeated visual changes while the renderer does unnecessary work.
A safer default is:
- automatically downgrade during a session;
- keep that lower tier for the rest of the session;
- allow a deliberate user retry or a fresh probe in a later session;
- record why the downgrade happened.
If you do implement automatic recovery, require substantially more good evidence than bad evidence. For example, degradation might require three poor windows while recovery requires a much longer stable interval. Those values still need calibration rather than guesswork.
Failure drills the beauty preview should not hide
Consent is withdrawn while GAN assets are loading
The revision check prevents a late PROFILE_APPLIED event from moving the application back to running. The renderer adapter must also release loaded resources and stop processing.
Capability telemetry is unavailable
Choose the conservative profile. Do not translate missing data into permission to enable segmentation, avatars, or GAN effects.
The balanced profile fails for a non-performance reason
The sample tries the constrained profile. In production, classify errors first. A missing credential or invalid configuration will not be repaired by reducing resolution.
The safest profile misses its frame budget
Disable Beauty AR while preserving the underlying call or live session. Beauty effects are optional; communication should not depend on them.
The app is backgrounded and restored
Graphics resources may no longer be in the state your controller expects. Treat restoration as revalidation: check consent, verify that the revision is still current, and reapply no more than the last approved tier.
The user requests “Full” on a constrained device
Keep the constrained profile and explain why richer effects are unavailable. Do not quietly pretend the GAN effect is active, but do not override the safety policy either.
What AI does—and what remains a human decision
GAN-based visual processing can produce effects that would be difficult to implement with simple color adjustments. That demonstrated capability does not answer the product questions around it:
- Should appearance alteration be on by default?
- Is the user aware that an effect is active?
- Which changes are acceptable in this context?
- When should visual quality yield to device stability?
- What telemetry may be retained without collecting images?
Those are application and human decisions, not outputs to delegate to a model.
For many developers, the underlying tension is that integrating an advanced effect can now take less code, while being confident in the result requires more judgment. That is not a loss of engineering value. The valuable work has moved toward defining constraints, making degradation visible, and proving that user control survives asynchronous failure.
Release checklist
Before shipping, verify all of the following:
- [ ] No appearance effect starts before explicit consent.
- [ ] Turning effects off invalidates in-flight apply operations.
- [ ] The user can always see whether an effect is active or degraded.
- [ ] Unknown device capability selects a conservative profile.
- [ ] GAN, segmentation, and other expensive effects are optional.
- [ ] Frame pressure must persist before a downgrade occurs.
- [ ] A downgrade does not automatically oscillate back upward.
- [ ] Failure of the safest profile disables effects without ending the RTC session.
- [ ] Renderer errors are separated from network-quality observations.
- [ ] Logs contain profile IDs, revisions, transitions, and normalized errors—not camera frames.
- [ ] Thresholds are calibrated against supported devices rather than presented as universal benchmarks.
- [ ] Your platform adapter follows the current official Tencent RTC documentation.
The goal is not to make every device render the most impressive effect. It is to make every outcome intentional: rich where supported, restrained where necessary, off when requested, and recoverable when rendering fails.
Disclosure: I wrote this article in connection with Tencent RTC, and I used the official Tencent RTC Beauty AR documentation as the implementation reference.
Top comments (0)