1. 为什么选择 live-pusher 来“模拟”人脸核身?

很多刚接触这个需求的开发者朋友可能会问,人脸核身听起来是AI算法和生物识别技术的事儿,为什么我们前端要用一个直播推流组件来做呢?这其实是一个典型的“曲线救国”思路,背后是我们在实际项目中遇到的真实困境。

我最早接手一个金融类App的实名认证模块时,客户要求嵌入人脸活体检测。第一反应当然是去找大厂的SDK,比如某度、某讯的。但对接下来发现几个头疼的问题:首先是费用,商用接口调用不便宜;其次是集成复杂度,SDK动辄几十兆,还会引入一堆原生依赖,让原本轻快的混合开发App变得臃肿;最麻烦的是审核和隐私,某些SDK的隐私协议会让App上架审核过程变得漫长。当时也尝试过H5方案,用了一个效果不错的JS人脸检测库,但在真机上跑起来,画面扭曲、摄像头方向错乱,用户体验非常糟糕。

正是在这种“前后夹击”的情况下,我把目光投向了uniapp的 live-pusher 组件。它的本质是调用手机摄像头进行实时视频流采集和推流。我突然想到,既然它能稳定地获取到高清的摄像头画面,那我能不能不直接做“识别”,而是做“采集+上传”,把复杂的算法交给服务端去处理呢?这样一来,前端的工作就变得纯粹且可控:我们只需要确保能拿到一张清晰、正脸的人脸图片。live-pusher 组件在跨平台(iOS/Android)的摄像头调用和画面渲染上非常成熟稳定,正好解决了我们最基础的“看得见”和“拍得清”的问题。

所以,这个方案的核心思路是:前端利用 live-pusher 实现一个稳定、体验良好的“人脸采集器”,通过定时抓拍或手动抓拍,将最佳帧的图片上传至服务端。服务端调用专业的人脸核身API进行活体检测和比对,再将结果返回给前端进行展示。 整个流程,前端模拟了“识别”的交互过程,而真正的“识别”工作后置了。这对于那些对实时性要求不是极端苛刻,但追求快速上线、成本可控和用户体验的金融、政务类应用来说,是一个非常实用的折中方案。

2. 从零开始:环境准备与权限处理

万事开头难,但第一步走稳了,后面就顺了。在开始写代码之前,我们需要把地基打好。

2.1 项目基础配置

首先,确保你的uniapp项目是较新的版本(HBuilderX 3.4+以上体验更佳)。在 manifest.json 文件中,我们需要进行两项关键配置:

  1. 模块配置:在 “App模块配置” 中,找到 “LivePusher(直播推流)”,务必勾选上。这是 live-pusher 组件能在App端生效的前提。
  2. 权限声明:在 “App权限配置” 中,添加摄像头和麦克风权限。虽然我们主要用摄像头,但 live-pusher 作为推流组件,麦克风权限通常也需要。对于Android,一般添加 <uses-permission android:name="android.permission.CAMERA" /> 和 <uses-permission android:name="android.permission.RECORD_AUDIO" />。iOS则需要在 ios->privacy 下添加 NSCameraUsageDescription 和 NSMicrophoneUsageDescription 的描述,告诉用户为什么需要这些权限。

这里有个小坑我踩过:如果你只用 nvue 页面,那按照上述配置即可。但如果你某些页面是 vue 页面,记得在 pages.json 里配置该页面的 style 时,加上 "usingComponents": true。虽然官方文档可能没强调,但有些情况下它能避免组件找不到的问题。

2.2 权限的动态申请与引导

权限处理是用户体验的第一道关,处理不好,用户刚打开功能就被系统弹窗吓退,或者干脆无法使用。我们不能依赖用户自己去设置里打开权限,必须主动、友好地引导。

我强烈推荐使用社区优秀的权限处理库,比如 wa-permission。自己从头写兼容Android和iOS的权限申请逻辑,会涉及大量原生API差异判断,非常繁琐。wa-permission 封装得很好,我们直接引入使用就行。

在你的项目里安装它(通常通过npm或直接拷贝js_sdk),然后在页面中引入:

// 在你的 liveb.nvue 或相关页面脚本中
import { requestAndroidPermission, gotoAppPermissionSetting } from '@/js_sdk/wa-permission/permission.js';

接下来,在页面初始化(比如 mounted 或 onLoad)时,调用权限检查函数。我的策略是:进入页面后,稍作延迟(比如600毫秒,让页面渲染稳定),然后检查摄像头权限。

