Dev.to AI πŸ€– Ai πŸ‘ 0 πŸ“– 12 min read

The Filter Changed After the User Tapped Off: Race-Proof Tencent RTC Beauty AR

A Beauty AR demo usually has one comfortable sequence: open the camera, select an effect, and admire the result. Production users are less cooperative. They tap three effects quickly. They withdraw consent while an effe

A Beauty AR demo usually has one comfortable sequence: open the camera, select an effect, and admire the result.

Production users are less cooperative. They tap three effects quickly. They withdraw consent while an effect is loading. A low-end device cannot sustain a segmentation or GAN workload. An asynchronous callback then completes late and applies something the user already turned off.

That creates a tension bigger than rendering: who is authoritativeβ€”the computer-vision pipeline or the person looking at the preview?

The answer should be encoded, not implied. In this tutorial, we will build a small TypeScript control plane where:

  • the latest user intent wins;
  • consent withdrawal immediately hides processed output;
  • expensive effects can resolve to an explicit fallback;
  • stale asynchronous work cannot become the final state;
  • adapter failures return to unmodified video instead of leaving the UI ambiguous.

Tencent RTC Beauty AR supports real-time capabilities including beauty filters, makeup, stickers, virtual backgrounds, avatars, gesture recognition, and image/video enhancement. Its official documentation also recommends adapting expensive segmentation, 3D, and GAN effects for constrained devices. See the Beauty AR overview and Low-End Device Performance Optimization Practice Guide.

This tutorial deliberately does not invent SDK method names. We will build an application-owned coordinator with a narrow adapter that you can map to the documented Beauty AR integration used by your project.

The failure timeline

Consider this sequence:

T0  User selects a GAN effect
T1  The app starts preparing its resources
T2  User taps β€œOff”
T3  The UI changes to Off
T4  The old preparation finishes
T5  The GAN effect is applied anyway

A boolean such as beautyEnabled cannot represent this correctly. It says nothing about which request is current, whether work is in flight, or whether processed frames may still be published.

We need two independent controls:

  1. Intent control: a monotonically increasing version makes old requests stale.
  2. Output control: turning an effect off or withdrawing consent immediately holds processed output while cleanup catches up.

The second control matters because cancellation is not magic. An SDK or GPU operation may already be executing when the application asks it to stop.

Define the observable contract

The UI should render the coordinator's state rather than reconstructing state from button clicks:

type ViewState =
  | { kind: "ready"; active: "off" }
  | { kind: "preparing"; requested: EffectId; resolved: EffectId }
  | { kind: "active"; requested: EffectId; active: EffectId; degraded: boolean }
  | { kind: "failed"; requested: EffectId; message: string };

requested and active are intentionally separate. If someone selects a virtual background but the device policy resolves it to a lighter beauty effect, the application must not pretend the original effect is active.

Here is the behavior we will enforce:

Event Immediate behavior Eventual behavior
A newer effect is selected Mark the previous preparation stale Apply only the latest resolved effect
Off is selected Hold processed output Clear the effect and release raw video
Consent is withdrawn Hold processed output immediately Clear the effect; require a new opt-in before reuse
Requested effect exceeds the device budget Show the resolved fallback Apply the fallback or remain off
Preparation fails Keep the failed request visible Clear processing and return to raw video
Apply fails after preparation Do not report the effect as active Clear processing and return to raw video

Holding output might appear as a frozen preview, placeholder, or temporary camera pause depending on your product. That is an application decision. The invariant is that a known-unwanted processed frame must not be treated as acceptable merely because cleanup is asynchronous.

Create the project

mkdir beauty-intent-coordinator
cd beauty-intent-coordinator
npm init -y
npm install --save-dev typescript tsx @types/node
mkdir src test

Add the following implementation as src/coordinator.ts.

export type EffectId = "off" | "soft-beauty" | "virtual-bg" | "gan-look";

type EffectDefinition = {
  id: EffectId;
  needsSegmentation: boolean;
  needsGan: boolean;
  fallback: EffectId;
};

