十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

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

uniapp Android视频录制:videoRec原生插件原理与实践

uniapp Android视频录制:videoRec原生插件原理与实践 简介面向uniapp打包Android端视频录制需求的开发者专门解决uni.chooseVideo在Android端的调用限制。插件以aar形式集成支持nvue页面中通过videoRec组件快速调用可自由切换前置/后置摄像头录制不设时长限制并配有回调返回临时路径适合需要自定义录制时长或原生能力扩展的中高级uniapp项目。压缩包共5个文件核心为Android camera-release.aar另含package.json配置和README说明文档总大小仅107KB接入门槛低。已有3018人学习下载对于在uni.chooseVideo无法满足需求、希望通过原生插件补齐视频录制功能的场景能帮助开发者快速建立插件接入思路并提示摄像头、录音、存储权限判断等关键注意事项。1. 为什么uniapp的chooseVideo不够用我拆了这个videoRec插件做过Android端视频录制的同学应该都有体会uni.chooseVideo在iOS上表现尚可到了Android上就成了玄学——部分机型调用系统相机后返回的临时路径偶尔以content://开头uniapp的plus.io转换经常失败更麻烦的是很多国产ROM对系统相机的录制时长做了隐性限制用户录到一分半钟直接被系统掐断你连个错误回调都拿不到。这个videoRec插件解决的问题很直接它把Android原生Camera2的录制能力封装成一个nvue组件通过videoRec标签直接渲染在页面上录制过程不走系统相机界面所有帧数据在应用内处理绕开了content://协议带来的路径转换问题也没有时长限制。适合那些对录制时长、画面比例、结束时机有强控制诉求的场景比如考勤打卡、实名认证、日志上报这类需要用户录制一段完整视频的功能。插件结构是标准的unicloud原生插件打包方式videoRec.zip里包含camera-release.aar、package.json和README.md。AAR是Android原生模块的编译产物包含了摄像头的预览、录制、回调全部逻辑nvue页面通过组件标签与它通信。下面按我实际接入时的顺序从AAR内部结构一路讲到权限申请最后给出几个排查崩溃的经验。2. 插件结构、AAR与nvue组件的关联方式2.1 解开videoRec.zip后先看package.json{ name: videoRec, version: 1.0.0, platforms: [ { name: Android, aar: camera-release.aar } ] }这个package.json是uni原生插件的声明文件dcloud插件市场打包时靠它识别AAR文件位置。如果你要自己修改插件后本地打包注意aar字段对应的路径必须是相对于package.json的相对路径。.DS_Store是macOS的元数据文件直接删掉或忽略不影响构建。2.2 AAR内部都装了什么解压camera-release.aar后你会看到classes.jar和若干资源目录。核心逻辑在classes.jar里通过反编译可以看到有几个关键类VideoRecView继承FrameLayout负责Camera2预览和MediaRecorder录制VideoRecModule继承UniModule暴露给js层的API入口VideoRecComponent继承UniComponent负责把VideoRecView实例化为nvue原生组件camera-release.aar编译时用的Android SDK版本必须和你项目的compileSdkVersion匹配。我用到的是Android 10API 29如果你的项目compileSdk是34那没问题但如果项目还在28以下建议先升级否则编译器会报Failed to transform camera-release.aar。2.3 nvue组件和原生View的绑定原理uniapp的nvue页面走的是原生渲染引擎Weex规范里规定自定义原生组件通过组件名标签映射到Android端的View类。映射关系写在插件内置的dcloud_uniplugins.json中{ nativePlugins: [ { plugins: [ { type: component, name: videoRec, class: com.example.videorec.VideoRecComponent } ] } ] }name字段就是你在nvue页面里写的标签名videoRecclass是组件类的全限定名。VideoRecComponent内部会实例化VideoRecView然后把它添加到Weex的视图树中。理解这个链路后你就知道为什么这个插件只能在nvue页面用不能用vue页面——vue页面走的是webview渲染无法直接挂载原生View。提示如果自定义标签在页面上不显示优先检查dcloud_uniplugins.json是不是被打进了assets目录以及class路径是否和AAR里的包名一致。3. 在nvue页面里接入videoRec代码、参数与回调时序3.1 最小可运行的nvue页面创建一个page_rec.nvue文件内容如下template view classcontainer videoRec classvideo refrec onTelonTel /videoRec view classcontrols button clickstartRecord开始录制/button button clickstopRecord结束录制/button /view /view /template script export default { methods: { startRecord() { this.$refs.rec.startRecord this.$refs.rec.startRecord() }, stopRecord() { this.$refs.rec.stopRecord this.$refs.rec.stopRecord() }, onTel(e) { // e.detail 里返回录制视频的临时路径 console.log(录制完成路径, e.detail) } } } /script style .container { flex: 1; } .video { flex: 1; background-color: #000; } .controls { flex-direction: row; justify-content: space-around; padding: 20px; } /style运行后页面上会出现一个全黑的原生相机预览区域。onTel是组件回调事件注意在nvue里原生组件触发事件时参数放在e.detail中和普通vue组件的$emit略有差异。如果回调里直接打印e对象Android端拿到的其实是一个UniRecord包装对象取e.detail才是插件返回的数据。3.2 原生端startRecord/stopRecord做了什么视频录制基于MediaRecorder实现调用时序必须是// 伪代码展示核心调用顺序 private void startRecord() { mediaRecorder new MediaRecorder(); camera.unlock(); mediaRecorder.setCamera(camera); mediaRecorder.setAudioSource(MediaRecorder.AudioSource.MIC); mediaRecorder.setVideoSource(MediaRecorder.VideoSource.CAMERA); mediaRecorder.setOutputFormat(MediaRecorder.OutputFormat.MPEG_4); mediaRecorder.setAudioEncoder(MediaRecorder.AudioEncoder.AAC); mediaRecorder.setVideoEncoder(MediaRecorder.VideoEncoder.H264); mediaRecorder.setOutputFile(currentVideoPath); mediaRecorder.prepare(); mediaRecorder.start(); }这里最容易出错的是顺序必须先setAudioSource再setVideoSource且setCamera必须在setAudioSource之前调用。如果先设置输出格式再关联摄像头部分机型会抛IllegalStateException。另外setOutputFile传的是应用私有目录路径不要传content://或公共目录否则MediaRecorder在prepare()阶段就会失败。停止录制时需要把操作放在子线程因为MediaRecorder.stop()在低性能设备上可能耗时数十毫秒public void stopRecord() { if (mediaRecorder null) return; try { mediaRecorder.stop(); } catch (RuntimeException e) { // stop()失败通常是因为没有有效数据录制时长太短 // 此时必须重置否则下次start会崩溃 mediaRecorder.reset(); } mediaRecorder.release(); mediaRecorder null; // 通过UniJSBridge通知前端 mUniComponent.fireEvent(onTel, recordResult); }mediRecorder.stop()如果录制时间小于1秒很多手机会抛RuntimeException。插件内部做了catch处理但你自己的业务层最好也做个最短时长的校验把开始时间戳和回调时间戳做差小于600ms就提示用户“按住录制至少一秒”而不是直接保存文件。3.3 组件暴露的参数通过attrs控制前后摄像头README里没有详细写参数但通过阅读AAR源码可以看到组件支持一个cameraType属性videoRec classvideo refrec cameraTypefront onTelonTel /videoReccameraType可取值front和back默认back。组件初始化时会在VideoRecView.onAttachedToWindow()里根据这个属性打开对应的摄像头IDcameraId front.equals(cameraType) ? findFrontCameraId() : findBackCameraId();如果你的业务需要在录制过程中切换前后摄像头不能直接修改components的属性因为原生View不会监听属性变化。正确的做法是给组件加一个switchCamera()方法在nvue调用this.$refs.rec.switchCamera this.$refs.rec.switchCamera()注意切换摄像头必须在未录制状态下调用。正在录制时切换会先触发stopRecord再重新start两端之间有个几十毫秒的gap视频会出现黑帧。我一般会在UI层禁止用户在录制状态点击切换按钮。4. 权限判断的完整链路摄像头、录音、存储与动态申请4.1 Android 6.0的动态权限模型插件要正常工作需要三个权限CAMERA、RECORD_AUDIO、WRITE_EXTERNAL_STORAGEAndroid 9及以下。Android 6.0以上必须在运行时申请Android 10及以上WRITE_EXTERNAL_STORAGE在targetSdk 29下仍需申请但targetSdk 30及以上系统会忽略这个权限——除非你显式声明requestLegacyExternalStoragetrue。uniapp在pages.json里可以配置原生权限但那只负责在打包时写入AndroidManifest.xml运行时还是得靠uni.authorize或原生插件自己去要。这个videoRec插件不提供权限申请能力所以页面逻辑里要自己判断。4.2 用plus.android实现权限判断与申请在nvue页面中可以通过plus.android调用原生API写一个公共授权方法checkAndRequestPermission() { return new Promise((resolve, reject) { const permissions [ android.permission.CAMERA, android.permission.RECORD_AUDIO, android.permission.WRITE_EXTERNAL_STORAGE ] const main plus.android.runtimeMainActivity() const PackageManager plus.android.importClass(android.content.pm.PackageManager) const pm main.getPackageManager() const deniedList [] for (let i 0; i permissions.length; i) { const granted pm.checkPermission(permissions[i], main.getPackageName()) if (granted ! PackageManager.PERMISSION_GRANTED) { deniedList.push(permissions[i]) } } if (deniedList.length 0) { resolve() return } // 调用系统权限弹窗 main.requestPermissions(deniedList, 1001) // 监听授权结果 plus.globalEvent.addEventListener(pause, function onPause() { // 系统权限弹窗会触发onPause用户操作后返回 setTimeout(() { // 二次确认真正判断权限 const grantedAfter checkPermissions(permissions) grantedAfter ? resolve() : reject(new Error(PERMISSION_DENIED)) }, 300) }) }) }这段代码的核心逻辑是先通过PackageManager.checkPermission检查权限是否已授予如果没有调用Activity的requestPermissions弹出系统授权框。注意requestPermissions是异步的授权结果通过Activity的onRequestPermissionsResult回调返回但nvue里拿不到这个回调只能用plus.globalEvent.addEventListener(pause)来监听弹窗关闭的时机因为权限弹窗会触发当前Activity的pause生命周期。建议不要依赖uni.getSetting去判断权限在nvue原生渲染层uni对象的部分API不保证可用。更稳妥的方式是用上面的plus.android方案或者干脆在插件内部做权限判断——这也是原生插件相对合理的做法不过我目前拿到的AAR并没有把权限逻辑内置需要业务层处理。4.3 权限被拒后的引导策略如果用户点击了“拒绝”下一次再申请会直接在系统层被拦住不会再弹窗。这种情况需要跳转到应用设置页const Intent plus.android.importClass(android.content.Intent) const Settings plus.android.importClass(android.provider.Settings) const Uri plus.android.importClass(android.net.Uri) const intent new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS) const uri Uri.parse(package: main.getPackageName()) intent.setData(uri) main.startActivity(intent)这段代码跳转到当前应用的系统设置页面用户手动开启权限后返回应用。注意从设置页返回时应用进程可能没有被杀死但页面会触发onShow这时候需要再次调用checkAndRequestPermission()确认权限状态否则用户可能已经开了权限插件却还停留在“未授权”的UI态。5. 进阶录制时长控制、码率参数与常见崩溃排查5.1 参数动态化在nvue层扩展组件方法AAR自带的录制参数是固定的默认分辨率720p、码率4Mbps、帧率30fps。如果产品要求1080p或者录像上传服务有限流需要动态调整。在原生组件里可以暴露一个setRecorderConfig方法public void setRecorderConfig(JSONObject config) { if (config.has(maxDuration)) { maxDuration config.optInt(maxDuration, 0); // 单位毫秒0为不限制 } if (config.has(videoBitrate)) { videoBitrate config.optInt(videoBitrate, 4 * 1024 * 1024); } }nvue侧调用时用$refs.rec.setRecorderConfig({ maxDuration: 60000, videoBitrate: 8 * 1024 * 1024 })。设置maxDuration后MediaRecorder.setMaxDuration会在达到时长时自动停止并触发MediaRecorder.OnInfoListener你需要在这个listener里手动回调onTel否则前端永远收不到完成事件。常见做法是在listener中调用一次fireEvent(onTel)并把e.detail里加上一个reason: timeout字段方便业务层区分是用户点击结束还是自动结束。5.2 排查崩溃从logcat日志定位问题接入过程中典型的崩溃有两类。第一类是java.lang.RuntimeException: start failed。这通常是MediaRecorder启动时没有权限或者摄像头被其他应用占用。排查方法adb logcat -s MediaRecorder: E AndroidRuntime: E看到Camera is being used by another app的日志基本可以确认是权限弹窗还没点“允许”就触发了startRecord()。解决办法是在checkAndRequestPermission()的resolve回调之后再开启录制按钮不要允许用户跳过授权直接点击。第二类是java.lang.IllegalStateException。这个集中在切换前后摄像头时。原因多半是switchCamera()里没有先stopRecord()就重设了CameraDevice导致底层状态机混乱。我在自己的项目里是这样处理的public void switchCamera() { if (isRecording) { stopRecord(); // 等待100ms让MediaRecorder完全释放 SystemClock.sleep(100); } releaseCamera(); openCamera(cameraId FRONT ? BACK : FRONT); }第三类是视频文件无法播放。检查文件路径是否有中文或空格MediaRecorder.setOutputFile不支持含中文的路径。另外存储权限如果没被授予文件虽然能创建但写入会被系统拦截stop()后文件大小为0。验证方法很简单adb pull /sdcard/Android/data/com.your.app/files/video/xxx.mp4 ./ ffprobe xxx.mp45.3 录制结果校验与路径转换onTel返回的临时路径可能是file://或纯路径建议统一处理onTel(e) { let path e.detail e.detail.path ? e.detail.path : e.detail if (path.startsWith(file://)) { path path.replace(file://, ) } // 检查文件是否存在且大小大于0 plus.io.resolveLocalFileSystemURL(path, entry { entry.getMetadata(meta { if (meta.size 0) { // 上传或继续处理 uni.uploadFile({ url: https://your-server.com/upload, filePath: path }) } }) }) }这里有个细节Android 10及以上即使拿到临时路径如果你把文件传给第三方SDK比如七牛、阿里云OSSSDK内部可能因为File对象访问受限而失败。稳妥做法是先复制到应用私有目录cp /sdcard/Android/data/{package}/files/video/xxx.mp4 /data/data/{package}/files/video/copy.mp4在nvue里用plus.io操作时注意resolveLocalFileSystemURL的路径分隔符Android端不支持\\一律用/。5.4 集成时的最后一个坑proguard混淆规则如果项目开了混淆务必在proguard-rules.pro里加入-keep class com.example.videorec.** { *; } -keep class com.taobao.weex.** { *; }否则Release包运行时会出现Component class com.example.videorec.VideoRecComponent not found的诡异崩溃——这是典型的混淆把类名改掉了而dcloud_uniplugins.json里的类路径还是原名字符串。Debug包一切正常Release包白屏优先怀疑这个。本文还有配套的精品资源点击获取
返回列表