不论你是因为什么场景用到 UniApp 来开发移动端 这里分享的都是我自己的实战经验;如果哪部分理解得不够准确 也欢迎各位大佬在评论区指点纠正 一起交流提升;

虽然 UniApp 官方提供了 camera 组件 但它目前对 App 端并不支持 只能在部分小程序平台正常使用;

uni-app camera组件官网说明

我这次使用的是 Vue3 项目结构 + Vue2 语法(主要是当时刚上手 HBuilderX 推荐什么就跟着用了);如果你不强求新特性、Composition API 那些 其实 Vue2 的语法现在依然够稳、够成熟 踩坑概率也更低;

目前遇到的问题是:相机组件在 App 端渲染出来的 只能是方形预览框 我尝试了多种方式仍没找到可行的方案把它“强制”改成圆形;如果有大佬实现过 欢迎在评论区分享思路 我也想继续探索一下新的写法;

文末附带完整源码 供大家参考~

live-pusher是什么

live-pusher = 系统原生摄像头 API + 腾讯/自研推流引擎封装

UniApp 官方说明很明确
App 端 live-pusher 是使用原生能力实现的 不是 H5  所以h5上是不能渲染的

底层使用

  • Android:Camera、Camera2、MediaCodec、SurfaceView、OpenGL

  • iOS:AVFoundation(AVCaptureSession、AVCaptureDevice)

  • 推流编码:硬件编码(H.264),有些版本是基于腾讯 TUIKit Lite 封装

在 App 上,它不是浏览器渲染,是直接调 Android/iOS 原生 API,所以使用稳定、延迟低、清晰度高;

live-pusher属性

<live-pusher
					id="livePusher"
					device-position="front"
					mode="SD"
					:enable-camera="true"
					:auto-focus="true"
					:muted="true"
					style="width: 100%; height: 100%"
				></live-pusher>

1. id="livePusher"

组件的唯一标识,后面用 JS 通过
uni.createLivePusherContext('livePusher')
来控制它(开始推流、停止、快照…)。


2. device-position="front"

选择摄像头角度:

  • front = 前置摄像头(自拍)

  • back = 后置摄像头

你做活体检测、身份验证,基本都是 front


3. mode="SD"

推流模式(视频质量):

  • SD:标清(更稳定,耗流量小)

  • HD:高清

  • FHD:全高清(清晰但更吃资源)

  • RTC:实时模式(视频通话那种实时性)

如果你只是采集脸图,不推流,其实模式影响不大。


4. :enable-camera="true"

是否打开摄像头。

  • true:启用

  • false:禁用(不显示画面)


5. :auto-focus="true"

是否自动对焦。

  • true:摄像头自动调整清晰度(推荐)

  • false:固定焦距(一般不用)


6. :muted="true"

是否关闭麦克风。

  • true:静音(只采集视频)

  • false:同时采集声音

你做人脸识别,只需要视频,静音没问题。


7. style="width: 100%; height: 100%"

占满整个父容器,如果外层 view 是全屏,这里就是全屏摄像头画面。

总结

属性 作用 场景建议
id 用于 JS 控制 live-pusher 必填
device-position 前后摄像头 活体、自拍用 front
mode 推流质量 离线采集无差别,SD 更稳
enable-camera 是否启用摄像头 必填 true
auto-focus 自动对焦 推荐 true
muted 静音模式 人脸识别必须开 true
style 宽高 自由控制

配置完上述属性还是不行 发现我们打开还是看不见人脸 那我们就要开始写js部分了

JavaScript部分

变量声明

data() {
		return {
			livePusher: null,	//查看是否创建好了摄像头
			isDetecting: false,	//标记是否开始检测
			isFaceExist: false, 	//人脸在不在框内
			desc: '请正对摄像头,保持面部在方框内',  //提示语
			currentBg: '#ffffff', //当前背景颜色
			actionList: ['眨眨眼', '张张嘴', '摇摇头'], //活体检测动作 
			colors: ['#ff4d4f', '#fadb14', '#1890ff', '#52c41a', '#722ed1'], //变换颜色
			actionTimer: null,	//计时器
			detectTimer: null, //计时器
			resourceUrl: ''
		};
	},

