WebRtc_QingGan/src/index.html
Song367 c080194bac
All checks were successful
Gitea Actions Demo / Explore-Gitea-Actions (push) Successful in 2m40s
html
2025-08-06 01:24:57 +08:00

511 lines
17 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
<title>Soulmate In Parallels - 壹和零人工智能</title>
<link rel="stylesheet" href="styles.css">
<link rel="icon" type="image/png" sizes="48x48" href="favicon.png" />
<style>
/* 全屏视频样式 */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
html, body {
height: 100%;
overflow: hidden;
background: linear-gradient(135deg, #87CEEB 0%, #B0E0E6 100%); /* 浅蓝色渐变背景 */
}
.container {
width: 100vw;
height: 100vh;
margin: 0;
padding: 0;
display: flex;
flex-direction: column;
position: relative;
}
.main-content {
flex: 1;
background: transparent;
border-radius: 0;
padding: 0;
box-shadow: none;
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
}
.recorded-video-section {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
width: 100%;
height: 100%;
position: relative;
/* 确保视频区域固定高度并居中 */
min-height: 100vh;
max-height: 100vh;
}
/* 视频容器样式 - 支持双缓冲固定9:16比例 */
.video-container {
position: relative;
width: 56.25vh; /* 9:16比例与视频宽度保持一致 */
height: 100vh;
overflow: hidden;
display: flex;
align-items: center;
justify-content: center;
margin: 0 auto; /* 水平居中 */
}
#recordedVideo, #recordedVideoBuffer {
position: absolute;
width: 56.25vh; /* 9:16比例高度为100vh时宽度为100vh * 9/16 = 56.25vh */
height: 100vh;
object-fit: cover;
border-radius: 0;
box-shadow: none;
transition: opacity 0.5s ease-in-out;
/* 确保视频始终居中 */
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
}
/* 主视频默认显示 */
#recordedVideo {
opacity: 1;
z-index: 2;
}
/* 缓冲视频默认隐藏 */
#recordedVideoBuffer {
opacity: 0;
z-index: 1;
}
/* 切换状态 */
#recordedVideo.switching {
opacity: 0;
}
#recordedVideoBuffer.switching {
opacity: 1;
}
/* 加载状态 */
.video-loading {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
z-index: 10;
color: white;
font-size: 18px;
opacity: 0;
transition: opacity 0.3s ease;
}
.video-loading.show {
opacity: 1;
}
/* 加载动画 */
.loading-spinner {
width: 40px;
height: 40px;
border: 3px solid rgba(255, 255, 255, 0.3);
border-top: 3px solid white;
border-radius: 50%;
animation: spin 1s linear infinite;
margin: 0 auto 10px;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
/* 响应式设计 - 确保在不同屏幕尺寸下视频容器保持9:16比例 */
@media (max-width: 768px) {
.video-container {
height: 100vh;
width: 56.25vh; /* 9:16比例与视频宽度保持一致 */
}
#recordedVideo, #recordedVideoBuffer {
width: 56.25vh; /* 9:16比例 */
height: 100vh;
object-fit: cover;
}
}
@media (min-width: 769px) {
.video-container {
height: 100vh;
width: 56.25vh; /* 9:16比例与视频宽度保持一致 */
}
#recordedVideo, #recordedVideoBuffer {
width: 56.25vh; /* 9:16比例 */
height: 100vh;
object-fit: cover;
}
}
/* 横屏模式优化 */
@media (orientation: landscape) and (max-height: 500px) {
.video-container {
height: 100vh;
width: 56.25vh; /* 9:16比例与视频宽度保持一致 */
}
.controls {
bottom: 20px;
}
}
/* 竖屏模式优化 */
@media (orientation: portrait) {
.video-container {
height: 100vh;
width: 56.25vh; /* 9:16比例与视频宽度保持一致 */
}
}
.controls {
position: absolute;
bottom: 50px;
left: 50%;
transform: translateX(-50%);
z-index: 10;
display: flex !important;
flex-direction: row !important;
justify-content: center;
align-items: center;
gap: 20px;
}
/* 确保移动端也保持同一行 */
@media (max-width: 768px) {
.controls {
flex-direction: row !important;
gap: 15px;
}
}
#startButton {
width: 60px;
height: 60px;
border-radius: 50%;
background: rgba(34, 197, 94, 0.9);
backdrop-filter: blur(10px);
border: none;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.3s ease;
box-shadow: 0 4px 15px rgba(34, 197, 94, 0.3);
min-width: auto;
padding: 15px 30px;
font-size: 1.1rem;
border-radius: 25px;
min-width: 200px;
}
#startButton:hover:not(:disabled) {
background: rgba(22, 163, 74, 0.95);
transform: scale(1.1);
box-shadow: 0 6px 20px rgba(34, 197, 94, 0.5);
}
#startButton.connecting {
background: rgba(255, 193, 7, 0.9);
cursor: not-allowed;
}
#startButton.connecting:hover {
background: rgba(255, 193, 7, 0.9);
transform: none;
}
#startButton.calling {
background: rgba(255, 193, 7, 0.9);
animation: pulse 2s infinite;
}
#startButton.calling:hover {
background: rgba(255, 193, 7, 0.95);
transform: scale(1.05);
}
@keyframes pulse {
0% {
box-shadow: 0 4px 15px rgba(255, 193, 7, 0.3);
}
50% {
box-shadow: 0 6px 25px rgba(255, 193, 7, 0.6);
}
100% {
box-shadow: 0 4px 15px rgba(255, 193, 7, 0.3);
}
}
.audio-status {
position: absolute;
top: 20px;
left: 50%;
transform: translateX(-50%);
background: rgba(0, 0, 0, 0.7);
color: white;
padding: 8px 16px;
border-radius: 20px;
font-size: 14px;
z-index: 1000;
transition: all 0.3s ease;
}
.audio-status.connecting {
background: rgba(255, 193, 7, 0.9);
color: #000;
}
.audio-status.connected {
background: rgba(40, 167, 69, 0.9);
color: white;
}
.audio-status.error {
background: rgba(220, 53, 69, 0.9);
color: white;
}
#startButton svg {
width: 24px;
height: 24px;
fill: white;
}
#startButton:disabled {
opacity: 0.5;
cursor: not-allowed;
}
#stopButton {
width: 60px;
height: 60px;
border-radius: 50%;
background: rgba(220, 53, 69, 0.9);
backdrop-filter: blur(10px);
border: none;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.3s ease;
box-shadow: 0 4px 15px rgba(220, 53, 69, 0.3);
padding: 0; /* 确保没有内边距影响居中 */
}
#stopButton:hover:not(:disabled) {
background: rgba(200, 35, 51, 0.95);
transform: scale(1.1);
}
#stopButton svg {
width: 24px;
height: 24px;
display: block; /* 确保SVG作为块级元素 */
margin: auto; /* 额外的居中保证 */
}
#stopButton:disabled {
opacity: 0.5;
cursor: not-allowed;
}
/* 头像样式 - 确保显示 */
.avatar-container {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
z-index: 15; /* 提高z-index确保在视频上方 */
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
transition: opacity 0.3s ease;
opacity: 1; /* 确保默认显示 */
}
.avatar-container.hidden {
opacity: 0;
pointer-events: none;
}
.avatar {
width: 120px;
height: 120px;
border-radius: 50%;
border: 4px solid rgba(255, 255, 255, 0.8);
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.2);
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
display: flex;
align-items: center;
justify-content: center;
color: white;
font-size: 48px;
font-weight: bold;
overflow: hidden; /* 确保图片不会溢出 */
}
.avatar img {
width: 100%;
height: 100%;
border-radius: 50%;
object-fit: cover;
display: block; /* 确保图片显示 */
}
/* 确保视频默认隐藏 */
#recordedVideo, #recordedVideoBuffer {
position: absolute;
width: 56.25vh;
height: 100vh;
object-fit: cover;
border-radius: 0;
box-shadow: none;
transition: opacity 0.5s ease-in-out;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
opacity: 0; /* 默认隐藏视频 */
z-index: 1; /* 确保在头像下方 */
}
/* 通话时隐藏头像,显示视频 */
.video-container.calling .avatar-container {
opacity: 0;
pointer-events: none;
}
.video-container.calling #recordedVideo {
opacity: 1;
z-index: 10;
}
</style>
</head>
<body>
<div class="container">
<!-- 隐藏的header -->
<header style="display: none;">
<h1>WebRTC 音频通话</h1>
<p>实时播放录制视频,支持文本和语音输入</p>
</header>
<div class="main-content">
<!-- 音频状态显示 - 完全隐藏 -->
<div class="audio-status" style="display: none;">
<div class="status-indicator">
<span id="audioStatus" style="display: none;">未连接</span>
</div>
</div>
<!-- 录制视频播放区域 - 全屏显示 -->
<div class="recorded-video-section">
<div class="video-container" id="videoContainer">
<!-- 头像容器 -->
<div class="avatar-container" id="avatarContainer">
<div class="avatar" id="avatar">
<!-- 使用相对路径引用图片 -->
<img src="./tx.png" alt="头像" onerror="this.style.display='none'; this.parentElement.innerHTML='AI';">
</div>
<!-- <div class="avatar-name">AI助手</div> -->
</div>
<!-- 主视频元素 -->
<video id="recordedVideo" autoplay muted>
<source src="" type="video/mp4">
您的浏览器不支持视频播放
</video>
<!-- 缓冲视频元素 -->
<video id="recordedVideoBuffer" autoplay muted>
<source src="" type="video/mp4">
您的浏览器不支持视频播放
</video>
<!-- 加载指示器 -->
<div class="video-loading" id="videoLoading">
<div class="loading-spinner"></div>
<div>正在切换视频...</div>
</div>
</div>
<div class="video-info" style="display: none;">
<span id="currentVideoName">未选择视频</span>
</div>
</div>
<!-- 控制按钮 - 悬浮在视频上方 -->
<div class="controls">
<button id="startButton" class="btn btn-primary" title="开始通话">
<!-- 默认通话图标 -->
<svg id="callIcon" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M6.62 10.79c1.44 2.83 3.76 5.14 6.59 6.59l2.2-2.2c.27-.27.67-.36 1.02-.24 1.12.37 2.33.57 3.57.57.55 0 1 .45 1 1V20c0 .55-.45 1-1 1-9.39 0-17-7.61-17-17 0-.55.45-1 1-1h3.5c.55 0 1 .45 1 1 0 1.25.2 2.45.57 3.57.11.35.03.74-.25 1.02l-2.2 2.2z" fill="white"/>
</svg>
<!-- 通话中文字显示(初始隐藏) -->
<span id="callingText" style="display: none; color: white; font-size: 14px;">正在通话中</span>
</button>
<button id="stopButton" class="btn btn-danger" disabled title="结束通话" style="display: none;">
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M19.23 15.26l-2.54-.29c-.61-.07-1.21.14-1.64.57l-1.84 1.84c-2.83-1.44-5.15-3.75-6.59-6.59l1.85-1.85c.43-.43.64-1.03.57-1.64l-.29-2.52c-.12-1.01-.97-1.77-1.99-1.77H5.03c-1.13 0-2.07.94-2 2.07.53 8.54 7.36 15.36 15.89 15.89 1.13.07 2.07-.87 2.07-2v-1.73c.01-1.01-.75-1.86-1.76-1.98z" fill="white"/>
<line x1="18" y1="6" x2="6" y2="18" stroke="white" stroke-width="2"/>
</svg>
</button>
</div>
<!-- 隐藏的输入区域 -->
<div class="input-section" style="display: none;">
<div class="text-input-group">
<input type="text" id="textInput" placeholder="输入文本内容..." />
<button id="sendTextButton" class="btn btn-primary">发送文本</button>
</div>
</div>
<!-- 隐藏的视频选择 -->
<div class="video-selection" style="display: none;">
<h3>选择要播放的视频</h3>
<div id="videoList" class="video-list">
<!-- 视频列表将在这里动态生成 -->
</div>
</div>
<!-- 隐藏的状态显示 -->
<div class="status-section" style="display: none;">
<div id="connectionStatus" class="status" style="display: none;">未连接</div>
<div id="messageLog" class="message-log"></div>
</div>
</div>
</div>
<!-- 隐藏的视频元素用于WebRTC连接 -->
<video id="localVideo" autoplay muted playsinline style="display: none;"></video>
<video id="remoteVideo" autoplay playsinline style="display: none;"></video>
<script src="/socket.io/socket.io.js"></script>
<script type="module" src="./index.js"></script>
</body>
</html>