async getCameraPermission() {
  const system = uni.getSystemInfoSync().platform; // 获取当前平台
  if (system === 'android') {
    // 使用 wa-permission 请求安卓权限
    const result = await requestAndroidPermission("android.permission.CAMERA");
    if (result === 1) {
      // 权限已授予,可以开始预览
      this.startPreview();
    } else {
      // 权限被拒绝,弹窗引导用户去设置
      uni.showModal({
        title: '提示',
        content: '需要摄像头权限才能进行人脸识别,请前往设置开启。',
        success: (res) => {
          if (res.confirm) {
            gotoAppPermissionSetting(); // 跳转到应用设置页面
          }
        }
      });
    }
  } else {
    // iOS 处理逻辑略有不同,通常系统弹窗会自动弹出
    // 这里可以直接调用 startPreview,如果用户拒绝,live-pusher 的 error 事件会捕获
    this.startPreview();
  }
}

对于iOS,由于系统设计,通常会在第一次调用 live-pusher 的 startPreview 时弹出系统权限申请框。我们需要做好错误监听,在 @error 事件中处理用户拒绝的情况,同样可以友好地提示用户去设置里打开。

3. 核心实现:live-pusher 的配置与抓拍逻辑

权限搞定,我们就可以专心对付 live-pusher 这个主角了。它的配置参数不少,但核心的就那么几个,调好了画面清晰又流畅。

3.1 live-pusher 组件的关键参数

在 nvue 页面中(再次强调,要用 nvue 页面以获得更好的性能和原生体验),我们这样定义组件:

<live-pusher
  id="livePusher"
  ref="livePusher"
  class="livePusher"
  url="" <!-- 推流地址,我们不需要推流,留空即可 -->
  mode="SD" <!-- 清晰度模式:SD标清,HD高清,FHD超清。权衡清晰度和性能,SD或HD足够 -->
  :muted="true" <!-- 静音,我们不需要声音 -->
  :enable-camera="true"
  :auto-focus="true" <!-- 自动对焦,很重要 -->
  :beauty="0" <!-- 美颜级别,根据业务需求调整,核身建议关掉或调低 -->
  :whiteness="0" <!-- 美白级别,同上 -->
  aspect="3:4" <!-- 画面比例,9:16是竖屏全屏,3:4更接近证件照比例,看UI设计 -->
  :device-position="front" <!-- 前置摄像头 -->
  @statechange="onStateChange"
  @error="onError"
  @netstatus="onNetStatus" <!-- 虽然不推流,但网络状态监听有时有用 -->
  :style="{ width: '400rpx', height: '500rpx' }"
></live-pusher>

几个经验之谈:

  • mode:不是越高越好。FHD 在部分老旧机型上可能导致预览卡顿或初始化失败。从 SD 开始测试,平衡清晰度和流畅度。
  • aspect:这个属性直接影响拍摄出的图片比例。如果你服务端的核身接口对图片比例有要求(比如必须是3:4),这里一定要设对。否则你拍出来的图片再上传,服务端算法可能认不出来。
  • beauty 和 whiteness:对于核身这种严肃场景,建议设为0。美颜可能会改变面部特征细节,影响后端识别的准确率。
  • local-mirror:这个属性控制预览画面的镜像。通常我们设置 ‘auto’ 或 ‘enable’,让用户看到习惯的镜像画面。但请注意,这不影响最终 snapshot 抓拍图片的镜像效果。抓拍的图片默认是非镜像的(即别人看你的视角)。如果你需要抓拍图片也是镜像的,可能需要后续用 canvas 处理,但一般核身接口都要求非镜像的原图。

3.2 创建实例与开始预览

live-pusher 组件需要通过上下文(Context)来操作。一个关键的坑点:必须在页面的 onReady 生命周期里创建这个上下文,在 created 或 mounted 里可能获取不到。

export default {
  onReady() {
    // 确保在这里创建实例
    this.livePusherCtx = uni.createLivePusherContext('livePusher', this);
    // 创建后,可以调用 startPreview 开始预览
    // 但通常我们把 startPreview 放在权限申请成功后调用
  },
  methods: {
    startPreview() {
      if (this.livePusherCtx) {
        this.livePusherCtx.startPreview({
          success: (res) => {
            console.log('摄像头预览开始成功');
            // 预览成功后,可以开始倒计时抓拍等逻辑
            this.startCountdown();
          },
          fail: (err) => {
            console.error('预览失败', err);
            // 处理失败,可能是权限问题或硬件问题
          }
        });
      }
    }
  }
}

