diff --git a/main/xiaozhi-server/test/css/test_page.css b/main/xiaozhi-server/test/css/test_page.css index 1bfc3bd7..9935395a 100644 --- a/main/xiaozhi-server/test/css/test_page.css +++ b/main/xiaozhi-server/test/css/test_page.css @@ -1400,6 +1400,20 @@ body { border-color: rgba(88, 101, 242, 0.5); } +@keyframes flipWithPosition { + 0% { + transform: var(--original-transform) rotateY(0deg); + } + 100% { + transform: var(--original-transform) rotateY(180deg); + } +} + + +.camera-container.flip { + animation: flipWithPosition 0.5s ease-in-out 1 forwards; +} + #cameraVideo { width: 100%; height: 100%; @@ -1407,4 +1421,17 @@ body { background: #1a1a1a; pointer-events: none; transform: scaleX(-1); +} + +.camera-switch { + display: none; + position: fixed; + bottom: 23px; + right: 20px; + z-index: 999; + color: #fff; +} + +.camera-switch.active { + display: block; } \ No newline at end of file diff --git a/main/xiaozhi-server/test/js/app.js b/main/xiaozhi-server/test/js/app.js index bca7e3a7..91f60bf6 100644 --- a/main/xiaozhi-server/test/js/app.js +++ b/main/xiaozhi-server/test/js/app.js @@ -25,6 +25,7 @@ class App { this.audioPlayer = null; this.live2dManager = null; this.cameraStream = null; + this.currentFacingMode = 'user'; } // 初始化应用 @@ -127,6 +128,8 @@ class App { async initCamera() { const cameraContainer = document.getElementById('cameraContainer'); const cameraVideo = document.getElementById('cameraVideo'); + const cameraSwitch = document.getElementById('cameraSwitch'); + const dialBtn = document.getElementById('dialBtn'); if (!cameraContainer || !cameraVideo) { log('摄像头元素未找到,跳过初始化', 'warning'); @@ -188,11 +191,24 @@ class App { } log('正在请求摄像头权限...', 'info'); this.cameraStream = await navigator.mediaDevices.getUserMedia({ - video: { width: 320, height: 240, facingMode: 'user' }, + video: { width: 320, height: 240, facingMode: this.currentFacingMode }, audio: false }); cameraVideo.srcObject = this.cameraStream; + const devices = await navigator.mediaDevices.enumerateDevices(); + const videoDevices = devices.filter(device => device.kind === 'videoinput'); + if (videoDevices.length > 1) { + if (cameraSwitch) cameraSwitch.classList.add('active'); + } cameraContainer.classList.add('active'); + + // 切换时挂断情况 + const hasActive = dialBtn.classList.contains('dial-active'); + if (!hasActive) { + cameraContainer.classList.remove('active'); + cameraSwitch.classList.remove('active'); + window.stopCamera(); + } log('摄像头已启动', 'success'); return true; } catch (error) { @@ -217,6 +233,30 @@ class App { } }; + window.switchCamera = async() => { + if (window.switchCameraTimer) return; + if (this.cameraStream) { + const currentTransform = window.getComputedStyle(cameraContainer).transform; + const originalTransform = currentTransform === 'none' ? 'translate(0px, 0px)' : currentTransform; + cameraContainer.style.setProperty('--original-transform', originalTransform); + cameraContainer.classList.add('flip'); + this.currentFacingMode = this.currentFacingMode === 'user' ? 'environment' : 'user'; + window.stopCamera(); + window.startCamera(); + + window.switchCameraTimer = setTimeout(() => { + if (this.currentFacingMode === 'user') { + cameraVideo.style.transform = 'scaleX(-1)'; + } else { + cameraVideo.style.transform = 'scaleX(1)'; + } + window.switchCameraTimer = null; + cameraContainer.classList.remove('flip'); + cameraContainer.style.removeProperty('--original-transform'); + }, 500); + } + }; + window.takePhoto = (question = '描述一下看到的物品') => { return new Promise(async (resolve) => { const canvas = document.createElement('canvas'); diff --git a/main/xiaozhi-server/test/js/ui/controller.js b/main/xiaozhi-server/test/js/ui/controller.js index bc23f298..947783a3 100644 --- a/main/xiaozhi-server/test/js/ui/controller.js +++ b/main/xiaozhi-server/test/js/ui/controller.js @@ -98,6 +98,14 @@ class UIController { }); } + // Camera switch button + const cameraSwitch = document.getElementById('cameraSwitch'); + if (cameraSwitch) { + cameraSwitch.addEventListener('click', () => { + window.switchCamera(); + }) + } + // Dial button const dialBtn = document.getElementById('dialBtn'); if (dialBtn) { @@ -115,6 +123,7 @@ class UIController { if (isConnected) { wsHandler.disconnect(); this.updateDialButton(false); + if (cameraSwitch) cameraSwitch.classList.remove('active'); this.addChatMessage('Disconnected, see you next time~😊', false); } else { // Check if OTA URL is filled @@ -153,6 +162,7 @@ class UIController { if (isActive) { // 关闭摄像头 if (typeof window.stopCamera === 'function') { + if (cameraSwitch) cameraSwitch.classList.remove('active'); window.stopCamera(); } cameraContainer.classList.remove('active'); diff --git a/main/xiaozhi-server/test/test_page.html b/main/xiaozhi-server/test/test_page.html index 22d4b0f6..7f85a127 100644 --- a/main/xiaozhi-server/test/test_page.html +++ b/main/xiaozhi-server/test/test_page.html @@ -44,6 +44,15 @@