const EFFECTS: Record<EffectId, EffectDefinition> = {
  off: {
    id: "off",
    needsSegmentation: false,
    needsGan: false,
    fallback: "off",
  },
  "soft-beauty": {
    id: "soft-beauty",
    needsSegmentation: false,
    needsGan: false,
    fallback: "off",
  },
  "virtual-bg": {
    id: "virtual-bg",
    needsSegmentation: true,
    needsGan: false,
    fallback: "soft-beauty",
  },
  "gan-look": {
    id: "gan-look",
    needsSegmentation: false,
    needsGan: true,
    fallback: "soft-beauty",
  },
};

export type DevicePolicy = {
  segmentationAllowed: boolean;
  ganAllowed: boolean;
  budget: "normal" | "constrained";
};

export type ViewState =
  | { kind: "ready"; active: "off" }
  | { kind: "preparing"; requested: EffectId; resolved: EffectId }
  | {
      kind: "active";
      requested: EffectId;
      active: EffectId;
      degraded: boolean;
    }
  | { kind: "failed"; requested: EffectId; message: string };

export interface BeautyAdapter {
  /** Fetch or prepare resources. It should honor cancellation where possible. */
  prepare(effect: EffectId, signal: AbortSignal): Promise<void>;

  /** Commit one prepared effect to the processing pipeline. */
  apply(effect: EffectId): Promise<void>;

  /** Remove appearance processing and return to unmodified video. */
  clear(): Promise<void>;
}

export interface OutputGate {
  /** Synchronously prevent processed output from being shown or published. */
  hold(reason: "off-requested" | "consent-withdrawn"): void;

  /** Release the unmodified camera path after processing has been cleared. */
  releaseRaw(): void;
}

export class BeautyCoordinator {
  private consent = false;
  private desired: EffectId = "off";
  private version = 0;
  private preparation?: AbortController;
  private work: Promise<void> = Promise.resolve();

  private state: ViewState = { kind: "ready", active: "off" };

  constructor(
    private readonly adapter: BeautyAdapter,
    private readonly gate: OutputGate,
    private policy: DevicePolicy,
  ) {}

  getState(): ViewState {
    return this.state;
  }

  setConsent(granted: boolean): void {
    if (granted === this.consent) return;

    this.consent = granted;
    this.version += 1;
    this.preparation?.abort();

    if (!granted) {
      // Consent withdrawal invalidates the old preference. Re-enabling consent
      // will not silently restore a previous appearance effect.
      this.desired = "off";
      this.gate.hold("consent-withdrawn");
    }

    this.enqueue();
  }

  select(effect: EffectId): void {
    if (effect !== "off" && !this.consent) {
      this.state = {
        kind: "failed",
        requested: effect,
        message: "Consent is required before applying an appearance effect.",
      };
      return;
    }

    this.desired = effect;
    this.version += 1;
    this.preparation?.abort();

    if (effect === "off") {
      this.gate.hold("off-requested");
    }

    this.enqueue();
  }

  updatePolicy(policy: DevicePolicy): void {
    this.policy = policy;
    this.version += 1;
    this.preparation?.abort();
    this.enqueue();
  }

  async idle(): Promise<void> {
    await this.work;
  }

  private enqueue(): void {
    // One writer reaches the adapter at a time. Each queued reconciliation
    // reads the latest intent, so bursts naturally coalesce.
    this.work = this.work.then(() => this.reconcile());
  }

  private resolve(requested: EffectId): EffectId {
    const effect = EFFECTS[requested];

    if (requested === "off") return "off";

    if (this.policy.budget === "constrained") {
      if (effect.needsGan || effect.needsSegmentation) {
        return effect.fallback;
      }
    }

    if (effect.needsGan && !this.policy.ganAllowed) {
      return effect.fallback;
    }

    if (effect.needsSegmentation && !this.policy.segmentationAllowed) {
      return effect.fallback;
    }

    return requested;
  }