3.3 设计抓拍策略:自动与手动

抓拍是整个流程的“快门”时刻。策略设计得好,用户体验就顺畅。

1. 自动倒计时抓拍(主流方案): 用户进入界面,调整好姿势后,界面开始一个5秒倒计时,倒计时结束自动抓拍。这能给予用户明确的准备时间,也避免了用户不知何时该点击的困惑。代码逻辑如下:

data() {
  return {
    countdown: 5, // 倒计时秒数
    countdownTimer: null,
    isCounting: false
  };
},
methods: {
  startCountdown() {
    this.isCounting = true;
    this.countdownTimer = setInterval(() => {
      if (this.countdown > 0) {
        this.countdown--;
        // 更新界面倒计时显示
      } else {
        clearInterval(this.countdownTimer);
        this.captureImage(); // 倒计时结束,执行抓拍
      }
    }, 1000);
  },
  async captureImage() {
    if (!this.livePusherCtx) return;
    this.livePusherCtx.snapshot({
      success: (res) => {
        // res.tempImagePath 就是临时图片路径
        console.log('抓拍成功,图片路径:', res.tempImagePath);
        this.uploadImage(res.tempImagePath); // 上传图片
      },
      fail: (err) => {
        console.error('抓拍失败', err);
        uni.showToast({ title: '抓拍失败,请重试', icon: 'none' });
      }
    });
  }
}

2. 手动抓拍(备用方案): 提供一个按钮,用户自觉准备好后点击抓拍。这对于某些希望用户主动控制的场景更合适。你可以将手动抓拍作为自动抓拍失败或用户中断后的备选方案。

抓拍时机优化:在倒计时还剩1秒时提前调用一次 snapshot。因为 snapshot 是异步操作,需要一点时间。提前一点调用,可以让“拍照”动作在倒计时为0时刚好完成,减少用户等待的“卡顿感”。

4. 图片上传、服务端交互与结果反馈

抓拍到图片只是成功了一半,把图片安全、正确地送到服务端,并优雅地展示结果,才是闭环的关键。

4.1 图片上传的格式与技巧

snapshot 成功后,我们拿到的是一个本地临时文件路径(tempImagePath)。上传这个文件,通常有两种方式:

方式一:使用 uni.uploadFile(推荐) 这是最直接的方式,以 multipart/form-data 格式上传文件流,适合大多数服务端接收。

uni.uploadFile({
  url: 'https://your-api-domain.com/face/verify', // 你的服务端接口
  filePath: this.tempImagePath,
  name: 'file', // 文件对应的 key,根据服务端要求调整
  formData: {
    // 可以附带其他参数,比如用户ID、业务类型等
    'userId': '12345',
    'bizType': 'login'
  },
  header: {
    'Authorization': 'Bearer your-token' // 如果需要认证
  },
  success: (uploadRes) => {
    const data = JSON.parse(uploadRes.data); // 注意:success 回调返回的 data 是字符串
    this.handleServerResponse(data);
  },
  fail: (err) => {
    console.error('上传失败', err);
    uni.showToast({ title: '网络异常,上传失败', icon: 'none' });
  }
});

方式二:转换为 Base64 如果服务端接口明确要求 Base64 字符串,则需要先转换。可以使用 uni.getFileSystemManager().readFile 进行 base64 编码。但要注意,Base64 字符串体积会比原文件大很多,在网络传输上不占优势,一般不建议。

const fs = uni.getFileSystemManager();
fs.readFile({
  filePath: this.tempImagePath,
  encoding: 'base64',
  success: (res) => {
    const base64Data = 'data:image/jpeg;base64,' + res.data;
    // 然后通过 uni.request 将 base64Data 作为参数发送
  }
});

性能与体验提示:

  • 上传前可以给个 uni.showLoading 提示“识别中...”,上传成功或失败后记得 uni.hideLoading()。
  • 图片过大可以前端先压缩。虽然 live-pusher 的 snapshot 质量不错,但你可以通过 quality 参数(如果组件支持)或上传前用 canvas 进行二次压缩,减少流量消耗和上传时间。

4.2 解析服务端响应与状态反馈

