241 lines
6.8 KiB
Markdown
241 lines
6.8 KiB
Markdown
# Android 设备录音兼容性问题
|
||
|
||
## 🐛 问题描述
|
||
|
||
在某些 Android 设备上,`recorderManager.onStop` 回调中的 `res` 对象缺少 `duration` 和 `fileSize` 字段:
|
||
|
||
```javascript
|
||
{
|
||
"tempFilePath": "_doc/uniapp_temp_xxx/recorder/xxx.pcm"
|
||
// duration: undefined ❌
|
||
// fileSize: undefined ❌
|
||
}
|
||
```
|
||
|
||
但是文件实际上已经生成了,只是这两个字段没有返回。
|
||
|
||
## 🔍 根本原因
|
||
|
||
这是 uni-app 在某些 Android 设备上的已知 bug:
|
||
- 录音文件确实生成了
|
||
- 但是 `duration` 和 `fileSize` 字段没有正确返回
|
||
- 特别是使用 PCM 格式时更容易出现
|
||
|
||
## ✅ 解决方案
|
||
|
||
### 修复前的代码(会失败)
|
||
|
||
```javascript
|
||
recorderManager.onStop((res) => {
|
||
if (!res.duration || res.duration < 500) {
|
||
// ❌ 在某些设备上 res.duration 是 undefined
|
||
// 导致这里总是返回,无法继续
|
||
console.error('录音时长太短')
|
||
return
|
||
}
|
||
|
||
// 永远不会执行到这里
|
||
fs.readFile({ filePath: res.tempFilePath, ... })
|
||
})
|
||
```
|
||
|
||
### 修复后的代码(兼容)
|
||
|
||
```javascript
|
||
recorderManager.onStop((res) => {
|
||
// 只检查文件路径是否存在
|
||
if (!res.tempFilePath) {
|
||
console.error('没有录音文件路径')
|
||
return
|
||
}
|
||
|
||
// ⚠️ 跳过 duration 和 fileSize 的检查
|
||
// 因为在某些设备上这些字段可能为 undefined
|
||
// 但文件实际上已经生成了
|
||
|
||
// 直接尝试读取文件
|
||
fs.readFile({
|
||
filePath: res.tempFilePath,
|
||
success: (fileRes) => {
|
||
// 文件读取成功后,可以从 fileRes.data 获取实际大小
|
||
console.log('实际文件大小:', fileRes.data.byteLength, 'bytes')
|
||
|
||
// 继续处理...
|
||
}
|
||
})
|
||
})
|
||
```
|
||
|
||
## 📊 兼容性对比
|
||
|
||
### 修复前
|
||
|
||
| 设备类型 | duration | fileSize | 结果 |
|
||
|---------|----------|----------|------|
|
||
| iOS | ✅ 有值 | ✅ 有值 | ✅ 正常 |
|
||
| Android (部分) | ✅ 有值 | ✅ 有值 | ✅ 正常 |
|
||
| Android (部分) | ❌ undefined | ❌ undefined | ❌ 失败 |
|
||
|
||
### 修复后
|
||
|
||
| 设备类型 | duration | fileSize | 结果 |
|
||
|---------|----------|----------|------|
|
||
| iOS | ✅ 有值 | ✅ 有值 | ✅ 正常 |
|
||
| Android (部分) | ✅ 有值 | ✅ 有值 | ✅ 正常 |
|
||
| Android (部分) | ❌ undefined | ❌ undefined | ✅ 正常(跳过检查) |
|
||
|
||
## 🔧 完整的修复代码
|
||
|
||
```javascript
|
||
recorderManager.onStop((res) => {
|
||
console.log('⏹️ 录音已停止')
|
||
console.log('📋 完整的 res 对象:', JSON.stringify(res))
|
||
|
||
// 只检查文件路径
|
||
if (!res.tempFilePath) {
|
||
console.error('❌ 没有录音文件路径!')
|
||
uni.showToast({
|
||
title: '录音失败:没有生成文件',
|
||
icon: 'none'
|
||
})
|
||
return
|
||
}
|
||
|
||
// ⚠️ 某些 Android 设备上 res.duration 和 res.fileSize 可能为 undefined
|
||
// 这是 uni-app 的已知问题,我们跳过这个检查,直接尝试读取文件
|
||
if (res.duration !== undefined && res.duration < 500) {
|
||
console.error('❌ 录音时长太短:', res.duration, 'ms')
|
||
uni.showToast({
|
||
title: '录音太短,请至少说 2 秒',
|
||
icon: 'none'
|
||
})
|
||
return
|
||
}
|
||
|
||
console.log('✅ 录音文件路径有效,准备读取文件...')
|
||
|
||
// 检查 WebSocket 状态
|
||
if (!this.socketTask || this.socketTask.readyState !== 1) {
|
||
console.error('❌ WebSocket 未连接')
|
||
return
|
||
}
|
||
|
||
// 读取文件
|
||
const fs = uni.getFileSystemManager()
|
||
fs.readFile({
|
||
filePath: res.tempFilePath,
|
||
success: (fileRes) => {
|
||
console.log('✅ 文件读取成功')
|
||
console.log('📊 实际文件大小:', fileRes.data.byteLength, 'bytes')
|
||
console.log('📊 预计录音时长:', (fileRes.data.byteLength / 32000).toFixed(2), '秒')
|
||
|
||
// 验证文件大小
|
||
if (fileRes.data.byteLength < 32000) {
|
||
console.error('❌ 文件太小,可能录音失败')
|
||
uni.showToast({
|
||
title: '录音文件太小,请重试',
|
||
icon: 'none'
|
||
})
|
||
return
|
||
}
|
||
|
||
// 确保是 ArrayBuffer
|
||
if (!(fileRes.data instanceof ArrayBuffer)) {
|
||
console.error('❌ 数据不是 ArrayBuffer')
|
||
return
|
||
}
|
||
|
||
// 发送音频数据
|
||
this.sendAudioInChunks(fileRes.data)
|
||
},
|
||
fail: (err) => {
|
||
console.error('❌ 文件读取失败:', err)
|
||
uni.showToast({
|
||
title: '文件读取失败',
|
||
icon: 'none'
|
||
})
|
||
}
|
||
})
|
||
})
|
||
```
|
||
|
||
## 🎓 经验总结
|
||
|
||
### 关键点
|
||
|
||
1. **不要依赖 `res.duration` 和 `res.fileSize`**
|
||
- 这两个字段在某些设备上可能为 undefined
|
||
- 只检查 `res.tempFilePath` 是否存在
|
||
|
||
2. **从文件内容获取实际大小**
|
||
- 读取文件后,使用 `fileRes.data.byteLength` 获取实际大小
|
||
- 计算录音时长:`byteLength / 32000` 秒(PCM 16kHz 单声道)
|
||
|
||
3. **添加文件大小验证**
|
||
- 读取文件后检查大小
|
||
- 如果太小(< 32000 bytes,即 < 1 秒),提示用户重试
|
||
|
||
### 最佳实践
|
||
|
||
```javascript
|
||
// ✅ 好的做法
|
||
if (!res.tempFilePath) {
|
||
return // 只检查文件路径
|
||
}
|
||
|
||
// 读取文件后验证
|
||
fs.readFile({
|
||
success: (fileRes) => {
|
||
const size = fileRes.data.byteLength
|
||
const duration = size / 32000 // 计算时长
|
||
|
||
if (duration < 2) {
|
||
console.error('录音太短')
|
||
return
|
||
}
|
||
|
||
// 继续处理...
|
||
}
|
||
})
|
||
|
||
// ❌ 不好的做法
|
||
if (!res.duration || res.duration < 500) {
|
||
return // 在某些设备上会失败
|
||
}
|
||
```
|
||
|
||
## 📱 测试结果
|
||
|
||
修复后,应该看到:
|
||
|
||
```
|
||
⏹️ 录音已停止
|
||
📋 完整的 res 对象: {"tempFilePath":"..."}
|
||
📁 文件路径: _doc/uniapp_temp_xxx/recorder/xxx.pcm
|
||
⏱️ 录音时长: undefined ms ← 可能是 undefined
|
||
📦 文件大小: undefined bytes ← 可能是 undefined
|
||
✅ 录音文件路径有效,准备读取文件...
|
||
✅ 文件读取成功
|
||
📊 实际文件大小: 320000 bytes ← 从文件内容获取
|
||
📊 预计录音时长: 10.00 秒 ← 计算得出
|
||
📦 开始分片发送(官方推荐参数)
|
||
...
|
||
```
|
||
|
||
## 🎉 预期结果
|
||
|
||
修复后,即使 `res.duration` 和 `res.fileSize` 为 undefined,也能:
|
||
|
||
1. ✅ 正确读取录音文件
|
||
2. ✅ 获取实际文件大小
|
||
3. ✅ 计算录音时长
|
||
4. ✅ 分片发送音频数据
|
||
5. ✅ 完成整个对话流程
|
||
|
||
---
|
||
|
||
**问题**: res.duration 和 res.fileSize 为 undefined
|
||
**原因**: uni-app 在某些 Android 设备上的已知 bug
|
||
**解决**: 跳过这些字段的检查,直接读取文件并从文件内容获取实际大小
|
||
**状态**: ✅ 已修复
|