  private async reconcile(): Promise<void> {
    const operationVersion = this.version;
    const requested = this.desired;

    if (!this.consent || requested === "off") {
      try {
        await this.adapter.clear();
        this.state = { kind: "ready", active: "off" };
        this.gate.releaseRaw();
      } catch (error) {
        this.state = {
          kind: "failed",
          requested: "off",
          message: `Could not clear the effect: ${messageOf(error)}`,
        };
        // Do not release processed output when clear has not been confirmed.
      }
      return;
    }

    const resolved = this.resolve(requested);
    this.state = { kind: "preparing", requested, resolved };

    const abort = new AbortController();
    this.preparation = abort;

    try {
      await this.adapter.prepare(resolved, abort.signal);

      // Preparation completed, but another user action may now be authoritative.
      if (operationVersion !== this.version) return;

      await this.adapter.apply(resolved);

      // apply() might not be cancellable. If intent changed while it ran, leave
      // final reconciliation to the queued operation.
      if (operationVersion !== this.version) return;

      this.state = {
        kind: "active",
        requested,
        active: resolved,
        degraded: requested !== resolved,
      };
    } catch (error) {
      if (operationVersion !== this.version || isAbort(error)) return;

      try {
        await this.adapter.clear();
        this.gate.releaseRaw();
      } catch {
        // Keep the output gate's existing safety posture if cleanup also fails.
      }

      this.state = {
        kind: "failed",
        requested,
        message: `Effect unavailable: ${messageOf(error)}`,
      };
    } finally {
      if (this.preparation === abort) this.preparation = undefined;
    }
  }
}

function isAbort(error: unknown): boolean {
  return error instanceof Error && error.name === "AbortError";
}

function messageOf(error: unknown): string {
  return error instanceof Error ? error.message : "unknown error";
}

Several choices here are deliberate:

  • Selection and consent are not the same state. Device support cannot grant permission.
  • Consent withdrawal forgets the old effect. Granting consent again does not silently restore it.
  • Adapter calls are serialized. Two asynchronous effect commits cannot race as independent writers.
  • Preparation is cancellable. Resource work can stop early where the underlying implementation supports it.
  • Application is still version-checked. Cancellation alone is insufficient because commit operations may be non-cancellable.
  • Cleanup must succeed before raw output is released. A failed clear() is not equivalent to an effect being off.

Reproduce the races

Create test/coordinator.test.ts:

import assert from "node:assert/strict";
import test from "node:test";
import {
  BeautyAdapter,
  BeautyCoordinator,
  EffectId,
  OutputGate,
} from "../src/coordinator.js";

const sleep = (ms: number) =>
  new Promise<void>((resolve) => setTimeout(resolve, ms));

class FakeAdapter implements BeautyAdapter {
  prepared: EffectId[] = [];
  applied: EffectId[] = [];
  clears = 0;
  prepareDelay = 0;
  applyDelay = 0;
  failApply = false;

  async prepare(effect: EffectId, signal: AbortSignal): Promise<void> {
    this.prepared.push(effect);

    await new Promise<void>((resolve, reject) => {
      const timer = setTimeout(resolve, this.prepareDelay);
      signal.addEventListener(
        "abort",
        () => {
          clearTimeout(timer);
          const error = new Error("aborted");
          error.name = "AbortError";
          reject(error);
        },
        { once: true },
      );
    });
  }

  async apply(effect: EffectId): Promise<void> {
    await sleep(this.applyDelay);
    if (this.failApply) throw new Error("GPU setup failed");
    this.applied.push(effect);
  }

  async clear(): Promise<void> {
    this.clears += 1;
  }
}

class FakeGate implements OutputGate {
  held = false;
  reasons: string[] = [];

  hold(reason: "off-requested" | "consent-withdrawn"): void {
    this.held = true;
    this.reasons.push(reason);
  }

  releaseRaw(): void {
    this.held = false;
  }
}

const normalPolicy = {
  segmentationAllowed: true,
  ganAllowed: true,
  budget: "normal" as const,
};

