mirror of
https://github.com/xinnan-tech/xiaozhi-esp32-server.git
synced 2026-07-27 09:33:55 +08:00
add: 前后摄像头切换
This commit is contained in:
@@ -1400,6 +1400,20 @@ body {
|
|||||||
border-color: rgba(88, 101, 242, 0.5);
|
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 {
|
#cameraVideo {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
@@ -1407,4 +1421,17 @@ body {
|
|||||||
background: #1a1a1a;
|
background: #1a1a1a;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
transform: scaleX(-1);
|
transform: scaleX(-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.camera-switch {
|
||||||
|
display: none;
|
||||||
|
position: fixed;
|
||||||
|
bottom: 23px;
|
||||||
|
right: 20px;
|
||||||
|
z-index: 999;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.camera-switch.active {
|
||||||
|
display: block;
|
||||||
}
|
}
|
||||||
@@ -25,6 +25,7 @@ class App {
|
|||||||
this.audioPlayer = null;
|
this.audioPlayer = null;
|
||||||
this.live2dManager = null;
|
this.live2dManager = null;
|
||||||
this.cameraStream = null;
|
this.cameraStream = null;
|
||||||
|
this.currentFacingMode = 'user';
|
||||||
}
|
}
|
||||||
|
|
||||||
// 初始化应用
|
// 初始化应用
|
||||||
@@ -127,6 +128,8 @@ class App {
|
|||||||
async initCamera() {
|
async initCamera() {
|
||||||
const cameraContainer = document.getElementById('cameraContainer');
|
const cameraContainer = document.getElementById('cameraContainer');
|
||||||
const cameraVideo = document.getElementById('cameraVideo');
|
const cameraVideo = document.getElementById('cameraVideo');
|
||||||
|
const cameraSwitch = document.getElementById('cameraSwitch');
|
||||||
|
const dialBtn = document.getElementById('dialBtn');
|
||||||
|
|
||||||
if (!cameraContainer || !cameraVideo) {
|
if (!cameraContainer || !cameraVideo) {
|
||||||
log('摄像头元素未找到,跳过初始化', 'warning');
|
log('摄像头元素未找到,跳过初始化', 'warning');
|
||||||
@@ -188,11 +191,24 @@ class App {
|
|||||||
}
|
}
|
||||||
log('正在请求摄像头权限...', 'info');
|
log('正在请求摄像头权限...', 'info');
|
||||||
this.cameraStream = await navigator.mediaDevices.getUserMedia({
|
this.cameraStream = await navigator.mediaDevices.getUserMedia({
|
||||||
video: { width: 320, height: 240, facingMode: 'user' },
|
video: { width: 320, height: 240, facingMode: this.currentFacingMode },
|
||||||
audio: false
|
audio: false
|
||||||
});
|
});
|
||||||
cameraVideo.srcObject = this.cameraStream;
|
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');
|
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');
|
log('摄像头已启动', 'success');
|
||||||
return true;
|
return true;
|
||||||
} catch (error) {
|
} 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 = '描述一下看到的物品') => {
|
window.takePhoto = (question = '描述一下看到的物品') => {
|
||||||
return new Promise(async (resolve) => {
|
return new Promise(async (resolve) => {
|
||||||
const canvas = document.createElement('canvas');
|
const canvas = document.createElement('canvas');
|
||||||
|
|||||||
@@ -98,6 +98,14 @@ class UIController {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Camera switch button
|
||||||
|
const cameraSwitch = document.getElementById('cameraSwitch');
|
||||||
|
if (cameraSwitch) {
|
||||||
|
cameraSwitch.addEventListener('click', () => {
|
||||||
|
window.switchCamera();
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// Dial button
|
// Dial button
|
||||||
const dialBtn = document.getElementById('dialBtn');
|
const dialBtn = document.getElementById('dialBtn');
|
||||||
if (dialBtn) {
|
if (dialBtn) {
|
||||||
@@ -115,6 +123,7 @@ class UIController {
|
|||||||
if (isConnected) {
|
if (isConnected) {
|
||||||
wsHandler.disconnect();
|
wsHandler.disconnect();
|
||||||
this.updateDialButton(false);
|
this.updateDialButton(false);
|
||||||
|
if (cameraSwitch) cameraSwitch.classList.remove('active');
|
||||||
this.addChatMessage('Disconnected, see you next time~😊', false);
|
this.addChatMessage('Disconnected, see you next time~😊', false);
|
||||||
} else {
|
} else {
|
||||||
// Check if OTA URL is filled
|
// Check if OTA URL is filled
|
||||||
@@ -153,6 +162,7 @@ class UIController {
|
|||||||
if (isActive) {
|
if (isActive) {
|
||||||
// 关闭摄像头
|
// 关闭摄像头
|
||||||
if (typeof window.stopCamera === 'function') {
|
if (typeof window.stopCamera === 'function') {
|
||||||
|
if (cameraSwitch) cameraSwitch.classList.remove('active');
|
||||||
window.stopCamera();
|
window.stopCamera();
|
||||||
}
|
}
|
||||||
cameraContainer.classList.remove('active');
|
cameraContainer.classList.remove('active');
|
||||||
|
|||||||
@@ -44,6 +44,15 @@
|
|||||||
|
|
||||||
<!-- 主容器 -->
|
<!-- 主容器 -->
|
||||||
<div class="container">
|
<div class="container">
|
||||||
|
|
||||||
|
<!-- 摄像头切换按钮 -->
|
||||||
|
<div class="camera-switch" id="cameraSwitch">
|
||||||
|
<svg class="btn-icon" viewBox="0 0 24 24" fill="currentColor">
|
||||||
|
<path
|
||||||
|
d="M12,18A6,6 0 0,1 6,12C6,11 6.25,10.03 6.7,9.2L5.24,7.74C4.46,8.97 4,10.43 4,12A8,8 0 0,0 12,20V23L16,19L12,15V18M12,4V1L8,5L12,9V6A6,6 0 0,1 18,12C18,13 17.75,13.97 17.3,14.8L18.76,16.26C19.54,15.03 20,13.57 20,12A8,8 0 0,0 12,4Z" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- 摄像头显示区域(可拖动) -->
|
<!-- 摄像头显示区域(可拖动) -->
|
||||||
<div class="camera-container" id="cameraContainer">
|
<div class="camera-container" id="cameraContainer">
|
||||||
<video id="cameraVideo" autoplay playsinline muted></video>
|
<video id="cameraVideo" autoplay playsinline muted></video>
|
||||||
|
|||||||
Reference in New Issue
Block a user