服务端在调用第三方人脸核身接口后,会返回一个结果。前端需要根据这个结果,给用户清晰、即时的反馈。

假设服务端返回的 JSON 结构如下:

{
  "code": "00000",
  "message": "成功",
  "data": {
    "isLive": true,
    "score": 0.92,
    "similarity": 0.88 // 与预留照片的相似度
  }
}

前端处理逻辑:

handleServerResponse(res) {
  uni.hideLoading();
  if (res.code === '00000') {
    const data = res.data;
    // 假设分数大于0.8认为通过
    if (data.score >= 0.8) {
      // 核身成功
      uni.showToast({ title: '认证成功', icon: 'success' });
      // 跳转到成功页面,或执行后续业务逻辑
      setTimeout(() => {
        uni.navigateTo({ url: '/pages/success/success' });
      }, 1500);
    } else {
      // 核身失败(活体检测分数低)
      this.showFailPage('人脸验证未通过,请确保是本人操作。');
    }
  } else if (res.code === 'AUTH_FAIL') {
    // 其他业务错误,如非活体、非本人等
    this.showFailPage(res.message || '认证失败');
  } else {
    // 系统错误
    uni.showModal({
      title: '提示',
      content: '系统繁忙,请稍后重试。',
      showCancel: false
    });
  }
},
showFailPage(msg) {
  // 跳转到统一的失败页面,并传递失败信息
  uni.navigateTo({
    url: `/pages/face-fail/face-fail?message=${encodeURIComponent(msg)}`
  });
}

4.3 失败页面的设计与重新尝试

失败页面 (face-fail.vue) 不能只是一个冷冰冰的提示。它应该:

  1. 明确告知原因:是光线太暗、人脸不正,还是非活体?
  2. 提供明确的操作路径:一个显眼的“重新认证”按钮。
  3. 保持状态连贯:点击“重新认证”返回上一页时,最好能自动重置并重新开始流程,而不是让用户看到一个停滞的界面。

这里可以用到 uni.$emit 和 uni.$on 进行跨页面通信。在失败页面返回时,触发一个事件,让人脸采集页面监听到,并执行重置逻辑。

失败页面 (face-fail.vue):

onBackPress() {
  // 返回时,通知上一个页面重新开始
  uni.$emit('faceVerifyRetry');
  return false; // 不阻止默认返回行为
}

人脸采集页面 (liveb.nvue):

onShow() {
  // 监听失败页面发出的重试事件
  uni.$on('faceVerifyRetry', () => {
    this.resetAndRestart(); // 重置倒计时、状态,并重新开始预览
  });
},
onUnload() {
  // 页面卸载时,移除监听,避免内存泄漏
  uni.$off('faceVerifyRetry');
},
methods: {
  resetAndRestart() {
    clearInterval(this.countdownTimer);
    this.countdown = 5;
    this.isCounting = false;
    // 重新开始预览
    this.livePusherCtx.startPreview();
  }
}

5. 多端兼容与性能优化实战

跨端开发,兼容性是永恒的课题。live-pusher 在 Android 和 iOS 上的表现有些细微差别,处理不好就会踩坑。

5.1 Android 与 iOS 的兼容性处理

  1. 摄像头切换 (switchCamera):

    • 在部分 Android 机型上,switchCamera 后可能需要短暂延迟再操作(如抓拍),否则可能失败。
    • iOS 上切换通常很流畅。建议在切换成功后,给一个简短的 Toast 提示,增强用户体验。
  2. 画面比例 (aspect):

    • 不同机型、不同摄像头(前后置)对某些比例的支持可能有差异。如果发现某个比例在特定机型上预览变形,可以尝试微调,比如从 ‘9:16’ 改为 ‘3:4’。
  3. 权限回调时机:

    • Android 上,我们主动调用 requestAndroidPermission,可以明确知道结果。
    • iOS 上,权限弹窗在 startPreview 时触发。如果用户拒绝,需要通过监听 live-pusher 的 @error 事件来捕获错误码,并引导用户去系统设置打开权限。错误码 1004 通常表示摄像头打开失败,可能源于权限问题。
  4. 页面生命周期:

    • 在 nvue 页面中,onReady 是操作组件上下文最安全的时机。从 vue 页面跳转到 nvue 页面时,注意生命周期的衔接,确保组件已渲染完成再调用方法。