我这边是写了多个颜色和多个动作 最好是颜色和动作数量对齐 ;

使用onReady钩子

onReady() {
		this.livePusher = uni.createLivePusherContext('livePusher', this);
		this.livePusher.startPreview({
			success: () => console.log('摄像头预览启动成功'),
			fail: (err) => console.error('摄像头预览失败', err)
		});
		this.switchCamera(); //调用摄像头
		this.startDetect();
	},
 this.livePusher = uni.createLivePusherContext('livePusher', this);

这里第一个参数 livePusher 就是我们开始在组件内部声明的id;

第二个参数this就是把 context 绑定到当前 Vue 实例;

	this.livePusher.startPreview({
			success: () => console.log('摄像头预览启动成功'),
			fail: (err) => console.error('摄像头预览失败', err)
		});

this.livePusher

就是你用 uni.createLivePusherContext() 创建的控制器;

startPreview()

告诉系统:

“打开摄像头,把画面展示出来,但我现在还不推流。

其他方法和区别

方法 作用
startPreview() 只显示摄像头画面,不推流(你做人脸识别就是用这个)
start() 开始真正的推流(直播、通话场景)

(tips:如打开调试发现还是前置摄像头的话可以使用这个方法调转在onReady钩子内

this.switchCamera()

switchCamera() {
			if (!this.livePusher) return;
			this.livePusher.switchCamera({
				success: () => {},
				fail: (err) => {}
			});
		},

在前置 / 后置摄像头之间切换

this.startDetect()

startDetect() { 
			if (this.isDetecting) return;
			this.isDetecting = true;
			this.desc = '正在检测人脸...';
			this.detectLoop();
		},

这里就在判断是否开始了人脸检测 ;变量声明是默认直接为false未开始 进入这个函数后直接开始;

this.detectLoop();

	detectLoop() {
			this.detectTimer = setInterval(async () => {
				const faceDetected = await this.detectFace();

				if (!faceDetected) {
					// 没检测到人脸
					this.isFaceExist = false;
					this.desc = '请正对摄像头,保持面部在方框内';
					this.stopActionDetect();
					this.randomBg('#ffffff');
					return;
				}

				// 检测到人脸
				if (!this.isFaceExist) {
					this.isFaceExist = true;
					this.desc = '检测到人脸,准备开始动作识别...';
					setTimeout(() => this.startActionDetect(), 1500);
				}
			}, 2000);
		},

每2秒执行一次 detectFace()
        ↓
判断是否有人脸
        ↓
有脸?─────────────┐
   ↓ 是             否 ↓
检查 isFaceExist   停止动作检测
是否从 false→true   提示“请对准摄像头”
   ↓
第一次检测到脸?
   ↓ 是
更新提示 → 1.5秒后启动动作检测
 

this.getRandomColors();

// 随机选择三个不重复的颜色
		getRandomColors() {
			let copy = [...this.colors];
			let selected = [];
			for (let i = 0; i < 3; i++) {
				const idx = Math.floor(Math.random() * copy.length);
				selected.push(copy[idx]);
				copy.splice(idx, 1);
			}
			return selected;
		},

this.startActionDetect()

	startActionDetect() {
			if (this.actionTimer) clearInterval(this.actionTimer);

			const colors = this.getRandomColors();
			const shuffledActions = this.actionList.sort(() => Math.random() - 0.5);
			let step = 0;

			this.actionTimer = setInterval(() => {
				if (!this.isFaceExist) {
					this.stopActionDetect();
					return;
				}

				if (step < shuffledActions.length) {
					this.desc = shuffledActions[step];
					this.currentBg = colors[step];
					step++;
				} else {
					this.desc = '动作检测完成,开始识别中...';
					clearInterval(this.actionTimer);
					this.actionTimer = null;
					setTimeout(() => this.verifyFace(), 1500);
				}
			}, 3000);
		},

按照随机顺序让用户依次做动作(眨眼、摇头、点头…),每个动作给 3 秒,全部完成后自动进入 verifyFace() 做最终活体校验

随机动作排列 + 随机背景
       ↓
每 3 秒执行一次动作循环
       ↓
画面中无人脸?
       ↓ 是
停止动作检测 ←─────────┐
       ↓ 否             │            │
当前动作还未做完?        │
       ↓ 是             │            │
显示动作提示 + 换背景    │
step++               │            │
       │                 │            │
       └───────继续下一次循环
       ↓ 否(动作都做完)
显示“完成检测”
1.5秒后 this.verifyFace()

this.verifyFace();

verifyFace() {
			this.desc = '人脸识别中...';
			setTimeout(() => {
				this.desc = '识别成功';
				this.randomBg('#ffffff');
				this.isDetecting = false;
				clearInterval(this.detectTimer);
				this.goTo(this.resourceUrl);
			}, 2000);
		}

 this.stopActionDetect()

	stopActionDetect() {
			clearInterval(this.actionTimer);
			this.actionTimer = null;
		},

① 停止动作检测的定时器

this.actionTimerstartActionDetect() 里创建的循环动作(比如:抬头、低头、左右转等)。

一旦:

  • 人脸丢失

  • 或者识别流程被打断

  • 或者你手动调用停止

就执行:

clearInterval(this.actionTimer);

意味着:动作指令不再继续播放。


② 清空状态

this.actionTimer = null;

把定时器变量置空,避免:

  • 重复创建定时器

  • 内存泄漏

  • 多个 actionTimer 同时触发导致动作乱跳


以上就是本次人脸校验渲染核心代码解释 接下来附上源码

附上源码

<template>
	<view :style="{ backgroundColor: currentBg, width: '100vw', height: '100vh', transition: 'background-color 1.5s ease' }">
		<view class="header">
			<last-step :active="true" title="人脸活体检测" />
		</view>

		<view class="body">
			<!-- 不是白色背景的时候全部变成其他的颜色 -->
			<view class="camera-box" :class="{ active: currentBg !== '#ffffff' }">
				<live-pusher
					id="livePusher"
					device-position="front"
					mode="SD"
					:enable-camera="true"
					:auto-focus="true"
					:muted="true"
					style="width: 100%; height: 100%"
				></live-pusher>
			</view>

			<view class="other">
				<view class="desc" :class="{ active: currentBg !== '#ffffff' }">{{ desc }}</view>
				<!-- <view class="btn" @click="startDetect">开始识别</view> -->
			</view>
		</view>
	</view>
</template>

<script>
export default {
	data() {
		return {                
			livePusher: null,    	                 //查看是否创建好了摄像头
			isDetecting: false,	                    //标记是否开始检测
			isFaceExist: false, 	                //人脸在不在框内
			desc: '请正对摄像头,保持面部在方框内',   //提示语
			currentBg: '#ffffff', //当前背景颜色
			actionList: ['眨眨眼', '张张嘴', '摇摇头'],     //活体检测动作 
			colors: ['#ff4d4f', '#fadb14', '#1890ff', '#52c41a', '#722ed1'],   //变换颜色
			actionTimer: null,	                        //计时器
			detectTimer: null,                         //计时器 
		};
	},
	onReady() {
		this.livePusher = uni.createLivePusherContext('livePusher', this);
		this.livePusher.startPreview({
			success: () => console.log('摄像头预览启动成功'),
			fail: (err) => console.error('摄像头预览失败', err)
		});
		this.switchCamera();     //调用摄像头
		this.startDetect();
	},
	onUnload() {
		clearInterval(this.detectTimer);
		clearInterval(this.actionTimer);
		if (this.livePusher) this.livePusher.stopPreview();
	},
	methods: {
		startDetect() {
			if (this.isDetecting) return;
			this.isDetecting = true;
			this.desc = '正在检测人脸...';
			this.detectLoop();
		},

		// 模拟或调用人脸检测接口
		async detectFace() { 
			// const res = await uni.request({
			// 	url: '识别接口',
			// 	method: 'POST',
			// 	data: { base64: 当前帧截图 }
			// });
			// return res.data?.faceDetected;

			// 模拟检测: 随机返回是否检测到人脸
			// return Math.random() > 0.3; // 70% 概率检测到
			return true;//这是用于测试 返回永远拿到人脸
		},

		detectLoop() {
			this.detectTimer = setInterval(async () => {
				const faceDetected = await this.detectFace();

				if (!faceDetected) {
					// 没检测到人脸
					this.isFaceExist = false;
					this.desc = '请正对摄像头,保持面部在方框内';
					this.stopActionDetect();
					this.randomBg('#ffffff');
					return;
				}

				// 检测到人脸
				if (!this.isFaceExist) {
					this.isFaceExist = true;
					this.desc = '检测到人脸,准备开始动作识别...';
					setTimeout(() => this.startActionDetect(), 1500);
				}
			}, 2000);
		},

		// 随机选择三个不重复的颜色
		getRandomColors() {
			let copy = [...this.colors];
			let selected = [];
			for (let i = 0; i < 3; i++) {
				const idx = Math.floor(Math.random() * copy.length);
				selected.push(copy[idx]);
				copy.splice(idx, 1);
			}
			return selected;
		},

		startActionDetect() {
			if (this.actionTimer) clearInterval(this.actionTimer);

			const colors = this.getRandomColors();
			const shuffledActions = this.actionList.sort(() => Math.random() - 0.5);
			let step = 0;

			this.actionTimer = setInterval(() => {
				if (!this.isFaceExist) {
					this.stopActionDetect();
					return;
				}

				if (step < shuffledActions.length) {
					this.desc = shuffledActions[step];
					this.currentBg = colors[step];
					step++;
				} else {
					this.desc = '动作检测完成,开始识别中...';
					clearInterval(this.actionTimer);
					this.actionTimer = null;
					setTimeout(() => this.verifyFace(), 1500);
				}
			}, 3000);
		},

		stopActionDetect() {
			clearInterval(this.actionTimer);
			this.actionTimer = null;
		},

		randomBg(color) {
			this.currentBg = color;
		},
		switchCamera() {
			if (!this.livePusher) return;
			this.livePusher.switchCamera({
				success: () => {},
				fail: (err) => {}
			});
		},
		verifyFace() {
			this.desc = '人脸识别中...';
			setTimeout(() => {
				this.desc = '识别成功';
				this.randomBg('#ffffff');
				this.isDetecting = false;
				clearInterval(this.detectTimer); 
			}, 2000);
		}
	}
};
</script>

<style lang="scss" scoped>
@import '@/uni.scss';//外部我自己的样式

.body {
	margin-top: 20vh;
}
.btn {
	padding: 0.5vh 2vw;
	width: 20vw;
	font-size: 26rpx;
	border-radius: 20rpx;
	text-align: center;
	margin: 0 auto;
	background: $main-font-color;
	color: #fff;
}
.desc {
	font-size: 26rpx;
	color: #585858;
	text-align: center;
	margin-bottom: 1vh;
	font-weight: 500;
	font-weight: bold;
	transition: color 1.5s ease;
	&.active {
		color: #fff !important;
		transition: color 1.5s ease;
	}
}
.camera-box {
	margin: 10vh 0 5vw 30vw;
	width: 40vw;
	height: 40vw;
	border-radius: 20rpx;
	overflow: hidden;
	border: 4rpx solid #2196f3;
	box-shadow: 0 0 20rpx rgba(33, 150, 243, 0.8);
	animation: glow 2s infinite alternate;

	&.active {
		box-shadow: 0 0 20rpx rgba(255, 252, 253, 0.8);
		border: 4rpx solid #fff;
		animation: glow_active 2s infinite alternate;
	}
}

@keyframes glow_active {
	from {
		box-shadow: 0 0 10rpx rgba(255, 255, 255, 0.3);
	}
	to {
		box-shadow: 0 0 30rpx rgba(255, 255, 255, 1);
	}
}

@keyframes glow {
	from {
		box-shadow: 0 0 10rpx rgba(33, 150, 243, 0.3);
	}
	to {
		box-shadow: 0 0 30rpx rgba(33, 150, 243, 1);
	}
}
</style>

结尾

以上分享完全基于个人在项目中的一些实战体会 不一定是最优解 但都是踩过坑之后沉下来的干货;如果你有更高效、更优雅、更稳妥的实现方式 真心欢迎在评论区一起交流讨论 让我们都能少走点弯路、学到更多东西;如果哪一段我讲得不够准确 或者有理解偏差的地方 也希望各位大佬不吝指正 我这边一定虚心接受、及时修正;技术就是这样 互相交流、互相补位 一起成长才是最爽的!

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