更新记录
0.4.6(2026-09-08)
- 修复 Android 平台未暴露 openPreview、closePreview 接口的问题,统一 Android 与 iOS 的预览控制 API。
- 新增 Android 预览软关闭能力,关闭预览时保留 CameraX 会话,再次打开可快速恢复。
- 新增 releaseCamera 资源释放接口,用于页面退出、进入后台或需要让出摄像头时彻底释放相机资源。
- 优化 Android WiFi 与移动网络切换时的 RTMP 推流重连逻辑,网络恢复后自动重建连接。
- startRtmp 新增 maxRetryCount 参数,支持配置首次连接失败后的最大重试次数,默认 3 次,范围 0-10。
- 优化 RTMP 重连状态和异常事件,补充实际重试次数及最大重试次数信息。
0.4.4(2026-09-07)
- iOS 优化相机预览关闭与快速重新打开,新增 openPreview、closePreview 软关闭接口。
- iOS 新增 releaseCamera 接口,用于彻底释放相机资源。
- iOS 优化 WiFi、移动网络切换时的 RTMP 推流重连逻辑。
- iOS 的 startRtmp 新增 maxRetryCount 动态参数,可配置最大重试次数,默认 3 次。
- iOS 优化相机、推流状态与异常事件回调。
0.4.3(2026-09-05)
IOS优化了网络切换以后,推流不稳定的情况
查看更多平台兼容性
uni-app(4.0)
| Vue2 | Vue2插件版本 | Vue3 | Vue3插件版本 | Chrome | Safari | app-vue | app-nvue | app-nvue插件版本 | Android | Android插件版本 | iOS | iOS插件版本 | 鸿蒙 |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| √ | 0.4.6 | √ | 0.4.6 | × | × | × | √ | 0.4.6 | 8.0 | 0.4.6 | 15 | 0.4.6 | × |
| 微信小程序 | 支付宝小程序 | 抖音小程序 | 百度小程序 | 快手小程序 | 京东小程序 | 鸿蒙元服务 | QQ小程序 | 飞书小程序 | 小红书小程序 | 快应用-华为 | 快应用-联盟 |
|---|---|---|---|---|---|---|---|---|---|---|---|
| × | × | × | × | × | × | × | × | × | × | × | × |
其他
| 多语言 | 暗黑模式 | 宽屏模式 | 蒸汽模式 |
|---|---|---|---|
| × | × | × | √ |
yzj-builtin-cam
App nvue 内置相机 easycom 组件。
支持传统 uni-app 的 App-Android 与 App-iOS nvue 页面,兼容 Vue 2 与 Vue 3。最低 Android API 26、iOS 15;Web、小程序和 HarmonyOS 不可用。
能力与限制
- Android 与 iOS 都支持拍照、录像和 RTMP/RTMPS 推流,可独立启停或组合运行。iOS 推流通过同一个
AVCaptureSession分发音视频样本,避免重复占用相机和麦克风。 - Android 录像和推流共享一套 H.264 视频编码器、
AudioRecord和 AAC 编码器,避免重复占用相机与麦克风。 - Android 默认视频参数为
1920x1080、30fps、4000000bps;iOS 默认推流参数为1280x720、30fps、2500000bps。音频为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:录像与 RTMP 推流同步采集声音。
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 标签可按上例调用公开方法。
维护与发布流程
本插件的 iOS Swift 源码、调试插件和正式交付二进制分开维护,避免将源码误提交到主项目或插件市场。
| 内容 | 位置 | Git 仓库 | 用途 |
|---|---|---|---|
| 本地调试插件 | uni_modules/yzj-builtin-cam-dev |
不提交 | 本地 iOS Swift 源码调试 |
| iOS 私有源码 | E:\junge\uniapp-project\yzj-builtin-cam-ios-core |
GitHub 私有仓库 | 维护 Swift 源码和构建脚本 |
| 正式插件 | uni_modules/yzj-builtin-cam |
主项目 Gitee 仓库 | 交付 UTS 入口和 XCFramework |
uni_modules/yzj-builtin-cam-dev/ 已在主项目 .gitignore 中。不要执行 git add -f uni_modules/yzj-builtin-cam-dev,否则会将调试 Swift 源码提交到主项目。
推流功能调试完成后
- 将已验证的 Swift 修改同步到
yzj-builtin-cam-ios-core私有源码仓库。 - 进入私有源码仓库,提交并推送到 GitHub:
cd E:\junge\uniapp-project\yzj-builtin-cam-ios-core
git status
git add Sources/YZJBuiltinCameraCore/BuiltinCameraIosNativeView.swift
git commit -m "优化 RTMP 推流"
git push origin main
若仓库默认分支不是 main,先执行 git branch --show-current,再将命令中的 main 替换为实际分支名。
- 触发私有仓库的 GitHub Actions,构建新的
YZJBuiltinCameraCore.xcframework。 - 用新生成的 XCFramework 替换正式插件中的:
uni_modules/yzj-builtin-cam/utssdk/app-ios/Frameworks/YZJBuiltinCameraCore.xcframework
- 进入主项目,将正式插件二进制和必要的文档更新提交到 Gitee:
cd E:\junge\uniapp-project\yzj_scanqrcode
git add uni_modules/yzj-builtin-cam
git commit -m "更新 iOS 相机推流组件"
git push origin master
主项目 origin 必须指向 Gitee。GitHub 推送只在 yzj-builtin-cam-ios-core 私有源码仓库中执行。
关闭与释放的区别
组件提供两组不同语义的方法:
openPreview() / closePreview()
用于页面内快速打开、关闭预览。closePreview() 会隐藏预览并保留已启动的相机会话(iOS 为 AVCaptureSession,Android 为 CameraX 会话),因此再次 openPreview() 可以快速恢复,避免重复初始化相机。
releaseCamera()
用于页面退出、App 进入后台或需要让出摄像头时彻底释放相机。该方法会停止相机会话并移除相关输入输出。调用后再次打开需要重新初始化相机会话。
原有的 open() 和 close() 仍保留用于兼容旧代码;新页面建议使用 openPreview() 和 closePreview()。软关闭后相机仍由当前 App 占用,需要让出摄像头时必须调用 releaseCamera()。
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 |
打开相机并设置初始视频参数。 |
openPreview(lensFacing, width, height, fps, bitrate) |
back/front, number, number, number, number |
快速显示已初始化的预览;首次调用时等价于 open()。 |
close() |
无 | closePreview() 的兼容别名,停止录像与推流并隐藏预览,保留相机会话以便快速重新打开。 |
closePreview() |
无 | 停止录像与推流并隐藏预览,保留相机会话;再次 openPreview() 不需要重新绑定相机。 |
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, maxRetryCount?) |
string, number |
启动带声音的 RTMP 推流。默认 3,表示首次连接失败后最多再重连 3 次,取值范围 0-10。Android 额外支持 RTMPT/RTMPTS;iOS 支持 RTMP/RTMPS。 |
stopRtmp() |
无 | 主动停止 RTMP 推流。 |
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,旧客户端回调不会终止新连接。
- Android 与 iOS 默认在首次连接失败后最多重连 3 次。可通过
startRtmp(url, maxRetryCount)设置为0-10次;总连接次数为首次连接加上该参数。Android 重试间隔按1 秒递增,iOS 按1.5 秒递增。 - 没有具备
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 |
网络正常但达到配置的最大重连次数后仍失败,检查服务端后由业务决定是否再次开始推流。 |
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,重复验证视频、声音、本地录像文件和页面状态。

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