165 lines
6.0 KiB
TypeScript
165 lines
6.0 KiB
TypeScript
/**
|
||
* 视频通话美颜设置
|
||
*
|
||
* 全部效果(磨皮/美白/红润/瘦脸/口红/腮红)默认由本地管线处理:
|
||
* MediaPipe Face Landmarker(开源免费)识别人脸关键点 + WebGL/Canvas 逐帧渲染,
|
||
* 经 getUserMedia 拦截注入通话视频流(见 utils/beauty/ 目录),预览所见即所得。
|
||
*
|
||
* 仅当浏览器不支持 WebGL 时,磨皮/美白/红润回退到腾讯云 TRTC 免费 BasicBeauty
|
||
* 插件(TUICallEngine.setBeautyLevel)。不依赖付费的腾讯特效 SDK。
|
||
*/
|
||
|
||
/** 高级美颜(本地 AI 处理)设置 */
|
||
export interface AdvancedBeautySettings {
|
||
/** 总开关;通话中途开启需重新开关摄像头才能接管视频流 */
|
||
enabled: boolean
|
||
/** 瘦脸强度 0-100 */
|
||
slimStrength: number
|
||
/** 口红浓度 0-100(0 为关闭) */
|
||
lipstickStrength: number
|
||
/** 口红颜色(hex) */
|
||
lipstickColor: string
|
||
/** 腮红浓度 0-100(0 为关闭) */
|
||
blushStrength: number
|
||
/** 腮红颜色(hex) */
|
||
blushColor: string
|
||
}
|
||
|
||
export interface CallBeautySettings {
|
||
/** 是否启用基础美颜(关闭时以全 0 参数下发,引擎会停用 BasicBeauty 插件) */
|
||
enabled: boolean
|
||
/** 磨皮算法:0 光滑(TRTCBeautyStyleSmooth)/ 1 自然(TRTCBeautyStyleNature) */
|
||
style: number
|
||
/** 磨皮级别 0-9 */
|
||
beautyLevel: number
|
||
/** 美白级别 0-9 */
|
||
whitenessLevel: number
|
||
/** 红润级别 0-9 */
|
||
ruddinessLevel: number
|
||
/** 高级美颜(瘦脸/口红/腮红,本地 AI) */
|
||
advanced: AdvancedBeautySettings
|
||
}
|
||
|
||
export const BEAUTY_STYLE_SMOOTH = 0
|
||
export const BEAUTY_STYLE_NATURE = 1
|
||
export const BEAUTY_LEVEL_MAX = 9
|
||
|
||
const STORAGE_KEY = 'tcm_call_beauty_settings'
|
||
|
||
export const defaultAdvancedBeautySettings = (): AdvancedBeautySettings => ({
|
||
enabled: false,
|
||
slimStrength: 30,
|
||
lipstickStrength: 0,
|
||
lipstickColor: '#c94f5e',
|
||
blushStrength: 0,
|
||
blushColor: '#e88193'
|
||
})
|
||
|
||
export const defaultBeautySettings = (): CallBeautySettings => ({
|
||
enabled: false,
|
||
style: BEAUTY_STYLE_NATURE,
|
||
beautyLevel: 5,
|
||
whitenessLevel: 3,
|
||
ruddinessLevel: 2,
|
||
advanced: defaultAdvancedBeautySettings()
|
||
})
|
||
|
||
const clampLevel = (value: unknown): number => {
|
||
const num = Math.round(Number(value))
|
||
if (!Number.isFinite(num)) return 0
|
||
return Math.min(BEAUTY_LEVEL_MAX, Math.max(0, num))
|
||
}
|
||
|
||
const clampPercent = (value: unknown, fallback: number): number => {
|
||
const num = Math.round(Number(value))
|
||
if (!Number.isFinite(num)) return fallback
|
||
return Math.min(100, Math.max(0, num))
|
||
}
|
||
|
||
const normalizeHexColor = (value: unknown, fallback: string): string => {
|
||
if (typeof value === 'string' && /^#[0-9a-fA-F]{6}$/.test(value)) return value
|
||
return fallback
|
||
}
|
||
|
||
/** 高级美颜是否有任一效果实际开启 */
|
||
export function hasAnyAdvancedEffect(advanced: AdvancedBeautySettings): boolean {
|
||
return (
|
||
advanced.enabled &&
|
||
(advanced.slimStrength > 0 || advanced.lipstickStrength > 0 || advanced.blushStrength > 0)
|
||
)
|
||
}
|
||
|
||
/** 全部美颜(基础 + 高级)是否有任一效果实际开启(决定是否接管摄像头流) */
|
||
export function hasAnyBeautyEffect(settings: CallBeautySettings): boolean {
|
||
const basicOn =
|
||
settings.enabled &&
|
||
(settings.beautyLevel > 0 || settings.whitenessLevel > 0 || settings.ruddinessLevel > 0)
|
||
return basicOn || hasAnyAdvancedEffect(settings.advanced)
|
||
}
|
||
|
||
export function loadBeautySettings(): CallBeautySettings {
|
||
const defaults = defaultBeautySettings()
|
||
try {
|
||
const raw = localStorage.getItem(STORAGE_KEY)
|
||
if (!raw) return defaults
|
||
const parsed = JSON.parse(raw)
|
||
const advDefaults = defaults.advanced
|
||
const adv = parsed?.advanced ?? {}
|
||
return {
|
||
enabled: Boolean(parsed?.enabled),
|
||
style:
|
||
parsed?.style === BEAUTY_STYLE_SMOOTH
|
||
? BEAUTY_STYLE_SMOOTH
|
||
: BEAUTY_STYLE_NATURE,
|
||
beautyLevel: clampLevel(parsed?.beautyLevel ?? defaults.beautyLevel),
|
||
whitenessLevel: clampLevel(parsed?.whitenessLevel ?? defaults.whitenessLevel),
|
||
ruddinessLevel: clampLevel(parsed?.ruddinessLevel ?? defaults.ruddinessLevel),
|
||
advanced: {
|
||
enabled: Boolean(adv?.enabled),
|
||
slimStrength: clampPercent(adv?.slimStrength, advDefaults.slimStrength),
|
||
lipstickStrength: clampPercent(adv?.lipstickStrength, advDefaults.lipstickStrength),
|
||
lipstickColor: normalizeHexColor(adv?.lipstickColor, advDefaults.lipstickColor),
|
||
blushStrength: clampPercent(adv?.blushStrength, advDefaults.blushStrength),
|
||
blushColor: normalizeHexColor(adv?.blushColor, advDefaults.blushColor)
|
||
}
|
||
}
|
||
} catch {
|
||
return defaults
|
||
}
|
||
}
|
||
|
||
export function saveBeautySettings(settings: CallBeautySettings) {
|
||
try {
|
||
localStorage.setItem(STORAGE_KEY, JSON.stringify(settings))
|
||
} catch {
|
||
// localStorage 不可用时静默忽略,仅本次会话生效
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 将美颜设置应用到 TUICallEngine 实例。
|
||
* 引擎内部对 setBeautyLevel 自带 try-catch(摄像头未开启等场景不会抛错),
|
||
* 这里只兜底引擎不存在 / 接口缺失的情况。
|
||
*/
|
||
export async function applyBeautyToEngine(
|
||
engine: any,
|
||
settings: CallBeautySettings
|
||
): Promise<boolean> {
|
||
if (!engine || typeof engine.setBeautyLevel !== 'function') {
|
||
return false
|
||
}
|
||
const enabled = settings.enabled
|
||
try {
|
||
await engine.setBeautyLevel({
|
||
style: settings.style === BEAUTY_STYLE_SMOOTH ? BEAUTY_STYLE_SMOOTH : BEAUTY_STYLE_NATURE,
|
||
beautyLevel: enabled ? clampLevel(settings.beautyLevel) : 0,
|
||
whitenessLevel: enabled ? clampLevel(settings.whitenessLevel) : 0,
|
||
ruddinessLevel: enabled ? clampLevel(settings.ruddinessLevel) : 0
|
||
})
|
||
return true
|
||
} catch (error) {
|
||
console.warn('[call-beauty] 应用美颜设置失败', error)
|
||
return false
|
||
}
|
||
}
|