Files
file-transfer-go/web/templates/video.html
MatrixSeven 70ad644a71 第一版本
2025-07-28 16:33:10 +08:00

572 lines
19 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.
{{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}}