test("latest selection wins while preparation is in flight", async () => {
  const adapter = new FakeAdapter();
  const gate = new FakeGate();
  adapter.prepareDelay = 20;

  const coordinator = new BeautyCoordinator(adapter, gate, normalPolicy);
  coordinator.setConsent(true);
  coordinator.select("virtual-bg");

  await sleep(5); // Let virtual-bg preparation begin.
  coordinator.select("gan-look");
  await coordinator.idle();

  assert.deepEqual(adapter.applied, ["gan-look"]);
  assert.deepEqual(coordinator.getState(), {
    kind: "active",
    requested: "gan-look",
    active: "gan-look",
    degraded: false,
  });
});

test("consent withdrawal holds output before a non-cancellable apply ends", async () => {
  const adapter = new FakeAdapter();
  const gate = new FakeGate();
  adapter.applyDelay = 25;

  const coordinator = new BeautyCoordinator(adapter, gate, normalPolicy);
  coordinator.setConsent(true);
  coordinator.select("gan-look");

  await sleep(5); // apply() is now in flight.
  coordinator.setConsent(false);

  assert.equal(gate.held, true);
  assert.equal(gate.reasons.at(-1), "consent-withdrawn");

  await coordinator.idle();

  assert.deepEqual(coordinator.getState(), { kind: "ready", active: "off" });
  assert.equal(gate.held, false);
  assert.ok(adapter.clears > 0);
});

test("a constrained budget resolves GAN to the declared fallback", async () => {
  const adapter = new FakeAdapter();
  const gate = new FakeGate();
  const coordinator = new BeautyCoordinator(adapter, gate, {
    segmentationAllowed: true,
    ganAllowed: true,
    budget: "constrained",
  });

  coordinator.setConsent(true);
  coordinator.select("gan-look");
  await coordinator.idle();

  assert.deepEqual(adapter.applied, ["soft-beauty"]);
  assert.deepEqual(coordinator.getState(), {
    kind: "active",
    requested: "gan-look",
    active: "soft-beauty",
    degraded: true,
  });
});

test("an apply failure returns to raw video and remains visible", async () => {
  const adapter = new FakeAdapter();
  const gate = new FakeGate();
  adapter.failApply = true;

  const coordinator = new BeautyCoordinator(adapter, gate, normalPolicy);
  coordinator.setConsent(true);
  coordinator.select("virtual-bg");
  await coordinator.idle();

  assert.equal(coordinator.getState().kind, "failed");
  assert.ok(adapter.clears > 0);
  assert.equal(gate.held, false);
});

Run the suite:

