mirror of
https://github.com/MatrixSeven/file-transfer-go.git
synced 2026-02-04 11:34:45 +08:00
572 lines
19 KiB
HTML
572 lines
19 KiB
HTML
{{define "content"}}
|
||
<div class="max-w-6xl mx-auto">
|
||
<!-- 页面标题 -->
|
||
<div class="text-center mb-8">
|
||
<h1 class="text-3xl font-bold text-gray-900 mb-4">📺 实时视频传输</h1>
|
||
<p class="text-lg text-gray-600">
|
||
基于WebRTC的P2P视频传输,低延迟高画质,支持多人连接。
|
||
</p>
|
||
</div>
|
||
|
||
<!-- 连接状态 -->
|
||
<div class="bg-white rounded-lg shadow-md p-6 mb-8">
|
||
<div class="flex items-center justify-between">
|
||
<div class="flex items-center">
|
||
<div id="connectionStatus" class="w-3 h-3 rounded-full bg-red-500 mr-3"></div>
|
||
<span id="statusText" class="font-medium">未连接</span>
|
||
</div>
|
||
<div class="space-x-4">
|
||
<button id="connectBtn" onclick="connectToRoom()"
|
||
class="bg-blue-500 hover:bg-blue-600 text-white px-4 py-2 rounded font-semibold">
|
||
🔗 连接
|
||
</button>
|
||
<button id="disconnectBtn" onclick="disconnectFromRoom()"
|
||
class="bg-red-500 hover:bg-red-600 text-white px-4 py-2 rounded font-semibold hidden">
|
||
❌ 断开
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 视频区域 -->
|
||
<div class="grid lg:grid-cols-2 gap-6 mb-8">
|
||
<!-- 本地视频 -->
|
||
<div class="bg-white rounded-lg shadow-md p-6">
|
||
<h3 class="text-lg font-semibold mb-4">本地视频</h3>
|
||
<div class="relative bg-gray-900 rounded-lg overflow-hidden" style="aspect-ratio: 16/9;">
|
||
<video id="localVideo" autoplay muted playsinline
|
||
class="w-full h-full object-cover">
|
||
</video>
|
||
<div id="localVideoOverlay" class="absolute inset-0 flex items-center justify-center text-white text-lg">
|
||
📹 点击连接开启摄像头
|
||
</div>
|
||
</div>
|
||
<div class="mt-4 space-x-2">
|
||
<button id="toggleVideo" onclick="toggleVideo()"
|
||
class="bg-green-500 hover:bg-green-600 text-white px-3 py-2 rounded text-sm">
|
||
📹 视频
|
||
</button>
|
||
<button id="toggleAudio" onclick="toggleAudio()"
|
||
class="bg-green-500 hover:bg-green-600 text-white px-3 py-2 rounded text-sm">
|
||
🎤 音频
|
||
</button>
|
||
<button onclick="switchCamera()"
|
||
class="bg-gray-500 hover:bg-gray-600 text-white px-3 py-2 rounded text-sm">
|
||
🔄 切换
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 远程视频 -->
|
||
<div class="bg-white rounded-lg shadow-md p-6">
|
||
<h3 class="text-lg font-semibold mb-4">远程视频</h3>
|
||
<div class="relative bg-gray-900 rounded-lg overflow-hidden" style="aspect-ratio: 16/9;">
|
||
<video id="remoteVideo" autoplay playsinline
|
||
class="w-full h-full object-cover">
|
||
</video>
|
||
<div id="remoteVideoOverlay" class="absolute inset-0 flex items-center justify-center text-white text-lg">
|
||
📡 等待远程连接...
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 聊天区域 -->
|
||
<div class="bg-white rounded-lg shadow-md p-6 mb-8">
|
||
<h3 class="text-lg font-semibold mb-4">💬 文字聊天</h3>
|
||
<div id="chatMessages" class="bg-gray-50 rounded-lg p-4 h-40 overflow-y-auto mb-4">
|
||
<div class="text-gray-500 text-center">聊天消息将显示在这里...</div>
|
||
</div>
|
||
<div class="flex">
|
||
<input type="text" id="chatInput" placeholder="输入消息..."
|
||
class="flex-1 px-4 py-2 border border-gray-300 rounded-l-lg focus:outline-none focus:ring-2 focus:ring-blue-500">
|
||
<button onclick="sendMessage()"
|
||
class="bg-blue-500 hover:bg-blue-600 text-white px-6 py-2 rounded-r-lg">
|
||
发送
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 技术信息 -->
|
||
<div class="bg-gray-50 rounded-lg p-6">
|
||
<h3 class="text-lg font-semibold mb-4">🔧 连接信息</h3>
|
||
<div class="grid md:grid-cols-2 gap-4 text-sm">
|
||
<div>
|
||
<strong>连接状态:</strong>
|
||
<span id="connectionState">未连接</span>
|
||
</div>
|
||
<div>
|
||
<strong>ICE状态:</strong>
|
||
<span id="iceState">new</span>
|
||
</div>
|
||
<div>
|
||
<strong>视频编解码器:</strong>
|
||
<span id="videoCodec">-</span>
|
||
</div>
|
||
<div>
|
||
<strong>音频编解码器:</strong>
|
||
<span id="audioCodec">-</span>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="mt-4">
|
||
<details>
|
||
<summary class="cursor-pointer font-medium">WebRTC统计信息</summary>
|
||
<div id="rtcStats" class="mt-2 p-3 bg-white rounded text-xs font-mono">
|
||
点击连接后显示详细统计信息...
|
||
</div>
|
||
</details>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 浏览器兼容性提示 -->
|
||
<div class="bg-yellow-50 border border-yellow-200 rounded-lg p-4 mt-6">
|
||
<h4 class="font-semibold text-yellow-800 mb-2">🌐 浏览器兼容性</h4>
|
||
<p class="text-yellow-800 text-sm">
|
||
本功能需要支持WebRTC的现代浏览器。推荐使用Chrome、Firefox、Safari、Edge最新版本。
|
||
部分功能在360浏览器、QQ浏览器等国产浏览器中可能受限。
|
||
</p>
|
||
</div>
|
||
</div>
|
||
{{end}}
|
||
|
||
{{define "scripts"}}
|
||
<script>
|
||
// WebRTC相关变量
|
||
let localStream = null;
|
||
let remoteStream = null;
|
||
let peerConnection = null;
|
||
let websocket = null;
|
||
let isConnected = false;
|
||
let videoEnabled = true;
|
||
let audioEnabled = true;
|
||
|
||
// DOM元素
|
||
const localVideo = document.getElementById('localVideo');
|
||
const remoteVideo = document.getElementById('remoteVideo');
|
||
const localVideoOverlay = document.getElementById('localVideoOverlay');
|
||
const remoteVideoOverlay = document.getElementById('remoteVideoOverlay');
|
||
const connectionStatus = document.getElementById('connectionStatus');
|
||
const statusText = document.getElementById('statusText');
|
||
const connectBtn = document.getElementById('connectBtn');
|
||
const disconnectBtn = document.getElementById('disconnectBtn');
|
||
const chatMessages = document.getElementById('chatMessages');
|
||
const chatInput = document.getElementById('chatInput');
|
||
|
||
// WebRTC配置
|
||
const rtcConfig = {
|
||
iceServers: [
|
||
// 阿里云STUN服务器
|
||
{ urls: 'stun:stun.chat.bilibili.com:3478' },
|
||
{ urls: 'stun:stun.voipbuster.com' },
|
||
{ urls: 'stun:stun.voipstunt.com' },
|
||
// 腾讯云STUN服务器
|
||
{ urls: 'stun:stun.qq.com:3478' },
|
||
// 备用国外服务器
|
||
{ urls: 'stun:stun.l.google.com:19302' },
|
||
{ urls: 'stun:stun1.l.google.com:19302' }
|
||
]
|
||
};
|
||
|
||
// 连接到房间
|
||
async function connectToRoom() {
|
||
try {
|
||
updateStatus('connecting', '正在连接...');
|
||
|
||
// 获取本地媒体流
|
||
await getUserMedia();
|
||
|
||
// 建立WebSocket连接
|
||
await connectWebSocket();
|
||
|
||
// 创建PeerConnection
|
||
createPeerConnection();
|
||
|
||
updateStatus('connected', '已连接');
|
||
connectBtn.classList.add('hidden');
|
||
disconnectBtn.classList.remove('hidden');
|
||
|
||
} catch (error) {
|
||
console.error('连接失败:', error);
|
||
updateStatus('error', '连接失败: ' + error.message);
|
||
}
|
||
}
|
||
|
||
// 断开连接
|
||
function disconnectFromRoom() {
|
||
if (websocket) {
|
||
websocket.close();
|
||
}
|
||
|
||
if (peerConnection) {
|
||
peerConnection.close();
|
||
peerConnection = null;
|
||
}
|
||
|
||
if (localStream) {
|
||
localStream.getTracks().forEach(track => track.stop());
|
||
localStream = null;
|
||
}
|
||
|
||
localVideo.srcObject = null;
|
||
remoteVideo.srcObject = null;
|
||
|
||
updateStatus('disconnected', '未连接');
|
||
connectBtn.classList.remove('hidden');
|
||
disconnectBtn.classList.add('hidden');
|
||
|
||
localVideoOverlay.style.display = 'flex';
|
||
remoteVideoOverlay.style.display = 'flex';
|
||
|
||
isConnected = false;
|
||
}
|
||
|
||
// 获取用户媒体
|
||
async function getUserMedia() {
|
||
try {
|
||
const constraints = {
|
||
video: {
|
||
width: { ideal: 1280 },
|
||
height: { ideal: 720 },
|
||
frameRate: { ideal: 30 }
|
||
},
|
||
audio: {
|
||
echoCancellation: true,
|
||
noiseSuppression: true,
|
||
autoGainControl: true
|
||
}
|
||
};
|
||
|
||
localStream = await navigator.mediaDevices.getUserMedia(constraints);
|
||
localVideo.srcObject = localStream;
|
||
localVideoOverlay.style.display = 'none';
|
||
|
||
} catch (error) {
|
||
throw new Error('无法访问摄像头或麦克风: ' + error.message);
|
||
}
|
||
}
|
||
|
||
// 建立WebSocket连接
|
||
function connectWebSocket() {
|
||
return new Promise((resolve, reject) => {
|
||
const wsProtocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
|
||
const wsUrl = `${wsProtocol}//${window.location.host}/ws/video`;
|
||
|
||
websocket = new WebSocket(wsUrl);
|
||
|
||
websocket.onopen = () => {
|
||
console.log('WebSocket连接已建立');
|
||
resolve();
|
||
};
|
||
|
||
websocket.onmessage = async (event) => {
|
||
const message = JSON.parse(event.data);
|
||
await handleWebSocketMessage(message);
|
||
};
|
||
|
||
websocket.onerror = (error) => {
|
||
console.error('WebSocket错误:', error);
|
||
reject(new Error('WebSocket连接失败'));
|
||
};
|
||
|
||
websocket.onclose = () => {
|
||
console.log('WebSocket连接已关闭');
|
||
if (isConnected) {
|
||
updateStatus('error', '连接已断开');
|
||
}
|
||
};
|
||
});
|
||
}
|
||
|
||
// 创建PeerConnection
|
||
function createPeerConnection() {
|
||
peerConnection = new RTCPeerConnection(rtcConfig);
|
||
|
||
// 添加本地流
|
||
if (localStream) {
|
||
localStream.getTracks().forEach(track => {
|
||
peerConnection.addTrack(track, localStream);
|
||
});
|
||
}
|
||
|
||
// 处理远程流
|
||
peerConnection.ontrack = (event) => {
|
||
remoteStream = event.streams[0];
|
||
remoteVideo.srcObject = remoteStream;
|
||
remoteVideoOverlay.style.display = 'none';
|
||
};
|
||
|
||
// 处理ICE候选
|
||
peerConnection.onicecandidate = (event) => {
|
||
if (event.candidate && websocket) {
|
||
sendWebSocketMessage({
|
||
type: 'ice-candidate',
|
||
payload: event.candidate
|
||
});
|
||
}
|
||
};
|
||
|
||
// 监听连接状态
|
||
peerConnection.onconnectionstatechange = () => {
|
||
updateConnectionInfo();
|
||
};
|
||
|
||
peerConnection.oniceconnectionstatechange = () => {
|
||
updateConnectionInfo();
|
||
};
|
||
}
|
||
|
||
// 处理WebSocket消息
|
||
async function handleWebSocketMessage(message) {
|
||
switch (message.type) {
|
||
case 'welcome':
|
||
console.log('收到欢迎消息:', message.payload);
|
||
break;
|
||
|
||
case 'offer':
|
||
await handleOffer(message.payload);
|
||
break;
|
||
|
||
case 'answer':
|
||
await handleAnswer(message.payload);
|
||
break;
|
||
|
||
case 'ice-candidate':
|
||
await handleICECandidate(message.payload);
|
||
break;
|
||
|
||
case 'chat':
|
||
displayChatMessage(message.payload);
|
||
break;
|
||
|
||
default:
|
||
console.log('未知消息类型:', message.type);
|
||
}
|
||
}
|
||
|
||
// 发送WebSocket消息
|
||
function sendWebSocketMessage(message) {
|
||
if (websocket && websocket.readyState === WebSocket.OPEN) {
|
||
websocket.send(JSON.stringify(message));
|
||
}
|
||
}
|
||
|
||
// 处理Offer
|
||
async function handleOffer(offer) {
|
||
try {
|
||
await peerConnection.setRemoteDescription(new RTCSessionDescription(offer));
|
||
const answer = await peerConnection.createAnswer();
|
||
await peerConnection.setLocalDescription(answer);
|
||
|
||
sendWebSocketMessage({
|
||
type: 'answer',
|
||
payload: answer
|
||
});
|
||
} catch (error) {
|
||
console.error('处理Offer失败:', error);
|
||
}
|
||
}
|
||
|
||
// 处理Answer
|
||
async function handleAnswer(answer) {
|
||
try {
|
||
await peerConnection.setRemoteDescription(new RTCSessionDescription(answer));
|
||
} catch (error) {
|
||
console.error('处理Answer失败:', error);
|
||
}
|
||
}
|
||
|
||
// 处理ICE候选
|
||
async function handleICECandidate(candidate) {
|
||
try {
|
||
await peerConnection.addIceCandidate(new RTCIceCandidate(candidate));
|
||
} catch (error) {
|
||
console.error('添加ICE候选失败:', error);
|
||
}
|
||
}
|
||
|
||
// 更新状态
|
||
function updateStatus(status, text) {
|
||
statusText.textContent = text;
|
||
connectionStatus.className = `w-3 h-3 rounded-full mr-3 ${getStatusColor(status)}`;
|
||
isConnected = status === 'connected';
|
||
}
|
||
|
||
// 获取状态颜色
|
||
function getStatusColor(status) {
|
||
switch (status) {
|
||
case 'connected': return 'bg-green-500';
|
||
case 'connecting': return 'bg-yellow-500';
|
||
case 'error': return 'bg-red-500';
|
||
default: return 'bg-gray-500';
|
||
}
|
||
}
|
||
|
||
// 切换视频
|
||
function toggleVideo() {
|
||
if (localStream) {
|
||
videoEnabled = !videoEnabled;
|
||
localStream.getVideoTracks().forEach(track => {
|
||
track.enabled = videoEnabled;
|
||
});
|
||
|
||
const btn = document.getElementById('toggleVideo');
|
||
btn.textContent = videoEnabled ? '📹 视频' : '📹 关闭';
|
||
btn.className = videoEnabled ?
|
||
'bg-green-500 hover:bg-green-600 text-white px-3 py-2 rounded text-sm' :
|
||
'bg-red-500 hover:bg-red-600 text-white px-3 py-2 rounded text-sm';
|
||
}
|
||
}
|
||
|
||
// 切换音频
|
||
function toggleAudio() {
|
||
if (localStream) {
|
||
audioEnabled = !audioEnabled;
|
||
localStream.getAudioTracks().forEach(track => {
|
||
track.enabled = audioEnabled;
|
||
});
|
||
|
||
const btn = document.getElementById('toggleAudio');
|
||
btn.textContent = audioEnabled ? '🎤 音频' : '🎤 静音';
|
||
btn.className = audioEnabled ?
|
||
'bg-green-500 hover:bg-green-600 text-white px-3 py-2 rounded text-sm' :
|
||
'bg-red-500 hover:bg-red-600 text-white px-3 py-2 rounded text-sm';
|
||
}
|
||
}
|
||
|
||
// 切换摄像头
|
||
async function switchCamera() {
|
||
if (localStream) {
|
||
const videoTrack = localStream.getVideoTracks()[0];
|
||
const devices = await navigator.mediaDevices.enumerateDevices();
|
||
const videoDevices = devices.filter(device => device.kind === 'videoinput');
|
||
|
||
if (videoDevices.length > 1) {
|
||
// 简单的前后摄像头切换逻辑
|
||
const currentDevice = videoTrack.getSettings().deviceId;
|
||
const newDevice = videoDevices.find(device => device.deviceId !== currentDevice);
|
||
|
||
if (newDevice) {
|
||
try {
|
||
const newStream = await navigator.mediaDevices.getUserMedia({
|
||
video: { deviceId: newDevice.deviceId },
|
||
audio: true
|
||
});
|
||
|
||
// 替换视频轨道
|
||
const newVideoTrack = newStream.getVideoTracks()[0];
|
||
if (peerConnection) {
|
||
const sender = peerConnection.getSenders().find(s =>
|
||
s.track && s.track.kind === 'video'
|
||
);
|
||
if (sender) {
|
||
sender.replaceTrack(newVideoTrack);
|
||
}
|
||
}
|
||
|
||
videoTrack.stop();
|
||
localStream.removeTrack(videoTrack);
|
||
localStream.addTrack(newVideoTrack);
|
||
localVideo.srcObject = localStream;
|
||
|
||
} catch (error) {
|
||
console.error('切换摄像头失败:', error);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// 发送聊天消息
|
||
function sendMessage() {
|
||
const message = chatInput.value.trim();
|
||
if (message && websocket) {
|
||
sendWebSocketMessage({
|
||
type: 'chat',
|
||
payload: {
|
||
text: message,
|
||
timestamp: new Date().toISOString()
|
||
}
|
||
});
|
||
|
||
displayChatMessage({
|
||
text: message,
|
||
sender: 'me',
|
||
timestamp: new Date().toISOString()
|
||
});
|
||
|
||
chatInput.value = '';
|
||
}
|
||
}
|
||
|
||
// 显示聊天消息
|
||
function displayChatMessage(messageData) {
|
||
const messageDiv = document.createElement('div');
|
||
messageDiv.className = `mb-2 ${messageData.sender === 'me' ? 'text-right' : 'text-left'}`;
|
||
|
||
const time = new Date(messageData.timestamp).toLocaleTimeString();
|
||
messageDiv.innerHTML = `
|
||
<div class="inline-block max-w-xs lg:max-w-md px-3 py-2 rounded-lg ${
|
||
messageData.sender === 'me' ?
|
||
'bg-blue-500 text-white' :
|
||
'bg-gray-200 text-gray-800'
|
||
}">
|
||
<div>${messageData.text}</div>
|
||
<div class="text-xs opacity-75 mt-1">${time}</div>
|
||
</div>
|
||
`;
|
||
|
||
chatMessages.appendChild(messageDiv);
|
||
chatMessages.scrollTop = chatMessages.scrollHeight;
|
||
}
|
||
|
||
// 更新连接信息
|
||
function updateConnectionInfo() {
|
||
if (peerConnection) {
|
||
document.getElementById('connectionState').textContent = peerConnection.connectionState;
|
||
document.getElementById('iceState').textContent = peerConnection.iceConnectionState;
|
||
|
||
// 获取统计信息
|
||
peerConnection.getStats().then(stats => {
|
||
let videoCodec = '-';
|
||
let audioCodec = '-';
|
||
let statsText = '';
|
||
|
||
stats.forEach(report => {
|
||
if (report.type === 'codec') {
|
||
if (report.mimeType && report.mimeType.includes('video')) {
|
||
videoCodec = report.mimeType.split('/')[1];
|
||
} else if (report.mimeType && report.mimeType.includes('audio')) {
|
||
audioCodec = report.mimeType.split('/')[1];
|
||
}
|
||
}
|
||
|
||
statsText += `${report.type}: ${JSON.stringify(report, null, 2)}\n\n`;
|
||
});
|
||
|
||
document.getElementById('videoCodec').textContent = videoCodec;
|
||
document.getElementById('audioCodec').textContent = audioCodec;
|
||
document.getElementById('rtcStats').textContent = statsText;
|
||
});
|
||
}
|
||
}
|
||
|
||
// 聊天输入回车发送
|
||
chatInput.addEventListener('keypress', (e) => {
|
||
if (e.key === 'Enter') {
|
||
sendMessage();
|
||
}
|
||
});
|
||
|
||
// 页面卸载时清理资源
|
||
window.addEventListener('beforeunload', () => {
|
||
disconnectFromRoom();
|
||
});
|
||
|
||
// 检查浏览器兼容性
|
||
if (!navigator.mediaDevices || !window.RTCPeerConnection) {
|
||
alert('您的浏览器不支持WebRTC功能,请使用Chrome、Firefox、Safari或Edge最新版本。');
|
||
}
|
||
</script>
|
||
{{end}}
|