201 lines
7.7 KiB
HTML
201 lines
7.7 KiB
HTML
|
|
<!DOCTYPE html>
|
|
<html>
|
|
<head>
|
|
<title>Multi-Stream WebRTC RTSP Player</title>
|
|
<style>
|
|
.video-container {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fit, minmax(320px, 1fr));
|
|
gap: 10px;
|
|
padding: 10px;
|
|
}
|
|
.video-wrapper {
|
|
position: relative;
|
|
}
|
|
video {
|
|
width: 100%;
|
|
height: auto;
|
|
border: 1px solid #ccc;
|
|
}
|
|
.stream-name {
|
|
position: absolute;
|
|
top: 10px;
|
|
left: 10px;
|
|
background: rgba(0,0,0,0.7);
|
|
color: white;
|
|
padding: 5px;
|
|
border-radius: 3px;
|
|
}
|
|
.stream-status {
|
|
position: absolute;
|
|
bottom: 10px;
|
|
right: 10px;
|
|
background: rgba(0,0,0,0.7);
|
|
color: white;
|
|
padding: 5px;
|
|
border-radius: 3px;
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="video-container" id="videoContainer"></div>
|
|
<script>
|
|
// 配置多个流
|
|
const streams = [
|
|
{ url: 'rtsp://10.0.0.17:8554/camera_test/2', name: 'Camera_2_A' },
|
|
{ url: 'rtsp://10.0.0.17:8554/camera_test/2', name: 'Camera_2_B' },
|
|
{ url: 'rtsp://10.0.0.17:8554/camera_test/2', name: 'Camera_2_C' },
|
|
// 添加更多流
|
|
];
|
|
|
|
function createVideoElement(streamConfig) {
|
|
const wrapper = document.createElement('div');
|
|
wrapper.className = 'video-wrapper';
|
|
|
|
const video = document.createElement('video');
|
|
video.autoplay = true;
|
|
video.playsinline = true;
|
|
video.id = `video-${streamConfig.name}`;
|
|
|
|
const nameLabel = document.createElement('div');
|
|
nameLabel.className = 'stream-name';
|
|
nameLabel.textContent = streamConfig.name;
|
|
|
|
const statusLabel = document.createElement('div');
|
|
statusLabel.className = 'stream-status';
|
|
statusLabel.style.position = 'absolute';
|
|
statusLabel.style.bottom = '10px';
|
|
statusLabel.style.right = '10px';
|
|
statusLabel.style.background = 'rgba(0,0,0,0.7)';
|
|
statusLabel.style.color = 'white';
|
|
statusLabel.style.padding = '5px';
|
|
statusLabel.style.borderRadius = '3px';
|
|
statusLabel.textContent = 'Connecting...';
|
|
|
|
wrapper.appendChild(video);
|
|
wrapper.appendChild(nameLabel);
|
|
wrapper.appendChild(statusLabel);
|
|
document.getElementById('videoContainer').appendChild(wrapper);
|
|
|
|
return { video, statusLabel };
|
|
}
|
|
|
|
async function startStream(streamConfig) {
|
|
const { video, statusLabel } = createVideoElement(streamConfig);
|
|
const pc = new RTCPeerConnection();
|
|
|
|
const maxRetries = 3;
|
|
let retryCount = 0;
|
|
|
|
// 添加更详细的连接状态监控
|
|
pc.onconnectionstatechange = () => {
|
|
const state = pc.connectionState;
|
|
statusLabel.textContent = `Connection: ${state}`;
|
|
console.log(`[${streamConfig.name}] Connection state changed to: ${state}`);
|
|
};
|
|
|
|
pc.oniceconnectionstatechange = () => {
|
|
const state = pc.iceConnectionState;
|
|
console.log(`[${streamConfig.name}] ICE state changed to: ${state}`);
|
|
};
|
|
|
|
pc.onicegatheringstatechange = () => {
|
|
console.log(`[${streamConfig.name}] ICE gathering state: ${pc.iceGatheringState}`);
|
|
};
|
|
|
|
pc.onicecandidate = event => {
|
|
if (event.candidate) {
|
|
console.log(`[${streamConfig.name}] New ICE candidate: ${event.candidate.candidate}`);
|
|
}
|
|
};
|
|
|
|
// 添加更详细的轨道监控
|
|
pc.ontrack = (event) => {
|
|
console.log(`[${streamConfig.name}] Track received:`, event.track);
|
|
console.log(`Track settings:`, event.track.getSettings());
|
|
console.log(`Track constraints:`, event.track.getConstraints());
|
|
|
|
if (event.track.kind === 'video') {
|
|
video.srcObject = event.streams[0];
|
|
video.onloadedmetadata = () => {
|
|
console.log(`[${streamConfig.name}] Video metadata loaded`);
|
|
statusLabel.textContent = 'Playing';
|
|
video.play().catch(e => {
|
|
console.error(`[${streamConfig.name}] Play error:`, e);
|
|
statusLabel.textContent = 'Play failed';
|
|
});
|
|
};
|
|
|
|
// 添加视频元素事件监听
|
|
video.onplay = () => console.log(`[${streamConfig.name}] Video started playing`);
|
|
video.onpause = () => console.log(`[${streamConfig.name}] Video paused`);
|
|
video.onwaiting = () => console.log(`[${streamConfig.name}] Video buffering`);
|
|
video.onerror = () => console.log(`[${streamConfig.name}] Video error:`, video.error);
|
|
}
|
|
};
|
|
|
|
const tryConnect = async () => {
|
|
try {
|
|
statusLabel.textContent = 'Creating offer...';
|
|
const offer = await pc.createOffer({
|
|
offerToReceiveVideo: true,
|
|
offerToReceiveAudio: false
|
|
});
|
|
|
|
statusLabel.textContent = 'Setting local description...';
|
|
await pc.setLocalDescription(offer);
|
|
|
|
statusLabel.textContent = 'Sending offer to server...';
|
|
const response = await fetch('http://localhost:8080/offer', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
},
|
|
body: JSON.stringify({
|
|
offer: offer,
|
|
streamConfig: streamConfig
|
|
})
|
|
});
|
|
|
|
if (!response.ok) {
|
|
const errorText = await response.text();
|
|
throw new Error(`HTTP error! status: ${response.status}, message: ${errorText}`);
|
|
}
|
|
|
|
statusLabel.textContent = 'Processing server response...';
|
|
const answer = await response.json();
|
|
|
|
statusLabel.textContent = 'Setting remote description...';
|
|
await pc.setRemoteDescription(answer);
|
|
|
|
console.log(`[${streamConfig.name}] Connection setup completed`);
|
|
} catch (e) {
|
|
console.error(`[${streamConfig.name}] Error:`, e);
|
|
statusLabel.textContent = `Error: ${e.message}`;
|
|
|
|
if (retryCount < maxRetries) {
|
|
retryCount++;
|
|
statusLabel.textContent = `Retrying (${retryCount}/${maxRetries})...`;
|
|
await new Promise(resolve => setTimeout(resolve, 2000)); // 等待2秒后重试
|
|
return tryConnect();
|
|
}
|
|
throw e;
|
|
}
|
|
};
|
|
|
|
try {
|
|
await tryConnect();
|
|
} catch (e) {
|
|
statusLabel.textContent = 'Failed to connect';
|
|
console.error(`[${streamConfig.name}] Final error:`, e);
|
|
}
|
|
}
|
|
|
|
// 启动所有流
|
|
streams.forEach(stream => startStream(stream));
|
|
</script>
|
|
</body>
|
|
</html>
|
|
|
|
|