更新记录
0.2.0(2026-08-26)
修复了已知的问题
0.1.0(2026-08-25)
新版本发布,暂时只支持android
平台兼容性
uni-app(4.0)
| Vue2 | Vue3 | Chrome | Safari | app-vue | app-nvue | Android | iOS | 鸿蒙 |
|---|---|---|---|---|---|---|---|---|
| √ | √ | × | × | × | √ | 8.0 | √ | × |
| 微信小程序 | 支付宝小程序 | 抖音小程序 | 百度小程序 | 快手小程序 | 京东小程序 | 鸿蒙元服务 | QQ小程序 | 飞书小程序 | 小红书小程序 | 快应用-华为 | 快应用-联盟 |
|---|---|---|---|---|---|---|---|---|---|---|---|
| × | × | × | × | × | × | × | × | × | × | × | × |
yzj-builtin-cam
App nvue 内置相机 easycom 组件。Android 基于 CameraX、MediaCodec 与 RootEncoder;iOS 基于 AVFoundation。提供相机预览、拍照、带声音的 MP4 录像、前后镜头切换、对焦和补光灯。
支持传统 uni-app 的 App-Android 与 App-iOS nvue 页面,兼容 Vue 2 与 Vue 3。最低 Android API 26、iOS 12;Web、小程序和 HarmonyOS 不可用。
能力与限制
- Android 支持拍照、录像和 RTMP 推流,可独立启停或组合运行;iOS 当前支持拍照与录像,RTMP 推流尚未实现。
- Android 录像和推流共享一套 H.264 视频编码器、
AudioRecord和 AAC 编码器,避免重复占用相机与麦克风。 - 默认视频参数为
1920x1080、30fps、4000000bps;音频为44100Hz、单声道、96kbpsAAC-LC。 - 组件宽高决定预览与照片的共同取景范围。推荐使用
16:9容器。 - 录像期间不能修改分辨率、帧率或切换镜头;Android 推流期间也遵循该限制。
- 空路径时,Android 照片与录像分别保存到应用图片、视频目录;iOS 保存到
Documents/yzj_builtin_camera/。录像必须收到recordchange的stopped且携带path后才能使用。
权限
Android 权限:
android.permission.CAMERA:打开相机、预览、拍照。android.permission.RECORD_AUDIO:录像和 RTMP 推流采集声音。android.permission.INTERNET:RTMP 推流。android.permission.ACCESS_NETWORK_STATE:网络切换后自动恢复推流。
iOS 权限:
NSCameraUsageDescription:相机预览与拍照。NSMicrophoneUsageDescription:录像同步采集声音。
Android 业务页面必须在调用 open() 前申请相机权限,在调用 startRecording() 或 startRtmp() 前申请麦克风权限。iOS 组件会在首次 open() 或 startRecording() 时请求对应系统权限;权限被拒绝时通过 error 返回错误码。
快速使用
在 nvue 页面直接使用 easycom 标签。组件应有确定宽高,推荐让宽高比与采集画幅一致。
<template>
<view class="page">
<yzj-builtin-cam
ref="camera"
class="camera"
@statechange="onNativeEvent('statechange', $event)"
@photo="onNativeEvent('photo', $event)"
@recordchange="onNativeEvent('recordchange', $event)"
@streamchange="onNativeEvent('streamchange', $event)"
@error="onNativeEvent('error', $event)"
/>
<button @click="openCamera">打开相机</button>
<button @click="takePhoto">拍照</button>
<button @click="toggleRecording">录像</button>
<button @click="toggleStream">推流</button>
</view>
</template>
<script>
export default {
data() {
return {
recording: false,
streaming: false,
rtmpUrl: ''
}
},
onUnload() {
this.callCamera('releaseCamera')
},
methods: {
getCamera() {
return this.$refs.camera || null
},
callCamera(methodName, args) {
const camera = this.getCamera()
if (camera && typeof camera[methodName] === 'function') {
camera[methodName].apply(camera, args || [])
}
},
openCamera() {
// 业务侧先确保已获得 CAMERA 权限。
this.callCamera('open', ['back', 1920, 1080, 30, 4000000])
},
takePhoto() {
this.callCamera('takePhoto', [''])
},
toggleRecording() {
if (this.recording) this.callCamera('stopRecording')
else {
// 业务侧先确保已获得 RECORD_AUDIO 权限。
this.callCamera('startRecording', [''])
}
},
toggleStream() {
if (this.streaming) this.callCamera('stopRtmp')
else {
// 业务侧先确保已获得 RECORD_AUDIO 权限。
this.callCamera('startRtmp', [this.rtmpUrl])
}
},
onNativeEvent(type, event) {
const raw = event && event.detail != null ? event.detail : event
const payload = typeof raw === 'string' ? JSON.parse(raw) : raw
if (type === 'recordchange') {
this.recording = payload.state === 'starting' || payload.state === 'started'
if (payload.state === 'stopped') console.log('录像文件:', payload.path)
}
if (type === 'streamchange') {
const activeStates = ['starting', 'connecting', 'authenticated', 'started', 'retrying', 'degraded']
this.streaming = activeStates.indexOf(payload.state) >= 0
console.log('RTMP 状态:', payload.state, payload.message)
}
if (type === 'photo') console.log('照片文件:', payload.path)
if (type === 'error') console.error(payload.code, payload.message)
}
}
}
</script>
<style>
.page { flex: 1; }
.camera { width: 750px; height: 422px; }
</style>
Android 页面事件载荷通常为 JSON 字符串,应先读取 $event.detail 并解析;iOS 页面事件载荷为对象 Map。上例的 onNativeEvent() 已兼容两种格式。手动 import 的组件 ref 需要使用 $callMethod();easycom 标签可按上例调用公开方法。
Vue 2 nvue 示例
以下完整页面用于传统 uni-app Vue 2 的 pages/camera/index.nvue,覆盖打开关闭、能力查询、镜头、采集参数、对焦、补光、拍照、录像和 RTMP 推流。closeCamera(command) 与 switchLens(command) 分别只是 close()、switchCamera() 的兼容别名,因此不重复演示。
<template>
<view class="page">
<yzj-builtin-cam
ref="builtinCam"
class="camera"
@statechange="handleCameraEvent('statechange', $event)"
@capabilitieschange="handleCameraEvent('capabilitieschange', $event)"
@photo="handleCameraEvent('photo', $event)"
@recordchange="handleCameraEvent('recordchange', $event)"
@streamchange="handleCameraEvent('streamchange', $event)"
@error="handleCameraEvent('error', $event)"
></yzj-builtin-cam>
<text class="status">{{ statusText }}</text>
<view class="row">
<button @click="openCamera">打开</button>
<button @click="closeCamera">关闭</button>
<button @click="switchCamera">切换镜头</button>
</view>
<view class="row">
<button @click="useBackCamera">后置</button>
<button @click="useFrontCamera">前置</button>
<button @click="showCapabilities">能力</button>
<button @click="showSession">会话</button>
</view>
<view class="row">
<button @click="use1080p">1080p</button>
<button @click="use720p">720p</button>
<button @click="use24Fps">24fps</button>
<button @click="use30Fps">30fps</button>
</view>
<view class="row">
<button @click="use2Mbps">2Mbps</button>
<button @click="use4Mbps">4Mbps</button>
<button @click="enableAutoFocus">自动对焦</button>
<button @click="focusCenter">中心对焦</button>
</view>
<view class="row">
<button @click="toggleTorch">{{ torchEnabled ? '关闭补光' : '开启补光' }}</button>
<button @click="takePhoto">拍照</button>
<button @click="toggleRecording">{{ recording ? '停止录像' : '开始录像' }}</button>
</view>
<view class="row">
<input class="rtmp-input" v-model="rtmpUrl" placeholder="rtmp://host:1935/live/stream"></input>
<button @click="toggleStream">{{ streaming ? '停止推流' : '开始推流' }}</button>
</view>
<text v-if="photoPath">照片:{{ photoPath }}</text>
<text v-if="videoPath">录像:{{ videoPath }}</text>
<text v-if="lastError" class="error">{{ lastError }}</text>
</view>
</template>
<script>
export default {
data: function () {
return {
previewing: false,
streaming: false,
recording: false,
torchEnabled: false,
statusText: '等待打开相机',
rtmpUrl: 'rtmp://your-host:1935/live/stream',
photoPath: '',
videoPath: '',
lastError: ''
}
},
onUnload: function () {
this.callCamera('releaseCamera')
},
methods: {
callCamera: function (methodName, args) {
var camera = this.$refs.builtinCam
if (camera && typeof camera[methodName] === 'function') {
return camera[methodName].apply(camera, args || [])
}
if (camera && typeof camera.$callMethod === 'function') {
return camera.$callMethod.apply(camera, [methodName].concat(args || []))
}
return null
},
requestPermission: function (permission, onGranted) {
var self = this
plus.android.requestPermissions([permission], function (result) {
var granted = result && result.granted ? result.granted : []
if (granted.indexOf(permission) >= 0) onGranted()
else self.statusText = '权限被拒绝:' + permission
}, function () {
self.statusText = '权限申请失败:' + permission
})
},
requestCameraPermission: function (onGranted) {
this.requestPermission('android.permission.CAMERA', onGranted)
},
requestAudioPermission: function (onGranted) {
this.requestPermission('android.permission.RECORD_AUDIO', onGranted)
},
openCamera: function () {
var self = this
this.requestCameraPermission(function () {
self.lastError = ''
self.statusText = '正在打开后置相机'
self.callCamera('open', ['back', 1920, 1080, 30, 4000000])
})
},
closeCamera: function () {
this.statusText = '正在关闭相机'
this.callCamera('close')
},
switchCamera: function () {
this.callCamera('switchCamera')
},
useBackCamera: function () {
this.callCamera('setLensFacing', ['back'])
},
useFrontCamera: function () {
this.callCamera('setLensFacing', ['front'])
},
use1080p: function () {
this.callCamera('setResolution', [1920, 1080])
},
use720p: function () {
this.callCamera('setResolution', [1280, 720])
},
use24Fps: function () {
this.callCamera('setFps', [24])
},
use30Fps: function () {
this.callCamera('setFps', [30])
},
use2Mbps: function () {
this.callCamera('setBitrate', [2000000])
},
use4Mbps: function () {
this.callCamera('setBitrate', [4000000])
},
enableAutoFocus: function () {
this.callCamera('setAutoFocus', [true])
},
focusCenter: function () {
this.callCamera('focusAt', [375, 211])
},
toggleTorch: function () {
this.torchEnabled = !this.torchEnabled
this.callCamera('setTorch', [this.torchEnabled])
},
takePhoto: function () {
this.callCamera('takePhoto', [''])
},
toggleRecording: function () {
var self = this
if (this.recording) {
this.callCamera('stopRecording')
return
}
this.requestAudioPermission(function () {
self.callCamera('startRecording', [''])
})
},
toggleStream: function () {
var self = this
if (this.streaming) {
this.callCamera('stopRtmp')
return
}
if (!this.rtmpUrl) {
this.statusText = '请输入 RTMP 地址'
return
}
this.requestAudioPermission(function () {
self.callCamera('startRtmp', [self.rtmpUrl])
})
},
showCapabilities: function () {
console.log('相机能力:', this.callCamera('getCapabilities'))
},
showSession: function () {
console.log('会话快照:', this.callCamera('getSessionSnapshot'))
},
decodeEvent: function (event) {
var raw = event && event.detail != null ? event.detail : event
if (typeof raw !== 'string') return raw || {}
try {
return JSON.parse(raw)
} catch (error) {
return { message: raw }
}
},
handleCameraEvent: function (type, event) {
var payload = this.decodeEvent(event)
if (type === 'statechange') {
this.previewing = payload.state === 'PREVIEWING'
if (payload.state === 'CLOSED') {
this.previewing = false
this.streaming = false
this.recording = false
this.torchEnabled = false
}
this.statusText = payload.state || this.statusText
}
if (type === 'photo') {
this.photoPath = payload.path || ''
this.statusText = this.photoPath ? '照片已保存' : this.statusText
}
if (type === 'recordchange') {
this.recording = payload.state === 'starting' || payload.state === 'started'
if (payload.state === 'stopped') this.videoPath = payload.path || ''
this.statusText = '录像状态:' + (payload.state || '')
}
if (type === 'streamchange') {
var activeStates = ['starting', 'connecting', 'authenticated', 'started', 'retrying', 'degraded']
this.streaming = activeStates.indexOf(payload.state) >= 0
this.statusText = 'RTMP:' + (payload.message || payload.state || '')
console.log('RTMP:', payload.state, payload.message)
}
if (type === 'error') {
if (payload.operation === 'stream' || payload.operation === 'startRtmp') this.streaming = false
if (payload.operation === 'recording' || payload.operation === 'startRecording') this.recording = false
this.lastError = (payload.code || 'INTERNAL_ERROR') + ': ' + (payload.message || '')
this.statusText = this.lastError
console.error(payload.code, payload.message)
}
}
}
}
</script>
<style>
.page { flex: 1; }
.camera { width: 750px; height: 422px; }
.row { flex-direction: row; margin-top: 12px; }
.row button { flex: 1; margin-right: 8px; }
.rtmp-input { flex: 1; height: 72px; border-width: 1px; border-color: #cccccc; }
.status { margin-top: 12px; font-size: 24px; }
.error { margin-top: 12px; color: #cc3333; }
</style>
属性
| 属性 | 类型 | 默认值 | 说明 |
|---|---|---|---|
initialLensFacing |
string |
back |
autoOpen=true 时使用的镜头,取值 back 或 front。 |
width |
number |
1920 |
autoOpen=true 时使用的采集宽度。 |
height |
number |
1080 |
autoOpen=true 时使用的采集高度。 |
initialFps |
number |
30 |
autoOpen=true 时使用的帧率。 |
initialBitrate |
number |
4000000 |
autoOpen=true 时使用的视频编码码率,单位 bps。 |
autoOpen |
boolean |
false |
组件原生视图加载完成后自动打开相机。 |
手势
相机预览区域支持以下手势:
- 单指轻触:在触点位置执行自动对焦与测光,并显示白色焦点框缩小、停留、淡出的反馈动画。
- 双指捏合:缩放倍率由当前设备摄像头能力限制。
相机未打开、已关闭自动对焦或正在切换前后镜头时,轻触对焦不会生效。手势操作不会中断录像或 RTMP 推流。
方法
| 方法 | 参数 | 说明 |
|---|---|---|
open(lensFacing, width, height, fps, bitrate) |
back/front, number, number, number, number |
打开相机并设置初始视频参数。 |
close() |
无 | 停止录像与推流,解绑相机;之后可再次 open()。 |
closeCamera(command) |
非空字符串 | close() 的兼容别名。 |
release() |
无 | Android 兼容方法:释放全部原生资源。iOS 不提供该方法,避免与 NSObject.release 冲突。 |
releaseCamera() |
无 | Android 与 iOS 通用:释放全部原生资源;组件卸载时自动执行,释放后不可复用该实例。 |
getCapabilities() |
无 | 返回 JSON 字符串,包含可用镜头、分辨率、补光灯与自动对焦能力。 |
getSessionSnapshot() |
无 | 返回当前会话快照 JSON 字符串,适合调试。 |
switchCamera() |
无 | 在前后镜头间切换。 |
switchLens(command) |
非空字符串 | switchCamera() 的兼容别名。 |
setLensFacing(lensFacing) |
back/front |
指定目标镜头。 |
setResolution(width, height) |
number, number |
设置下次编码使用的优选采集分辨率。 |
setFps(fps) |
number |
设置下次编码使用的帧率。 |
setBitrate(bitrate) |
number |
Android:设置下次编码使用的视频码率,单位 bps。iOS:保留参数兼容性,实际录像码率由系统决定。 |
setAutoFocus(enabled) |
boolean |
开启或关闭连续自动对焦。 |
focusAt(x, y) |
number, number |
按组件坐标执行点对焦。 |
setTorch(enabled) |
boolean |
控制后置摄像头补光灯。 |
takePhoto(outputPath) |
string |
拍照;传空字符串时自动生成 JPG 路径。 |
startRecording(outputPath) |
string |
开始带声音 MP4 录像;传空字符串时自动生成 MP4 路径。 |
stopRecording() |
无 | 停止录像并异步完成 MP4 文件。 |
startRtmp(url) |
string |
Android:启动带声音的 RTMP/RTMPS/RTMPT/RTMPTS 推流。iOS:返回 RTMP_NOT_STARTED 错误。 |
stopRtmp() |
无 | Android:主动停止 RTMP 推流。iOS:返回 RTMP_NOT_STARTED 错误。 |
setResolution()、setFps() 和 setBitrate() 必须在未录像、未推流时调用。调用后会重建采集配置;若设备不支持对应组合,将通过 error 返回失败。iOS 的 setBitrate() 不会强制改变 AVCaptureMovieFileOutput 的实际编码码率。
视频码率
initialBitrate 是 autoOpen=true 时首次创建视频编码器所用的码率;setBitrate(bitrate) 用于在未录像、未推流时调整下一次编码会话的码率。单位均为 bps,例如 4000000 等于 4Mbps,不是 4000kbps 或 4MB/s。
视频码率同时影响 MP4 录像清晰度、RTMP 推流清晰度、文件大小和上行带宽占用。可按采集参数选择以下起始值:
| 分辨率与帧率 | 建议码率 | 适用情况 |
|---|---|---|
1280x720 @ 24fps |
1500000 到 2000000 |
带宽受限或以流畅性优先。 |
1280x720 @ 30fps |
2000000 到 3000000 |
常规移动网络推流。 |
1920x1080 @ 24fps |
3000000 到 4000000 |
画面运动较少的高清录制或推流。 |
1920x1080 @ 30fps |
4000000 到 6000000 |
默认高清推流场景。 |
网络不稳定时优先降低分辨率、帧率或码率。码率过高可能触发 RTMP_NETWORK_UNSTABLE,或导致服务端缓冲;码率过低会使运动画面出现明显压缩块。
照片与录像路径
takePhoto(outputPath) 和 startRecording(outputPath) 都支持自动路径与自定义完整路径。iOS 自动路径位于应用 Documents/yzj_builtin_camera/,文件会通过事件 path 返回。
Android 传入空字符串时,组件使用应用外部私有目录自动生成文件:
| 操作 | 默认目录 | 文件名 |
|---|---|---|
takePhoto('') |
getExternalFilesDir(DIRECTORY_PICTURES)/yzj_builtin_camera/ |
photo_<时间戳>.jpg |
startRecording('') |
getExternalFilesDir(DIRECTORY_MOVIES)/yzj_builtin_camera/ |
video_<时间戳>.mp4 |
可以传入完整本地路径指定文件名。目录必须可写,建议仍放在当前应用的外部私有目录中:
var packageName = 'uni.app.UNID0E57B9'
var photoPath = '/storage/emulated/0/Android/data/' + packageName + '/files/Pictures/inspection.jpg'
var videoPath = '/storage/emulated/0/Android/data/' + packageName + '/files/Movies/inspection.mp4'
this.callCamera('takePhoto', [photoPath])
this.callCamera('startRecording', [videoPath])
照片成功后通过 photo.path 返回最终路径。录像停止是异步操作,必须等待 recordchange 返回 state: 'stopped',并使用事件中的 path;在此之前 MP4 文件可能尚未完成索引,不能播放、上传或移动。
事件
| 事件 | 载荷 | 说明 |
|---|---|---|
statechange |
{ state, operation, lensFacing } |
相机状态,常见 PREVIEWING、CLOSED。 |
photo |
{ path } |
照片已保存,path 为 JPG 本地路径。 |
recordchange |
{ state, path } |
录像状态:starting、started、stopping、stopped、failed。 |
streamchange |
{ state, code, message, causeCode, stage, recoverable, rawReason, retryCount, retryMax } |
RTMP 状态与重试信息。 |
error |
{ success, code, message, operation, causeCode, stage, recoverable, rawReason, retryCount, retryMax } |
相机、音频、录像或推流失败。 |
capabilitieschange 是保留事件名;当前能力数据应通过 getCapabilities() 主动获取。
RTMP 重连与声音
RTMP 使用共享 H.264/AAC 编码结果。发生 Wi-Fi 与移动网络切换时,旧 TCP 连接报 Broken pipe、Connection abort 或 Socket closed 属于预期现象,不应将该单条底层日志当作应用崩溃。
- 可恢复连接失败会淘汰旧
RtmpClient并创建全新客户端,避免复用已经停止的 RootEncoder sender。 - 每个连接使用独立 attempt ID,旧客户端回调不会终止新连接。
- 存在网络时按
1.5 秒、2 秒、3 秒退避重试,最多消耗 3 次服务端连接额度。 - 没有具备
INTERNET能力的活动网络时,每 3 秒等待网络恢复,不消耗服务端重试额度。 - 新连接会等待视频关键帧,并缓存最新 AAC 样本,在首个关键帧后立即发送,避免重连后偶发无声。
- RootEncoder 2.7.5 的客户端 ping 在断链竞态下可能导致未捕获异常,因此组件关闭该 ping,依赖服务端存活检测、读失败检测与连接回调处理断线。
业务侧应以 streamchange.state === 'started' 作为推流实际恢复的依据。retrying、RTMP_WAITING_FOR_NETWORK 表示组件仍在自动恢复,不能主动再次调用 startRtmp()。
常见错误码
| 错误码 | 处理建议 |
|---|---|
CAMERA_PERMISSION_REQUIRED |
申请 CAMERA 权限后重新打开相机。 |
AUDIO_PERMISSION_REQUIRED |
申请 RECORD_AUDIO 权限后重新开始录像或推流。 |
AUDIO_BUSY |
上一麦克风会话仍在释放,稍后重试。 |
AUDIO_CAPTURE_FAILED / AUDIO_ENCODER_FAILED |
检查麦克风是否被其他应用占用,或设备是否支持当前音频采集。 |
CAMERA_NOT_OPEN |
先等待 statechange/PREVIEWING,再调用拍照、录像或推流。 |
CAMERA_COMBINATION_UNSUPPORTED |
当前相机用例、分辨率或镜头组合不受设备支持。 |
RTMP_URL_INVALID |
使用 rtmp://、rtmps://、rtmpt:// 或 rtmpts:// 地址。 |
RTMP_CONNECTION_TIMEOUT / RTMP_CONNECTION_REFUSED |
检查服务器、端口、防火墙和网络。 |
RTMP_DNS_FAILED |
检查域名、DNS 或网络可用性。 |
RTMP_PUBLISH_REJECTED / RTMP_AUTH_FAILED |
检查流名称、鉴权参数及服务器发布权限。 |
RTMP_WAITING_FOR_NETWORK |
等待 Wi-Fi 或移动网络恢复,无需停止后重开。 |
RTMP_RETRY_EXHAUSTED |
网络正常但连续 3 次连接失败,检查服务端后由业务决定是否再次开始推流。 |
RTMP_AUDIO_PROCESSING_FAILED |
检查 RTMP 连接和服务端音频接收能力。 |
完整错误载荷中的 code 为最终结果,causeCode 为原始分类,rawReason 保留底层异常信息。
录像、取景与资源说明
CameraX 负责预览、拍照和 ImageAnalysis;YUV 帧编码为 H.264,麦克风 PCM 编码为 AAC-LC。MP4 与 RTMP 共享编码结果,最后一个输出停止后才释放编码器与麦克风。
MP4 会等待音视频轨道均就绪,并从视频关键帧开始写入。手机断电、系统杀进程或应用崩溃时,正在写入的 MP4 可能没有完成索引;对关键业务建议采用固定时长分段录像。
预览、拍照和图像分析共享同一个 CameraX UseCaseGroup 与组件画幅。照片内容按组件画幅裁切;页面展示竖图时应根据图片实际比例设置容器,避免把竖图强制放入横向容器造成黑边。
使用注意事项
- 不要在同一时间启动其他 CameraX 或 RootEncoder 相机 source,否则可能争抢同一摄像头。
- 不要在收到
CLOSED、页面卸载或调用release()后继续调用组件方法。 stopRecording()是异步收尾操作,收到stopped前不要使用文件或立即再次开始录像。- 停止最后一个录像或推流输出后立即重启,若收到
AUDIO_BUSY,等待旧麦克风会话释放后重试。 - 推流重连期间不要重复调用
startRtmp();等待started、最终error或业务主动stopRtmp()。 - Android Studio 的
ViewRootImpl.dumpViewHierarchy、ProfileSaver、BufferPoolManager日志通常是布局检查、GC 或媒体缓冲统计,不等同于 RTMP 故障。排查推流优先关注RtmpClient、RtmpSender、TcpSocket、CommandsManager和SocketException。
真机验收
至少覆盖以下场景:
- 单独拍照、单独录像、单独推流。
- 拍照加录像、拍照加推流、录像加推流和三项同时运行。
- 录像、推流和两者同时运行时的前后镜头切换、停止、页面退出、快速停止后重启。
- Wi-Fi 推流后关闭 Wi-Fi,等待移动数据接管,确认状态最终回到
started且服务端音视频均恢复。 - 移动数据推流后切回 Wi-Fi,重复验证视频、声音、本地录像文件和页面状态。
构建原生 AAR
修改 android-library/camera 下的 Kotlin 源码后,在项目根目录执行:
cd "uni_modules\yzj-builtin-cam\android-library"
.\build-and-copy-aar.ps1
脚本会构建 Release AAR、覆盖 utssdk/app-android/libs/yzj-builtin-cam-native-release.aar,并校验两份 AAR 的 SHA-256 一致。
随后必须重新制作并安装 Android 自定义调试基座。普通差量编译不能替换已安装基座中的 AAR。
如需手动构建:
cd "uni_modules\yzj-builtin-cam\android-library"
& "D:\Android\gradle\gradle-8.7\bin\gradle.bat" --no-daemon :camera:assembleRelease
Copy-Item ".\camera\build\outputs\aar\camera-release.aar" "..\utssdk\app-android\libs\yzj-builtin-cam-native-release.aar" -Force
实现结构
utssdk/app-android/index.vue:easycom 组件入口、公开方法与事件转发。utssdk/app-android/native-view.uts:UTS 到原生 AAR 的桥接。android-library/camera:CameraX、MediaCodec、MediaMuxer 和 RootEncoder 原生实现。android-library/build-and-copy-aar.ps1:一键构建、复制与哈希校验脚本。utssdk/app-android/libs/yzj-builtin-cam-native-release.aar:自定义基座实际加载的原生库。

收藏人数:
购买源码授权版(
试用
赞赏(0)
下载 27
赞赏 0
下载 12534727
赞赏 1947
赞赏
京公网安备:11010802035340号