voxa_beauty 0.4.7 copy "voxa_beauty: ^0.4.7" to clipboard
voxa_beauty: ^0.4.7 copied to clipboard

PlatformAndroid

Real-time beauty for VoxaRTC on the camera capture path — smoothing and tone for free; colour filters, retouch, makeup looks, stickers and a cloud catalog with a Beauty key.

voxa_beauty #

pub package platform

Real-time beauty for voxa_rtc_engine, on the camera capture path — what the streamer sees is what the room receives.

Retouch, filters and stickers

Smoothing and tone are free. A Beauty key adds colour filters, face shaping, makeup looks, stickers, and a cloud catalog that grows without an app update.

dependencies:
  voxa_beauty: ^0.4.7

That is the whole integration. No gradle edit, no MainActivity change, no ProGuard rule, no native code — the filter registers itself.

final beauty = VoxaBeauty(engine);
await beauty.enable();
await beauty.setRetouch(smooth: 0.5, white: 0.4);

What you get #

Free With a Beauty key
Skin smoothing, brightness, warmth, saturation
Colour filters
Face shaping — slim, enlarge, narrow
Makeup looks
Face stickers
Cloud catalog

Gate your control on beauty.isSupported rather than assuming. It is false on platforms this package does not cover yet, so you show no toggle instead of one that does nothing.


Retouch #

Retouch off and on

Skin is found by colour, not by a face-shaped mask, so hair, brows, eyes and the room behind you stay exactly as they were — and no face tracking has to run for the free controls. Brightening is a gamma lift rather than a push toward white: mid-tones come up, black stays black, a lit forehead never blows out.

await beauty.setRetouch(smooth: 0.5, white: 0.4);
await beauty.setRetouch(slim: 0.2, enlarge: 0.2, narrow: 0.1);  // needs a key

All controls are 0..1 and take effect on the next frame, so they are cheap to drive from a slider.

Needs key Notes
smooth no Around 0.5 reads as a good camera. Past ~0.8 skin starts to look plastic.
white no Brightens skin. Mid-tones come up, highlights never blow out.
warmth no Nudges the white balance.
saturation no Extra colour.
slim yes Draws the jaw toward the face centre.
enlarge yes Magnifies the eyes.
narrow yes Draws the nose wings in.

setSmoothing, setBrightness, setWarmth and setSaturation are the same four controls under their 0.3 names, so an app written against 0.3 keeps working.


Colour filters #

The filters that ship in the package

Five filters ship inside the package, so the first frame never waits on the network.

final bundles = await VoxaBeautyBundles.builtIn();
final peach = bundles.firstWhere((b) => b.id == 'fresh_peach');

await beauty.setFilter(peach, strength: 0.7);
await beauty.setFilterStrength(0.4);   // dial it back live
await beauty.setFilter(null);          // off

Looks and stickers #

Two makeup looks and five stickers ship with the package. A look follows the mouth and eyes rather than sitting on top of them; a sticker hangs off face landmarks, and some react — a part shows while the mouth is open, or plays once on a blink. The face is tracked in every frame the camera produces, so a sticker stays on it through a turn or a shake rather than catching up after.

await beauty.setMakeupLook(bundles.firstWhere((b) => b.id == '604zc'), strength: 0.8);
await beauty.setSticker(bundles.firstWhere((b) => b.id == '2676'));

The cloud catalog #

A slice of the catalog

Over eight hundred more effects live in the catalog and need no app update.

final catalog = await VoxaBeautyCatalog.load();

for (final item in catalog.stickers) {
  // item.name, item.category and item.thumbUrl are what a picker needs
}

final bundle = await catalog.stickers.first.download(
  onProgress: (received, total) => setState(() => _progress = received / total),
);
await beauty.setSticker(bundle);

Effects are cached once installed, and VoxaBeautyBundles.installed() lists everything on the device, built-in and downloaded, so one picker can show it all.

A key can be limited to a chosen set of effects, so what your app lists is the slice picked for you rather than everything published. Catalog effects are encrypted and open only for an app whose key is live, so a copy of the file on its own is nothing. A download that is refused says why, and there is a daily download limit per device — both are on the key's card in the console.


With another engine #

The effects run inside voxa_rtc_engine's own capture path, where the frame never leaves the GPU. An engine that will not lend out its capture path — Agora among them — can still have them: this package runs the camera instead, and hands finished frames back.

final beauty = VoxaBeauty.standalone();   // no engine to attach to
final camera = VoxaBeautyCamera();

await beauty.setLicenseKey('vbk_…');
await beauty.setRetouch(smooth: 0.5, white: 0.35);

// Agora: publish a track of our own, so the camera can be released.
// Do not call setExternalVideoSource — a custom track needs only its id.
final trackId = await agora.createCustomVideoTrack();
await agora.enableLocalVideo(false);
await agora.updateChannelMediaOptions(ChannelMediaOptions(
    publishCameraTrack: false,
    publishCustomVideoTrack: true,
    customVideoTrackId: trackId));

await camera.start();                     // camera.preview is a widget

camera.frames.listen((f) {
  agora.getMediaEngine().pushVideoFrame(
    videoTrackId: trackId,
    frame: ExternalVideoFrame(
      type: VideoBufferType.videoBufferRawData,
      format: VideoPixelFormat.videoPixelI420,
      buffer: f.data,
      stride: f.width,
      height: f.height,
      rotation: f.rotation,
      timestamp: f.timestampMs,
    ),
  );
});

Two things there are not optional, and both fail quietly. The camera can only be held by one thing at a time, so the engine has to let go of it before this package can open it — for Agora that means enableLocalVideo(false), and stopPreview() is not enough. But that also stops the camera track carrying anything, which is why the frames go to a track of their own instead. Get one without the other and you have either a frozen picture or a room watching an unfiltered one.

