|
|
@@ -270,8 +270,75 @@ export default {
|
|
|
methods: {
|
|
|
// 清理 iframe 录音会话(统一清理入口)
|
|
|
_cleanupTranscriber(iframe) {
|
|
|
- iframe.contentWindow.onSessionStopped = null;
|
|
|
- iframe.contentWindow.window.onRecognizedResult = null;
|
|
|
+ const win = iframe && iframe.contentWindow;
|
|
|
+ if (!win) return;
|
|
|
+ try {
|
|
|
+ win.onSessionStopped = null;
|
|
|
+ win.onSessionStarted = null;
|
|
|
+ win.window.onRecognizedResult = null;
|
|
|
+ } catch (e) {
|
|
|
+ console.warn("[cleanupTranscriber] 清理 iframe 回调失败", e);
|
|
|
+ }
|
|
|
+ },
|
|
|
+ // 查询麦克风权限状态:永不抛错、永不挂起
|
|
|
+ // 不支持 / 查询失败 / 超时 统一返回 'unknown',避免停止流程被卡死
|
|
|
+ _queryMicPermission(timeout = 1500) {
|
|
|
+ return new Promise(resolve => {
|
|
|
+ let settled = false;
|
|
|
+ let timer = null;
|
|
|
+ const finish = state => {
|
|
|
+ if (settled) return;
|
|
|
+ settled = true;
|
|
|
+ if (timer) clearTimeout(timer);
|
|
|
+ resolve(state || 'unknown');
|
|
|
+ };
|
|
|
+ timer = setTimeout(() => finish('unknown'), timeout);
|
|
|
+ try {
|
|
|
+ if (!navigator.permissions || typeof navigator.permissions.query !== 'function') {
|
|
|
+ return finish('unknown');
|
|
|
+ }
|
|
|
+ const result = navigator.permissions.query({ name: 'microphone' });
|
|
|
+ if (result && typeof result.then === 'function') {
|
|
|
+ result
|
|
|
+ .then(status => finish(status && status.state))
|
|
|
+ .catch(e => {
|
|
|
+ console.warn("[stopRecord] permissions.query 失败,按未知权限处理", e);
|
|
|
+ finish('unknown');
|
|
|
+ });
|
|
|
+ } else if (result && result.state) {
|
|
|
+ finish(result.state);
|
|
|
+ } else {
|
|
|
+ finish('unknown');
|
|
|
+ }
|
|
|
+ } catch (e) {
|
|
|
+ // Firefox/Safari 等对 { name: 'microphone' } 直接抛 TypeError
|
|
|
+ console.warn("[stopRecord] permissions.query 不支持,按未知权限处理", e);
|
|
|
+ finish('unknown');
|
|
|
+ }
|
|
|
+ });
|
|
|
+ },
|
|
|
+ // 绑定 iframe 停止回调(必须在点击停止按钮之前绑定,避免回调先于监听到达)
|
|
|
+ _bindSessionStopped(iframe) {
|
|
|
+ const win = iframe && iframe.contentWindow;
|
|
|
+ if (!win) return;
|
|
|
+ win.onSessionStopped = (s, e) => {
|
|
|
+ console.log("onSessionStopped", s, e);
|
|
|
+ this._finishStop(iframe, this.lang.ssKWLRecStopped);
|
|
|
+ };
|
|
|
+ },
|
|
|
+ // 统一收尾:复位状态、解绑回调、清理 iframe、给出提示
|
|
|
+ _finishStop(iframe, tipMsg, tipType = "success") {
|
|
|
+ if (this._stopFallbackTimer) {
|
|
|
+ clearTimeout(this._stopFallbackTimer);
|
|
|
+ this._stopFallbackTimer = null;
|
|
|
+ }
|
|
|
+ this.recordObj.status = "0";
|
|
|
+ this.recordObj.loading = false;
|
|
|
+ this.recordObj.startTime = 0;
|
|
|
+ this._cleanupTranscriber(iframe);
|
|
|
+ if (tipMsg && this.$message) {
|
|
|
+ this.$message[tipType](tipMsg);
|
|
|
+ }
|
|
|
},
|
|
|
changeWorkData(newValue) {
|
|
|
const str = JSON.stringify(newValue);
|
|
|
@@ -386,67 +453,74 @@ export default {
|
|
|
// }
|
|
|
},
|
|
|
// 停止录音
|
|
|
- stopRecord() {
|
|
|
- if (new Date().getTime() - this.recordObj.startTime < 2000) {
|
|
|
+ // 旧实现的问题:
|
|
|
+ // 1. navigator.permissions 不存在时,`permissions && permissions.query(...).then(...)`
|
|
|
+ // 整段短路成 undefined,回调不执行,录音实际没停、状态也没复位;
|
|
|
+ // 2. permissions.query 在 Firefox/Safari 上同步 throw,未捕获导致停止流程中断;
|
|
|
+ // 3. 只有 "granted" 才继续,state 为 "prompt"(权限弹窗未回应)时误判成无权限;
|
|
|
+ // 4. onSessionStopped 在点击停止按钮「之后」才绑定,回调可能先于绑定到达,状态永远不复位。
|
|
|
+ async stopRecord() {
|
|
|
+ // 防重复点击:已在停止中或已停止则忽略
|
|
|
+ if (this.recordObj.loading || this.recordObj.status != '1') {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ if (this.recordObj.startTime && new Date().getTime() - this.recordObj.startTime < 2000) {
|
|
|
this.$message.info(this.lang.ssKWLRecordShort);
|
|
|
return;
|
|
|
}
|
|
|
const iframe = this.$refs["iframeRef"];
|
|
|
- navigator.permissions && navigator.permissions.query({ name: 'microphone' }).then(permissionStatus => {
|
|
|
- if (permissionStatus.state !== "granted") {
|
|
|
- this.recordObj.status = "0";
|
|
|
- this.recordObj.loading = false;
|
|
|
- if (iframe && iframe.contentWindow) {
|
|
|
- this._cleanupTranscriber(iframe);
|
|
|
- }
|
|
|
- this.$message.warning(this.lang.ssKWLNoMicPerm);
|
|
|
+ if (!iframe || !iframe.contentWindow) {
|
|
|
+ // iframe 已不存在,直接复位本地状态
|
|
|
+ this._finishStop(iframe);
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ this.recordObj.loading = true;
|
|
|
+ try {
|
|
|
+ // 权限查询失败/不支持/超时都返回 'unknown',只有明确 denied 才算无权限
|
|
|
+ const permissionState = await this._queryMicPermission(1500);
|
|
|
+ if (permissionState === 'denied') {
|
|
|
+ this._finishStop(iframe, this.lang.ssKWLNoMicPerm, "warning");
|
|
|
return;
|
|
|
}
|
|
|
- if (this.recordObj.status == '1' && !this.recordObj.loading) {
|
|
|
- // let iframe = this.$refs["iframeRef"];
|
|
|
- if (!iframe || !iframe.contentWindow) {
|
|
|
- this.recordObj.status = "0";
|
|
|
- this.recordObj.loading = false;
|
|
|
- return;
|
|
|
- }
|
|
|
- this.recordObj.loading = true;
|
|
|
- try {
|
|
|
- iframe.contentWindow.window.document
|
|
|
- .getElementById("scenarioStopButton")
|
|
|
- .click();
|
|
|
- } catch (err) {
|
|
|
- console.error("stop click error:", err);
|
|
|
- this.recordObj.status = "0";
|
|
|
- this.recordObj.loading = false;
|
|
|
- return;
|
|
|
- }
|
|
|
- iframe.contentWindow.onSessionStopped = () => {
|
|
|
- this.recordObj.status = "0";
|
|
|
- this.recordObj.loading = false;
|
|
|
- this.$message.success(this.lang.ssKWLRecStopped);
|
|
|
- this._cleanupTranscriber(iframe);
|
|
|
- };
|
|
|
- } else {
|
|
|
- this.recordObj.status = "0";
|
|
|
- this.recordObj.loading = false;
|
|
|
- let iframe = this.$refs["iframeRef"];
|
|
|
- if (iframe && iframe.contentWindow) {
|
|
|
- this._cleanupTranscriber(iframe);
|
|
|
- }
|
|
|
- }
|
|
|
- }).catch(() => {
|
|
|
- this.recordObj.status = "0";
|
|
|
- this.recordObj.loading = false;
|
|
|
- let iframe = this.$refs["iframeRef"];
|
|
|
- if (iframe && iframe.contentWindow) {
|
|
|
- this._cleanupTranscriber(iframe);
|
|
|
+
|
|
|
+ // 关键:先绑定回调,再触发 iframe 内的停止按钮
|
|
|
+ this._bindSessionStopped(iframe);
|
|
|
+
|
|
|
+ const stopBtn = iframe.contentWindow.window.document.getElementById("scenarioStopButton");
|
|
|
+ if (!stopBtn) {
|
|
|
+ // iframe 内页面尚未就绪,无法优雅停止,直接本地收尾
|
|
|
+ console.warn("[stopRecord] 未找到 scenarioStopButton,直接结束录音会话");
|
|
|
+ this._finishStop(iframe, this.lang.ssKWLRecStopped);
|
|
|
+ return;
|
|
|
}
|
|
|
- this.$message.success(this.lang.ssKWLRecStopped);
|
|
|
- });
|
|
|
+ stopBtn.click();
|
|
|
+
|
|
|
+ // 兜底:iframe 若因故不再回调 onSessionStopped,超时后强制复位,避免卡在「录音中」
|
|
|
+ if (this._stopFallbackTimer) clearTimeout(this._stopFallbackTimer);
|
|
|
+ this._stopFallbackTimer = setTimeout(() => {
|
|
|
+ this._stopFallbackTimer = null;
|
|
|
+ if (this.recordObj.status == '1') {
|
|
|
+ console.warn("[stopRecord] 等待 onSessionStopped 超时,强制结束录音会话");
|
|
|
+ this._finishStop(iframe, this.lang.ssKWLRecStopped);
|
|
|
+ }
|
|
|
+ }, 3000);
|
|
|
+ } catch (error) {
|
|
|
+ console.error("[stopRecord] 停止录音异常", error);
|
|
|
+ this._finishStop(iframe, this.lang.ssKWLRecStopped);
|
|
|
+ }
|
|
|
}
|
|
|
},
|
|
|
mounted() { },
|
|
|
- beforeUnmount() { }
|
|
|
+ // 注意:当前工程是 Vue 2.6,销毁钩子为 beforeDestroy(beforeUnmount 在 2.6 不会触发)
|
|
|
+ beforeDestroy() {
|
|
|
+ // 组件卸载时清掉停止录音的兜底定时器与 iframe 回调,避免内存泄漏
|
|
|
+ if (this._stopFallbackTimer) {
|
|
|
+ clearTimeout(this._stopFallbackTimer);
|
|
|
+ this._stopFallbackTimer = null;
|
|
|
+ }
|
|
|
+ this._cleanupTranscriber(this.$refs["iframeRef"]);
|
|
|
+ }
|
|
|
};
|
|
|
</script>
|
|
|
|