☰
微信小程序录制视频:camera组件与wx.chooseMedia选型及上传实践
2026/9/30 16:30:02 网站建设 项目流程

1. 从 wx.chooseVideo 到 camera 组件:录制方案到底该怎么选

做过微信小程序录制视频功能的人,多半都在选型这一步卡过壳。需求方一句"要能录视频",背后可能是完全不同的两套实现路径:一套是调用系统的拍摄界面,录完把文件拿回来;另一套是自己用 camera 组件搭一个录制页,从取景框到按钮全都自己画。这两条路能做的事、要写的代码量、踩的坑完全不在一个量级,选错了后期返工的成本非常高。我前后做过直播答题、健身打卡、家校作业提交这几个场景的录制功能,每次方案都不一样,所以这里先把选型的判断逻辑讲透,再往下讲具体的实现细节。

先把这个功能的核心目标说清楚:微信小程序录制视频,本质上是让用户在授权相机和麦克风之后,拿到一段视频的本地临时路径(tempFilePath 或其变体),然后再决定是预览、上传还是本地留存。围绕这个目标,小程序提供了两条主线的能力,一条是wx.chooseMedia(老项目里常见的是wx.chooseVideo),另一条是camera组件配合CameraContext。很多人一上来就抄一段 chooseVideo 的代码,跑通之后发现产品要加个录制倒计时、要在取景框上叠一行提示文字、要限制只能录 15 秒并自动停,结果发现自己根本没有控制权,只能推倒重来。

1.1 两条录制路径的能力边界差在哪

wx.chooseMedia走的是"借系统界面"的路子。调用之后,微信会拉起一个拍摄/相册选择的界面,用户在里面录完点确认,回调里给你一个临时文件信息,包含 tempFilePath、size、duration、height、width、thumbTempFilePath 这些字段。这个过程里,你几乎不参与取景和录制过程,界面是系统给的,录制时长、分辨率这些只能通过参数做有限的约束,比如 maxDuration 限制最大时长,camera 指定前后置。

camera组件走的是"自建录制页"的路子。你在页面的 wxml 里放一个<camera>,它就是一块实时的取景画面,你可以在它上面叠 UI(虽然原生组件有层级坑,后面会讲),然后通过wx.createCameraContext()拿到上下文对象,调用startRecord开始录、stopRecord结束录。结束的时候回调会给你 tempThumbPath(封面图)和 tempVideoPath(视频文件),这两个路径才是你后续要用的东西。这条路的代价是你要自己处理权限申请、自己写录制按钮、自己处理超时停止、自己处理各种真机异常,但换来的是完整的过程控制权。

选型的判断其实就归结到几个问题。第一,录制过程中用户需不需要看到业务相关的浮层?比如要显示"请朗读屏幕上的文字"、要叠一个美颜滤镜、要显示题目的倒计时。有这类需求,chooseMedia 直接出局。第二,录制时长和分辨率的控制精度要求高不高?chooseMedia 的 maxDuration 在真机上偶有不准的情况,而且用户可以在系统界面里手动提前结束,节奏不完全由你掌控;camera 组件则可以直接用 timeoutCallback 做硬性截断。第三,团队有没有精力维护一套自绘 UI?如果只是最朴素的"录一段传上去",chooseMedia 半天就能交付,没必要为了炫技去做 camera 组件。

1.2 为什么官方一直在推 chooseMedia 而不是 chooseVideo

老代码里满地都是wx.chooseVideo,新项目里官方文档更推荐wx.chooseMedia。这不是简单的改名,chooseMedia把图片和视频的选择收敛到一个 API 里,用 mediaType 数组来指定你要哪种,同时 sourceType 可以控制是从相册选还是现场拍。对录制场景来说,最关键的变化是它能明确区分"拍摄"和"相册选择"这两件事。wx.chooseVideo时代,用户点进去既可以拍也可以从相册翻一段已有的视频出来,如果你的业务是"现场作业打卡",这种从相册选的能力反而是一个漏洞——用户完全可以从相册挑一段旧的视频交上来。

