chore:语音气泡正常
This commit is contained in:
@@ -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
|
||||
Reference in New Issue
Block a user