【m】删除无用代码
This commit is contained in:
@@ -1,77 +1,731 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
|
||||
<head>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
|
||||
<link rel="icon" href="images/favicon.ico" type="image/x-icon">
|
||||
<link rel="stylesheet" href="css/main.css" />
|
||||
<title>Unity Render Streaming Samples</title>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>VideoCall - 一对一视频通话</title>
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" rel="stylesheet">
|
||||
<link rel="stylesheet" href="css/style.css">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="container">
|
||||
<h1>Unity Render Streaming Samples</h1>
|
||||
<body class="h-screen w-screen flex flex-col text-white bg-grid relative">
|
||||
<!--
|
||||
============================================================
|
||||
connect视图:初始连接界面(输入连接ID、创建/加入通话)
|
||||
WebSocket在此视图建立连接
|
||||
============================================================
|
||||
-->
|
||||
<div id="connectView" class="h-full w-full flex flex-col">
|
||||
<!-- 用户设置区域 -->
|
||||
<div class="absolute top-4 right-4 z-10">
|
||||
<button id="userSettingsBtn" class="flex items-center gap-2 glass px-3 py-2 rounded-full hover:bg-white/10 transition-colors">
|
||||
<img id="userAvatar" src="/images/p1.png" class="w-8 h-8 rounded-full object-cover">
|
||||
<span id="userName" class="text-sm font-medium">我</span>
|
||||
<i class="fas fa-chevron-down text-xs text-gray-400"></i>
|
||||
</button>
|
||||
|
||||
<section>
|
||||
<p>These are WebClient samples for use with <a href="https://docs.unity3d.com/Packages/com.unity.renderstreaming@latest/index.html">Unity Render
|
||||
Streaming</a>.</p>
|
||||
</section>
|
||||
<!-- 设置菜单 -->
|
||||
<div id="settingsMenu" class="hidden absolute top-full right-0 mt-2 glass rounded-xl shadow-lg w-48 z-20">
|
||||
<div class="p-4 border-b border-white/10">
|
||||
<h3 class="text-sm font-medium mb-2">个人设置</h3>
|
||||
<div class="space-y-3">
|
||||
<div>
|
||||
<label class="block text-xs text-gray-400 mb-1">昵称</label>
|
||||
<input type="text" id="nicknameInput" class="w-full bg-transparent border border-white/20 rounded-lg px-3 py-2 text-sm text-white placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-indigo-500" placeholder="输入昵称">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-xs text-gray-400 mb-1">头像</label>
|
||||
<div class="flex items-center gap-3">
|
||||
<img id="avatarPreview" src="/images/p1.png" class="w-10 h-10 rounded-full object-cover">
|
||||
<input type="file" id="avatarInput" accept="image/*" class="hidden" onchange="handleAvatarUpload(event)">
|
||||
<button onclick="document.getElementById('avatarInput').click()" class="text-xs text-indigo-400 hover:text-indigo-300 transition-colors">更换头像</button>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-xs text-gray-400 mb-1">用户ID</label>
|
||||
<div class="flex items-center gap-2">
|
||||
<input type="text" id="userIdInput" class="w-full bg-transparent border border-white/20 rounded-lg px-3 py-2 text-sm text-white placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-indigo-500" readonly>
|
||||
<button onclick="copyUserId()" class="text-xs text-indigo-400 hover:text-indigo-300 transition-colors">复制</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="p-2">
|
||||
<button onclick="saveSettings()" class="w-full px-4 py-2 text-sm text-white bg-indigo-600 hover:bg-indigo-700 rounded-lg transition-colors">保存设置</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<section>
|
||||
<h2>Server Configuration</h2>
|
||||
<div id="startup"></div>
|
||||
</section>
|
||||
<!-- 连接表单 -->
|
||||
<div class="h-full w-full flex items-center justify-center bg-black/90">
|
||||
<div class="text-center max-w-md px-8">
|
||||
<div class="w-24 h-24 bg-gradient-to-br from-indigo-500 to-purple-600 rounded-full flex items-center justify-center mx-auto mb-8 shadow-lg">
|
||||
<i class="fas fa-video text-white text-4xl"></i>
|
||||
</div>
|
||||
<h1 class="text-3xl font-bold text-white mb-2">VideoCall</h1>
|
||||
<p class="text-gray-400 mb-8">一对一视频通话</p>
|
||||
|
||||
<section id="iceServers">
|
||||
<h2>ICE servers</h2>
|
||||
<select id="servers" size="4">
|
||||
</select>
|
||||
<div>
|
||||
<label for="url">STUN or TURN URI:</label>
|
||||
<input id="url">
|
||||
</div>
|
||||
<div>
|
||||
<label for="username">TURN username:</label>
|
||||
<input id="username">
|
||||
</div>
|
||||
<div>
|
||||
<label for="password">TURN password:</label>
|
||||
<input id="password">
|
||||
</div>
|
||||
<div>
|
||||
<button id="add">Add Server</button>
|
||||
<button id="remove">Remove Server</button>
|
||||
<button id="reset">Reset to defaults</button>
|
||||
</div>
|
||||
</section>
|
||||
<div class="space-y-4 mb-8">
|
||||
<div class="glass rounded-xl p-4">
|
||||
<label class="block text-sm font-medium text-gray-300 mb-2">连接ID</label>
|
||||
<input type="text"
|
||||
id="connectionIdInput"
|
||||
placeholder="输入连接ID"
|
||||
class="w-full bg-transparent border border-white/20 rounded-lg px-4 py-3 text-white placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-indigo-500"
|
||||
autocomplete="off">
|
||||
</div>
|
||||
<p class="text-xs text-gray-500">
|
||||
连接ID是用于建立点对点通话的唯一标识,由发起方生成并分享给接收方。
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<section>
|
||||
<h2 id="receiver"><a href="receiver/index.html">Receiver Sample</a></h2>
|
||||
<p>This is a sample for receiving video / audio from Unity.</p>
|
||||
<p>It can be used in combination with the <code>Broadcast</code> scene of Unity Render Streaming.</p>
|
||||
</section>
|
||||
<div class="flex flex-col sm:flex-row gap-4 justify-center mb-6">
|
||||
<button id="connectBtn" class="flex-1 px-6 py-3 bg-indigo-600 hover:bg-indigo-700 rounded-xl transition-colors flex items-center justify-center gap-2">
|
||||
<i class="fas fa-phone"></i>
|
||||
<span>加入通话</span>
|
||||
</button>
|
||||
<button id="createCallBtn" class="flex-1 px-6 py-3 glass hover:bg-white/10 rounded-xl transition-colors flex items-center justify-center gap-2">
|
||||
<i class="fas fa-plus"></i>
|
||||
<span>创建通话</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<section>
|
||||
<h2 id="bidirectional"><a href="bidirectional/index.html">Bidirectional Sample</a></h2>
|
||||
<p>This is a sample for sending and receiving video in both directions.</p>
|
||||
<p>It can be used in combination with the <code>Bidirectional</code> scene of Unity Render Streaming.</p>
|
||||
<p>The WebApp must be running in Private mode.</p>
|
||||
</section>
|
||||
<!-- 浏览全部ID按钮 -->
|
||||
<button id="browseIdsBtn" class="w-full px-6 py-3 glass hover:bg-white/10 rounded-xl transition-colors flex items-center justify-center gap-2 mb-4">
|
||||
<i class="fas fa-list"></i>
|
||||
<span>浏览全部ID</span>
|
||||
</button>
|
||||
|
||||
<section>
|
||||
<h2 id="multiplay"><a href="multiplay/index.html">Multiplay Sample</a></h2>
|
||||
<p>This sample connects as a Guest in the <code>Multiplay</code> scene of Unity Render Streaming.</p>
|
||||
</section>
|
||||
<!-- 连接ID列表 -->
|
||||
<div id="connectionIdsList" class="glass rounded-xl p-4 mb-6 hidden">
|
||||
<h3 class="text-sm font-medium text-gray-300 mb-2">可用的连接ID</h3>
|
||||
<div id="idsContainer" class="max-h-40 overflow-y-auto space-y-2">
|
||||
<!-- 连接ID将在这里动态生成 -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<section>
|
||||
<h2 id="videoplayer"><a href="videoplayer/index.html">VideoPlayer Sample</a></h2>
|
||||
<p>This is a sample to receive the camera image rendered on Unity. You can operate the camera in Unity from the
|
||||
browser.</p>
|
||||
<p>It can be used in combination with the <code>WebBrowserInput</code> scene of Unity Render Streaming.</p>
|
||||
<p>The WebApp must be running in Public mode.</p>
|
||||
</section>
|
||||
<div id="onlineUsersList" class="glass rounded-xl p-4 mb-6 hidden">
|
||||
<div class="flex items-center justify-between mb-2">
|
||||
<h3 class="text-sm font-medium text-gray-300">全部WebSocket用户</h3>
|
||||
<span id="onlineUsersSummary" class="text-xs text-gray-500">0 个用户在线</span>
|
||||
</div>
|
||||
<div id="usersContainer" class="max-h-56 overflow-y-auto space-y-3">
|
||||
<!-- 在线用户将在这里动态生成 -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- WebSocket连接状态指示 -->
|
||||
<div id="wsStatus" class="mt-4 flex items-center justify-center gap-2 text-xs text-gray-500">
|
||||
<span id="wsStatusDot" class="w-2 h-2 bg-gray-500 rounded-full"></span>
|
||||
<span id="wsStatusText">未连接</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!--
|
||||
============================================================
|
||||
call视图:视频通话界面(创建/加入房间后显示)
|
||||
============================================================
|
||||
-->
|
||||
<div id="callView" class="hidden h-full w-full flex flex-col">
|
||||
|
||||
<!--
|
||||
============================================================
|
||||
数据模型定义 (Data Models)
|
||||
============================================================
|
||||
|
||||
1. CallSession 通话会话
|
||||
interface CallSession {
|
||||
id: string; // 通话唯一ID [PRIMARY_KEY]
|
||||
type: 'video' | 'audio'; // 通话类型
|
||||
status: 'connecting' | 'ongoing' | 'ended' | 'failed';
|
||||
startTime: string; // ISO 8601 格式
|
||||
duration: number; // 已进行秒数
|
||||
isEncrypted: boolean; // 是否端到端加密
|
||||
localUser: LocalUser; // 本地用户信息
|
||||
remoteUser: RemoteUser; // 远端用户信息
|
||||
}
|
||||
|
||||
2. LocalUser 本地用户
|
||||
interface LocalUser {
|
||||
id: string; // 用户ID
|
||||
name: string; // 显示名称
|
||||
avatar: string; // 头像URL
|
||||
isHost: boolean; // 是否主持人
|
||||
mediaState: MediaState; // 媒体状态
|
||||
}
|
||||
|
||||
3. RemoteUser 远端用户
|
||||
interface RemoteUser {
|
||||
id: string; // 用户ID
|
||||
name: string; // 显示名称
|
||||
avatar: string; // 头像URL
|
||||
status: 'online' | 'offline' | 'connecting';
|
||||
mediaState: MediaState; // 媒体状态
|
||||
networkQuality: 'excellent' | 'good' | 'fair' | 'poor'; // 网络质量
|
||||
}
|
||||
|
||||
4. MediaState 媒体状态
|
||||
interface MediaState {
|
||||
audio: boolean; // 音频是否开启
|
||||
video: boolean; // 视频是否开启
|
||||
screenShare: boolean; // 是否屏幕共享
|
||||
isSpeaking: boolean; // 是否正在说话(VAD检测)
|
||||
}
|
||||
|
||||
5. ChatMessage 聊天消息
|
||||
interface ChatMessage {
|
||||
id: string; // 消息ID
|
||||
senderId: string; // 发送者ID
|
||||
senderName: string; // 发送者名称
|
||||
senderAvatar: string; // 发送者头像
|
||||
content: string; // 消息内容
|
||||
type: 'text' | 'file' | 'system';
|
||||
timestamp: string; // ISO 8601 格式
|
||||
isSelf: boolean; // 是否自己发送
|
||||
}
|
||||
|
||||
============================================================
|
||||
API 接口定义 (API Endpoints)
|
||||
============================================================
|
||||
|
||||
[GET] /api/call/:callId // 获取通话信息
|
||||
[POST] /api/call/:callId/join // 加入通话
|
||||
[POST] /api/call/:callId/leave // 离开通话
|
||||
[POST] /api/call/:callId/media // 更新媒体状态 {audio?: boolean, video?: boolean}
|
||||
[GET] /api/call/:callId/messages?limit=50&before=timestamp // 获取历史消息
|
||||
[POST] /api/call/:callId/message // 发送消息 {content: string, type: 'text'}
|
||||
|
||||
WebSocket Events:
|
||||
- connect: 连接建立
|
||||
- disconnect: 连接断开
|
||||
- user-joined: {userId, timestamp}
|
||||
- user-left: {userId, timestamp}
|
||||
- media-state-changed: {userId, audio, video, screenShare, isSpeaking}
|
||||
- message-received: {message: ChatMessage}
|
||||
- network-quality: {userId, quality: 'excellent' | 'good' | 'fair' | 'poor'}
|
||||
- call-ended: {reason: 'user_hangup' | 'network_error' | 'timeout'}
|
||||
-->
|
||||
|
||||
<!--
|
||||
============================================================
|
||||
区域: 顶部栏 (Header)
|
||||
数据源: CallSession
|
||||
更新频率: 实时 (WebSocket + 本地计时器)
|
||||
============================================================
|
||||
-->
|
||||
<header class="glass-strong h-16 flex items-center justify-between px-6 z-50 border-b border-white/10">
|
||||
<div class="flex items-center gap-3">
|
||||
<div
|
||||
class="w-10 h-10 bg-gradient-to-br from-indigo-500 to-purple-600 rounded-xl flex items-center justify-center shadow-lg">
|
||||
<i class="fas fa-video text-white text-lg"></i>
|
||||
</div>
|
||||
<div>
|
||||
<!-- [DATA_FIELD: callSession.remoteUser.name] [TYPE: string] [REQUIRED] -->
|
||||
<h1 class="font-bold text-lg tracking-tight" data-field="remoteUser.name" id="headerTitle">
|
||||
与 Sarah 的通话
|
||||
</h1>
|
||||
<div class="flex items-center gap-3 text-xs text-gray-400">
|
||||
<span id="remoteNetworkIndicator" class="w-2 h-2 bg-green-500 rounded-full animate-pulse"></span>
|
||||
<span id="remoteNetworkQuality" class="flex items-center gap-1">
|
||||
<i class="fas fa-signal"></i>
|
||||
<span>优秀</span>
|
||||
</span>
|
||||
<!-- [DATA_FIELD: callSession.duration] [TYPE: string] [FORMAT: MM:SS] [UPDATE: 每秒] -->
|
||||
<span data-field="callSession.duration" id="callDuration">00:00</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-4">
|
||||
<!-- [CONDITIONAL_RENDER: callSession.isEncrypted === true] -->
|
||||
<div class="hidden md:flex items-center gap-2 px-4 py-2 glass rounded-full text-sm" id="encryptionBadge">
|
||||
<i class="fas fa-shield-alt text-green-400"></i>
|
||||
<span class="text-gray-300">端到端加密</span>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main class="flex-1 flex overflow-hidden relative">
|
||||
|
||||
<!--
|
||||
============================================================
|
||||
区域: 视频区域 (Video Area)
|
||||
数据源: CallSession.remoteUser (对方) + CallSession.localUser (自己)
|
||||
更新频率: 实时 (WebRTC MediaStream + WebSocket 状态)
|
||||
============================================================
|
||||
-->
|
||||
<div class="flex-1 relative bg-black/40 overflow-hidden" id="videoArea">
|
||||
|
||||
<!--
|
||||
子区域: 多Participant视频网格(Host端显示)
|
||||
动态生成,每个participant一个视频格子
|
||||
-->
|
||||
<div id="participantGrid" class="hidden absolute inset-0 grid gap-3 p-3 auto-rows-fr" style="grid-template-columns: 1fr;">
|
||||
<!-- 动态生成的 participant 视频格子将插入这里 -->
|
||||
</div>
|
||||
|
||||
<!--
|
||||
子区域: 远端视频 (Remote Video) - 单路(Participant端显示Host画面)
|
||||
数据源: RemoteUser
|
||||
-->
|
||||
<div class="absolute inset-0 video-fade-in">
|
||||
<!-- [DATA_FIELD: remoteUser.videoStream] [TYPE: MediaStream | null] [BIND: srcObject] -->
|
||||
<!-- [FALLBACK: remoteUser.avatar] [TYPE: string] [URL] -->
|
||||
<video id="remoteVideo" alt="对方视频" class="w-full h-full object-contain" autoplay
|
||||
data-field="remoteUser.videoStream">
|
||||
</video>
|
||||
|
||||
<!-- 远端未连接时的占位背景 -->
|
||||
<div id="remoteVideoPlaceholder"
|
||||
class="absolute inset-0 flex items-center justify-center bg-gradient-to-br from-indigo-900 to-purple-900 z-10">
|
||||
<div class="text-center">
|
||||
<div
|
||||
class="w-32 h-32 rounded-full bg-indigo-700/50 flex items-center justify-center mx-auto mb-4">
|
||||
<i class="fas fa-user text-4xl text-white/70"></i>
|
||||
</div>
|
||||
<p class="text-white text-lg font-medium">等待对方连接...</p>
|
||||
<p class="text-sm text-gray-400 mt-2">请确保对方已加入通话</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 网络状态提示 -->
|
||||
<!-- [CONDITIONAL_RENDER: remoteUser.networkQuality !== 'excellent'] -->
|
||||
<div id="networkStatus"
|
||||
class="absolute top-6 right-6 glass px-3 py-1.5 rounded-full flex items-center gap-2 text-xs hidden"
|
||||
data-field="remoteUser.networkQuality">
|
||||
<i class="fas fa-exclamation-triangle text-yellow-500"></i>
|
||||
<!-- [DATA_FIELD: remoteUser.networkQuality] [TYPE: string] [TRANSFORM: quality => text] -->
|
||||
<span class="text-gray-300" id="networkStatusText">网络不稳定</span>
|
||||
</div>
|
||||
|
||||
<!-- 连接中/重连提示 -->
|
||||
<!-- [CONDITIONAL_RENDER: callSession.status === 'connecting'] -->
|
||||
<div id="connectingOverlay" class="absolute inset-0 bg-black/60 flex items-center justify-center hidden"
|
||||
data-field="callSession.status">
|
||||
<div class="text-center">
|
||||
<div
|
||||
class="w-12 h-12 border-4 border-indigo-500 border-t-transparent rounded-full animate-spin mx-auto mb-3">
|
||||
</div>
|
||||
<p class="text-white font-medium">正在连接...</p>
|
||||
<p class="text-sm text-gray-400 mt-1" id="connectingText">等待对方接受邀请</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
<!--
|
||||
子区域: 本地视频 (Local Video - Picture in Picture)
|
||||
数据源: LocalUser
|
||||
-->
|
||||
<div
|
||||
class="absolute bottom-6 right-6 w-64 h-48 rounded-2xl overflow-hidden shadow-2xl border-2 border-white/20 video-fade-in z-10">
|
||||
<!-- [DATA_FIELD: localUser.videoStream] [TYPE: MediaStream | null] [BIND: srcObject] -->
|
||||
<!-- [FALLBACK: localUser.avatar] [TYPE: string] [URL] -->
|
||||
<video id="localVideo"
|
||||
src="https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?w=400&h=300&fit=crop" alt="本地视频"
|
||||
class="w-full h-full object-cover" autoplay muted data-field="localUser.videoStream">
|
||||
</video>
|
||||
|
||||
<!-- [CONDITIONAL_RENDER: localUser.mediaState.video === false] -->
|
||||
<div id="localVideoPlaceholder"
|
||||
class="absolute inset-0 flex items-center justify-center bg-gradient-to-br from-indigo-900 to-purple-900 hidden"
|
||||
data-field="localUser.videoOff">
|
||||
<span class="text-4xl font-bold" id="localInitials">我</span>
|
||||
</div>
|
||||
|
||||
<div class="absolute bottom-3 left-3 glass px-2 py-1 rounded text-xs flex items-center gap-2">
|
||||
<span>我</span>
|
||||
<!-- [CONDITIONAL_RENDER: localUser.mediaState.isSpeaking === true] -->
|
||||
<div id="localAudioWave" class="audio-wave w-4 hidden" data-field="localUser.isSpeaking">
|
||||
<span></span><span></span><span></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 本地视频悬停控制 -->
|
||||
<div
|
||||
class="absolute inset-0 bg-black/40 opacity-0 hover:opacity-100 transition-opacity flex items-center justify-center gap-2">
|
||||
<button onclick="toggleLocalVideo()"
|
||||
class="w-8 h-8 rounded-full bg-white/20 hover:bg-white/30 flex items-center justify-center transition-colors">
|
||||
<i class="fas fa-video text-xs" id="localVideoIcon"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<!--
|
||||
============================================================
|
||||
区域: 侧边栏 (Sidebar)
|
||||
数据源: ChatMessage[] + User[]
|
||||
更新频率: 实时 (WebSocket)
|
||||
============================================================
|
||||
-->
|
||||
<aside class="w-80 glass-strong border-l border-white/10 flex flex-col hidden" id="sidebar">
|
||||
|
||||
<!--
|
||||
子区域: 用户列表 (User List)
|
||||
数据源: [localUser, remoteUser]
|
||||
-->
|
||||
<div class="p-4 border-b border-white/10">
|
||||
<h3 class="text-sm font-medium text-gray-400 mb-3" id="userCountDisplay">
|
||||
通话成员 (1)
|
||||
</h3>
|
||||
<div class="space-y-2" id="userList">
|
||||
<!-- [LOOP_START: users as user] -->
|
||||
|
||||
<!-- 远端用户项 -->
|
||||
<div class="flex items-center gap-3 p-2 rounded-lg bg-white/5" data-user-id="remote">
|
||||
<div class="relative">
|
||||
<!-- [DATA_FIELD: remoteUser.avatar] -->
|
||||
<img src="" class="w-10 h-10 rounded-full object-cover" data-field="remoteUser.avatar">
|
||||
<!-- [CONDITIONAL_RENDER: remoteUser.status === 'online'] -->
|
||||
<div
|
||||
class="absolute -bottom-1 -right-1 w-3 h-3 bg-green-500 rounded-full border-2 border-slate-900">
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex-1">
|
||||
<!-- [DATA_FIELD: remoteUser.name] -->
|
||||
<div class="text-sm font-medium" data-field="remoteUser.name">Sarah Chen</div>
|
||||
<!-- [DATA_FIELD: remoteUser.mediaState] [TRANSFORM: state => statusText] -->
|
||||
<div class="text-xs text-gray-500" data-field="remoteUser.mediaStatus">在线</div>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<!-- [CONDITIONAL_RENDER: remoteUser.mediaState.audio === false] -->
|
||||
<i class="fas fa-microphone-slash text-gray-500 text-xs hidden"
|
||||
data-field="remoteUser.muteIcon"></i>
|
||||
<!-- [CONDITIONAL_RENDER: remoteUser.mediaState.isSpeaking === true] -->
|
||||
<div class="audio-wave w-6 hidden" data-field="remoteUser.speakingIndicator">
|
||||
<span></span><span></span><span></span><span></span><span></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 本地用户项 -->
|
||||
<div class="flex items-center gap-3 p-2 rounded-lg hover:bg-white/5" data-user-id="local">
|
||||
<!-- [DATA_FIELD: localUser.avatar] -->
|
||||
<img src="" class="w-10 h-10 rounded-full object-cover" data-field="localUser.avatar">
|
||||
<div class="flex-1">
|
||||
<div class="text-sm font-medium">
|
||||
<!-- [DATA_FIELD: localUser.name] -->我
|
||||
<!-- [CONDITIONAL_RENDER: localUser.isHost === true] -->
|
||||
<span class="text-xs bg-indigo-500 px-1.5 rounded ml-1">主持人</span>
|
||||
</div>
|
||||
<!-- [DATA_FIELD: localUser.mediaState] [TRANSFORM: state => statusText] -->
|
||||
<div class="text-xs text-gray-500" id="localMediaStatus" data-field="localUser.mediaStatus">
|
||||
静音中</div>
|
||||
</div>
|
||||
<!-- [CONDITIONAL_RENDER: localUser.mediaState.audio === false] -->
|
||||
<i class="fas fa-microphone-slash text-gray-500 text-xs" data-field="localUser.muteIcon"></i>
|
||||
</div>
|
||||
|
||||
<!-- [LOOP_END: users] -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!--
|
||||
子区域: 聊天消息列表 (Chat Messages)
|
||||
数据源: ChatMessage[]
|
||||
排序: 按 timestamp 升序
|
||||
-->
|
||||
<div class="flex-1 overflow-y-auto p-4 space-y-4 custom-scrollbar" id="chatContent">
|
||||
<!-- [STATIC] 通话开始时间 -->
|
||||
<div class="text-center text-xs text-gray-500 my-4">
|
||||
通话开始 <!-- [DATA_FIELD: callSession.startTime] [FORMAT: HH:MM] -->14:30
|
||||
</div>
|
||||
|
||||
<!-- [LOOP_START: messages as message] -->
|
||||
|
||||
<!-- 消息模板 (对方) -->
|
||||
<!-- [CONDITIONAL_RENDER: message.isSelf === false] -->
|
||||
<div class="chat-bubble" data-message-id="${message.id}">
|
||||
<div class="flex gap-3">
|
||||
<!-- [DATA_FIELD: message.senderAvatar] -->
|
||||
<img src="https://images.unsplash.com/photo-1494790108377-be9c29b29330?w=100&h=100&fit=crop"
|
||||
class="w-8 h-8 rounded-full object-cover" data-field="message.senderAvatar">
|
||||
<div class="flex-1">
|
||||
<div class="flex items-baseline gap-2 mb-1">
|
||||
<!-- [DATA_FIELD: message.senderName] -->
|
||||
<span class="text-sm font-medium text-indigo-400" data-field="message.senderName">Sarah
|
||||
Chen</span>
|
||||
<!-- [DATA_FIELD: message.timestamp] [FORMAT: HH:MM] -->
|
||||
<span class="text-xs text-gray-500" data-field="message.time">14:32</span>
|
||||
</div>
|
||||
<!-- [DATA_FIELD: message.content] -->
|
||||
<div class="glass px-3 py-2 rounded-2xl rounded-tl-none text-sm text-gray-200"
|
||||
data-field="message.content">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 消息模板 (自己) -->
|
||||
<!-- [CONDITIONAL_RENDER: message.isSelf === true] -->
|
||||
<div class="chat-bubble">
|
||||
<div class="flex gap-3 flex-row-reverse">
|
||||
<!-- [DATA_FIELD: message.senderAvatar] -->
|
||||
<img src="https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?w=100&h=100&fit=crop"
|
||||
class="w-8 h-8 rounded-full object-cover" data-field="message.senderAvatar">
|
||||
<div class="flex-1 flex flex-col items-end">
|
||||
<div class="flex items-baseline gap-2 mb-1 flex-row-reverse">
|
||||
<span class="text-sm font-medium text-green-400">我</span>
|
||||
<span class="text-xs text-gray-500" data-field="message.time">14:32</span>
|
||||
</div>
|
||||
<div class="bg-indigo-600 px-3 py-2 rounded-2xl rounded-tr-none text-sm text-white"
|
||||
data-field="message.content">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- [LOOP_END: messages] -->
|
||||
</div>
|
||||
|
||||
<!--
|
||||
子区域: 消息输入 (Message Input)
|
||||
API: [POST] /api/call/:callId/message
|
||||
-->
|
||||
<div class="p-4 border-t border-white/10">
|
||||
<div class="glass rounded-2xl flex items-center gap-2 p-2">
|
||||
<button
|
||||
class="w-8 h-8 rounded-full hover:bg-white/10 flex items-center justify-center text-gray-400 transition-colors"
|
||||
onclick="openImagePicker()">
|
||||
<i class="fas fa-plus"></i>
|
||||
</button>
|
||||
<!-- 隐藏的文件输入元素 -->
|
||||
<input type="file" id="imageInput" accept="image/*" class="hidden"
|
||||
onchange="handleImageUpload(event)">
|
||||
<!-- [INPUT_FIELD] [BIND: inputValue] [EVENT: onEnter => sendMessage()] -->
|
||||
<input type="text" id="chatInput" placeholder="输入消息..."
|
||||
class="flex-1 bg-transparent border-none outline-none text-sm text-white placeholder-gray-500 px-2"
|
||||
data-field="chatInput" onkeypress="handleChatSubmit(event)">
|
||||
<!-- [BUTTON] [EVENT: onclick => sendMessage()] -->
|
||||
<button onclick="sendMessage()"
|
||||
class="w-8 h-8 rounded-full bg-indigo-600 hover:bg-indigo-700 flex items-center justify-center transition-colors">
|
||||
<i class="fas fa-paper-plane text-xs"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
</main>
|
||||
|
||||
<!--
|
||||
============================================================
|
||||
区域: 底部控制栏 (Control Bar)
|
||||
数据源: LocalUser.mediaState
|
||||
API: [POST] /api/call/:callId/media
|
||||
WebSocket: emit 'media-state-changed'
|
||||
============================================================
|
||||
-->
|
||||
<footer class="glass-strong h-20 border-t border-white/10 flex items-center justify-center px-6 gap-4 z-50">
|
||||
|
||||
<!-- 左侧连接信息 -->
|
||||
<div class="absolute left-6 hidden md:flex items-center gap-3">
|
||||
<div class="text-left">
|
||||
<div class="text-sm font-medium">一对一通话</div>
|
||||
<!-- [DATA_FIELD: remoteUser.networkQuality] [TRANSFORM: quality => displayText] -->
|
||||
<div class="text-xs text-gray-400" id="connectionQuality" data-field="connectionQualityText">
|
||||
连接质量: 优秀
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 中间控制按钮组 -->
|
||||
<div class="flex items-center gap-3">
|
||||
|
||||
<!-- 麦克风控制 -->
|
||||
<!-- [DATA_FIELD: localUser.mediaState.audio] [TYPE: boolean] -->
|
||||
<button
|
||||
class="control-btn w-12 h-12 rounded-full glass flex items-center justify-center text-white hover:bg-white/10 relative group"
|
||||
onclick="toggleMute(this)" id="micBtn" data-field="localUser.audio" data-active="false">
|
||||
<i class="fas fa-microphone text-lg" data-icon="default"></i>
|
||||
<i class="fas fa-microphone-slash text-lg hidden text-red-400" data-icon="active"></i>
|
||||
<span
|
||||
class="absolute -top-10 left-1/2 transform -translate-x-1/2 px-2 py-1 bg-black/80 rounded text-xs whitespace-nowrap opacity-0 group-hover:opacity-100 transition-opacity">
|
||||
静音 (Space)
|
||||
</span>
|
||||
</button>
|
||||
|
||||
<!-- 摄像头控制 -->
|
||||
<!-- [DATA_FIELD: localUser.mediaState.video] [TYPE: boolean] -->
|
||||
<button
|
||||
class="control-btn w-12 h-12 rounded-full glass flex items-center justify-center text-white hover:bg-white/10 relative group"
|
||||
onclick="toggleVideo(this)" id="videoBtn" data-field="localUser.video" data-active="false">
|
||||
<i class="fas fa-video text-lg" data-icon="default"></i>
|
||||
<i class="fas fa-video-slash text-lg hidden text-red-400" data-icon="active"></i>
|
||||
<span
|
||||
class="absolute -top-10 left-1/2 transform -translate-x-1/2 px-2 py-1 bg-black/80 rounded text-xs whitespace-nowrap opacity-0 group-hover:opacity-100 transition-opacity">
|
||||
关闭视频 (Ctrl+V)
|
||||
</span>
|
||||
</button>
|
||||
|
||||
<!-- 录屏控制 -->
|
||||
<!-- [DATA_FIELD: localUser.mediaState.recording] [TYPE: boolean] -->
|
||||
<button
|
||||
class="control-btn w-12 h-12 rounded-full glass flex items-center justify-center text-white hover:bg-white/10 relative group"
|
||||
onclick="toggleRecording(this)" id="recordBtn" data-field="localUser.recording" data-active="false">
|
||||
<i class="fas fa-circle text-lg" data-icon="default"></i>
|
||||
<i class="fas fa-stop text-lg hidden text-red-400" data-icon="active"></i>
|
||||
<span
|
||||
class="absolute -top-10 left-1/2 transform -translate-x-1/2 px-2 py-1 bg-black/80 rounded text-xs whitespace-nowrap opacity-0 group-hover:opacity-100 transition-opacity">
|
||||
录制
|
||||
</span>
|
||||
</button>
|
||||
|
||||
<!-- 更多选项 -->
|
||||
<div class="relative">
|
||||
<button id="moreOptionsBtn"
|
||||
class="control-btn w-12 h-12 rounded-full glass flex items-center justify-center text-white hover:bg-white/10 relative group">
|
||||
<i class="fas fa-ellipsis-h text-lg"></i>
|
||||
<span
|
||||
class="absolute -top-10 left-1/2 transform -translate-x-1/2 px-2 py-1 bg-black/80 rounded text-xs whitespace-nowrap opacity-0 group-hover:opacity-100 transition-opacity">
|
||||
更多选项
|
||||
</span>
|
||||
</button>
|
||||
<!-- 更多选项下拉菜单 -->
|
||||
<div id="moreOptionsMenu" class="hidden absolute bottom-full left-1/2 transform -translate-x-1/2 mb-3 glass rounded-xl shadow-lg w-52 z-50">
|
||||
<!-- 分辨率选项 -->
|
||||
<div class="p-3 border-b border-white/10">
|
||||
<h4 class="text-xs font-medium text-gray-400 mb-2 flex items-center gap-2">
|
||||
<i class="fas fa-desktop text-xs"></i>
|
||||
视频分辨率
|
||||
</h4>
|
||||
<div class="space-y-1" id="resolutionOptions">
|
||||
<button onclick="changeResolution(480, 270)" data-resolution="480"
|
||||
class="resolution-option w-full flex items-center justify-between px-3 py-2 text-sm rounded-lg hover:bg-white/10 transition-colors">
|
||||
<span>流畅 480p</span>
|
||||
<span class="text-xs text-gray-500">省流量</span>
|
||||
</button>
|
||||
<button onclick="changeResolution(1280, 720)" data-resolution="720"
|
||||
class="resolution-option w-full flex items-center justify-between px-3 py-2 text-sm rounded-lg hover:bg-white/10 transition-colors">
|
||||
<span>高清 720p</span>
|
||||
<span class="text-xs text-gray-500">推荐</span>
|
||||
</button>
|
||||
<button onclick="changeResolution(1920, 1080)" data-resolution="1080"
|
||||
class="resolution-option active w-full flex items-center justify-between px-3 py-2 text-sm rounded-lg hover:bg-white/10 transition-colors">
|
||||
<span>超清 1080p</span>
|
||||
<span class="text-xs text-gray-500"></span>
|
||||
</button>
|
||||
<button onclick="changeResolution(2560, 1440)" data-resolution="1440"
|
||||
class="resolution-option w-full flex items-center justify-between px-3 py-2 text-sm rounded-lg hover:bg-white/10 transition-colors">
|
||||
<span>2K 1440p</span>
|
||||
<span class="text-xs text-gray-500">最高画质</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 当前分辨率指示 -->
|
||||
<div class="px-3 py-2 flex items-center gap-2">
|
||||
<i class="fas fa-info-circle text-xs text-gray-500"></i>
|
||||
<span id="currentResolutionText" class="text-xs text-gray-500">当前: 1080p</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 结束通话 -->
|
||||
<!-- [EVENT: onclick => endCall()] [API: POST /api/call/:callId/leave] -->
|
||||
<button
|
||||
class="control-btn w-14 h-14 rounded-full bg-red-500 hover:bg-red-600 flex items-center justify-center text-white end-call-pulse ml-4 relative group"
|
||||
onclick="endCall()">
|
||||
<i class="fas fa-phone-slash text-xl"></i>
|
||||
<span
|
||||
class="absolute -top-10 left-1/2 transform -translate-x-1/2 px-2 py-1 bg-black/80 rounded text-xs whitespace-nowrap opacity-0 group-hover:opacity-100 transition-opacity">
|
||||
结束通话
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- 右侧聊天按钮 -->
|
||||
<div class="absolute right-6 flex items-center gap-3">
|
||||
<button
|
||||
class="control-btn w-10 h-10 rounded-full glass flex items-center justify-center text-gray-300 hover:text-white hover:bg-white/10 transition-colors relative"
|
||||
onclick="toggleSidebar()">
|
||||
<i class="fas fa-comment-alt"></i>
|
||||
<!-- 未读消息计数角标 -->
|
||||
<span id="unreadBadge"
|
||||
class="absolute -top-1 -right-1 w-5 h-5 bg-red-500 rounded-full flex items-center justify-center text-xs font-bold text-white hidden">0</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
</footer>
|
||||
|
||||
</div><!-- /callView -->
|
||||
|
||||
<!-- 通知组件 -->
|
||||
<div id="notification"
|
||||
class="fixed top-20 left-1/2 transform -translate-x-1/2 glass px-6 py-3 rounded-full flex items-center gap-3 opacity-0 pointer-events-none transition-all duration-300 z-50 translate-y-[-20px]">
|
||||
<i class="fas fa-info-circle text-indigo-400"></i>
|
||||
<span class="text-sm" id="notificationText">通知内容</span>
|
||||
</div>
|
||||
|
||||
<!-- 通话结束确认对话框 -->
|
||||
<div id="endCallDialog" class="fixed inset-0 bg-black/70 flex items-center justify-center z-50 hidden">
|
||||
<div class="glass rounded-2xl p-6 w-80 max-w-md">
|
||||
<div class="text-center mb-6">
|
||||
<div class="w-16 h-16 bg-red-500/20 rounded-full flex items-center justify-center mx-auto mb-4">
|
||||
<i class="fas fa-phone-slash text-red-500 text-2xl"></i>
|
||||
</div>
|
||||
<h3 class="text-xl font-bold mb-2">结束通话</h3>
|
||||
<p class="text-gray-400 text-sm">确定要结束当前通话吗?</p>
|
||||
</div>
|
||||
<div class="flex gap-3">
|
||||
<button id="cancelEndCall" class="flex-1 py-2 rounded-lg glass hover:bg-white/10 transition-colors">
|
||||
取消
|
||||
</button>
|
||||
<button id="confirmEndCall"
|
||||
class="flex-1 py-2 rounded-lg bg-red-500 hover:bg-red-600 transition-colors">
|
||||
结束通话
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 通话请求弹窗 -->
|
||||
<div id="callRequestDialog" class="fixed inset-0 bg-black/70 flex items-center justify-center z-50 hidden">
|
||||
<div class="glass rounded-2xl p-6 w-80 max-w-md">
|
||||
<div class="text-center mb-6">
|
||||
<div class="w-16 h-16 bg-indigo-500/20 rounded-full flex items-center justify-center mx-auto mb-4">
|
||||
<i class="fas fa-video text-indigo-500 text-2xl"></i>
|
||||
</div>
|
||||
<h3 class="text-xl font-bold mb-2" id="callRequestName">Sarah Chen</h3>
|
||||
<p class="text-gray-400 text-sm" id="callRequestText">正在请求与您进行视频通话</p>
|
||||
<div class="mt-4 flex items-center justify-center gap-4">
|
||||
<img id="callRequestAvatar"
|
||||
src="https://images.unsplash.com/photo-1494790108377-be9c29b29330?w=100&h=100&fit=crop"
|
||||
class="w-16 h-16 rounded-full object-cover border-4 border-indigo-500">
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex gap-3">
|
||||
<button id="rejectCall" class="flex-1 py-2 rounded-lg glass hover:bg-white/10 transition-colors">
|
||||
<div class="flex items-center justify-center gap-2">
|
||||
<i class="fas fa-phone-slash"></i>
|
||||
<span>拒绝</span>
|
||||
</div>
|
||||
</button>
|
||||
<button id="acceptCall"
|
||||
class="flex-1 py-2 rounded-lg bg-green-500 hover:bg-green-600 transition-colors">
|
||||
<div class="flex items-center justify-center gap-2">
|
||||
<i class="fas fa-phone"></i>
|
||||
<span>接受</span>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 引入模块化JavaScript文件 -->
|
||||
<script type="module" src="connectview.js"></script>
|
||||
<script type="module" src="main.js"></script>
|
||||
|
||||
<section>
|
||||
<a href="https://github.com/Unity-Technologies/UnityRenderStreaming/tree/develop/WebApp" title="View source for this page on GitHub" id="viewSource">View source on GitHub</a>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<script type="module" src="js/main.js"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
|
||||
Reference in New Issue
Block a user