5.2 性能优化与体验打磨

  1. 预览流畅度:

    • 如果感觉预览卡顿,首先尝试降低 mode,如从 HD 降到 SD。
    • 检查页面是否有多余的复杂动画或频繁的渲染,它们可能会抢占摄像头预览的资源。
  2. 内存管理:

    • 页面退出 (onUnload) 时,务必调用 livePusherCtx.stop() 和 livePusherCtx.stopPreview() 来关闭摄像头释放资源。否则,摄像头可能不会立即关闭,影响用户体验和电量。
    • 清除所有定时器,如倒计时定时器。
  3. UI/UX 细节:

    • 引导框:使用 cover-image 在 live-pusher 上层覆盖一个镂空的引导框(比如圆形),帮助用户将人脸对准。这是模拟专业人脸识别SDK体验的关键。
    • 提示文案:倒计时文案要友好,如“请正对屏幕”、“请保持静止”。抓拍和识别过程中的加载状态也要有提示。
    • 错误恢复:网络上传失败、识别超时等情况,要提供“重试”按钮,而不是让用户完全退出流程。
    • 姿态提示:如果条件允许,可以简单通过检测人脸在预览框中的位置(这需要额外的轻量级JS库或简单计算),给出“请靠近一点”、“请将脸移入框内”的提示,进一步提升通过率。

6. 避坑指南与进阶思考

把功能跑通只是第一步,要让它在各种真实场景下稳定可靠,还需要注意下面这些我踩过的“坑”。

坑一:nvue 与 vue 的差异 live-pusher 在 vue 页面也能用,但在复杂交互或低端安卓机上,nvue 的性能和稳定性优势明显。如果你的页面交互复杂(比如有复杂的倒计时动画、状态切换),强烈建议使用 nvue。cover-image 覆盖引导图也只在 nvue 下能稳定层级。

坑二:图片上传的路径问题 snapshot 返回的 tempImagePath 在不同平台上的前缀可能不同(如 ‘file://’, ‘http://tmp’)。uni.uploadFile 方法内部会处理这些差异,所以直接传入这个路径即可,不要自己去做字符串截取或判断,容易出错。

坑三:后台运行与锁屏 当App进入后台或手机锁屏时,摄像头会被系统释放。再次回到前台时,预览画面可能是黑的。需要在 onShow 生命周期里,检查一下预览状态,如果已经停止,就重新调用 startPreview()。同样,在 onHide 里主动 stopPreview() 是个好习惯。

坑四:服务端接口的稳定性 这个方案强依赖服务端接口的响应速度和成功率。一定要和服务端约定好超时时间(比如10秒),前端设置上传超时,并做好超时后的用户提示和重试机制。可以考虑加入“弱网优化”,比如图片压缩后再上传。

进阶思考:从“模拟”到“增强” 目前我们只是完成了采集和上传。其实前端还能做得更多:

  • 本地初步校验:集成一个超轻量级的JS人脸检测库(如 face-api.js 的微型模型),在抓拍前先本地判断一下画面中是否有人脸、是否正脸。如果没有,就不开始倒计时或给出提示,减少无效的上传请求,提升用户体验和通过率。
  • 多张抓拍与优选:倒计时期间,可以间隔抓拍2-3张图片,前端或服务端挑选最清晰、正脸度最高的一张进行上传,进一步提高核身成功率。
  • 动作活体模拟:虽然做不到SDK级的眨眼、张嘴检测,但可以通过UI引导用户“缓慢点头”、“左右转头”,并在动作过程中抓拍多张图片序列上传,由服务端分析动作连续性,作为活体判断的辅助依据。

这个基于 live-pusher 的模拟方案,其精髓在于在有限的混合开发环境下,利用成熟稳定的音视频组件,最大程度地构建一个可靠、体验可控的人脸采集前端。它可能不适合对安全等级要求极高的支付场景,但对于大多数需要平衡开发成本、上线速度和用户体验的实名认证、辅助登录等场景,无疑是一个经过实战检验的优选方案。

Logo

火山引擎视频云技术社区,是面向 AI 音视频开发者的技术交流平台。这里汇聚源自抖音、豆包等亿级 DAU 产品的 RTC、直播、点播、AI 媒体处理、音视频互动技术,提供接入指南、最佳实践、性能调优、场景案例、Demo 代码、开源项目、白皮书和 API 文档。社区汇聚官方工程师与一线开发者,为 AI 视频通话、数字人、AI 视频处理等应用的开发与落地提供技术支持。

更多推荐