npx tsx --test test/*.test.ts

These tests verify protocol behavior rather than image quality. Visual quality still requires real-device review, but it should not be used as a substitute for testing control-flow races.

Connect the adapter without guessing SDK APIs

Keep the Tencent RTC integration behind BeautyAdapter:

class TencentBeautyAdapter implements BeautyAdapter {
  constructor(
    private readonly beautyRuntime: YourDocumentedBeautyIntegration,
  ) {}

  async prepare(effect: EffectId, signal: AbortSignal): Promise<void> {
    // Map your application's effect ID to the documented resource-loading path.
    // If that path cannot consume AbortSignal, check signal.aborted before and
    // after loading and let the coordinator reject stale completion.
  }

  async apply(effect: EffectId): Promise<void> {
    // Map exactly one resolved effect to your documented Beauty AR setup.
  }

  async clear(): Promise<void> {
    // Remove appearance processing using the documented integration path.
  }
}

Do the same for OutputGate. It belongs at the point in your architecture where you can prevent processed frames from reaching the preview or publication path. Do not hide this behavior inside a button component; consent can change because of navigation, account state, or another product-level event.

The official low-end optimization guide discusses choosing suitable performance modes, disabling expensive segmentation or 3D/GAN effects, and controlling resolution and frame rate. Treat those as measurable deployment policies, not universal constants. This coordinator only chooses an effect workload. Your media configuration should separately own resolution and frame-rate decisions.

Decide the budget from evidence

Avoid classifying a device from a model name alone. Build your deployment policy from observed sessions and real-device testing.

Useful measurements include:

  • preview and published frame stability;
  • render or processing duration where observable;
  • thermal behavior over a representative session;
  • memory pressure during effect switching;
  • camera interruptions and application lifecycle changes;
  • whether audio or RTC behavior degrades when visual processing is enabled.

Then make one of three decisions for each effect-and-device class:

  1. Allow: the effect stays within the accepted product budget.
  2. Resolve to fallback: the requested effect is too expensive, but a declared lighter option is acceptable.
  3. Keep off: neither the request nor its fallback meets the budget.

Do not silently label fallback as success. If gan-look resolves to soft-beauty, render that distinction in the UI and telemetry.

Hidden costs this design makes visible

Output gating can create a visual interruption

Immediately holding processed output is safer than displaying an effect after withdrawal, but it may produce a short freeze or placeholder. Product and design should choose that presentation explicitly.

Serialization can make switching feel slower

A single-writer pipeline sacrifices some apparent parallelism. In return, it prevents competing commits from owning the renderer. Preparation can still be optimized or cached, but commits should remain ordered.

Caches create another policy surface

Caching effect resources can improve repeat selection, but it also consumes memory and complicates invalidation. Cache ownership should not leak into the consent model: retaining a resource is not permission to reactivate it.

Graceful degradation increases the QA matrix

Every fallback adds states to verify: request, resolved effect, visible label, recovery, and telemetry. That is a real engineering cost. If a fallback cannot be explained or tested, keeping the feature off may be the better decision.

Where the AI isβ€”and is not

Segmentation, tracking, enhancement, and GAN-based appearance effects can involve real-time computer vision. That is demonstrated capability, not authority.

The model or visual pipeline can transform pixels. It should not decide:

  • whether a person consented to appearance modification;
  • whether a fallback is aesthetically acceptable;
  • whether an effect should silently return after consent changes;
  • whether thermal, battery, or accessibility trade-offs are acceptable;
  • whether a processed image still represents the user as intended.

This reframes the anxiety around AI changing engineering roles. The valuable work does not disappear when a visual model becomes more capable. It moves toward defining invariants, measuring budgets, designing reversible controls, and testing the asynchronous boundaries where automated systems routinely fail.

Real-device verification pass

Before release, run this checklist on each supported capability tier:

  • [ ] Select three effects rapidly; only the final selection becomes active.
  • [ ] Select Off while preparation is running; the old effect never becomes the final visible state.
  • [ ] Withdraw consent during application; processed output is held immediately.
  • [ ] Grant consent again; the old effect does not reactivate automatically.
  • [ ] Deny GAN capability; the UI reports the actual fallback.
  • [ ] Deny segmentation capability; virtual background does not pretend to be active.
  • [ ] Force preparation failure; raw video remains available with a retry path.
  • [ ] Force apply failure; the application does not report success.
  • [ ] Force clear failure; processed output remains gated rather than falsely reporting Off.
  • [ ] Background and restore the application during preparation.
  • [ ] Test a representative-length session for heat, memory pressure, and frame stability.
  • [ ] Confirm that Beauty AR stress does not conceal media or audio failures elsewhere in the RTC experience.

Discussion: make the contract shared

A useful organizational change does not begin with announcing that AI will automate more work. It begins when product, engineering, QA, and design stop carrying incompatible assumptions.

For this feature, the shared artifact is the transition contract:

  • Product defines what each fallback means.
  • Design defines how gating and degradation are shown.
  • Engineering guarantees ordering and cleanup.
  • QA attacks the asynchronous boundaries.
  • Users remain the authority over whether appearance effects are active.

That is a more durable outcome than a flawless demo. The hard part of Beauty AR is not merely making an effect appearβ€”it is ensuring that it disappears when the person says it should.

Disclosure: I contribute developer content for Tencent RTC. I used the official Tencent RTC Beauty AR documentation as the implementation reference for this article.

πŸ“° Read the original article on Dev.to AI

Originally published by Dev.to AI. Aggregated on AIWithGhost for educational purposes β€” full credit and traffic to the original publisher.