chore:语音输入有返回
This commit is contained in:
@@ -181,19 +181,27 @@ export function createVoiceRecorder(opts = {}) {
|
||||
class VoiceRecorder {
|
||||
constructor(opts) {
|
||||
this._maxDuration = opts.maxDuration || DEFAULT_MAX_DURATION_MS
|
||||
this._format = opts.format || 'mp3'
|
||||
// PR-FRONTEND-VOICE-FORMAT (2026-09-29): 微信小程序在部分 Android 机型上对 mp3 兼容性差,
|
||||
// 表现为一启动就 onStop(duration=0,fileSize=0)。aac 是微信录音默认/最稳格式。
|
||||
this._format = opts.format || 'aac'
|
||||
this._sampleRate = opts.sampleRate || 16000
|
||||
this._channels = opts.numberOfChannels || 1
|
||||
this._bitRate = opts.encodeBitRate || 48000
|
||||
|
||||
this._listeners = { progress: [], error: [] }
|
||||
this._isRecording = false
|
||||
this._isStarting = false // start() 已调用, 但 onStart 尚未回调
|
||||
this._stopResolve = null // 当前 stop() 等待 onStop 解析的 resolve
|
||||
this._pendingStop = false // onStart 之前就调用了 stop() → 等开始后立即停
|
||||
this._startTs = 0
|
||||
this._id = Math.random().toString(36).slice(2, 8)
|
||||
this._autoStopTimer = null
|
||||
this._earlyStopResult = null // PR-FRONTEND-VOICE-RACE: onStop 在 stop() 之前触发时缓存结果
|
||||
|
||||
// 平台相关句柄
|
||||
this._h5 = null // { mediaRecorder, stream, audioCtx, analyser, chunks, mime, rafId }
|
||||
this._uniRec = null // uni.getRecorderManager() 返回值
|
||||
console.log(`[voice-recorder:${this._id}] created`)
|
||||
}
|
||||
|
||||
on(event, cb) {
|
||||
@@ -226,6 +234,9 @@ class VoiceRecorder {
|
||||
if (this._isRecording) {
|
||||
return Promise.reject(_err('ALREADY_STARTED', 'recorder already started'))
|
||||
}
|
||||
if (this._isStarting) {
|
||||
return Promise.reject(_err('ALREADY_STARTED', 'recorder is starting'))
|
||||
}
|
||||
|
||||
if (isH5()) {
|
||||
return this._startH5()
|
||||
@@ -241,7 +252,29 @@ class VoiceRecorder {
|
||||
* resolve(null) 表示已取消 / 无产物,调用方应忽略。
|
||||
*/
|
||||
stop() {
|
||||
if (!this._isRecording) return Promise.resolve(null)
|
||||
console.log(`[voice-recorder:${this._id}] stop() called`, { isRecording: this._isRecording, isStarting: this._isStarting, hasUniRec: !!this._uniRec, hasStopResolve: !!this._stopResolve, earlyResult: !!this._earlyStopResult })
|
||||
if (!this._isRecording) {
|
||||
// PR-FRONTEND-VOICE-RACE (2026-09-29): 真机常见"按住即松手"——
|
||||
// onStart 尚未回调 (录音未真正开始) 就调用 stop(), 原逻辑直接返回 null
|
||||
// 导致上层判定"录音失败,请重试"。这里改为挂起, 等 onStart 触发后立即停,
|
||||
// 产物仍走 onStop 正常返回 (太短的音轨后端可自行拒绝)。
|
||||
if (this._isStarting && this._uniRec) {
|
||||
console.log(`[voice-recorder:${this._id}] stop() deferred: waiting for onStart`)
|
||||
return new Promise((resolve) => {
|
||||
this._pendingStop = true
|
||||
this._stopResolve = resolve
|
||||
})
|
||||
}
|
||||
// onStop 在 stop() 之前触发, 且有有效产物则返回缓存结果
|
||||
if (this._earlyStopResult) {
|
||||
const r = this._earlyStopResult
|
||||
this._earlyStopResult = null
|
||||
console.log(`[voice-recorder:${this._id}] stop() returning early onStop result`)
|
||||
return Promise.resolve(r)
|
||||
}
|
||||
console.log(`[voice-recorder:${this._id}] stop() early return null (not recording)`)
|
||||
return Promise.resolve(null)
|
||||
}
|
||||
if (isH5()) return this._stopH5()
|
||||
return this._stopUni()
|
||||
}
|
||||
@@ -444,46 +477,68 @@ class VoiceRecorder {
|
||||
}
|
||||
|
||||
this._uniRec = rec
|
||||
this._isStarting = true
|
||||
console.log(`[voice-recorder:${this._id}] _startUni: start() invoked on singleton`)
|
||||
|
||||
rec.onStart(() => {
|
||||
console.log(`[voice-recorder:${this._id}] onStart fired`)
|
||||
this._isRecording = true
|
||||
this._isStarting = false
|
||||
this._startTs = Date.now()
|
||||
this._setAutoStopTimer()
|
||||
if (this._pendingStop) {
|
||||
// 用户在录音真正开始前就松手: 立即结束, 产物经 onStop 返回
|
||||
console.log(`[voice-recorder:${this._id}] onStart: pendingStop detected, calling rec.stop()`)
|
||||
this._pendingStop = false
|
||||
try { rec.stop() } catch (e) { /* onStop 自会兜底 */ }
|
||||
}
|
||||
resolve()
|
||||
})
|
||||
|
||||
rec.onStop((res) => {
|
||||
// res = { tempFilePath, duration, fileSize }
|
||||
console.log(`[voice-recorder:${this._id}] onStop fired`, res, 'hasStopResolve=', !!this._stopResolve)
|
||||
this._uniRec = null
|
||||
this._isRecording = false
|
||||
this._isStarting = false
|
||||
this._clearAutoStopTimer()
|
||||
const mime = this._format === 'wav' ? 'audio/wav'
|
||||
: this._format === 'aac' ? 'audio/aac'
|
||||
: 'audio/mpeg'
|
||||
if (this._uniResolve) {
|
||||
const r = this._uniResolve
|
||||
this._uniResolve = null
|
||||
r({
|
||||
filePath: res.tempFilePath,
|
||||
blob: null,
|
||||
fileName: '',
|
||||
duration: typeof res.duration === 'number' ? res.duration : (Date.now() - this._startTs),
|
||||
mime,
|
||||
size: res.fileSize || 0,
|
||||
cancelled: false
|
||||
})
|
||||
// PR-FRONTEND-VOICE-DURATION-FIX (2026-09-29): 微信 onStop 的 duration
|
||||
// 偶尔为 0, 用 start→stop 真实时长兜底, 避免下游按 duration=0 丢弃。
|
||||
const realDuration = this._startTs ? Date.now() - this._startTs : 0
|
||||
const reportedDuration = (res && typeof res.duration === 'number') ? res.duration : 0
|
||||
const result = {
|
||||
filePath: res.tempFilePath,
|
||||
blob: null,
|
||||
fileName: '',
|
||||
duration: reportedDuration > 0 ? reportedDuration : realDuration,
|
||||
mime,
|
||||
size: res.fileSize || 0,
|
||||
cancelled: false
|
||||
}
|
||||
if (this._stopResolve) {
|
||||
const r = this._stopResolve
|
||||
this._stopResolve = null
|
||||
r(result)
|
||||
} else if (result.filePath && result.duration > 0) {
|
||||
// PR-FRONTEND-VOICE-RACE: onStop 在 stop() 之前触发且产物有效, 缓存起来
|
||||
this._earlyStopResult = result
|
||||
}
|
||||
})
|
||||
|
||||
rec.onError((err) => {
|
||||
const e = _err('UNI_RECORD_ERROR', (err && (err.errMsg || err.message)) || 'record failed')
|
||||
console.error(`[voice-recorder:${this._id}] UNI_RECORD_ERROR raw:`, err, 'errMsg=', err && err.errMsg, 'message=', err && err.message)
|
||||
this._uniRec = null
|
||||
this._isRecording = false
|
||||
this._isStarting = false
|
||||
this._clearAutoStopTimer()
|
||||
this._emit('error', e)
|
||||
if (this._uniResolve) {
|
||||
const r = this._uniResolve
|
||||
this._uniResolve = null
|
||||
if (this._stopResolve) {
|
||||
const r = this._stopResolve
|
||||
this._stopResolve = null
|
||||
r(null)
|
||||
} else {
|
||||
// 启动阶段的 reject
|
||||
@@ -530,25 +585,39 @@ class VoiceRecorder {
|
||||
|
||||
_stopUni() {
|
||||
return new Promise((resolve) => {
|
||||
console.log(`[voice-recorder:${this._id}] _stopUni called`)
|
||||
const rec = this._uniRec
|
||||
if (!rec) { resolve(null); return }
|
||||
this._uniResolve = resolve
|
||||
this._earlyStopResult = null
|
||||
this._stopResolve = resolve
|
||||
try { rec.stop() } catch (e) { resolve(null) }
|
||||
})
|
||||
}
|
||||
|
||||
_cancelUni() {
|
||||
return new Promise((resolve) => {
|
||||
console.log(`[voice-recorder:${this._id}] _cancelUni called`)
|
||||
this._earlyStopResult = null
|
||||
const rec = this._uniRec
|
||||
if (!rec) {
|
||||
// 挂起的 stop 需要一并了结, 避免松手后卡在 pending
|
||||
if (this._stopResolve && this._pendingStop) {
|
||||
const r = this._stopResolve
|
||||
this._stopResolve = null
|
||||
this._pendingStop = false
|
||||
r(null)
|
||||
}
|
||||
this._isRecording = false
|
||||
this._isStarting = false
|
||||
this._clearAutoStopTimer()
|
||||
resolve()
|
||||
return
|
||||
}
|
||||
this._uniResolve = () => resolve()
|
||||
this._stopResolve = () => resolve()
|
||||
this._pendingStop = false
|
||||
try { rec.stop() } catch (e) {
|
||||
this._isRecording = false
|
||||
this._isStarting = false
|
||||
this._clearAutoStopTimer()
|
||||
resolve()
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user