所以在真实项目里,如果你的场景对"必须现场拍"有强要求,要么用 camera 组件把相册这条路彻底堵死,要么在用 chooseMedia 时把 sourceType 设成只允许 camera(拍摄),不给相册留口子。这一点在作业提交、考勤打卡、外卖取餐这类场景里非常关键,很多团队上线之后才被风控同学发现"用户能上传相册里的旧视频",回头补这个洞的成本远高于一开始就想清楚。

我把两条路径的关键差异整理成一张表,选型的时候对着看会清楚很多:

对比维度wx.chooseMediacamera 组件 + CameraContext
界面控制权系统界面,几乎不可定制完全自绘,想叠什么叠什么
录制过程干预无法干预可做倒计时、超时自动停、实时提示
相册视频混入风险存在,需靠 sourceType 约束不存在,只能现场录
权限处理框架代为申请,但拒绝后仍需兜底需自行申请 scope.camera / scope.record
实现成本低,几十行能跑通高,涉及原生组件层级、真机兼容
适用场景通用拍摄、发帖、简单收集打卡、作业、直播连麦前的本地录制

选型做完,方案定下来,接下来真正动手的时候,第一个拦路虎往往不是 API 怎么调,而是权限。很多人写的代码在开发者工具里一路绿灯,一上真机就黑屏不录,十有八九就是权限链路没走通。

2. 权限链路:相机与录音的两道门

小程序里的相机和麦克风属于敏感权限,用户没点头你连取景框都点不亮。这套权限机制在真机上比工具里严格得多,我见过太多"工具里好好的、真机上一片漆黑"的案例。要平稳地把这道门推开,得先搞清楚它到底是两把锁还是一把锁——答案是两把:scope.camera管摄像头,scope.record管麦克风。哪怕你只是录一段没有声音的画面,录音权限缺失也可能让部分机型在录制时直接报错或者录出一段无声文件,具体表现因系统版本而异。

2.1 scope.camera 与 scope.record 的申请时机

小程序的权限申请有个硬规则:必须在用户主动交互的回调里触发。你不能在 onLoad 或者页面一进来就默默调wx.authorize去要权限,那样在真机上大概率会被静默拒绝或者直接失败。正确的做法是把申请动作挂在用户点击"开始录制"这个按钮的回调里,或者放在进入录制页时那个明确的"开启相机"按钮上。

标准的权限检查流程是三步走。第一步用wx.getSetting查一下当前授权状态,看看authSetting['scope.camera']和authSetting['scope.record']分别是true、false还是undefined。undefined表示从没问过,true表示已授权,false表示用户明确拒绝过。这三种状态对应完全不同的处理逻辑:

  • undefined:直接从交互回调里调wx.authorize发起申请,系统会弹窗。
  • true:直接进入录制页,不用再问。
  • false:这时候再调wx.authorize是无效的,系统不会弹窗,只能引导用户去设置页手动开。

第二步拿到授权结果之后,再决定是初始化 camera 组件还是走引导。这里有个细节容易被忽略:wx.authorize一旦被用户在弹窗里点了"拒绝",这个权限在本次会话内就变false了,你再调 authorize 也不会弹窗。所以权限被拒之后的兜底引导是必须写的,不能省。

第三步是把"去设置页"的动作串起来,用wx.openSetting打开小程序的授权设置页,让用户手动把相机和录音打开。用户从设置页回来之后,在 onShow 里重新用 getSetting 检查一遍状态,如果两个权限都齐了,就把录制界面渲染出来。这套流程写下来大概是这样的结构:

// 检查并申请权限 async function ensurePermission() { const setting = await wx.getSetting() const cameraAuth = setting.authSetting['scope.camera'] const recordAuth = setting.authSetting['scope.record'] // 已授权 if (cameraAuth && recordAuth) return true // 从未申请过,发起申请 if (cameraAuth === undefined || recordAuth === undefined) { try { await wx.authorize({ scope: 'scope.camera' }) await wx.authorize({ scope: 'scope.record' }) return true } catch (e) { return false } } // 被拒绝过,需要引导去设置页 return false }