Leave setExternalVideoSource out. It is Agora's older single-source API, and Agora does not switch sources inside a channel. Call it and switching the effects on during a live session leaves the room frozen on the last camera frame while every frame you push is quietly dropped — with no error anywhere. A custom track needs only its id: create it, publish it, push to it.

Media options belong to a channel, so apply them again after joining if beauty was switched on before.

Set the encoder configuration, or the room sees half the frames. Agora's default is 960x540 at 15 fps and it applies to an external video source exactly as it does to Agora's own camera. The camera captures 30, the preview shows 30, and the encoder throws half of them away — which reads as "the effects made the video slow" when the effects had nothing to do with it. Measured on a Pixel 6, same build, one call apart:

default configured for 30
handed to Agora 28.8 fps 28.8 fps
Agora encodes and sends 14 fps 29 fps
viewer renders 14 fps 26 fps

Then run the camera at the encoder's rate. VoxaBeautyCamera.start(fps:) takes the number; give it the same one you gave the encoder. A camera at 30 feeding an encoder set to 24 makes Agora drop one frame in six, and dropped frames never leave evenly — the host preview, which shows every frame, looks fine while the room stutters. Measured on the same call: a 29 fps camera into a 24 fps encoder had viewers decoding anywhere from 22 to 33 fps from one second to the next; the camera at 24 into the same encoder gave them a steady 24, and used less bandwidth doing it.

await agora.setVideoEncoderConfiguration(const VideoEncoderConfiguration(
    dimensions: VideoDimensions(width: 540, height: 960),
    frameRate: 24,
    degradationPreference: DegradationPreference.maintainFramerate));

Pick the number deliberately: 24 roughly doubles the bitrate of 15, which a multi-seat room pays once per publisher.

Nothing else in the app may start the camera while the effects are on. This is the one that bites late, because it looks harmless until you are in a call. camera.start() holds the camera; an app whose go-live path then calls enableLocalVideo(true) or startPreview() has two components reaching for one device. Outside a channel the engine's attempt just fails and you see nothing wrong. Inside a channel the engine wins — this package's capture session is closed under it, the preview stops on its last frame, and the room loses the video. Ask before you start capture:

if (!camera.isRunning) {
  await agora.enableLocalVideo(true);
  await agora.startPreview();
}
// the camera is running either way

The package no longer suffers this in silence. It notices, reports through cameraStatus, and keeps trying to open the camera again:

camera.cameraStatus.listen((s) {
  switch (s.state) {
    case VoxaBeautyCameraState.interrupted:   // frozen; coming back
    case VoxaBeautyCameraState.recovered:     // frames again
    case VoxaBeautyCameraState.failed:        // still held by whatever took it
  }
});

A brief interruption — backgrounding the app, something borrowing the camera for a moment — recovers on its own. failed means it did not, and is the point to tell the user or to turn the effects off and give the camera up.

This route costs more, and it is worth knowing why. Turning a GPU texture into bytes an engine will accept is a copy per frame. On a Pixel 6 the same effects hold the camera's full rate through voxa_rtc_engine and around 24 fps this way. Frames are handed over as I420 rather than RGBA, which halves what crosses the channel, and one that arrives while the last is still being delivered is dropped rather than queued.

The camera needs a WebRTC media library in the app. Apps on voxa_rtc_engine already have one.


Beauty key #

Create a key under Beauty in the console and hand it over once.

await beauty.setLicenseKey('vbk_…');
beauty.licenseStatus.listen((status) {
  if (status.isRefused) debugPrint('beauty key refused: ${status.code}');
});

The key binds to the first app that uses it, up to the number of apps it allows. Bound apps are listed in the console, where the key — or one app — can be switched off, and switching one off stops the next download rather than waiting out the day.

A refused key is never silent. licenseStatus carries the reason, and so does logcat:

Code What happened
unknown_key The gateway does not know this key.
disabled The key was switched off in the console.
app_disabled This app was switched off on the key.
too_many_apps The key already has as many apps as it allows.
signature_mismatch This bundle id was first seen signed by a different certificate.

Why it costs so little #

The filter runs in the graphics context the capturer and the hardware encoder already share. A frame arrives as a GPU texture, goes through the shader passes, and leaves as a GPU texture — no readback, no CPU copy, no pixel ever touching system memory, and nothing crossing into Dart.

Measured on a Pixel 6 at 640×360, cycling through every effect in the catalog in turn: 30 fps, held for the whole run — the camera's own capture rate, with a colour filter, retouch and a sticker on the frame. Face tracking runs on its own thread and drops frames rather than queueing them, so capture never waits for it. The camera itself slows in poor light; the beauty pass does not add to that.

There is no per-frame millisecond figure here on purpose. GL commands are queued rather than executed, so timing the draw call measures how long it took to submit work, not what the GPU spent on it. Sustained capture rate is the honest number.


Platforms #

Platform Status
Android Supported, API 21+
iOS Planned

Documentation #

Full setup, the catalog API and the console walkthrough are at voxartc.com/docs/beauty.

The face in these images is computer-generated and is not a real person. Every effect shown on it was produced by this package's own shaders; the panel is the example app.

1
likes
160
points
381
downloads
screenshot

Documentation

Documentation
API reference

Publisher

verified publishervoxartc.com

Weekly Downloads

Real-time beauty for VoxaRTC on the camera capture path — smoothing and tone for free; colour filters, retouch, makeup looks, stickers and a cloud catalog with a Beauty key.

Homepage

Topics

#beauty #camera #video #filters #webrtc

License

MIT (license)

Dependencies

flutter, voxa_rtc_engine

More

Packages that depend on voxa_beauty

Packages that implement voxa_beauty