"use strict"; const common_vendor = require("../../common/vendor.js"); const common_config = require("../../common/config.js"); const _sfc_main = { name: "IdentityVerify", data() { return { loading: false, responses: [], processedResponses: [], assistantResponse: "", audioTranscript: "", videoPlaying: false, showDebugInfo: false, // 设置为true可以显示调试信息 videoUrl: "http://121.36.251.245:9000/minlong/9c139c99-a613-49f2-986b-8a3ac20a9b1b.mp4", // 用于存储AI数字人视频URL showReplayButton: false, cameraStream: null, // 存储摄像头流 cameraError: null, // 存储摄像头错误信息 useMiniProgramCameraComponent: false, // 添加小程序相机组件标志 cameraContext: null, // 添加相机上下文 currentSubtitle: "", subtitles: [ { startTime: 0, // 开始时间(秒) endTime: 5, // 结束时间(秒) text: "你好,我是本次面试的面试官,欢迎参加本公司的线上面试!" }, { startTime: 5, endTime: 13, text: "面试预计需要15分钟,请你提前安排在网络良好、光线亮度合适、且相对安静的环境参加这次面试" }, { startTime: 13, endTime: 20, text: "以免影响本次面试的结果。如果你在面试过程中遇到问题,请与我们的招聘人员联系。" } ], secondVideoSubtitles: [ { startTime: 0, endTime: 10, text: "请结合您的基本信息与过往履历进行简单的自我介绍,并讲一讲您有哪些优势胜任本岗位:" } ], thirdVideoSubtitles: [ { startTime: 0, endTime: 3, text: "在工作中,你如何确保个人防护装备的正确使用?" } ], fourthVideoSubtitles: [ { startTime: 0, endTime: 3, text: "描述一次你与团队合作改善生产流程的经历。" } ], fifthVideoSubtitles: [ { startTime: 0, endTime: 6, text: "你在团队合作中曾遇到过哪些挑战?如何解决团队内部的分歧?" } ], sixthVideoSubtitles: [ { startTime: 0, endTime: 5, text: "您已完成本次面试全部题目,请问您对于这个岗位还有什么想要了解的吗?" } ], showAnswerButton: false, // 控制答题按钮显示 currentVideoIndex: 0, // 当前播放的视频索引 videoList: [ "http://121.36.251.245:9000/minlong/9c139c99-a613-49f2-986b-8a3ac20a9b1b.mp4", // 第一段视频 "http://121.36.251.245:9000/minlong/15467aca-2c1e-4d40-bebc-11fc984f0a24.mp4", // 第二段视频 "http://121.36.251.245:9000/minlong/a96f295c-ecba-4b08-b627-0ed0dc72d727.mp4", // 第三段视频 "http://121.36.251.245:9000/minlong/e152eb38-7e03-4bb0-8b8d-73d51342d2aa.mp4", "http://121.36.251.245:9000/minlong/6381eec3-1373-400f-aa8c-e229b60522ea.mp4", "http://121.36.251.245:9000/minlong/2a11fe78-38d5-4af3-91c8-a080feb122e0.mp4" //结束 ], isRecording: false, recordingTimer: null, showStopRecordingButton: false, mediaRecorder: null, recordedChunks: [], recorder: null, lastUploadedVideoUrl: "", showStartRecordingButton: false, showRetryButton: false, // 控制重试按钮显示 lastVideoToRetry: null, // 存储上次失败的视频URL,用于重试 stopIOSRecording: null, // 存储iOS特殊停止录制函数 cameraInitialized: false, iosDevice: false }; }, mounted() { this.playDigitalHumanVideo(); this.checkAudioPermission(); this.initCamera(); setTimeout(() => { if (this.cameraStream && !this.useMiniProgramCameraComponent) { this.testAudioInput(); } }, 3e3); }, beforeDestroy() { this.stopUserCamera(); }, methods: { // 初始化相机 async initCamera() { const systemInfo = common_vendor.index.getSystemInfoSync(); this.iosDevice = systemInfo.platform === "ios"; console.log("当前设备平台:", systemInfo.platform, "是否iOS:", this.iosDevice); const isMiniProgram = systemInfo.uniPlatform === "mp-weixin" || systemInfo.uniPlatform === "mp-alipay" || systemInfo.uniPlatform === "mp-baidu"; if (isMiniProgram) { this.useMiniProgramCameraComponent = true; await this.checkMiniProgramPermissions(); setTimeout(() => { if (!this.cameraContext) { this.cameraContext = common_vendor.index.createCameraContext(); console.log("相机上下文创建完成"); } }, 500); } else { try { const constraints = { audio: { echoCancellation: true, noiseSuppression: true, autoGainControl: true }, video: { width: { ideal: 640, max: 1280 }, // 控制视频宽度 height: { ideal: 480, max: 720 }, // 控制视频高度 frameRate: { ideal: 15, max: 24 }, // 控制帧率 facingMode: "user" } }; const stream = await navigator.mediaDevices.getUserMedia(constraints); this.cameraStream = stream; const audioTracks = stream.getAudioTracks(); console.log("音频轨道数量:", audioTracks.length); if (audioTracks.length > 0) { console.log("音频轨道已获取:", audioTracks[0].label); audioTracks[0].enabled = true; } else { console.warn("未检测到音频轨道,尝试单独获取音频"); this.tryGetAudioOnly(); } const videoElement = this.$refs.userCameraVideo; if (videoElement) { videoElement.srcObject = stream; videoElement.muted = true; } } catch (error) { console.error("获取摄像头失败:", error); this.cameraError = error.message || "无法访问摄像头"; common_vendor.index.showToast({ title: "无法访问摄像头,请检查权限设置", icon: "none" }); } } }, // 停止用户摄像头 stopUserCamera() { if (this.cameraStream) { this.cameraStream.getTracks().forEach((track) => { track.stop(); }); this.cameraStream = null; } }, async fetchData() { this.loading = true; this.assistantResponse = ""; this.audioTranscript = ""; this.processedResponses = []; try { const requestTask = common_vendor.index.request({ url: "https://dashscope.aliyuncs.com/compatible-mode/v1/chat/completions", method: "POST", header: { "Content-Type": "application/json", "Authorization": "Bearer sk-9e1ec73a7d97493b8613c63f06b6110c" }, data: { "model": "qwen-omni-turbo", "messages": [ { "role": "user", "content": [ { "type": "input_audio", "input_audio": { "data": "https://help-static-aliyun-doc.aliyuncs.com/file-manage-files/zh-CN/20250211/tixcef/cherry.wav", "format": "wav" } }, { "type": "text", "text": "这段音频在说什么" } ] } ], "stream": true, "stream_options": { "include_usage": true }, "modalities": ["text", "audio"], "audio": { "voice": "Cherry", "format": "wav" } }, success: (res) => { console.log("请求成功,响应数据:", res.data); if (typeof res.data === "string" && res.data.includes("data: {")) { const chunks = res.data.split("data: ").filter((chunk) => chunk.trim() !== ""); chunks.forEach((chunk) => { this.handleStreamResponse(chunk); }); } else { this.handleStreamResponse(res.data); } this.playDigitalHumanVideo(); }, fail: (err) => { console.error("请求失败:", err); }, complete: () => { this.loading = false; } }); } catch (error) { console.error("获取数据失败:", error); this.loading = false; } }, handleStreamResponse(data) { if (typeof data === "string") { if (data === "[DONE]") return; try { const cleanData = data.trim(); if (cleanData.startsWith("{") && cleanData.endsWith("}")) { const jsonData = JSON.parse(cleanData); this.processStreamChunk(jsonData); } } catch (e) { console.error("解析JSON失败:", e, "原始数据:", data); } } else { this.processStreamChunk(data); } }, processStreamChunk(chunk) { if (chunk.choices && chunk.choices.length > 0) { const choice = chunk.choices[0]; if (choice.delta && choice.delta.content) { this.assistantResponse += choice.delta.content; } if (choice.delta && choice.delta.audio && choice.delta.audio.transcript) { this.audioTranscript += choice.delta.audio.transcript; } if (choice.delta) { const result = {}; if (choice.delta.role) { result.role = choice.delta.role; } if (choice.delta.audio && choice.delta.audio.transcript) { result.transcript = choice.delta.audio.transcript; } if (Object.keys(result).length > 0) { this.processedResponses.push(result); } } } }, processResponseData() { this.processedResponses = this.responses.map((item) => { const result = {}; if (item.delta && item.delta.role) { result.role = item.delta.role; } if (item.delta && item.delta.audio && item.delta.audio.transcript) { result.transcript = item.delta.audio.transcript; } return result; }).filter((item) => Object.keys(item).length > 0); }, // 播放数字人视频 playDigitalHumanVideo() { this.videoUrl = this.videoList[this.currentVideoIndex]; this.videoPlaying = true; this.$nextTick(() => { const videoContext = common_vendor.index.createVideoContext("myVideo", this); if (videoContext) { videoContext.play(); setTimeout(() => { if (this.videoPlaying && this.$refs.videoPlayer) { console.log("视频应该正在播放"); } else { console.log("视频可能未成功播放,尝试替代方案"); this.tryAlternativeVideoPath(); } }, 1e3); } else { console.error("无法创建视频上下文"); this.tryAlternativeVideoPath(); } }); }, // 修改 tryAlternativeVideoPath 方法 tryAlternativeVideoPath() { console.log("尝试使用替代路径"); const alternativePaths = [ "./static/demo.mp4", "../static/demo.mp4", "static/demo.mp4", "/static/demo.mp4", // 添加绝对路径 `${window.location.origin}/static/demo.mp4` ]; const currentPathIndex = alternativePaths.indexOf(this.videoUrl); const nextPathIndex = (currentPathIndex + 1) % alternativePaths.length; this.videoUrl = alternativePaths[nextPathIndex]; console.log("尝试新路径:", this.videoUrl); this.$nextTick(() => { const videoContext = common_vendor.index.createVideoContext("myVideo", this); if (videoContext) { videoContext.stop(); videoContext.play(); setTimeout(() => { if (nextPathIndex === alternativePaths.length - 1 && !this.videoPlaying) { console.log("所有路径均失败,尝试使用uni.getVideoInfo检查视频"); this.checkVideoWithAPI(); } }, 1e3); } }); }, // 添加新方法:使用uni API检查视频 checkVideoWithAPI() { common_vendor.index.getVideoInfo({ src: "/static/demo.mp4", success: (res) => { console.log("视频信息获取成功:", res); this.videoUrl = "/static/demo.mp4"; this.$nextTick(() => { const videoContext = common_vendor.index.createVideoContext("myVideo", this); if (videoContext) { videoContext.play(); } }); }, fail: (err) => { console.error("视频信息获取失败:", err); this.fallbackToLocalVideo(); } }); }, // 添加新方法:回退到本地视频 fallbackToLocalVideo() { console.log("尝试使用本地视频资源"); const platform = common_vendor.index.getSystemInfoSync().platform; if (platform === "android" || platform === "ios") { this.videoUrl = platform === "android" ? "android.resource://package_name/raw/demo" : "file:///assets/demo.mp4"; this.$nextTick(() => { const videoContext = common_vendor.index.createVideoContext("myVideo", this); if (videoContext) { videoContext.play(); } }); } else { this.videoPlaying = false; common_vendor.index.showToast({ title: "视频加载失败,显示静态图片", icon: "none" }); } }, // 修改 handleVideoError 方法 handleVideoError(e) { console.error("视频加载错误:", e); if (e && e.detail) { console.error("详细错误信息:", e.detail); } common_vendor.index.getFileInfo({ filePath: this.videoUrl.startsWith("/") ? this.videoUrl.substring(1) : this.videoUrl, success: (res) => { console.log("文件存在,大小:", res.size); this.tryDifferentFormat(); }, fail: (err) => { console.error("文件不存在或无法访问:", err); this.tryAlternativeVideoPath(); } }); common_vendor.index.showToast({ title: "视频加载失败,请检查文件是否存在", icon: "none", duration: 2e3 }); }, // 添加新方法:尝试不同格式 tryDifferentFormat() { console.log("尝试不同的视频格式"); const formats = [ { ext: "mp4", mime: "video/mp4" }, { ext: "webm", mime: "video/webm" }, { ext: "ogg", mime: "video/ogg" }, { ext: "mov", mime: "video/quicktime" } ]; const currentPath = this.videoUrl; const basePath = currentPath.substring(0, currentPath.lastIndexOf(".")) || "/static/demo"; let nextFormat = formats.find((f) => !currentPath.endsWith(f.ext)); if (nextFormat) { this.videoUrl = `${basePath}.${nextFormat.ext}`; console.log("尝试新格式:", this.videoUrl); this.$nextTick(() => { const videoContext = common_vendor.index.createVideoContext("myVideo", this); if (videoContext) { videoContext.stop(); videoContext.play(); } }); } else { this.useBuiltInResource(); } }, // 添加新方法:使用内置资源 useBuiltInResource() { console.log("尝试使用内置资源"); const platform = common_vendor.index.getSystemInfoSync().platform; if (platform === "windows") { process.env.UNI_INPUT_DIR || ""; this.videoUrl = `./static/demo.mp4`; console.log("Windows平台尝试路径:", this.videoUrl); } else if (platform === "android" || platform === "ios") { this.useNativeVideo(); } else { const baseUrl = window.location.origin; this.videoUrl = `${baseUrl}/static/demo.mp4`; console.log("Web平台尝试URL:", this.videoUrl); } this.$nextTick(() => { const videoContext = common_vendor.index.createVideoContext("myVideo", this); if (videoContext) { videoContext.play(); } }); }, // 添加新方法:使用原生视频能力 useNativeVideo() { console.log("尝试使用原生视频能力"); common_vendor.index.chooseVideo({ sourceType: ["album"], success: (res) => { this.videoUrl = res.tempFilePath; this.$nextTick(() => { const videoContext = common_vendor.index.createVideoContext("myVideo", this); if (videoContext) { videoContext.play(); } }); }, fail: () => { this.videoPlaying = false; common_vendor.index.showToast({ title: "无法加载视频,显示静态图片", icon: "none" }); } }); }, // 处理视频结束事件 handleVideoEnded() { console.log("视频播放结束"); this.videoPlaying = false; if (this.currentVideoIndex >= 1) { this.showStartRecordingButton = true; } else { this.showAnswerButton = true; } }, // 添加新方法:开始录制用户回答 startRecordingAnswer() { console.log("开始录制用户回答"); this.isRecording = true; const systemInfo = common_vendor.index.getSystemInfoSync(); const isMiniProgram = systemInfo.uniPlatform && systemInfo.uniPlatform.startsWith("mp-"); if (isMiniProgram) { this.startMiniProgramRecording(); } else { this.startBrowserRecording(); } this.showStopRecordingButton = true; }, // 修改小程序环境下的录制方法 startMiniProgramRecording() { console.log("开始小程序录像,相机初始化状态:", this.cameraInitialized); if (!this.cameraInitialized) { common_vendor.index.showToast({ title: "相机正在初始化,请稍候", icon: "none" }); setTimeout(() => { this.startMiniProgramRecording(); }, 1e3); return; } if (!this.cameraContext) { this.cameraContext = common_vendor.index.createCameraContext(); console.log("创建相机上下文"); } const options = { timeout: 6e4, // 最长录制60秒 success: () => { console.log("小程序录像开始成功"); this.isRecording = true; }, fail: (err) => { console.error("小程序录像开始失败:", err); if (this.iosDevice && !this.hasRetriedIOS) { this.hasRetriedIOS = true; console.log("iOS设备录制失败,尝试使用备用方法"); setTimeout(() => { this.cameraContext = common_vendor.index.createCameraContext(); this.cameraContext.startRecord({ success: () => { console.log("iOS备用方法录像开始成功"); this.isRecording = true; }, fail: (retryErr) => { console.error("iOS备用方法录像失败:", retryErr); common_vendor.index.showToast({ title: "录制失败,请检查相机权限", icon: "none" }); this.proceedToNextQuestion(); } }); }, 500); return; } common_vendor.index.showToast({ title: "录制失败,请检查相机权限", icon: "none" }); this.proceedToNextQuestion(); } }; if (this.iosDevice) { delete options.timeout; console.log("使用iOS专用录制参数"); } else { options.quality = "medium"; options.compressed = true; options.timeoutCallback = () => { console.log("录制超时自动停止"); this.stopRecordingAnswer(); }; } console.log("调用startRecord,参数:", JSON.stringify(options)); this.recorder = this.cameraContext.startRecord(options); }, // 添加新方法:请求小程序权限 async checkMiniProgramPermissions() { return new Promise((resolve) => { common_vendor.index.getSetting({ success: (res) => { const hasRecordAuth = res.authSetting["scope.record"]; const hasCameraAuth = res.authSetting["scope.camera"]; const authPromises = []; if (!hasRecordAuth) { authPromises.push( new Promise((resolveAuth) => { common_vendor.index.authorize({ scope: "scope.record", success: () => { console.log("录音权限已获取"); resolveAuth(); }, fail: (err) => { console.error("录音权限获取失败:", err); this.showPermissionDialog("录音"); resolveAuth(); } }); }) ); } if (!hasCameraAuth) { authPromises.push( new Promise((resolveAuth) => { common_vendor.index.authorize({ scope: "scope.camera", success: () => { console.log("相机权限已获取"); resolveAuth(); }, fail: (err) => { console.error("相机权限获取失败:", err); this.showPermissionDialog("相机"); resolveAuth(); } }); }) ); } if (authPromises.length > 0) { Promise.all(authPromises).then(() => { resolve(); }); } else { resolve(); } }, fail: () => { resolve(); } }); }); }, // 修改浏览器环境下的录制方法 startBrowserRecording() { if (!this.cameraStream) { console.error("没有可用的摄像头流"); common_vendor.index.showToast({ title: "录制失败,摄像头未就绪", icon: "none" }); this.proceedToNextQuestion(); return; } try { const isIOS = /iPad|iPhone|iPod/.test(navigator.userAgent) && !window.MSStream; console.log("当前设备是否为iOS:", isIOS); const hasAudio = this.cameraStream.getAudioTracks().length > 0; if (!hasAudio) { console.warn("警告:媒体流中没有音频轨道,尝试重新获取带音频的媒体流"); navigator.mediaDevices.getUserMedia({ audio: { echoCancellation: true, noiseSuppression: true, autoGainControl: true }, video: true }).then((newStream) => { const audioTracks = newStream.getAudioTracks(); if (audioTracks.length > 0) { console.log("成功获取音频轨道:", audioTracks[0].label); const videoTrack = this.cameraStream.getVideoTracks()[0]; const audioTrack = newStream.getAudioTracks()[0]; const combinedStream = new MediaStream(); if (videoTrack) combinedStream.addTrack(videoTrack); if (audioTrack) combinedStream.addTrack(audioTrack); this.cameraStream = combinedStream; const videoElement = this.$refs.userCameraVideo; if (videoElement) { videoElement.srcObject = combinedStream; videoElement.muted = true; } this.setupMediaRecorder(combinedStream, isIOS); } else { console.warn("仍然无法获取音频轨道"); this.setupMediaRecorder(this.cameraStream, isIOS); } }).catch((err) => { console.error("获取音频失败:", err); this.setupMediaRecorder(this.cameraStream, isIOS); }); } else { console.log("检测到音频轨道,直接使用"); this.setupMediaRecorder(this.cameraStream, isIOS); } } catch (error) { console.error("浏览器录制失败:", error); common_vendor.index.showToast({ title: "录制失败,浏览器可能不支持此功能", icon: "none" }); this.proceedToNextQuestion(); } }, // 修改 setupMediaRecorder 方法,添加 isIOS 参数 setupMediaRecorder(stream, isIOS) { const videoTracks = stream.getVideoTracks(); const audioTracks = stream.getAudioTracks(); console.log("设置MediaRecorder - 视频轨道:", videoTracks.length, "音频轨道:", audioTracks.length); if (isIOS) { console.log("检测到iOS设备,使用替代录制方法"); this.setupIOSRecording(stream); return; } let mimeType = ""; const supportedTypes = [ "video/webm;codecs=vp9,opus", "video/webm;codecs=vp8,opus", "video/webm;codecs=h264,opus", "video/mp4;codecs=h264,aac", "video/webm", "video/mp4" ]; for (const type of supportedTypes) { if (MediaRecorder.isTypeSupported(type)) { mimeType = type; console.log("使用支持的MIME类型:", mimeType); break; } } const options = { mimeType: mimeType || "", audioBitsPerSecond: 64e3, // 降低音频比特率 videoBitsPerSecond: 1e6 // 降低视频比特率到1Mbps }; try { this.mediaRecorder = new MediaRecorder(stream, options); console.log("MediaRecorder创建成功,使用选项:", options); } catch (e) { console.warn("使用指定选项创建MediaRecorder失败,尝试使用默认选项"); this.mediaRecorder = new MediaRecorder(stream); } this.recordedChunks = []; this.mediaRecorder.ondataavailable = (event) => { if (event.data && event.data.size > 0) { this.recordedChunks.push(event.data); console.log(`收到数据块: ${event.data.size} 字节`); } }; this.mediaRecorder.onstop = async () => { console.log("MediaRecorder停止,数据块数量:", this.recordedChunks.length); if (this.recordedChunks.length === 0) { console.error("没有录制到数据"); common_vendor.index.showToast({ title: "录制失败,未捕获到数据", icon: "none" }); this.proceedToNextQuestion(); return; } const mimeType2 = this.mediaRecorder.mimeType || "video/webm"; const blob = new Blob(this.recordedChunks, { type: mimeType2 }); console.log("创建Blob,原始大小:", blob.size, "类型:", mimeType2); common_vendor.index.showLoading({ title: "正在处理视频...", mask: true }); try { const compressedBlob = await this.compressVideo(blob); const fileName = `answer_${this.currentVideoIndex}_${Date.now()}.webm`; const file = new File([compressedBlob], fileName, { type: mimeType2 }); common_vendor.index.hideLoading(); this.uploadRecordedVideo(file); } catch (error) { console.error("视频处理失败:", error); common_vendor.index.hideLoading(); const fileName = `answer_${this.currentVideoIndex}_${Date.now()}.webm`; const file = new File([blob], fileName, { type: mimeType2 }); this.uploadRecordedVideo(file); } }; this.mediaRecorder.onerror = (event) => { console.error("MediaRecorder错误:", event.error); }; try { this.mediaRecorder.start(1e3); console.log("MediaRecorder开始录制"); } catch (e) { console.error("开始录制失败:", e); } }, // 添加新方法:为iOS设备设置特殊录制方法 setupIOSRecording(stream) { console.log("设置iOS特殊录制方法"); const videoElement = this.$refs.userCameraVideo; const canvas = document.createElement("canvas"); const ctx = canvas.getContext("2d"); canvas.width = 640; canvas.height = 480; this.recordedFrames = []; this.recordingStartTime = Date.now(); this.frameCount = 0; const audioTracks = stream.getAudioTracks(); let audioRecorder = null; let audioChunks = []; if (audioTracks.length > 0) { const audioStream = new MediaStream([audioTracks[0]]); try { audioRecorder = new MediaRecorder(audioStream); audioRecorder.ondataavailable = (e) => { if (e.data.size > 0) { audioChunks.push(e.data); } }; audioRecorder.start(1e3); } catch (e) { console.warn("iOS音频录制器创建失败:", e); } } const captureFrame = () => { if (!this.isRecording) return; try { ctx.drawImage(videoElement, 0, 0, canvas.width, canvas.height); const imageData = canvas.toDataURL("image/jpeg", 0.8); this.recordedFrames.push({ image: imageData, timestamp: Date.now() - this.recordingStartTime }); this.frameCount++; if (this.frameCount % 15 === 0) { console.log(`已捕获 ${this.frameCount} 帧`); } this.frameCapturingId = requestAnimationFrame(captureFrame); } catch (e) { console.error("捕获视频帧失败:", e); } }; this.frameCapturingId = requestAnimationFrame(captureFrame); this.stopIOSRecording = async () => { console.log("停止iOS录制,已捕获帧数:", this.recordedFrames.length); if (this.frameCapturingId) { cancelAnimationFrame(this.frameCapturingId); this.frameCapturingId = null; } if (audioRecorder && audioRecorder.state !== "inactive") { audioRecorder.stop(); } common_vendor.index.showLoading({ title: "正在处理视频...", mask: true }); try { if (audioRecorder) { await new Promise((resolve) => { audioRecorder.onstop = resolve; if (audioRecorder.state === "inactive") resolve(); }); } const videoBlob = await this.createVideoFromFrames(this.recordedFrames, audioChunks); const fileName = `answer_${this.currentVideoIndex}_${Date.now()}.mp4`; const file = new File([videoBlob], fileName, { type: "video/mp4" }); common_vendor.index.hideLoading(); this.uploadRecordedVideo(file); } catch (error) { console.error("iOS视频处理失败:", error); common_vendor.index.hideLoading(); common_vendor.index.showToast({ title: "视频处理失败", icon: "none" }); this.proceedToNextQuestion(); } }; }, // 添加新方法:从帧创建视频 async createVideoFromFrames(frames, audioChunks) { console.log("从帧创建视频,帧数:", frames.length); const videoData = { frames: frames.slice(0, Math.min(frames.length, 300)), // 限制帧数以减小大小 audioBlob: audioChunks.length > 0 ? await new Blob(audioChunks, { type: "audio/webm" }).arrayBuffer() : null, metadata: { duration: frames.length > 0 ? frames[frames.length - 1].timestamp : 0, frameCount: frames.length, hasAudio: audioChunks.length > 0, deviceInfo: navigator.userAgent, timestamp: Date.now() } }; const jsonString = JSON.stringify(videoData); return new Blob([jsonString], { type: "application/json" }); }, // 添加新方法:停止录制用户回答 stopRecordingAnswer() { console.log("停止录制用户回答"); this.isRecording = false; if (this.recordingTimer) { clearTimeout(this.recordingTimer); this.recordingTimer = null; } common_vendor.index.hideLoading(); this.showStopRecordingButton = false; const isIOS = /iPad|iPhone|iPod/.test(navigator.userAgent) && !window.MSStream; const systemInfo = common_vendor.index.getSystemInfoSync(); const isMiniProgram = systemInfo.uniPlatform && systemInfo.uniPlatform.startsWith("mp-"); if (isMiniProgram) { this.stopMiniProgramRecording(); } else if (isIOS && this.stopIOSRecording) { this.stopIOSRecording(); } else { this.stopBrowserRecording(); } }, // 添加新方法:停止小程序录制 stopMiniProgramRecording() { console.log("停止小程序录像"); if (!this.cameraContext) { console.error("相机上下文不存在"); this.proceedToNextQuestion(); return; } common_vendor.index.showLoading({ title: "正在处理视频...", mask: true }); this.cameraContext.stopRecord({ success: (res) => { console.log("小程序录像停止成功:", res); common_vendor.index.hideLoading(); const tempFilePath = res.tempVideoPath || res.tempThumbPath || res.tempFilePath; if (tempFilePath) { console.log("获取到视频文件路径:", tempFilePath); this.uploadRecordedVideo(tempFilePath); } else { console.error("未获取到视频文件路径"); common_vendor.index.showToast({ title: "录制失败,未获取到视频文件", icon: "none" }); this.proceedToNextQuestion(); } }, fail: (err) => { common_vendor.index.hideLoading(); console.error("小程序录像停止失败:", err); common_vendor.index.showToast({ title: "录制失败", icon: "none" }); this.proceedToNextQuestion(); } }); }, // 添加新方法:停止浏览器录制 stopBrowserRecording() { if (this.mediaRecorder && this.mediaRecorder.state !== "inactive") { this.mediaRecorder.stop(); console.log("浏览器录制停止成功"); } else { console.error("MediaRecorder不存在或已经停止"); this.proceedToNextQuestion(); } }, // 修改上传录制的视频方法 uploadRecordedVideo(fileOrPath) { console.log("准备上传视频:", typeof fileOrPath === "string" ? fileOrPath : fileOrPath.name); const isIOSSpecialFormat = typeof fileOrPath !== "string" && fileOrPath.type === "application/json"; common_vendor.index.showLoading({ title: "正在上传...", mask: true }); const userInfo = common_vendor.index.getStorageSync("userInfo"); const openid = userInfo ? JSON.parse(userInfo).openid || "" : ""; const tenant_id = common_vendor.index.getStorageSync("tenant_id") || "1"; let questionId; switch (this.currentVideoIndex) { case 1: questionId = 10; break; case 2: questionId = 11; break; case 3: questionId = 12; break; case 4: questionId = 13; break; default: questionId = 10; } if (typeof fileOrPath !== "string") { const formData = new FormData(); formData.append("file", fileOrPath); formData.append("openid", openid); formData.append("tenant_id", tenant_id); formData.append("application_id", common_vendor.index.getStorageSync("appId")); formData.append("question_id", questionId); formData.append("video_duration", 0); formData.append("has_audio", "true"); if (isIOSSpecialFormat) { formData.append("is_ios_format", "true"); } const xhr = new XMLHttpRequest(); xhr.open("POST", `${common_config.apiBaseUrl}/api/system/upload/`, true); xhr.timeout = 12e4; xhr.onload = () => { common_vendor.index.hideLoading(); if (xhr.status === 200) { try { const res = JSON.parse(xhr.responseText); console.log("上传响应:", res); if (res.code === 2e3) { const videoUrl = res.data.url || res.data.photoUrl || ""; if (videoUrl) { this.submitVideoToInterview(videoUrl); } else { common_vendor.index.showToast({ title: "视频URL获取失败", icon: "none" }); } } else { common_vendor.index.showToast({ title: res.msg || "上传失败,请重试", icon: "none" }); } } catch (e) { console.error("解析响应失败:", e); common_vendor.index.showToast({ title: "解析响应失败", icon: "none" }); } } else { common_vendor.index.showToast({ title: "上传失败,HTTP状态: " + xhr.status, icon: "none" }); } }; xhr.onerror = () => { common_vendor.index.hideLoading(); console.error("上传网络错误"); common_vendor.index.showToast({ title: "网络错误,请重试", icon: "none" }); }; xhr.upload.onprogress = (event) => { if (event.lengthComputable) { const percentComplete = Math.round(event.loaded / event.total * 100); console.log("上传进度:", percentComplete + "%"); } }; xhr.send(formData); return; } common_vendor.index.uploadFile({ url: `${common_config.apiBaseUrl}/api/system/upload/`, filePath: fileOrPath, name: "file", formData: { openid, tenant_id, application_id: common_vendor.index.getStorageSync("appId"), question_id: questionId, video_duration: 0, has_audio: "true" // 明确标记视频包含音频 }, success: (uploadRes) => { try { const res = JSON.parse(uploadRes.data); console.log("上传响应:", res); if (res.code === 2e3) { const videoUrl = res.data.permanent_link || res.data.url || ""; if (videoUrl) { this.submitVideoToInterview(videoUrl); } else { common_vendor.index.showToast({ title: "视频URL获取失败", icon: "none" }); } } else { common_vendor.index.showToast({ title: res.msg || "上传失败,请重试", icon: "none" }); } } catch (e) { console.error("解析响应失败:", e); common_vendor.index.showToast({ title: "解析响应失败", icon: "none" }); } }, fail: (err) => { console.error("上传失败:", err); common_vendor.index.showToast({ title: "上传失败,请重试", icon: "none" }); }, complete: () => { common_vendor.index.hideLoading(); } }); }, // 修改 submitVideoToInterview 方法 submitVideoToInterview(videoUrl) { console.log("提交视频URL到面试接口:", videoUrl); common_vendor.index.showLoading({ title: "正在提交...", mask: true }); let questionId; switch (this.currentVideoIndex) { case 1: questionId = 10; break; case 2: questionId = 11; break; case 3: questionId = 12; break; case 4: questionId = 13; break; default: questionId = 10; } const requestData = { application_id: common_vendor.index.getStorageSync("appId"), question_id: questionId, video_url: videoUrl, tenant_id: common_vendor.index.getStorageSync("tenant_id") || "1" }; common_vendor.index.request({ url: `${common_config.apiBaseUrl}/api/job/upload_question_video`, method: "POST", data: requestData, // 使用data而不是formData header: { "content-type": "application/x-www-form-urlencoded" // 修改为form-data格式的content-type }, success: (res) => { console.log("面试接口提交成功:", res); common_vendor.index.hideLoading(); if (res.data.code === 0 || res.data.code === 2e3) { common_vendor.index.showToast({ title: "回答已提交", icon: "success" }); this.lastUploadedVideoUrl = videoUrl; this.showRetryButton = false; this.lastVideoToRetry = null; setTimeout(() => { if (this.currentVideoIndex === 4) { common_vendor.index.navigateTo({ url: "/pages/camera/camera" }); } else { this.proceedToNextQuestion(); } }, 1500); } else { common_vendor.index.showToast({ title: res.data.msg || "提交失败,请重试", icon: "none" }); this.lastVideoToRetry = videoUrl; this.showRetryButton = true; } }, fail: (err) => { console.error("面试接口提交失败:", err); common_vendor.index.hideLoading(); common_vendor.index.showToast({ title: "网络错误,请重试", icon: "none" }); this.lastVideoToRetry = videoUrl; this.showRetryButton = true; } }); }, // 修改 proceedToNextQuestion 方法 proceedToNextQuestion() { if (this.currentVideoIndex === 4) { common_vendor.index.navigateTo({ url: "/pages/camera/camera" }); return; } this.currentVideoIndex++; if (this.currentVideoIndex < this.videoList.length) { this.videoUrl = this.videoList[this.currentVideoIndex]; this.videoPlaying = true; this.currentSubtitle = ""; this.$nextTick(() => { const videoContext = common_vendor.index.createVideoContext("myVideo", this); if (videoContext) { videoContext.play(); } }); } else { common_vendor.index.showToast({ title: "面试完成", icon: "success", duration: 2e3 }); setTimeout(() => { common_vendor.index.navigateTo({ url: "/pages/interview-result/interview-result" }); }, 2e3); } }, // 修改 handleAnswerButtonClick 方法 handleAnswerButtonClick() { this.showAnswerButton = false; this.proceedToNextQuestion(); }, // 处理相机错误 handleCameraError(e) { console.error("相机错误:", e); common_vendor.index.showToast({ title: "相机初始化失败,请检查权限设置", icon: "none" }); this.tryFallbackOptions(); }, // 添加新方法:尝试备用选项 tryFallbackOptions() { const systemInfo = common_vendor.index.getSystemInfoSync(); if (systemInfo.uniPlatform === "mp-weixin" || systemInfo.uniPlatform === "mp-alipay") { this.useMiniProgramCamera(); } else { this.showStaticCameraPlaceholder(); } }, // 添加新方法:使用小程序相机API useMiniProgramCamera() { console.log("尝试使用小程序相机组件"); this.useMiniProgramCameraComponent = true; }, // 添加新方法:显示静态图像 showStaticCameraPlaceholder() { console.log("显示静态摄像头占位图"); const img = document.createElement("img"); img.src = "/static/images/camera-placeholder.png"; img.className = "static-camera-image"; img.style.width = "100%"; img.style.height = "100%"; img.style.objectFit = "cover"; const container = this.$refs.userCameraVideo.parentNode; container.appendChild(img); }, // 处理视频时间更新事件 handleTimeUpdate(e) { const currentTime = e.target.currentTime; let currentSubtitles; if (this.currentVideoIndex === 0) { currentSubtitles = this.subtitles; } else if (this.currentVideoIndex === 1) { currentSubtitles = this.secondVideoSubtitles; } else if (this.currentVideoIndex === 2) { currentSubtitles = this.thirdVideoSubtitles; } else if (this.currentVideoIndex === 3) { currentSubtitles = this.fourthVideoSubtitles; } else if (this.currentVideoIndex === 4) { currentSubtitles = this.fifthVideoSubtitles; } else if (this.currentVideoIndex === 5) { currentSubtitles = this.sixthVideoSubtitles; } else { currentSubtitles = []; } const subtitle = currentSubtitles.find( (sub) => currentTime >= sub.startTime && currentTime < sub.endTime ); this.currentSubtitle = subtitle ? subtitle.text : ""; }, // Add a new method to handle the "Start Recording" button click handleStartRecordingClick() { this.showStartRecordingButton = false; this.startRecordingAnswer(); }, // 修改 checkAudioPermission 方法,确保在录制前获取音频权限 checkAudioPermission() { const systemInfo = common_vendor.index.getSystemInfoSync(); const isMiniProgram = systemInfo.uniPlatform && systemInfo.uniPlatform.startsWith("mp-"); if (isMiniProgram) { common_vendor.index.getSetting({ success: (res) => { if (!res.authSetting["scope.record"]) { common_vendor.index.authorize({ scope: "scope.record", success: () => { console.log("录音权限已获取"); }, fail: (err) => { console.error("录音权限获取失败:", err); this.showPermissionDialog("录音"); } }); } if (!res.authSetting["scope.camera"]) { common_vendor.index.authorize({ scope: "scope.camera", success: () => { console.log("相机权限已获取"); }, fail: (err) => { console.error("相机权限获取失败:", err); this.showPermissionDialog("相机"); } }); } } }); } }, // 添加新方法:测试音频输入 testAudioInput() { if (!this.cameraStream) { console.warn("没有可用的媒体流,无法测试音频"); return; } const audioTracks = this.cameraStream.getAudioTracks(); if (audioTracks.length === 0) { console.warn("没有检测到音频轨道,尝试重新获取"); this.tryGetAudioOnly(); return; } console.log("音频轨道信息:", audioTracks[0].getSettings()); try { const AudioContext = window.AudioContext || window.webkitAudioContext; if (!AudioContext) { console.warn("浏览器不支持AudioContext"); return; } const audioContext = new AudioContext(); const analyser = audioContext.createAnalyser(); const microphone = audioContext.createMediaStreamSource(this.cameraStream); microphone.connect(analyser); analyser.fftSize = 256; const bufferLength = analyser.frequencyBinCount; const dataArray = new Uint8Array(bufferLength); let silenceCounter = 0; const checkAudio = () => { if (this.isRecording) return; analyser.getByteFrequencyData(dataArray); let sum = 0; for (let i = 0; i < bufferLength; i++) { sum += dataArray[i]; } const average = sum / bufferLength; if (average > 10) { console.log("检测到音频输入,音量:", average); silenceCounter = 0; } else { silenceCounter++; if (silenceCounter > 10) { console.warn("持续检测不到音频输入,可能麦克风未正常工作"); silenceCounter = 0; } } requestAnimationFrame(checkAudio); }; checkAudio(); } catch (e) { console.error("音频测试失败:", e); } }, // 添加新方法:尝试单独获取音频 tryGetAudioOnly() { navigator.mediaDevices.getUserMedia({ audio: true }).then((audioStream) => { if (this.cameraStream) { const videoTrack = this.cameraStream.getVideoTracks()[0]; const audioTrack = audioStream.getAudioTracks()[0]; const combinedStream = new MediaStream(); if (videoTrack) combinedStream.addTrack(videoTrack); if (audioTrack) combinedStream.addTrack(audioTrack); this.cameraStream = combinedStream; const videoElement = this.$refs.userCameraVideo; if (videoElement) { videoElement.srcObject = combinedStream; videoElement.muted = true; } console.log("成功合并音频和视频轨道"); } else { console.warn("没有视频流可合并"); } }).catch((err) => { console.error("单独获取音频失败:", err); }); }, // 添加新方法:显示权限对话框 showPermissionDialog(permissionType) { common_vendor.index.showModal({ title: "需要权限", content: `请允许使用${permissionType}权限,否则可能影响面试功能`, confirmText: "去设置", success: (res) => { if (res.confirm) { common_vendor.index.openSetting({ success: (settingRes) => { console.log("设置页面打开成功", settingRes); } }); } } }); }, // 添加重试上传方法 retryVideoUpload() { if (this.lastVideoToRetry) { this.showRetryButton = false; common_vendor.index.showLoading({ title: "正在重新提交...", mask: true }); this.submitVideoToInterview(this.lastVideoToRetry); } else { common_vendor.index.showToast({ title: "没有可重试的视频", icon: "none" }); } }, // 添加一个新方法用于压缩视频 async compressVideo(videoBlob) { if (videoBlob.size < 5 * 1024 * 1024) { return videoBlob; } console.log("开始压缩视频,原始大小:", videoBlob.size); const videoElement = document.createElement("video"); videoElement.muted = true; videoElement.autoplay = false; const canvas = document.createElement("canvas"); const ctx = canvas.getContext("2d"); videoElement.src = URL.createObjectURL(videoBlob); return new Promise((resolve) => { videoElement.onloadedmetadata = () => { const width = Math.floor(videoElement.videoWidth / 2); const height = Math.floor(videoElement.videoHeight / 2); canvas.width = width; canvas.height = height; const stream = canvas.captureStream(15); if (videoElement.captureStream) { const originalStream = videoElement.captureStream(); const audioTracks = originalStream.getAudioTracks(); if (audioTracks.length > 0) { stream.addTrack(audioTracks[0]); } } const options = { mimeType: "video/webm;codecs=vp8,opus", audioBitsPerSecond: 64e3, videoBitsPerSecond: 8e5 // 800kbps }; const mediaRecorder = new MediaRecorder(stream, options); const chunks = []; mediaRecorder.ondataavailable = (e) => { if (e.data.size > 0) { chunks.push(e.data); } }; mediaRecorder.onstop = () => { const compressedBlob = new Blob(chunks, { type: "video/webm" }); console.log("视频压缩完成,压缩后大小:", compressedBlob.size); resolve(compressedBlob); }; videoElement.onplay = () => { mediaRecorder.start(100); const drawFrame = () => { if (videoElement.paused || videoElement.ended) { mediaRecorder.stop(); return; } ctx.drawImage(videoElement, 0, 0, width, height); requestAnimationFrame(drawFrame); }; drawFrame(); }; videoElement.play(); }; }); }, // 添加相机初始化完成的处理方法 handleCameraInit() { console.log("相机初始化完成"); this.cameraInitialized = true; if (!this.cameraContext && this.useMiniProgramCameraComponent) { this.cameraContext = common_vendor.index.createCameraContext(); } } } }; function _sfc_render(_ctx, _cache, $props, $setup, $data, $options) { return common_vendor.e({ a: $data.videoUrl, b: common_vendor.o((...args) => $options.handleVideoError && $options.handleVideoError(...args)), c: common_vendor.o((...args) => $options.handleVideoEnded && $options.handleVideoEnded(...args)), d: common_vendor.o((...args) => $options.handleTimeUpdate && $options.handleTimeUpdate(...args)), e: $data.currentSubtitle }, $data.currentSubtitle ? { f: common_vendor.t($data.currentSubtitle) } : {}, { g: $data.showAnswerButton }, $data.showAnswerButton ? { h: common_vendor.o((...args) => $options.handleAnswerButtonClick && $options.handleAnswerButtonClick(...args)) } : {}, { i: $data.useMiniProgramCameraComponent }, $data.useMiniProgramCameraComponent ? { j: common_vendor.o((...args) => $options.handleCameraError && $options.handleCameraError(...args)), k: common_vendor.o((...args) => $options.handleCameraInit && $options.handleCameraInit(...args)) } : {}, { l: $data.loading }, $data.loading ? {} : {}, { m: $data.showDebugInfo }, $data.showDebugInfo ? common_vendor.e({ n: $data.assistantResponse }, $data.assistantResponse ? { o: common_vendor.t($data.assistantResponse) } : {}, { p: $data.audioTranscript }, $data.audioTranscript ? { q: common_vendor.t($data.audioTranscript) } : {}, { r: common_vendor.f($data.processedResponses, (item, index, i0) => { return common_vendor.e({ a: item.role }, item.role ? { b: common_vendor.t(item.role) } : {}, { c: item.transcript }, item.transcript ? { d: common_vendor.t(item.transcript) } : {}, { e: index }); }) }) : {}, { s: $data.showStopRecordingButton }, $data.showStopRecordingButton ? { t: common_vendor.o((...args) => $options.stopRecordingAnswer && $options.stopRecordingAnswer(...args)) } : {}, { v: $data.isRecording }, $data.isRecording ? {} : {}, { w: $data.showStartRecordingButton }, $data.showStartRecordingButton ? { x: common_vendor.o((...args) => $options.handleStartRecordingClick && $options.handleStartRecordingClick(...args)) } : {}, { y: $data.showRetryButton }, $data.showRetryButton ? { z: common_vendor.o((...args) => $options.retryVideoUpload && $options.retryVideoUpload(...args)) } : {}); } const MiniProgramPage = /* @__PURE__ */ common_vendor._export_sfc(_sfc_main, [["render", _sfc_render], ["__scopeId", "data-v-464e78c6"]]); wx.createPage(MiniProgramPage);