注意这里我用wx.authorize的顺序是先 camera 后 record,因为有些机型在同时申请两个权限时弹窗会连弹两次,用户体验割裂,分开申请在逻辑上更清晰。这里可以用 async/await 是因为新一代基础库对大部分 wx API 都做了 Promise 化支持,如果你的基础库版本较老,就得回退到 success/fail 回调的写法。

2.2 用户拒绝之后的兜底处理

用户拒绝权限这件事,产品经理往往觉得"不可能发生",但实际数据里拒绝率并不低,尤其是录音权限,很多人出于隐私顾虑会直接点拒绝。所以拒绝之后的引导页一定要设计好,不能只是干巴巴一句"请开启权限"。我一般会在拒绝之后弹一个自定义的弹窗(不是wx.showModal,是页面内的浮层),把"为什么需要这两个权限"讲清楚,然后给一个明确的"去开启"按钮触发wx.openSetting。

这里有个真机上的坑要特别提醒:iOS 上如果用户在系统层面把微信的相机权限关了,wx.openSetting打开的页面里可能根本不显示相机这一个开关,因为系统权限在小程序授权之上还有一层。这种情况只能提示用户去手机的"设置 - 微信"里把相机权限打开,小程序层面无能为力。所以完整的兜底文案应该分两层:小程序授权被拒,引导去 openSetting;如果 openSetting 之后还是不行,再提示去系统设置。

另一个容易踩的坑是权限申请和 camera 组件渲染的时序。camera 组件在页面渲染的时候就会尝试去拉摄像头,如果这时候权限还没拿到,真机上可能出现组件初始化失败、黑屏、或者 binderror 被触发。所以稳妥的做法是:先把相机区域占位渲染成一张静态图加一个"开启相机"按钮,等用户点击、权限拿到之后,再通过条件渲染把真正的 camera 组件挂上去。这样既符合"必须在交互中申请权限"的规则,也避免了权限没到位就硬渲染相机导致的异常。

权限这道门走通了,接下来才是 camera 组件本体的实操。这一块是整个功能的骨架,我会把页面结构、上下文调用、录制时序完整地拆一遍。

3. camera 组件录制实操:从初始化到拿到临时文件

真正动手写录制页的时候,你会发现文档里的示例和生产环境里的实现差距挺大。文档给的是一段能跑通的最小 demo,但真实项目要考虑:录制按钮的状态管理、录制时长的实时反馈、超时自动停止、录制失败的重试、用户中途退出页面的清理。我按一个能直接上线的实现来讲,把每一步为什么这么写都交代清楚。

3.1 页面结构:camera 组件与自定义 UI 的配合

页面骨架分两块:一块是 camera 组件负责取景,另一块是叠在上面的操作区。wxml 大致是这样的结构,要注意 camera 的各个属性都有明确用途,不是随便写的:

<view class="record-page"> <camera wx:if="{{cameraReady}}" device-position="{{devicePosition}}" flash="{{flashMode}}" frame-size="medium" resolution="medium" bindinitdone="onCameraInit" binderror="onCameraError" class="camera-view" ></camera> <view class="overlay"> <view class="timer">{{recordTimeText}}</view> <view class="tips">{{tipText}}</view> </view> <view class="control-bar"> <view class="btn-switch" bindtap="switchCamera">翻转</view> <view class="btn-record {{isRecording ? 'recording' : ''}}" bindtap="toggleRecord"> {{isRecording ? '停止' : '开始'}} </view> </view> </view>

这里有几个属性的取值需要解释。device-position控制前后置,值只能是back或front。frame-size影响的是取景回调的帧数据尺寸,如果你不做人脸识别或者实时图像处理,用medium就够,用large会白白增加内存压力,低端机上有卡顿甚至崩溃的风险。resolution控制录制分辨率,取值low、medium、high,这个直接影响录出来的文件大小。我一般根据业务选:作业类、打卡类用medium,文件大小和清晰度能平衡;需要看清细节的(比如商品瑕疵拍摄)才用high。

3.2 CameraContext 的调用时序与录制流程

