ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

微信小程序视频录制实践:camera组件、权限配置与上传封装

微信小程序视频录制实践:camera组件、权限配置与上传封装 1. 先想清楚小程序里录制视频到底有几条路可走微信小程序录制视频听上去是个很朴素的需求——用户点一下按钮开始录再点一下结束拿到文件去上传。真动手做的时候你会发现光用什么组件去录这个问题就有三种截然不同的答案而且它们的能力边界、适配成本、能踩的坑完全不一样。我见过不少团队在项目启动会上拍脑袋说用 camera 组件录呗结果做到一半发现录制时长被卡死在 30 秒又要推倒重来。所以这一节我不急着贴代码先把三条路径摆到台面上说清楚每条路适合什么样的业务。1.1 camera 组件 CameraContext把控制权握在自己手里这是最原生的一条路。页面上直接放一个camera组件它就是一个实时的取景器画面直接渲染在页面里再通过wx.createCameraContext()拿到上下文对象调用startRecord()和stopRecord()控制录制。整个过程中用户看到的界面是你自己写的按钮长什么样、加不加计时器、要不要美颜遮罩这个得另做统统由你决定。它最大的优势是交互自由度高。比如你想做一个按住说话式的短视频打卡功能需要在录制过程中实时显示已录秒数、在后置摄像头和前置摄像头之间切换、还要在画面角落叠一个引导文案——这些用 camera 组件都能实现。另外它是一个页面内取景的形态用户不需要跳出你的小程序操作路径最短。代价也很明确它是一个原生组件层级最高普通view盖不住它虽然新基础库在部分机型上支持同层渲染但兼容老版本还是要靠cover-view录制时长有硬性上限官方给的是 30 秒左右超时会走timeoutCallback回调而且在开发者工具里的表现和真机差异极大基本必须真机调试。1.2 wx.chooseMedia 调起系统相机一行代码换来的省事第二条路是直接把活儿交给系统。调用wx.chooseMedia({ mediaType: [video], sourceType: [camera] })微信会拉起手机自带的相机应用用户用系统相机录完视频以临时文件的形式回传给小程序。你只需要处理返回值就行。这条路的好处是实现成本极低交互体验也符合用户习惯——用户对系统相机太熟悉了。而且maxDuration参数可以设到 60 秒比 camera 组件的限制更宽松录像的清晰度、防抖、对焦这些也都由系统相机负责画质普遍比组件录制更好。缺点同样明显你完全控制不了这个中间过程。用户点进系统相机之后会不会切到相册、会不会取消、录了多久、横屏还是竖屏你都只能被动接收结果。做 C 端内容类产品时这种跳出感对转化率是有影响的。另外系统相机界面在不同安卓机型上长得千奇百怪品牌定制 ROM 还会加各种滤镜按钮体验一致性很难保证。1.3 三条路径横向对比与选型建议还有第三条路就是页面里嵌一个web-view用 H5 的MediaRecorder或者getUserMedia来录。这条路我不太推荐因为web-view内的摄像头权限链路在小程序环境里相当绕兼容性判断复杂而且录出来的文件格式在不同机型上五花八门后端的转码压力会明显增加。除非你的项目里 H5 那套东西本来就很成熟否则不建议在新项目里走这条路。把前两条路放在一起对比决策逻辑就清楚了对比维度camera 组件 CameraContextwx.chooseMedia 调系统相机单次录制上限约 30 秒超时触发 timeoutCallback默认 10 秒最大可设 60 秒界面自定义程度完全自定义系统相机界面无法干预画质由 resolution 属性控制三档由系统相机决定通常更好前后置切换代码控制可实时切换用户自己操作开发者工具支持预览勉强能用录制基本不可用可用会拉起调试环境的模拟选择主要适用场景打卡、身份核验、短视频、连续录制一次性上传、表单附件、大文件我的经验是**如果录制是产品的核心交互用 camera 组件如果录制只是表单里的一个附件入口用 chooseMedia。**别为了看起来更专业硬上 camera 组件30 秒的时长限制会在需求评审后期突然变成致命问题。2. 上线前的合规与配置这一步漏了真机上直接失败代码写得再漂亮配置没做对真机上一按录制就报错——这是最近一两年最容易翻车的地方而且报错信息往往很含糊让人一头雾水。2.1 用户隐私保护指引必须声明对应权限微信对小程序调用摄像头、麦克风、相册这些敏感能力做了强约束。你必须在小程序管理后台的「设置」-「服务内容声明」-「用户隐私保护指引」里如实勾选并声明会用到的权限类型。录制视频这个场景至少要声明这三项摄像头对应scope.camera麦克风对应scope.record录制带声音的视频需要相册仅写入对应scope.writePhotosAlbum如果你提供保存到手机相册按钮漏声明的典型表现是wx.authorize或者startRecord直接失败errMsg里出现和隐私协议相关的关键词。更麻烦的是这类失败在开发者工具里往往不出现工具走的是调试通道只有真机才暴露很容易拖到提测阶段才发现。注意隐私协议审核通过后不是立即全网生效审核通过到生效之间有一个时间窗口建议在开发早期就把这块配置做掉别等到发版前一天。2.2 scope.camera 与 scope.record 的授权时机小程序的权限模型是先申请、再使用。camera组件本身在你把它渲染到页面上时就会触发授权弹窗但那个弹窗的触发时机不受你控制用户体验并不好。更稳妥的做法是在用户点击开始录制按钮时主动调用wx.authorize让授权和用户的操作意图绑定在一起通过率会高很多。// 主动申请摄像头 麦克风权限 function ensurePermission() { return new Promise((resolve, reject) { wx.authorize({ scope: scope.camera, success: () { wx.authorize({ scope: scope.record, success: resolve, fail: reject }); }, fail: reject }); }); }这里有个细节wx.authorize一旦被用户拒绝过后续调用会直接进 fail 回调不会再弹窗。所以你必须区分首次拒绝和已经被永久拒绝后者的正确做法是引导用户去设置页手动打开wx.getSetting({ success: (res) { if (res.authSetting[scope.camera] false) { // 用户之前拒绝过引导去设置页 wx.showModal({ title: 需要摄像头权限, content: 请在设置中打开摄像头权限后重试, success: (m) { if (m.confirm) wx.openSetting(); } }); } } });这段判断逻辑看起来啰嗦但没有它你的用户会卡在一个点了没反应的死胡同里客诉电话就来了。2.3 基础库版本、页面配置与开发者工具的正确用法camera组件的录像能力和基础库版本强相关。startRecord/stopRecord这一对 API 是从基础库 2.7.0 开始提供的低于这个版本调用会直接报is not a function。所以project.config.json或者后台的低版本兼容设置里建议把最低基础库定在 2.10.0 以上给一些老版本的行为差异留出缓冲。页面配置方面camera组件需要整屏或者接近整屏的空间通常会把所在页面的navigationStyle设成custom自己做导航栏。这里有个所有人都踩过的坑自定义导航栏要考虑状态栏高度也就是wx.getSystemInfoSync().statusBarHeight新版本推荐用wx.getWindowInfo()。而这个高度是逻辑像素 px不是 rpx直接写进样式里要做单位换算否则在不同机型上要么被顶部挖孔挡住要么留出一大块空白。至于开发者工具我的建议是只用它来验证页面结构和数据流凡涉及录制的功能一律真机调试。工具里的摄像头预览依赖电脑摄像头录制链路经常是残缺的stopRecord返回的临时文件也可能异常。用工具的真机调试或者直接扫码预览才是靠谱的验证方式。3. 核心 API 逐个拆解从取景到落盘这一节是整篇内容里最需要逐字看的部分。我把camera组件和 CameraContext 的关键配置项拆开讲并说清楚每个参数背后的取舍。3.1 camera 组件属性清单与取值逻辑camera组件上真正需要你关心的属性其实就几个但每一个都有讲究属性可选值实际影响modenormal / scanCode录像场景保持 normal扫码模式会禁用录制能力device-positionback / front默认后置切换需要重新绑定数据并重建组件flashauto / on / off / torchtorch 是常亮补光录像场景比 on 更实用resolutionlow / medium / high直接影响输出体积默认 mediumframe-sizesmall / medium / large影响onCameraFrame的回调帧尺寸纯录像场景可忽略flash这个属性值得单独说一句。on表示拍照或录像瞬间闪一下但录像是个持续过程闪一下意义不大torch才是录像场景该用的值它让补光灯在整个录制过程中常亮。做夜间打卡功能的时候这个区别决定了用户能不能看清画面。另外device-position切换时不要天真地改个变量就完事。实测下来部分安卓机型直接切换会出现画面卡死或者绿屏稳妥做法是给 camera 组件加一个wx:if配合key让它彻底重建一次。代价是有零点几秒的黑屏但比卡死强。3.2 startRecord / stopRecord 的参数与回调时序startRecord的调用非常简单几乎没有必填参数但它有两个容易忽略的回调const ctx wx.createCameraContext(); ctx.startRecord({ // 超过 30 秒或者录制被系统中断来电、锁屏、切后台时触发 timeoutCallback: (res) { console.log(自动结束, res); // 注意这里可能已经有可用的视频文件了需要主动调 stopRecord 取结果 }, success: () { console.log(录制已启动); }, fail: (err) { console.error(启动失败, err); } });timeoutCallback是我认为整个 API 里最容易被低估的一个。它不是失败回调而是录制被结束的通知。用户接了个电话、锁了屏、切到微信聊天再回来都会触发它。如果业务上不做处理用户回来看到的界面还停留在录制中状态计时器一直在跑但实际上视频早就断了。正确做法是在这个回调里立刻调用stopRecord收尾把已有内容保存下来并给用户一个明确的提示。stopRecord的返回值才是真正的产出ctx.stopRecord({ success: (res) { // res.tempThumbPath 视频首帧图可直接当封面用 // res.tempVideoPath 视频临时文件路径 console.log(res.tempThumbPath, res.tempVideoPath); }, fail: (err) { console.error(停止录制失败, err); } });这里有两个必须记住的点。第一tempVideoPath是临时文件路径小程序重启或者系统清理缓存之后就可能失效想长期保留必须主动持久化。第二这两个路径都是本地路径不能直接丢给video组件的src做网络播放以外的操作——本地播放是可以的但要分享出去就必须先上传。3.3 分辨率与码率的取值推演很多人问录出来的视频为什么这么大其实体积是可以提前算出来的公式很简单文件体积 ≈ 码率 × 时长 ÷ 8以 720p 常用码率 2 Mbps 为例30 秒的录像体积约等于2 × 30 ÷ 8 7.5 MB。如果开到 1080p、码率拉到 8 Mbps同样 30 秒就是 30 MB。这个数字直接决定了你的上传策略和后端存储成本。resolution大致分辨率参考码率30 秒体积适用场景low480p 左右约 0.5 Mbps约 1.9 MB打卡签到、纯内容识别medium720p 左右约 2 Mbps约 7.5 MB通用场景性价比最高high1080p 左右约 8 Mbps约 30 MB需要看清细节的展示类内容我的建议是默认用 medium只在有明确画质诉求时开 high。理由很实在小程序的上传走的是wx.uploadFile移动网络下 30 MB 的文件失败率相当高用户等待时间也长中间切个后台就前功尽弃。真需要高清不如降低时长比如把 30 秒拆成 10 秒用清晰度换体积。4. 手把手实操一个可以直接抄的录像页前面讲了原理这一节给完整可运行的实现从页面结构一路写到上传和保存相册。你可以直接拿去改。4.1 页面结构与样式骨架先看 WXML。核心思路是把camera铺满屏幕所有可交互元素用cover-view叠在上面。!-- pages/record/record.wxml -- view classpage !-- 有权限时渲染取景器wx:if 保证切换摄像头时可重建 -- camera wx:if{{authed}} classcamera modenormal resolution{{resolution}} device-position{{devicePosition}} flash{{flash}} binderroronCameraError bindstoponCameraStop !-- 原生组件内部的子节点必须是 cover-view / cover-image -- cover-view classhud cover-view classtimer{{mmss}}/cover-view cover-view classdot wx:if{{recording}}/cover-view /cover-view /camera !-- 无权限时的引导层 -- view classplaceholder wx:else text classph-title需要摄像头权限才能录制/text view classph-btn bindtapopenSetting去开启/view /view !-- 控制条普通 view 即可在 camera 外层 -- view classtoolbar view classtool-btn bindtapswitchCamera text翻转/text /view view classrecord-btn {{recording ? recording : }} bindtaptoggleRecord view classinner/view /view view classtool-btn bindtaptoggleFlash text{{flash torch ? 补光开 : 补光关}}/text /view /view /view样式部分有两个经验点。第一.camera用position: fixed; top: 0; left: 0; width: 100vw; height: 100vh;铺满别用100%在某些机型上百分比高度会算不准。第二控制条要加padding-bottom: env(safe-area-inset-bottom)否则在全面屏手机上按钮会被底部横条压住。/* pages/record/record.wxss */ page { background: #000; } .camera { position: fixed; top: 0; left: 0; width: 100vw; height: 100vh; } .hud { position: absolute; top: 120rpx; left: 0; width: 100%; display: flex; justify-content: center; } .timer { color: #fff; font-size: 32rpx; padding: 8rpx 24rpx; background: rgba(0,0,0,.45); border-radius: 999rpx; } .toolbar { position: fixed; left: 0; bottom: 0; width: 100%; display: flex; align-items: center; justify-content: space-around; padding: 40rpx 0 calc(40rpx env(safe-area-inset-bottom)); background: rgba(0,0,0,.35); } .record-btn { width: 140rpx; height: 140rpx; border-radius: 50%; border: 6rpx solid #fff; display: flex; align-items: center; justify-content: center; } .record-btn .inner { width: 100rpx; height: 100rpx; border-radius: 50%; background: #ff3b30; transition: all .2s; } .record-btn.recording .inner { width: 56rpx; height: 56rpx; border-radius: 12rpx; }那个.recording状态下内圈从圆形变方块的小动画是模仿主流录像 App 的视觉语言用户一看就知道现在是在录不需要额外文案。4.2 逻辑层完整代码逻辑部分的关键是状态管理要跟录制状态严格对齐。计时器、按钮样式、timeoutCallback、页面卸载这四处任何一处漏掉都会导致状态错乱。// pages/record/record.js const app getApp(); let cameraCtx null; let timer null; Page({ data: { authed: false, recording: false, seconds: 0, mmss: 00:00, devicePosition: back, flash: off, resolution: medium }, onLoad() { this.checkAuth(); }, onUnload() { // 页面销毁必须停录制、清计时器否则摄像头会一直占用 this.cleanup(); }, onHide() { // 切后台或跳到其他页面主动收尾避免录制被系统中断后状态不一致 if (this.data.recording) { this.stopRecord(); } this.cleanup(); }, checkAuth() { wx.getSetting({ success: (res) { const cam res.authSetting[scope.camera]; if (cam true) { this.setData({ authed: true }); this.initCtx(); } else if (cam undefined) { // 从未申请过 wx.authorize({ scope: scope.camera, success: () { this.setData({ authed: true }); this.initCtx(); }, fail: () { this.setData({ authed: false }); } }); } else { this.setData({ authed: false }); } } }); }, initCtx() { if (!cameraCtx) cameraCtx wx.createCameraContext(); }, openSetting() { wx.openSetting({ success: (res) { if (res.authSetting[scope.camera]) { this.setData({ authed: true }); this.initCtx(); } } }); }, toggleRecord() { if (this.data.recording) { this.stopRecord(); } else { this.startRecord(); } }, startRecord() { this.initCtx(); cameraCtx.startRecord({ timeoutCallback: () { // 录制被系统中断或超过 30 秒主动收尾 wx.showToast({ title: 录制已自动结束, icon: none }); this.stopRecord(); }, success: () { this.setData({ recording: true, seconds: 0, mmss: 00:00 }); timer setInterval(() { const s this.data.seconds 1; this.setData({ seconds: s, mmss: ${String(Math.floor(s / 60)).padStart(2, 0)}:${String(s % 60).padStart(2, 0)} }); }, 1000); }, fail: (err) { console.error(startRecord fail, err); wx.showToast({ title: 启动录制失败, icon: none }); } }); }, stopRecord() { if (!this.data.recording) return; this.clearTimer(); cameraCtx.stopRecord({ success: (res) { this.setData({ recording: false, seconds: 0, mmss: 00:00 }); this.handleVideo(res.tempVideoPath, res.tempThumbPath, this.data.seconds); }, fail: (err) { console.error(stopRecord fail, err); this.setData({ recording: false }); } }); }, handleVideo(videoPath, thumbPath, duration) { // 这里先落一份本地持久化防止后续上传失败导致文件丢失 const fs wx.getFileSystemManager(); const localPath ${wx.env.USER_DATA_PATH}/rec_${Date.now()}.mp4; fs.saveFile({ tempFilePath: videoPath, filePath: localPath, success: (r) { console.log(本地已保存, r.savedFilePath); }, fail: (e) { console.warn(本地保存失败不影响上传, e); } }); wx.showLoading({ title: 处理中 }); wx.uploadFile({ url: https://your-domain.com/api/record/upload, // 需在小程序后台配置为合法域名 filePath: videoPath, name: file, formData: { duration: String(duration), thumb: thumbPath || }, header: { Authorization: Bearer ${app.globalData.token || } }, success: (res) { wx.hideLoading(); try { const data JSON.parse(res.data); if (data.code 0) { wx.showToast({ title: 上传成功 }); } else { wx.showToast({ title: data.msg || 上传失败, icon: none }); } } catch (e) { wx.showToast({ title: 返回数据异常, icon: none }); } }, fail: (err) { wx.hideLoading(); console.error(upload fail, err); wx.showToast({ title: 网络异常稍后重试, icon: none }); } }); }, clearTimer() { if (timer) { clearInterval(timer); timer null; } }, cleanup() { this.clearTimer(); if (this.data.recording) this.setData({ recording: false, seconds: 0, mmss: 00:00 }); }, switchCamera() { // 部分安卓机型直接改值会卡死用 wx:if 重建组件更稳 this.setData({ authed: false, devicePosition: this.data.devicePosition back ? front : back }); wx.nextTick(() this.setData({ authed: true })); }, toggleFlash() { this.setData({ flash: this.data.flash torch ? off : torch }); }, onCameraError(e) { console.error(camera error, e.detail); wx.showToast({ title: 摄像头异常, icon: none }); }, onCameraStop() { // 摄像头被系统关闭权限被收回、被其他应用占用等 this.cleanup(); } });注意handleVideo里我先做了一次本地持久化再上传。这个顺序是刻意的上传可能因为网络失败而tempVideoPath指向的临时文件在用户下次打开小程序时可能就没了。先落本地用户至少还能在我的录制里找到草稿体验上差别很大。4.3 录制完的上传、回放与保存到相册上传这一步有几个细节必须提前准备好。首先是上传域名白名单wx.uploadFile的url必须在「开发管理」-「开发设置」-「服务器域名」的uploadFile合法域名里配好否则真机直接失败工具里还可能因为勾了不校验合法域名而看不出来。回放就简单了把上传成功后服务端返回的 URL 丢给video组件即可video src{{playUrl}} controls show-center-play-btn /如果用户还想要一份到手机相册那就需要scope.writePhotosAlbumwx.saveVideoToPhotosAlbum({ filePath: localPath, // 本地文件路径不是网络地址 success: () wx.showToast({ title: 已保存到相册 }), fail: (err) { if (err.errMsg.indexOf(auth) -1) { wx.showModal({ title: 需要相册权限, content: 请在设置中允许保存到相册, success: (m) m.confirm wx.openSetting() }); } } });一个常见误解用户以为保存到相册是复制一份实际上安卓上某些机型保存后相册需要几秒钟刷新才能看到如果用户立刻切到相册发现没有会以为失败了。所以在成功提示里加一句可在相册的最近项目中查看能省掉很多解释成本。5. 踩坑实录文档里不会写的那些细节这一节全部来自真实项目每一条我都至少被坑过一次。5.1 黑屏、无声、回调不触发黑屏通常有三个原因。最常见的是wx:if反复切换导致组件还没准备好就被重建解决办法是给操作加一点延迟或者干脆只切一次。第二个原因是页面在onShow后立刻渲染 camera这时候权限判断还没回来组件其实是无权限状态表现就是黑屏。第三个是切后台再回来部分安卓机型摄像头资源没有正确释放需要在onHide里彻底停掉录制、必要时重建组件。录制无声的问题更隐蔽。它跟scope.record的授权状态强相关。如果用户在系统层面拒绝了麦克风camera组件不会报错照样录但录出来的是静音视频。这种问题在测试阶段极难发现——因为测试机的权限大多是开着的。建议在进入录制页时把scope.camera和scope.record一起申请并且在界面上明确告知需要麦克风才能录制声音。回调不触发的情况多半是时序问题。比如用户快速连点两次录制按钮第一次startRecord还没回调第二次stopRecord就发出来了两个操作的时序错乱最后谁都不返回。解决办法是加一个操作锁recording状态切换过程中禁用按钮并在 UI 上给出视觉反馈。5.2 授权被拒之后的兜底流程授权被拒是小程序的常态不是异常。一套完整的兜底流程应该长这样判断authSetting[scope.camera]的值是undefined没问过还是false问过被拒是undefined就走wx.authorize弹窗是false就展示自定义引导页说明为什么需要这个权限并提供去设置按钮从设置页返回时onShow里重新getSetting刷新页面状态第三步的文案很有讲究。别写请开启摄像头权限这种冷冰冰的话写为了录制打卡视频需要访问你的摄像头视频仅用于打卡存档不会上传到其他位置。用户对隐私的敏感度很高说明用途能显著提升开启率。5.3 iOS 与 Android 的差异清单差异点iOS 表现Android 表现组件重建相对温和黑屏时间短部分机型需要更长准备时间摄像头切换基本正常少数机型切换后花屏需重建组件文件体积同分辨率下略大同分辨率下略小编码器差异相册保存即时可见部分机型需等待相册刷新视频方向一般正确少数机型输出旋转 90 度需服务端兜底最后一行那个旋转 90 度的问题值得重点提一下。它的根因是部分安卓机型的摄像头传感器方向与容器写入的旋转元数据不一致小程序侧拿不到足够的元信息去修正。稳妥的做法是在服务端做一次转码读取视频的旋转标记并统一校正。虽然增加了后端成本但比让用户在客户端看到横着的视频强太多。6. 常见问题速查表把上面的经验整理成一张表遇到问题直接对照排查比翻文档快得多。现象可能原因排查方向startRecord 报 is not a function基础库版本过低检查基础库是否低于 2.7.0提升最低版本要求真机提示隐私相关错误未在后台声明隐私权限检查「用户隐私保护指引」是否勾选摄像头、麦克风录制界面正常但没有声音scope.record 被拒申请麦克风权限检查系统级开关30 秒自动结束触发了 timeoutCallback这是组件限制考虑拆分为多段录制上传一直失败域名未加入 uploadFile 白名单检查服务器域名配置注意区分 request 和 uploadFile上传成功但播放不了服务端返回的 URL 不可访问检查对象存储的读写权限和跨域配置视频体积异常大resolution 设为 high降到 medium或缩短录制时长切后台回来界面卡死录制被中断但状态未同步onHide 主动 stopRecordonShow 重置状态视频画面旋转安卓机型传感器方向差异服务端转码统一校正或读取元数据前端旋转开发者工具正常真机异常工具与真机环境差异一律以真机调试结果为准关于录制后怎么把视频拿出来这个问题很多人第一反应是找小程序的临时目录。这里要说明白**小程序的临时文件对用户是不可见的只有通过wx.saveVideoToPhotosAlbum保存到系统相册用户才能在相册里看到。**如果你把视频上传到了自建的对象存储服务那用户要在小程序外的环境获取就必须通过你的业务后台提供下载入口——比如在小程序里生成一个下载链接用户复制到浏览器打开。这条链路涉及域名白名单和文件访问权限建议在需求阶段就明确下来。7. 录制之后压缩、存储与工程化封装录制只是开头真正影响项目成败的是后面这一串环节。这一节聊三个我踩过较多坑的方向。7.1 体积控制与转码策略前面算过1080p 录 30 秒就有 30 MB这对移动网络是很不友好的。小程序端能做的主要是控制resolution和限制时长真正的压缩得放在服务端或者云函数里做。常见的做法是客户端按 medium 录制保证可用性服务端收到后用转码工具统一压到 720p、1 Mbps 左右再归档这样存储成本和带宽成本都能降下来。这里有个取舍要提前想清楚。如果你做的是身份核验类场景压得太狠会影响识别准确率那就不能为了省成本无脑压。判断标准很简单这个视频未来会不会被看还是只被机器读。只被机器读的视频保留关键帧和足够分辨率即可不必保留完整码率。7.2 临时文件的生命周期管理tempVideoPath指向的文件会在小程序退出后被系统清理这个特性决定了你不能把录制完成当成任务完成。我的做法是三层兜底第一层录制成功立刻用FileSystemManager.saveFile存到wx.env.USER_DATA_PATH目录下这个目录是小程序自己的持久化空间重启后依然存在。但它有容量上限具体数值随基础库版本调整所以绝对不能当硬盘用必须配合定期清理。第二层上传成功后立刻删除本地副本避免空间被撑满。删除用fs.unlink并且在删除前确认服务端返回了可用的资源标识。第三层上传失败的记录进一个待重试队列存到wx.setStorageSync里下次打开小程序时尝试补传。这套机制对弱网环境下的用户体验提升非常明显做表单类产品时几乎是标配。7.3 把录制能力封装成组件复用如果项目里多个页面都要录制别复制粘贴。封装成一个自定义组件对外暴露bind:success、bind:error两个事件和start()、stop()两个方法就够了内部的权限申请、组件重建、计时器管理、本地持久化统统藏在组件里。封装的收益不只是少写代码。更重要的是把摄像头是单例资源这个约束收拢到一个地方管理。你想想如果两个页面同时持有 camera 上下文用户快速跳转时就会出现资源争抢表现出来就是画面黑一下、或者录制失败。组件化之后入口唯一这类问题从根上就不容易发生。组件对外的事件设计我建议用detail传一个结构化的对象而不是直接传路径this.triggerEvent(success, { localPath: savedFilePath, remoteUrl: res.data.url, duration: seconds, thumb: thumbPath, size: fileSize });这样上层页面拿到的是一个完整的结果对象不用再去猜哪个字段是什么后面要加字段也不用改签名。这种细节在项目初期看不出价值等到第五个页面接入的时候你会庆幸当初这么设计了。
返回列表