最近接了一个需求,后端给一个webrtc的流的地址,然后前端展示出来,还需要实时的更新状态,状态这个好更新,后端给了一个接口,只要成功就对了,但是这个webrtc怎么展示,不太会,查看文档,新的api,RTCPeerConnection,浏览器自带的,通过new创建,然后展示到video元素上,具体的注释代码都有,直接复制粘贴即可使用,希望能帮助到大家。
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>WebRTC流媒体服务器监控</title>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<style>
:root {
--primary: #1a2035;
--secondary: #252b42;
--accent: #4a6cf7;
--success: #2ecc71;
--warning: #f39c12;
--danger: #e74c3c;
--text: #e4e8f0;
--card-bg: rgba(37, 43, 66, 0.8);
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
}
body {
background: linear-gradient(135deg, var(--primary), #0d1220);
color: var(--text);
min-height: 100vh;
padding: 20px;
}
.container {
max-width: 1400px;
margin: 0 auto;
display: grid;
grid-template-columns: 1fr 1fr;
gap: 20px;
}
header {
grid-column: 1 / -1;
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
padding: 15px 20px;
background: var(--card-bg);
border-radius: 10px;
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2);
}
h1 {
font-size: 2.2rem;
display: flex;
align-items: center;
gap: 15px;
}
h1 i {
color: var(--accent);
}
.server-address {
background: rgba(0, 0, 0, 0.3);
padding: 8px 15px;
border-radius: 5px;
font-family: monospace;
font-size: 0.9rem;
}
.status-bar {
display: flex;
gap: 15px;
}
.status-indicator {
display: flex;
align-items: center;
gap: 5px;
padding: 5px 10px;
background: rgba(0, 0, 0, 0.3);
border-radius: 20px;
}
.status-dot {
width: 10px;
height: 10px;
border-radius: 50%;
}
.status-active {
background: var(--success);
}
.status-warning {
background: var(--warning);
}
.status-inactive {
background: var(--danger);
}
.dashboard {
display: flex;
flex-direction: column;
gap: 20px;
}
.card {
background: var(--card-bg);
border-radius: 10px;
padding: 20px;
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2);
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 15px;
padding-bottom: 10px;
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
}
.card-title {
font-size: 1.2rem;
display: flex;
align-items: center;
gap: 10px;
}
.metric {
display: flex;
justify-content: space-between;
margin-bottom: 10px;
padding: 8px 0;
border-bottom: 1px dashed rgba(255, 255, 255, 0.1);
}
.metric-value {
font-weight: bold;
color: var(--accent);
}
.video-container {
position: relative;
width: 100%;
background: #000;
border-radius: 10px;
overflow: hidden;
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2);
min-height: 400px;
display: flex;
align-items: center;
justify-content: center;
}
#video {
width: 100%;
max-height: 70vh;
background: #000;
}
.video-placeholder {
position: absolute;
color: #555;
text-align: center;
z-index: 1;
padding: 20px;
}
.video-placeholder i {
font-size: 4rem;
margin-bottom: 20px;
color: #333;
}
.controls {
display: flex;
gap: 10px;
margin-top: 15px;
}
button {
background: var(--secondary);
color: var(--text);
border: none;
padding: 12px 20px;
border-radius: 5px;
cursor: pointer;
display: flex;
align-items: center;
gap: 8px;
transition: all 0.3s;
flex: 1;
justify-content: center;
}
button:hover {
background: var(--accent);
transform: translateY(-2px);
}
.btn-connect {
background: var(--success);
}
.btn-disconnect {
background: var(--danger);
}
.stream-info {
margin-top: 15px;
padding: 15px;
background: rgba(0, 0, 0, 0.2);
border-radius: 5px;
}
.info-line {
display: flex;
justify-content: space-between;
margin-bottom: 8px;
}
.info-label {
color: #aaa;
}
.info-value {
font-weight: bold;
}
.loading {
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
gap: 20px;
padding: 40px;
}
.spinner {
width: 50px;
height: 50px;
border: 5px solid rgba(255, 255, 255, 0.1);
border-radius: 50%;
border-top-color: var(--accent);
animation: spin 1s linear infinite;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
.api-response {
margin-top: 20px;
padding: 15px;
background: rgba(0, 0, 0, 0.2);
border-radius: 5px;
max-height: 200px;
overflow-y: auto;
font-family: monospace;
font-size: 0.85rem;
}
.error-box {
background: rgba(231, 76, 60, 0.2);
border-left: 4px solid var(--danger);
padding: 10px 15px;
margin: 10px 0;
border-radius: 4px;
}
.debug-info {
margin-top: 20px;
padding: 15px;
background: rgba(0, 0, 0, 0.2);
border-radius: 5px;
font-size: 0.9rem;
}
.debug-title {
font-weight: bold;
margin-bottom: 10px;
display: flex;
align-items: center;
gap: 8px;
}
.solution-list {
margin: 10px 0;
padding-left: 20px;
}
.solution-list li {
margin-bottom: 8px;
}
.stat-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 15px;
margin-top: 15px;
}
.stat-item {
background: rgba(0, 0, 0, 0.2);
padding: 15px;
border-radius: 8px;
text-align: center;
}
.stat-value {
font-size: 1.8rem;
font-weight: bold;
color: var(--accent);
margin: 10px 0;
}
.stat-label {
font-size: 0.9rem;
color: #aaa;
}
@media (max-width: 1024px) {
.container {
grid-template-columns: 1fr;
}
header {
flex-direction: column;
gap: 15px;
text-align: center;
}
.status-bar {
justify-content: center;
}
}
@media (max-width: 768px) {
h1 {
font-size: 1.8rem;
}
.status-bar {
flex-wrap: wrap;
justify-content: center;
}
.controls {
flex-direction: column;
}
.stat-grid {
grid-template-columns: 1fr 1fr;
}
}
</style>
</head>
<body>
<div class="container">
<header>
<h1><i class="fas fa-broadcast-tower"></i> WebRTC流媒体服务器监控</h1>
<div class="server-address">
<i class="fas fa-link"></i> API: 自己的接口拿到的状态api
</div>
<div class="status-bar">
<div class="status-indicator">
<div class="status-dot" id="api-status"></div>
<span>API连接</span>
</div>
<div class="status-indicator">
<div class="status-dot" id="stream-status-indicator"></div>
<span>流服务</span>
</div>
<div class="status-indicator">
<div class="status-dot" id="webrtc-status"></div>
<span>WebRTC</span>
</div>
</div>
</header>
<div class="dashboard">
<div class="card">
<div class="card-header">
<div class="card-title"><i class="fas fa-microchip"></i> 服务器状态</div>
</div>
<div class="stat-grid">
<div class="stat-item">
<i class="fas fa-microchip"></i>
<div class="stat-value" id="server-pid">-</div>
<div class="stat-label">进程ID</div>
</div>
<div class="stat-item">
<i class="fas fa-server"></i>
<div class="stat-value" id="server-name">-</div>
<div class="stat-label">服务器名称</div>
</div>
<div class="stat-item">
<i class="fas fa-cogs"></i>
<div class="stat-value" id="server-service">-</div>
<div class="stat-label">服务</div>
</div>
<div class="stat-item">
<i class="fas fa-code"></i>
<div class="stat-value" id="server-code">-</div>
<div class="stat-label">状态码</div>
</div>
</div>
</div>
<div class="card">
<div class="card-header">
<div class="card-title"><i class="fas fa-info-circle"></i> API信息</div>
</div>
<div class="metric">
<span>版本信息:</span>
<span class="metric-value" id="version">接口信息</span>
</div>
<div class="metric">
<span>系统摘要:</span>
<span class="metric-value" id="summaries">接口信息</span>
</div>
<div class="metric">
<span>流管理:</span>
<span class="metric-value" id="streams">接口信息</span>
</div>
<div class="metric">
<span>客户端管理:</span>
<span class="metric-value" id="clients">接口信息</span>
</div>
</div>
<div class="card">
<div class="card-header">
<div class="card-title"><i class="fas fa-network-wired"></i> 系统功能</div>
</div>
<div class="metric">
<span>性能统计:</span>
<span class="metric-value" id="perf">接口信息</span>
</div>
<div class="metric">
<span>内存信息:</span>
<span class="metric-value" id="meminfos">接口信息</span>
</div>
<div class="metric">
<span>虚拟主机:</span>
<span class="metric-value" id="vhosts">接口信息</span>
</div>
<div class="metric">
<span>功能特性:</span>
<span class="metric-value" id="features">接口信息</span>
</div>
</div>
<div class="card">
<div class="card-header">
<div class="card-title"><i class="fas fa-code"></i> 原始API响应</div>
</div>
<div class="api-response" id="api-response">
// API响应数据将显示在这里
</div>
</div>
</div>
<div class="card">
<div class="card-header">
<div class="card-title"><i class="fas fa-video"></i> WebRTC流播放器</div>
</div>
<div class="video-container">
<video id="video" autoplay playsinline muted></video>
<div class="video-placeholder" id="video-placeholder">
<i class="fas fa-satellite-dish"></i>
<p>等待连接WebRTC流</p>
<p>WHEP URL: webrtc的流,后端给的链接</p>
</div>
</div>
<div class="controls">
<button class="btn-connect" id="connect-btn">
<i class="fas fa-plug"></i> 连接流
</button>
<button class="btn-disconnect" id="disconnect-btn" disabled>
<i class="fas fa-ban"></i> 断开连接
</button>
<button id="refresh-btn">
<i class="fas fa-sync-alt"></i> 刷新状态
</button>
</div>
<div class="stream-info">
<div class="info-line">
<span class="info-label">流状态:</span>
<span class="info-value" id="stream-status">未连接</span>
</div>
<div class="info-line">
<span class="info-label">连接方式:</span>
<span class="info-value" id="connection-type">-</span>
</div>
<div class="info-line">
<span class="info-label">ICE状态:</span>
<span class="info-value" id="ice-state">-</span>
</div>
<div class="info-line">
<span class="info-label">信令状态:</span>
<span class="info-value" id="signaling-state">-</span>
</div>
</div>
<div class="debug-info">
<div class="debug-title"><i class="fas fa-bug"></i> 连接问题诊断</div>
<p>如果您遇到断开后重新连接视频不显示的问题,请尝试:</p>
<ol class="solution-list">
<li>完全刷新页面(Ctrl+F5)清除WebRTC状态</li>
<li>检查浏览器控制台是否有错误信息</li>
<li>确认服务器端流是否仍在运行</li>
<li>尝试使用Chrome浏览器,它对WebRTC支持最好</li>
</ol>
<div class="error-box" id="error-info" style="display: none;">
<strong>错误详情:</strong> <span id="error-message"></span>
</div>
</div>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', function () {
const connectBtn = document.getElementById('connect-btn');
const disconnectBtn = document.getElementById('disconnect-btn');
const refreshBtn = document.getElementById('refresh-btn');
const videoElement = document.getElementById('video');
const videoPlaceholder = document.getElementById('video-placeholder');
const streamStatus = document.getElementById('stream-status');
const apiResponseElement = document.getElementById('api-response');
const connectionType = document.getElementById('connection-type');
const iceState = document.getElementById('ice-state');
const signalingState = document.getElementById('signaling-state');
const errorInfo = document.getElementById('error-info');
const errorMessage = document.getElementById('error-message');
const apiStatus = document.getElementById('api-status');
const streamStatusIndicator = document.getElementById('stream-status-indicator');
const webrtcStatus = document.getElementById('webrtc-status');
let pc = null; // WebRTC对等连接对象
let streamConnected = false;
let apiData = null;
// API端点
const API_BASE_URL = '状态的api';
const WHEP_URL = 'webrtc的流';
// 显示错误信息
function showError(message) {
errorMessage.textContent = message;
errorInfo.style.display = 'block';
console.error('Error:', message);
}
// 隐藏错误信息
function hideError() {
errorInfo.style.display = 'none';
}
// 更新状态指示器
function updateStatusIndicators() {
// API连接状态
if (apiData) {
apiStatus.className = 'status-dot status-active';
} else {
apiStatus.className = 'status-dot status-inactive';
}
// 流服务状态
if (apiData && apiData.code === 0) {
streamStatusIndicator.className = 'status-dot status-active';
} else {
streamStatusIndicator.className = 'status-dot status-inactive';
}
// WebRTC状态
if (streamConnected) {
webrtcStatus.className = 'status-dot status-active';
} else {
webrtcStatus.className = 'status-dot status-inactive';
}
}
// 从API获取数据
async function fetchData() {
try {
hideError();
const response = await fetch(API_BASE_URL, {
method: 'GET',
mode: 'cors',
headers: {
'Accept': 'application/json',
}
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
apiData = await response.json();
updateUI(apiData);
updateStatusIndicators();
return true;
} catch (error) {
console.error('获取API数据失败:', error);
showError('获取API数据失败: ' + error.message);
apiData = null;
updateStatusIndicators();
return false;
}
}
// 更新UI显示
function updateUI(data) {
// 显示原始API响应
apiResponseElement.textContent = JSON.stringify(data, null, 2);
// 更新服务器信息
document.getElementById('server-pid').textContent = data.pid || '-';
document.getElementById('server-name').textContent = data.server || '-';
document.getElementById('server-service').textContent = data.service || '-';
document.getElementById('server-code').textContent = data.code || '-';
}
// 完全重置WebRTC连接
function resetWebRTC() {
if (pc) {
// 关闭所有轨道
if (videoElement.srcObject) {
videoElement.srcObject.getTracks().forEach(track => {
track.stop();
});
}
// 关闭连接
pc.close();
pc = null;
}
// 重置视频元素
videoElement.srcObject = null;
videoElement.removeAttribute('src');
videoElement.load();
// 重置状态
streamConnected = false;
iceState.textContent = '-';
signalingState.textContent = '-';
console.log('WebRTC连接已完全重置');
}
// 连接WebRTC流
async function connectStream() {
if (streamConnected) return;
streamStatus.textContent = '连接中...';
videoPlaceholder.style.display = 'flex';
videoPlaceholder.innerHTML = '<div class="loading"><div class="spinner"></div><p>正在连接WebRTC流...</p></div>';
hideError();
try {
// 先完全重置之前的连接
resetWebRTC();
// 1. 创建新的RTCPeerConnection(配置STUN服务器)
pc = new RTCPeerConnection({
iceServers: [{ urls: 'stun:stun.l.google.com:19302' }]
});
// 2. 监听视频轨道并添加到video元素
pc.ontrack = (event) => {
console.log('收到轨道:', event.track.kind);
if (event.track.kind === 'video') {
videoElement.srcObject = event.streams[0];
streamConnected = true;
videoPlaceholder.style.display = 'none';
videoElement.style.display = 'block';
streamStatus.textContent = '已连接';
connectionType.textContent = 'WebRTC (WHEP)';
connectBtn.disabled = true;
disconnectBtn.disabled = false;
updateStatusIndicators();
console.log('视频轨道已添加,播放开始');
}
};
// 监听连接状态变化
pc.oniceconnectionstatechange = () => {
iceState.textContent = pc.iceConnectionState;
console.log('ICE连接状态:', pc.iceConnectionState);
};
pc.onsignalingstatechange = () => {
signalingState.textContent = pc.signalingState;
console.log('信令状态:', pc.signalingState);
};
pc.onconnectionstatechange = () => {
console.log('连接状态:', pc.connectionState);
};
// 3. 创建Offer并发送给WHEP服务器
const offer = await pc.createOffer({ offerToReceiveVideo: true });
await pc.setLocalDescription(offer);
console.log('已创建本地SDP Offer');
// 4. 将SDP Offer通过HTTP POST发送到WHEP端点
const response = await fetch(WHEP_URL, {
method: 'POST',
body: pc.localDescription.sdp,
headers: { 'Content-Type': 'application/sdp' }
});
if (!response.ok) {
const errorText = await response.text();
throw new Error(`WHEP错误! 状态: ${response.status}, 响应: ${errorText}`);
}
const sdpAnswer = await response.text();
console.log('已收到SDP Answer');
// 5. 将服务器返回的SDP Answer设置为远程描述
await pc.setRemoteDescription({ type: 'answer', sdp: sdpAnswer });
console.log('WebRTC连接已建立');
} catch (error) {
console.error('连接错误:', error);
streamStatus.textContent = '连接失败';
showError('连接失败: ' + error.message);
videoPlaceholder.innerHTML = `
<i class="fas fa-exclamation-triangle"></i>
<p>连接失败: ${error.message}</p>
`;
// 重置连接状态
resetWebRTC();
connectBtn.disabled = false;
disconnectBtn.disabled = true;
updateStatusIndicators();
}
}
// 断开WebRTC流
function disconnectStream() {
if (!streamConnected) return;
// 完全重置WebRTC连接
resetWebRTC();
// 隐藏视频,显示占位符
videoElement.style.display = 'none';
videoPlaceholder.style.display = 'flex';
videoPlaceholder.innerHTML = `
<i class="fas fa-satellite-dish"></i>
<p>等待连接WebRTC流</p>
`;
streamStatus.textContent = '未连接';
connectionType.textContent = '-';
connectBtn.disabled = false;
disconnectBtn.disabled = true;
updateStatusIndicators();
console.log('WebRTC连接已断开');
}
// 刷新服务器状态
async function refreshStats() {
refreshBtn.innerHTML = '<i class="fas fa-sync-alt fa-spin"></i> 刷新中...';
refreshBtn.disabled = true;
const success = await fetchData();
refreshBtn.innerHTML = '<i class="fas fa-sync-alt"></i> 刷新状态';
refreshBtn.disabled = false;
if (!success) {
showError('获取服务器状态失败,请检查API地址和网络连接');
}
}
// 添加事件监听器
connectBtn.addEventListener('click', connectStream);
disconnectBtn.addEventListener('click', disconnectStream);
refreshBtn.addEventListener('click', refreshStats);
// 初始化
refreshStats();
});
</script>
</body>
</html>