chore:语音气泡正常

This commit is contained in:
2026-09-30 09:28:22 +08:00
parent a978858254
commit 5f4dc370f8
2 changed files with 219 additions and 99 deletions

View File

@@ -73,12 +73,12 @@ function _setSingletonBusy(instance, maxDurationMs = DEFAULT_MAX_DURATION_MS) {
_singletonIdleResolver = resolve
})
_singletonBusyInstance = instance
// 保险: 即使 onStop/onError 因异常没触发, 最多等 maxDuration + 5s 也要释放,
// 保险: 即使 onStop/onError 因异常没触发, 最多等 10s 也要释放,
// 避免死锁导致后续所有 start() 永远挂起。
_singletonIdleTimer = setTimeout(() => {
console.warn('[voice-recorder:singleton] idle timeout, force release')
_setSingletonIdle(instance)
}, maxDurationMs + 5000)
}, Math.min(maxDurationMs + 5000, 10000))
}
function _setSingletonIdle(instance) {
@@ -254,6 +254,7 @@ class VoiceRecorder {
this._startTs = 0
this._id = Math.random().toString(36).slice(2, 8)
this._autoStopTimer = null
this._stopFallbackTimer = null // PR-FRONTEND-VOICE-FALLBACK: stop()/cancel() 兜底定时器
this._earlyStopResult = null // PR-FRONTEND-VOICE-RACE: onStop 在 stop() 之前触发时缓存结果
// 平台相关句柄
@@ -296,13 +297,22 @@ class VoiceRecorder {
return Promise.reject(_err('ALREADY_STARTED', 'recorder is starting'))
}
this._isStarting = true
let p = null
if (isH5()) {
return this._startH5()
p = this._startH5()
} else if (isMpOrApp()) {
p = this._startUni()
} else {
this._isStarting = false
return Promise.reject(_err('NOT_SUPPORTED', 'voice input not supported on this platform'))
}
if (isMpOrApp()) {
return this._startUni()
}
return Promise.reject(_err('NOT_SUPPORTED', 'voice input not supported on this platform'))
// 启动失败时复位状态, 避免后续 stop()/cancel() 一直挂起
p.catch((e) => {
this._isStarting = false
return Promise.reject(e)
})
return p
}
/**
@@ -316,7 +326,7 @@ class VoiceRecorder {
// onStart 尚未回调 (录音未真正开始) 就调用 stop(), 原逻辑直接返回 null
// 导致上层判定"录音失败,请重试"。这里改为挂起, 等 onStart 触发后立即停,
// 产物仍走 onStop 正常返回 (太短的音轨后端可自行拒绝)。
if (this._isStarting && this._uniRec) {
if (this._isStarting) {
console.log(`[voice-recorder:${this._id}] stop() deferred: waiting for onStart`)
return new Promise((resolve) => {
this._pendingStop = true
@@ -341,6 +351,16 @@ class VoiceRecorder {
* 中断录音并丢弃产物。
*/
cancel() {
if (this._isStarting) {
// PR-FRONTEND-VOICE-CANCEL-RACE (2026-09-29): onStart 尚未触发就取消,
// 不能立即返回, 否则录音器启动后会继续录制。挂起 pendingStop,
// 等 onStart 触发后立即 stop(), 产物被丢弃。
console.log(`[voice-recorder:${this._id}] cancel() deferred: waiting for onStart`)
return new Promise((resolve) => {
this._pendingStop = true
this._stopResolve = () => resolve()
})
}
if (!this._isRecording) {
this._clearAutoStopTimer()
return Promise.resolve()
@@ -524,7 +544,13 @@ class VoiceRecorder {
// PR-FRONTEND-VOICE-SINGLETON: 串行化所有对微信录音单例的访问,
// 防止"上一个 onStop 还没触发, 下一个 start() 就报 audio is recording"。
return _singletonIdlePromise.then(() => {
// 防竞态: 注册 .then() 后到回调执行前, 可能有其他实例先占用了单例, 需要重新排队。
if (_singletonBusyInstance && _singletonBusyInstance !== this) {
console.log(`[voice-recorder:${this._id}] _startUni: singleton busy by ${_singletonBusyInstance._id}, re-queue`)
return this._startUni()
}
_setSingletonBusy(this, this._maxDuration)
console.log(`[voice-recorder:${this._id}] _startUni: singleton busy acquired`)
return this._doStartUni()
})
}
@@ -567,6 +593,7 @@ class VoiceRecorder {
rec.onStop((res) => {
// res = { tempFilePath, duration, fileSize }
console.log(`[voice-recorder:${this._id}] onStop fired`, res, 'hasStopResolve=', !!this._stopResolve)
this._clearStopFallbackTimer()
this._uniRec = null
this._isRecording = false
this._isStarting = false
@@ -610,6 +637,7 @@ class VoiceRecorder {
})
rec.onError((err) => {
this._clearStopFallbackTimer()
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
@@ -662,9 +690,20 @@ class VoiceRecorder {
// 可能导致录音被提前中断。
})
} catch (e) {
this._clearStopFallbackTimer()
this._uniRec = null
this._isRecording = false
this._isStarting = false
this._clearAutoStopTimer()
_setSingletonIdle(this)
// 启动阶段就被 stop()/cancel() 抢占, 需要 Fulfill 挂起的 Promise, 而不是 reject
if (this._stopResolve && this._pendingStop) {
const r = this._stopResolve
this._stopResolve = null
this._pendingStop = false
r(null)
return
}
reject(e)
}
})
@@ -677,7 +716,30 @@ class VoiceRecorder {
if (!rec) { resolve(null); return }
this._earlyStopResult = null
this._stopResolve = resolve
try { rec.stop() } catch (e) { resolve(null) }
this._clearStopFallbackTimer()
// PR-FRONTEND-VOICE-FALLBACK: 真机偶发 onStop 不回调, 导致 stop() 永远挂起,
// UI 无法释放旧实例, 新录音提示"recorder already exists"。最多等 3s 强制 resolve。
this._stopFallbackTimer = setTimeout(() => {
console.warn(`[voice-recorder:${this._id}] _stopUni fallback timeout, force resolve`)
this._stopResolve = null
this._pendingStop = false
this._isRecording = false
this._isStarting = false
this._clearAutoStopTimer()
this._uniRec = null
_setSingletonIdle(this)
resolve(null)
}, 3000)
try { rec.stop() } catch (e) {
this._clearStopFallbackTimer()
this._stopResolve = null
this._isRecording = false
this._isStarting = false
this._clearAutoStopTimer()
this._uniRec = null
_setSingletonIdle(this)
resolve(null)
}
})
}
@@ -703,10 +765,24 @@ class VoiceRecorder {
}
this._stopResolve = () => resolve()
this._pendingStop = false
try { rec.stop() } catch (e) {
this._clearStopFallbackTimer()
// PR-FRONTEND-VOICE-FALLBACK: cancel() 同样可能因 onStop 不回调而挂起。
this._stopFallbackTimer = setTimeout(() => {
console.warn(`[voice-recorder:${this._id}] _cancelUni fallback timeout, force resolve`)
this._stopResolve = null
this._isRecording = false
this._isStarting = false
this._clearAutoStopTimer()
this._uniRec = null
_setSingletonIdle(this)
resolve()
}, 3000)
try { rec.stop() } catch (e) {
this._clearStopFallbackTimer()
this._isRecording = false
this._isStarting = false
this._clearAutoStopTimer()
this._uniRec = null
_setSingletonIdle(this)
resolve()
}
@@ -731,6 +807,13 @@ class VoiceRecorder {
this._autoStopTimer = null
}
}
_clearStopFallbackTimer() {
if (this._stopFallbackTimer) {
clearTimeout(this._stopFallbackTimer)
this._stopFallbackTimer = null
}
}
}
export default createVoiceRecorder