拿到 camera 组件之后,用wx.createCameraContext()创建上下文,注意这个上下文是跟页面绑定的,一个页面一个就够,不要每次点录制都重新 create 一次,重复创建在部分基础库上会拿到指向混乱的实例。整个录制流程是:startRecord 开始,stopRecord 结束并拿到结果,中间用 timeoutCallback 处理超时。

const ctx = wx.createCameraContext() Page({ data: { isRecording: false, recordTime: 0, recordTimeText: '00:00', maxDuration: 15, tipText: '请保持面部在取景框内' }, toggleRecord() { if (this.data.isRecording) { this.stopRecord() } else { this.startRecord() } }, startRecord() { this.setData({ isRecording: true, recordTime: 0 }) this.timer = setInterval(() => { const t = this.data.recordTime + 1 this.setData({ recordTime: t, recordTimeText: formatTime(t) }) }, 1000) ctx.startRecord({ timeoutCallback: (res) => { // 到达 maxDuration 自动停止,返回临时文件 this.clearTimer() this.setData({ isRecording: false }) this.handleRecordResult(res) }, success: () => {}, fail: (err) => { this.clearTimer() this.setData({ isRecording: false }) wx.showToast({ title: '录制启动失败', icon: 'none' }) console.error('startRecord fail', err) } }) }, stopRecord() { this.clearTimer() ctx.stopRecord({ success: (res) => { this.setData({ isRecording: false }) this.handleRecordResult(res) }, fail: (err) => { this.setData({ isRecording: false }) wx.showToast({ title: '录制结束失败', icon: 'none' }) console.error('stopRecord fail', err) } }) }, handleRecordResult(res) { // res.tempThumbPath 封面图,res.tempVideoPath 视频文件 this.setData({ videoPath: res.tempVideoPath, coverPath: res.tempThumbPath, showPreview: true }) }, clearTimer() { if (this.timer) { clearInterval(this.timer) this.timer = null } }, onUnload() { this.clearTimer() } })

这段代码里有几处是踩过坑才补上的。第一,计时器一定要在组件卸载(onUnload)和停止录制时清掉,否则页面退出了定时器还在跑,轻则内存泄漏,重则用户已经离开页面了却弹出录制结束的提示。第二,timeoutCallback和success的职责要分清楚:timeoutCallback 是到达你设定的 maxDuration 时框架主动停录并回调,success 是你手动调 stopRecord 成功后的回调,两者拿到的结果结构是一样的(都带 tempThumbPath 和 tempVideoPath),但触发路径不同,都要处理。第三,startRecord 的失败要捕获,真机上权限被中途收回、摄像头被其他 App 占用,都会在这里报错,不处理的话用户会看到一个卡住的录制按钮。

3.3 录制时长的两种控制方式,别混着用

录制时长这里有个设计上容易打架的地方:maxDuration到底设多少、超时之后是自动完成还是允许用户提前结束。我的做法是统一一个上限(比如 15 秒或 60 秒),到达上限自动停并直接进入下一步,同时允许用户随时手动点停止提前结束。这样用户想短录就短录,超时了也不会无限录下去。

要特别注意的是,maxDuration是传给 startRecord 的一个参数(或通过 CameraContext 的相关配置约束),它的实际精度在不同机型上会有百毫秒到几百毫秒的误差,所以你在 UI 上显示的秒数一定是以你自己的计时器为准,不要试图去跟系统回调对齐。我见过有同学把 UI 显示的时长直接绑在超时回调上,结果用户看到进度条走到一半突然就停了,体验很差。计时器自己维护、显示自己算、停止以实际回调为准,这三件事解耦开,逻辑最清晰。

另外,录制的分辨率设置和实际输出文件大小直接相关。用medium分辨率录 15 秒,文件通常在 2MB 到 5MB 之间浮动,具体取决于画面复杂度;high分辨率下同样时长可能到十几兆。如果你的后端对上传大小有限制,或者用户网络环境差,就要在录制前就把分辨率压下来,而不是事后去压缩——压缩很费时,用户等不起。

录制拿到临时文件只是上半场,下半场是怎么把这个文件展示给用户确认、然后稳妥地传上去。这两步看似简单,实际也是坑比较集中的地方。

4. 自定义录制界面:那些原生 UI 给不了的东西

选择 camera 组件,本质上就是为了这个"自定义"的权利。但自绘 UI 在小程序里不是普通的前端绘图,因为 camera 是原生组件,它和普通 view 的层级关系跟你想的不一样,稍微不注意就会遇到"我明明写了浮层,为什么被相机画面盖住了"的经典问题。这一节把界面这块的关键点讲清楚。

4.1 浮层的层级陷阱与 cover-view 的取舍

在早期的微信基础库里,原生组件(camera、video、map、canvas、live-player 这些)会浮在所有普通组件的最上层,你在它上面叠普通 view 是没有用的,会被相机画面直接盖住。官方的解决方案是cover-view和cover-image,这两种组件能盖在原生组件之上。但 cover-view 的能力很有限,它不支持很多 CSS 属性,动画、复杂的 flex 布局、自定义字体有时候都不太听话,写起来很别扭。

好在较新的基础库版本放宽了这个限制,普通 view 在大多数场景下已经可以正常叠加在 camera 之上,同层渲染的兼容性好了很多。我现在的做法是:先在目标基础库版本上实测,如果普通 view 能正常显示在相机画面上,就用普通 view,省得跟 cover-view 的各种限制较劲;如果发现某些机型上还是被盖住,再回退到 cover-view。判断标准以真机为准,工具里的表现不完全可信。

如果确实要用 cover-view,有几个约束得记牢:cover-view 里只能嵌套 cover-view 和 cover-image,你不能在里面塞一个普通的 view 或者 button;文本必须放在 cover-view 里,不能用 text;它支持的基础 CSS 有限,圆角、阴影这些效果可能不生效。我一般只在必须覆盖的极简 UI(比如一行提示文字、一个录制计时)上用它,复杂的控件区还是尽量放在相机画面之外,也就是相机只占页面的一部分,操作按钮放在相机区域下面的普通区域里,彻底绕开层级问题。

4.2 前后置切换、镜像与闪光灯的实际表现

device-position切换前后置的时候,真机上会有一个短暂的画面黑一下再刷新,这是正常的,不用惊慌。但有个点要注意:前置摄像头的画面默认是镜像的,你看到自己的脸是左右反的,这是取景预览的常见行为,录出来的文件通常也是镜像的。对于打卡、人脸核验这类场景,镜像与否通常不影响业务,但如果你的业务对左右方向敏感(比如要识别画面里的文字),就得提前和产品确认清楚,必要时在后端或前端做翻转处理。

闪光灯这块,flash属性支持auto、on、off、torch几个值。注意torch(常亮手电)在部分安卓机型上前置摄像头是不支持的,切换的时候要做能力判断,或者干脆只在后置模式下开放闪光灯控制。我踩过一次坑:在前后置切换时没有重置 flash 状态,导致切到前置之后 flash 还停留在torch,结果前置相机莫名报错。后来改成每次切换 device-position 就把 flash 重置成off,问题就没了。

再说说取景框的宽高比和适配。camera 组件的默认比例和不同手机屏幕的宽高比差异,会导致画面出现拉伸或黑边。稳妥的做法是给 camera 设一个固定的宽高比(比如 3:4 或 9:16),用容器约束住,别让它随屏幕自由伸缩。同时要留意刘海屏、水滴屏这些异形屏对顶部提示文字的遮挡,把重要信息放在安全区域内。这些细节单独看都是小事,但叠在一起就是"这个录制页看着不太专业"和"这个录制页很顺眼"的区别。

界面做顺了,用户能顺畅地录完一段视频,接下来的核心问题就变成了:这段临时文件怎么预览、怎么压缩、怎么可靠地上传到服务端。这才是整个功能能不能真正落地的关键。

5. 录制完成之后:预览、压缩与上传

录完视频拿到的tempVideoPath是一个本地临时路径,它的有效期只在小程序本次运行期间,页面刷新、小程序被杀掉之后就失效了。所以拿到路径之后要么尽快上传,要么在本地留存一份。这一节把预览、压缩、上传三个方面拆开讲,尤其是上传,是问题最多的一环。

5.1 预览用的 video 组件与临时路径的坑

预览视频直接用一个<video>组件,src 指向刚拿到的 tempVideoPath 就行。但这里有几个实际问题。第一,video 组件本身也是原生组件,同样有层级问题,如果你在预览页还要叠 UI,参考上一节的层级处理。第二,预览页最好把录制好的封面图 tempThumbPath 作为 poster,这样视频加载前不会是一片黑。第三,用户的预览确认流程要设计好,给"重录"和"确认提交"两个按钮,重录就回到相机页重新来一遍。

还有一点很关键:临时文件在开发者工具和真机上的路径格式不一样。工具里拿到的是类似http://tmp/开头的本地路径,真机上是wxfile://或者http://usr/这样的格式,你在做路径判断、拼接或者传给后端做校验的时候,绝对不能用工具里的路径做写死的判断,否则一上真机就出问题。我的做法是:拿到路径后一律不做格式假设,直接原样透传给 video 组件或者上传接口,让框架自己去解析。

5.2 用 wx.uploadFile 上传的完整流程

上传这件事,最直接的方式是wx.uploadFile。它接受一个 filePath、一个 name(服务端接收文件的字段名)和 formData(附带的其他参数),通过 multipart/form-data 的方式把文件传上去。用起来很简单,但生产环境里要考虑的远比一次调用多:

function uploadVideo(filePath, extraData) { return new Promise((resolve, reject) => { const task = wx.uploadFile({ url: 'https://your-domain.com/api/upload', filePath: filePath, name: 'video', formData: { token: extraData.token, bizType: 'homework', duration: extraData.duration }, timeout: 60000, success: (res) => { if (res.statusCode === 200) { try { resolve(JSON.parse(res.data)) } catch (e) { reject(new Error('返回数据解析失败')) } } else { reject(new Error('上传失败,状态码 ' + res.statusCode)) } }, fail: (err) => { reject(err) } }) // 监听上传进度,用于 UI 展示 task.onProgressUpdate((res) => { console.log('上传进度', res.progress) }) }) }

这段代码里,timeout设成 60 秒是必要的,视频文件几百 KB 到几兆,弱网环境下很容易超时,默认超时有时候太短。onProgressUpdate拿到的进度可以用来做进度条,让用户知道还要等多久,不然一个转圈圈转半天用户会以为卡死了。还有一点:上传接口的域名必须提前在小程序后台的服务器域名里配置好,开发阶段可以在开发者工具里勾选"不校验合法域名",但真机调试和上线之后必须配置,这个坑几乎每个人都会踩一次。

5.3 上传前的压缩:什么时候该压、怎么压

是不是所有视频都要压缩?不一定。如果录的时候已经把分辨率设成medium、时长控制在 15 秒以内,文件本身就不大,直接传就行。真正需要压缩的是那些高分辨率、长时长、文件达到十几兆的场景。小程序的压缩能力有限,比较实用的思路有两条:一是在录制源头就把参数压下来(低分辨率、短时长),从根上减少文件体积;二是如果确实需要处理已有文件,可以用一些图像/视频处理的第三方能力,但实话讲小程序端做视频压缩的性能和兼容性都不算理想,能不在前端压就别在前端压。

我更推荐的做法是:前端只负责录一个合理大小、合理时长的视频,把真正的转码、压缩、多清晰度生成放到服务端去做。前端做预处理,服务端做后处理,职责分明,兼容性问题也少。前端这边要做的,是在录制前用数据说话——比如告诉用户"为节约流量,将录制 15 秒标清视频",让用户对结果有预期。

上传成功了不代表万事大吉,真正让开发者头疼的是各种只在真机才会冒出来的问题。下面这一节,我把这几年踩过的典型坑整理成排查思路,方便你遇到问题时能快速定位。

6. 真机上才会暴露的坑:几个典型问题的排查链路

开发者工具是个"温室",它能跑通不代表真机能跑通。相机和麦克风这两个硬件相关的功能,恰恰是工具和真机差异最大的地方。我按"现象 - 排查 - 根因 - 修复"的思路,把几个高频问题讲一遍,方便你照着自己遇到的现象对号入座。

6.1 现象:真机上面一片漆黑,录制按钮点了没反应

这是最高频的问题,没有之一。排查链路应该这样走:第一步,确认权限是否真的拿到了。很多人以为自己在 onLoad 里申请了权限,实际上真机上那个申请根本没弹窗或者被静默拒绝了。用 getSetting 把 authSetting 打出来看一眼最直接。第二步,确认 camera 组件是不是在权限到位之前就渲染了。前面讲过,正确顺序是"先按钮占位 - 用户点击 - 申请权限 - 拿到后再渲染 camera"。如果组件在权限没好时就挂载,真机上初始化失败的表现就是黑屏。第三步,看 binderror 有没有被触发,把错误信息打出来,常见的错误信息能指向具体原因,比如摄像头被占用、初始化超时。

还有一种黑屏是在开发者工具里预览正常,但真机上取景框是黑的,按钮却能点。这种往往是 camera 组件的层级或者尺寸问题——组件虽然初始化了,但被别的东西盖住了,或者宽高算出来是 0。检查一下 camera 容器的宽高有没有正确计算,尤其是用了百分比高度又没给父容器定高的时候,在真机上算出来可能是 0。

6.2 现象:iOS 能录、安卓录出来没声音,或者反过来

不同平台对录音权限和音频输入的处理差异,是这类"平台相关故障"的根源。排查的时候先确认scope.record在出问题的那个平台上是不是真的授权了——用户可能只授权了相机没授权录音。其次要注意,有些安卓机型在录制期间如果系统来了电话、或者其他 App 抢占音频焦点,会出现录出来无声或者录制中断的情况,这是系统层面的行为,前端只能通过监听录制中断、给出友好提示来处理,没法完全规避。

我遇到的另一个平台差异是静音模式下录制的声音处理。iOS 设备如果开了静音,有些场景下录制出来的音频表现和预期不同,这个跟具体系统和微信版本都有关系。对付这类问题,最靠谱的办法不是查文档,而是列一台目标机型清单,逐个真机实测。文档只能告诉你"应该是什么样",真机才能告诉你"实际是什么样"。

6.3 现象:录制中途切到后台,回来之后状态错乱

用户正在录视频的时候,一个电话进来,或者用户手滑切到了后台,这时候 camera 组件会被系统挂起,录制可能被中断,但你的计时器还在跑,isRecording 还是 true,回来之后 UI 状态和实际状态完全对不上,用户点停止也停不下来。这个问题在真机上很常见,处理的核心是监听页面的隐藏事件。

在onHide里要做两件事:一是把计时器停掉,二是把录制状态重置成"未录制",同时如果当时正在录制,主动调一次 stopRecord 把已经录了的部分保存下来(或者直接丢弃并提示用户重新录)。光处理 onHide 还不够,onShow回来的时候也要重新检查一遍相机状态,因为切后台之后 camera 组件可能需要重新初始化。我一般会在 onShow 里做一次轻量的状态自检:如果 isRecording 是 true 但页面已经重新显示,说明中途出了状况,直接重置状态并提示用户重录,避免带着脏状态往下跑。

除了上面这三个典型问题,还有一个值得单独说的坑是原生组件的滚动穿透。当录制页或预览页里要滚动,而页面上又放着 camera 或 video 这类原生组件时,滚动行为可能跟预期不一致,出现滚动到相机区域就卡住、或者页面整体滚动而相机不动的情况。处理办法是尽量让原生组件所在区域不参与整体滚动,给它独立的固定布局,把滚动限制在普通组件区域里。这些细节说到底都指向同一个原则:凡是涉及原生组件的地方,都别用普通组件的思维去假设它的行为,一切以真机实测为准,把目标机型列清楚,一个机型一个机型地过,才敢说这套录制功能是真的稳了。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询