Process Videos with Beauty Effects
Overview
ArRecorderSdk, added in SDK 2.0.3, performs secondary processing on an existing mp4 file or online video URL — applying beauty effects, stickers, filters, and the like — and exports a new mp4. Compared with the realtime ArSdk, ArRecorderSdk:Does not depend on a camera or live stream; it operates on local or remote recorded mp4 files.
Awaits each frame sequentially with no dropped frames; processing time depends on device performance — the faster the device, the faster the processing.
Passes audio through unchanged (no decode / re-encode).
Uses WebCodecs hardware decode/encode (no software fallback).
Use cases
Local beauty processing on UGC videos before upload.
Batch-adding filters or sticker effects to recorded mp4 files.
Implementing a "record first, beautify later" workflow.
Notes
Browser requirements
WebCodecs (
VideoEncoder / VideoDecoder) is required: Chrome 102+ / Edge 102+. Safari and most mobile browsers are not supported.For large outputs (> 200 MB), File System Access API support (Chrome / Edge desktop) is recommended; otherwise the SDK falls back to in-memory mode, where files larger than ~1.5 GB may cause OOM.
Usage notes
Cannot coexist with ArSdk: instantiating
ArSdk and ArRecorderSdk on the same page simultaneously corrupts shared state via the AROptionsManager singleton. Always destroy() one before creating the other.Parameters locked during process: calls to
setBeautify / setEffect / setFilter while process() is running will throw. To set parameters before the first encoded frame, listen on the beforeRun event and await your set* calls inside the handler:recorder.on('beforeRun', async ({ mode }) => {await recorder.setEffect({ id: 'xxx', intensity: 0.5 })})
First-call lazy load: when
beautify is omitted in the constructor, the first runtime call to setBeautify / setEffect triggers a pipeline lazy-load (mediapipe + wasm, ~2-3 s). To make the first frame include effects, pass beautify in the constructor so init() can pre-warm.WebCodecs guard: when the browser lacks
VideoEncoder / VideoDecoder, the constructor emits an error event with ENVIRAONMENT_NOT_COMPATIBLE. Gate the SDK with environment detection up front.Audio is not re-encoded: source audio is muxed into the output mp4 unchanged; no decode / re-encode happens during processing.
Demo download
Importing
ArRecorderSdk ships in the same npm package as ArSdk:import { ArRecorder } from 'tencentcloud-webar'const recorder = new ArRecorder.ArRecorderSdk({auth: { licenseKey, appId, authFunc },beautify: { whiten: 0.3, dermabrasion: 0.5 },})
Note:
Do not instantiate
ArSdk and ArRecorderSdk on the same page simultaneously — they will pollute each other's state. destroy() one before instantiating the other.Full example
import { ArRecorder } from 'tencentcloud-webar'async function processVideo(file) {const recorder = new ArRecorder.ArRecorderSdk({auth: {licenseKey: 'YOUR_LICENSE_KEY',appId: 'YOUR_APP_ID',authFunc: yourAuthFunc, // Same auth callback as ArSdk},beautify: {whiten: 0.3,dermabrasion: 0.5,},})// 1. Authenticate and pre-warm the processing pipelineawait recorder.init()// 2. Optionally set beauty / effects / filters at runtimeawait recorder.setBeautify({ whiten: 0.5, lift: 0.2 })await recorder.setEffect({ id: 'effectId', intensity: 0.7 })await recorder.setFilter('filterId', 0.5)// 3. Processconst result = await recorder.process(file, {onProgress: (current, total) => {console.log(`Processed ${current}/${total} frames`)},})// 4. Consume the resultif (result.mode === 'in-memory') {const url = URL.createObjectURL(result.blob)document.querySelector('video').src = url} else {// file mode: already written to the user-selected disk fileconst file = await result.fileHandle.getFile()}// 5. Release resourcesrecorder.destroy()}
Constructor parameters
new ArRecorder.ArRecorderSdk(options) accepts the following config:Parameter | Description | Type | Required |
auth | Authentication parameters; same shape as ArSdk's auth | {licenseKey: stringappId: stringsdkAppId?: stringauthFunc: () => Promise<{ signature, timestamp }>language?: string} | Yes |
beautify | Beauty parameters; same shape as ArSdk's BeautifyOptions. When omitted, init() does not pre-warm the detect+render pipeline (saving load time), but the first runtime call to setBeautify / setEffect triggers a lazy load (~2-3 s). | BeautifyOptions | No |
module | Module config; same shape as ArSdk's ModuleConfig. Recorder currently consumes only the faceDetectModel field; the remaining fields (segmentation / handLandmark, etc.) keep the same names for migration compatibility but are not yet honored. | {faceDetectModel?: 'short' | 'full'// Other ArSdk fields accepted for compatibility but no-op} | No |
output | Output mp4 encoding config | {codec?: 'avc' // Only H.264 supported in current releasebitrate?: number | 'auto' // bps; 'auto' follows sourcefps?: number | 'auto' // 'auto' follows source} | No |
initReport | Whether to initialize the reporting module | Boolean | No, default true |
APIs
API | Parameters | Returns | Description |
async init() | - | Promise | Authenticate and (optionally) pre-warm the detect+render pipeline. When beautify is provided in the constructor, init() also pre-loads it so the first frame is not stalled by 2-3 s. |
async process(source, options?) | source: Blob | File | string — local file or online URL (CORS must allow access)options: see "process options" | Promise | Process the source video offline and export a new mp4. set* methods are forbidden during processing. |
async setBeautify(options) | BeautifyOptions (same as ArSdk) | Promise | Hot-update beauty parameters; effective on the next frame. Forbidden while process() is running. |
async setEffect(input) | input: effect ID | Effect object | array of either | empty array (clear) Same parameter style as ArSdk's setEffect. | Promise | Set / switch / clear effects (filter / sticker / makeup / 3D try-on). Forbidden while process() is running. |
async setFilter(id?, intensity?) | id: filter ID. Omit / '' / 'none' clears the current filter.intensity: 0-1, default 1 | Promise | Standalone LUT filter; uses a separate (mutually exclusive) channel from setEffect. Forbidden while process() is running. |
startPreview(source) | source: HTMLVideoElement | MediaStream | MediaStream | Start realtime preview. Returns a processed MediaStream the caller can attach to `.srcObject`. Source video play / pause / seek automatically syncs to the preview. |
stopPreview() | - | - | Stop the preview loop and release reader/writer. Called automatically before process(). |
destroy() | - | - | Destroy the instance and release detector / renderer / GPU resources. Required before instantiating ArSdk on the same page. |
process options
The
options argument to process(source, options):Parameter | Description | Type |
signal | AbortSignal for caller-side cancellation | AbortSignal |
onProgress | Encoding progress callback. current is the count of fully encoded frames; total is the total frame count. Reported on encode completion to avoid the "100% then waits a few minutes" feel caused by encoder queue draining. | (current: number, total: number) => void |
onDownloadProgress | Download progress for URL inputs. total = -1 means the server did not return Content-Length. | (loaded: number, total: number) => void |
stallTimeoutMs | URL mode: if no new bytes are received within this many ms, treat as a stalled network and abort. Default 30000; set 0 to disable. | Number |
mode | Output mode: 'in-memory': accumulate the whole mp4 in memory, return a Blob from finalize. Subject to V8 single-buffer limit (~1.5-2 GB).'file': stream-write to a user-selected disk file. Requires File System Access API (Chrome / Edge desktop only).'auto' (default): switch automatically based on estimated output size. | 'in-memory' \| 'file' \| 'auto' |
autoFileModeThreshold | Threshold (bytes) above which 'auto' switches to 'file'. Default 200 MB. | Number |
suggestedName | Default file name shown in the browser's save dialog ( 'file' mode). | String |
fileHandle | In 'file' / 'auto' modes, the recommended pattern is to call window.showSaveFilePicker() synchronously in the click handler and pass the resulting FileSystemFileHandle here, to avoid losing user activation (see below). | FileSystemFileHandle |
process() returns ProcessResult:{blob: Blob | null, // in-memory mode: the full mp4 Blob; file mode: nullfileHandle: FileSystemFileHandle | null, // file mode: handle to the saved file; in-memory: nullmode: 'in-memory' | 'file', // resolved mode (auto resolves to a concrete value)durationMs: number, // processing durationframeCount: number, // total frames processed}
Output mode selection
in-memory mode (default for small files)
Suitable for videos under 200 MB. The returned Blob is ready to use with
URL.createObjectURL(blob) for preview or upload.file mode (recommended for large files)
Suitable for videos larger than 200 MB to avoid memory exhaustion. You must call
window.showSaveFilePicker() synchronously on the user gesture path (e.g., a button click handler) to obtain a fileHandle, then pass it to process():button.onclick = async () => {const fileHandle = await window.showSaveFilePicker({suggestedName: 'output.mp4',types: [{ description: 'MP4 Video', accept: { 'video/mp4': ['.mp4'] } }],})const result = await recorder.process(file, {mode: 'file',fileHandle,})}
Note:
After
await input.open() and similar async steps, user activation has expired and any subsequent showSaveFilePicker() call inside the SDK will fail. Always grab the fileHandle at the very top of the click handler before any await.auto mode (default)
The SDK estimates output size as
source.bitrate × duration / 8. If it exceeds autoFileModeThreshold (default 200 MB), the SDK switches to 'file' mode. If File System Access API is unavailable, it falls back to 'in-memory' and prints a warning.