众所周知 摄像头取流推流显示前端延迟大
传统方法是服务器取摄像头的rtsp流 然后客户端连服务器
中转多了,延迟一定不小。
假设相机没有专网
公网 1相机自带推流 直接推送到云服务器 然后客户端拉去
2相机只有rtsp ,边缘服务器拉流推送到云服务器
私网 情况类似
但是我想能不能直接点对点
于是(我这边按照这个参数可以到和大华相机,海康相机web预览的画面实时的延迟速度)
import lombok.extern.slf4j.Slf4j;
import org.bytedeco.ffmpeg.global.avutil;
import org.bytedeco.javacv.FFmpegFrameGrabber;
import org.bytedeco.javacv.Frame;
import org.bytedeco.javacv.Java2DFrameConverter;
import org.springframework.scheduling.annotation.Async;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.stereotype.Component;
import javax.imageio.ImageIO;
import java.awt.image.BufferedImage;
import java.io.ByteArrayOutputStream;
import java.util.HashMap;
/**
* 获取rtsp流,抓取每帧,通过websocket传递给前台显示
*/
@Slf4j
@Component
@EnableAsync
public class RTSPToImage {
public static String urls="";
public static String pds="0";
/**
* 异步开启获取rtsp流,通过websocket传输数据
*/
@Async
public void live(String rtspUrl) {
rtspUrl="rtsp://admin:admin123@192.168.1.108:554/cam/realmonitor?channel=1&subtype=0";//大华rtsp取流地址
if(urls.equals(rtspUrl)) {
return;
}else {
pds="1";
}
FFmpegFrameGrabber grabber = null;
try {
grabber = new FFmpegFrameGrabber(rtspUrl);
grabber.setVideoCodecName("H.264");
grabber.setFrameRate(grabber.getFrameRate());
grabber.setImageWidth(960);//宽高设置小一点,否则会有延迟
grabber.setImageHeight(540);
// rtsp格式一般添加TCP配置,否则丢帧会比较严重
grabber.setOption("rtsp_transport", "tcp"); // 使用TCP传输方式,避免丢包
//grabber.setOption("buffer_size", "1024"); // 设置缓冲区大小为1MB,提高流畅度
grabber.setOption("rtsp_flags", "prefer_tcp"); // 设置优先使用TCP方式传输
//设置帧率
grabber.setFrameRate(25);
//设置视频bit率
grabber.setVideoBitrate(3000000);
//设置日志等级
avutil.av_log_set_level(avutil.AV_LOG_ERROR);
grabber.start();
log.info("创建并启动grabber成功");
}catch (Exception e){
e.printStackTrace();
}
pds="0";
//推送图片
Java2DFrameConverter java2DFrameConverter = new Java2DFrameConverter();
while (true) {
try {
if(pds.equals("1")){
try {
grabber.stop();
} catch (Exception e1) {
e1.printStackTrace();
} finally {
grabber = null;
}
return;
}
if (grabber != null) {
Frame frame = grabber.grabImage();
if (null == frame) {
continue;
}
BufferedImage bufferedImage =
java2DFrameConverter.getBufferedImage(frame);
ByteArrayOutputStream out = new ByteArrayOutputStream();
ImageIO.write(bufferedImage, "jpg", out);
byte[] imageData = out.toByteArray();
//通过WebSocket推送到前端 WebSocket具体代码网上有
WebSocketServer.sendMessageByObject(out.toByteArray());
// 4. 控制帧率 (30fps)
//Thread.sleep(33);
}
} catch (Exception e) {
e.printStackTrace();
if (grabber != null) {
try {
grabber.stop();
} catch (Exception e1) {
e1.printStackTrace();
} finally {
grabber = null;
}
}
}
}
}
}
前端
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>WebSocket实时图像传输</title>
<style>
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background: linear-gradient(135deg, #1a2a6c, #b21f1f, #1a2a6c);
color: white;
margin: 0;
padding: 20px;
min-height: 100vh;
}
.container {
max-width: 1200px;
margin: 0 auto;
padding: 20px;
}
header {
text-align: center;
margin-bottom: 30px;
padding: 20px;
background: rgba(0, 0, 0, 0.3);
border-radius: 15px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.4);
}
h1 {
margin: 0;
font-size: 2.5rem;
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.5);
}
.subtitle {
font-size: 1.2rem;
opacity: 0.9;
}
.content {
display: flex;
flex-wrap: wrap;
gap: 30px;
}
.video-container {
flex: 1;
min-width: 300px;
background: rgba(0, 0, 0, 0.3);
border-radius: 15px;
padding: 20px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.4);
}
.video-title {
margin-top: 0;
display: flex;
align-items: center;
gap: 10px;
}
.video-title i {
font-size: 1.5rem;
color: #4CAF50;
}
#videoStream {
width: 100%;
background: #000;
border-radius: 8px;
display: block;
aspect-ratio: 4/3;
}
.stats-container {
flex: 1;
min-width: 300px;
background: rgba(0, 0, 0, 0.3);
border-radius: 15px;
padding: 20px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.4);
}
.stat-cards {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 20px;
margin-top: 20px;
}
.stat-card {
background: rgba(255, 255, 255, 0.1);
border-radius: 10px;
padding: 15px;
text-align: center;
}
.stat-value {
font-size: 2rem;
font-weight: bold;
margin: 10px 0;
color: #4CAF50;
}
.stat-label {
font-size: 0.9rem;
opacity: 0.8;
}
.controls {
display: flex;
gap: 15px;
margin-top: 20px;
flex-wrap: wrap;
}
button {
flex: 1;
min-width: 120px;
padding: 12px 20px;
background: #4CAF50;
color: white;
border: none;
border-radius: 8px;
cursor: pointer;
font-size: 1rem;
font-weight: bold;
transition: all 0.3s;
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.3);
}
button:hover {
background: #45a049;
transform: translateY(-2px);
box-shadow: 0 6px 14px rgba(0, 0, 0, 0.4);
}
button:active {
transform: translateY(1px);
}
button.stop {
background: #f44336;
}
button.stop:hover {
background: #d32f2f;
}
.info {
margin-top: 20px;
padding: 15px;
background: rgba(0, 0, 0, 0.3);
border-radius: 10px;
font-size: 0.9rem;
}
.latency-graph {
height: 100px;
background: rgba(0, 0, 0, 0.2);
border-radius: 8px;
margin-top: 20px;
position: relative;
overflow: hidden;
}
.graph-bar {
position: absolute;
bottom: 0;
width: 4px;
background: #4CAF50;
transition: left 0.1s linear;
}
footer {
text-align: center;
margin-top: 40px;
padding: 20px;
font-size: 0.9rem;
opacity: 0.7;
}
@media (max-width: 768px) {
.content {
flex-direction: column;
}
}
</style>
</head>
<body>
<div class="container">
<header>
<h1>WebSocket实时图像传输</h1>
<p class="subtitle">使用二进制数据传输实现高性能视频流</p>
</header>
<div class="content">
<div class="video-container">
<h2 class="video-title">
<i>▶️</i> 实时视频流
</h2>
<img id="videoStream" src="" alt="视频流">
<div class="controls">
<button id="startBtn">开始传输</button>
<button id="stopBtn" class="stop">停止传输</button>
</div>
<div class="info">
<p><strong>技术说明:</strong> 图像数据通过WebSocket以二进制格式传输,前端使用Blob和ObjectURL高效渲染,避免了Base64编码开销。</p>
</div>
</div>
<div class="stats-container">
<h2>性能指标</h2>
<div class="stat-cards">
<div class="stat-card">
<div class="stat-label">帧率 (FPS)</div>
<div id="fps" class="stat-value">0</div>
</div>
<div class="stat-card">
<div class="stat-label">延迟 (ms)</div>
<div id="latency" class="stat-value">0</div>
</div>
<div class="stat-card">
<div class="stat-label">数据大小</div>
<div id="dataSize" class="stat-value">0 KB</div>
</div>
<div class="stat-card">
<div class="stat-label">连接状态</div>
<div id="status" class="stat-value">断开</div>
</div>
</div>
<div class="latency-container">
<div class="stat-label">延迟变化趋势</div>
<div id="latencyGraph" class="latency-graph"></div>
</div>
</div>
</div>
<footer>
<p>WebSocket实时图像传输演示 | 使用Java WebSocket服务端</p>
</footer>
</div>
<script>
// 全局变量
const videoElement = document.getElementById('videoStream');
const startBtn = document.getElementById('startBtn');
const stopBtn = document.getElementById('stopBtn');
const fpsElement = document.getElementById('fps');
const latencyElement = document.getElementById('latency');
const dataSizeElement = document.getElementById('dataSize');
const statusElement = document.getElementById('status');
const latencyGraph = document.getElementById('latencyGraph');
let ws = null;
let frameCount = 0;
let lastFrameTime = 0;
let fps = 0;
let latencyValues = [];
let animationFrameId = null;
// 初始化
function init() {
startBtn.addEventListener('click', startStream);
stopBtn.addEventListener('click', stopStream);
updateStats();
}
// 启动视频流
function startStream() {
if (ws && ws.readyState === WebSocket.OPEN) return;
stopStream(); // 确保先关闭现有连接
// 创建WebSocket连接
ws = new WebSocket('ws://192.168.1.103/ws/10002');
// 设置二进制类型为arraybuffer
ws.binaryType = 'arraybuffer';
// 连接打开
ws.onopen = () => {
statusElement.textContent = '已连接';
statusElement.style.color = '#4CAF50';
lastFrameTime = performance.now();
frameCount = 0;
fps = 0;
latencyValues = [];
clearGraph();
};
// 接收消息
ws.onmessage = (event) => {
const receiveTime = performance.now();
// 处理二进制图像数据
const arrayBuffer = event.data;
const blob = new Blob([arrayBuffer], { type: 'image/jpeg' });
const url = URL.createObjectURL(blob);
// 释放前一个URL的内存
if (videoElement.previousUrl) {
URL.revokeObjectURL(videoElement.previousUrl);
}
videoElement.previousUrl = url;
videoElement.src = url;
// 计算帧率和延迟
frameCount++;
const now = performance.now();
const elapsed = now - lastFrameTime;
if (elapsed >= 1000) {
fps = Math.round((frameCount * 1000) / elapsed);
frameCount = 0;
lastFrameTime = now;
}
// 计算数据大小
const kb = (arrayBuffer.byteLength / 1024).toFixed(1);
// 更新性能指标
fpsElement.textContent = fps;
dataSizeElement.textContent = `${kb} KB`;
// 添加到延迟图表
addLatencyPoint(kb);
};
// 错误处理
ws.onerror = (error) => {
console.error('WebSocket Error:', error);
statusElement.textContent = '错误';
statusElement.style.color = '#f44336';
};
// 连接关闭
ws.onclose = () => {
statusElement.textContent = '已断开';
statusElement.style.color = '#ff9800';
};
}
// 停止视频流
function stopStream() {
if (ws) {
if (ws.readyState === WebSocket.OPEN) {
ws.close();
}
ws = null;
}
if (videoElement.previousUrl) {
URL.revokeObjectURL(videoElement.previousUrl);
videoElement.previousUrl = null;
}
videoElement.src = '';
statusElement.textContent = '已断开';
statusElement.style.color = '#ff9800';
}
// 更新统计信息
function updateStats() {
// 模拟延迟值变化
if (ws && ws.readyState === WebSocket.OPEN && latencyValues.length > 0) {
const avgLatency = latencyValues.reduce((a, b) => a + b, 0) / latencyValues.length;
latencyElement.textContent = avgLatency.toFixed(1);
}
requestAnimationFrame(updateStats);
}
// 添加延迟点
function addLatencyPoint(value) {
latencyValues.push(parseFloat(value));
if (latencyValues.length > 100) {
latencyValues.shift();
}
// 更新图表
updateGraph();
}
// 清除图表
function clearGraph() {
latencyGraph.innerHTML = '';
}
// 更新图表
function updateGraph() {
if (latencyValues.length === 0) return;
const maxValue = Math.max(...latencyValues) * 1.2 || 10;
const graphHeight = latencyGraph.clientHeight;
const barWidth = Math.max(2, latencyGraph.clientWidth / 50);
// 清空现有图表
latencyGraph.innerHTML = '';
// 添加新数据点
latencyValues.forEach((value, index) => {
const barHeight = (value / maxValue) * graphHeight;
const bar = document.createElement('div');
bar.className = 'graph-bar';
bar.style.height = `${barHeight}px`;
bar.style.left = `${index * (barWidth + 1)}px`;
bar.style.width = `${barWidth}px`;
bar.style.backgroundColor = value > maxValue * 0.8 ? '#f44336' : '#4CAF50';
latencyGraph.appendChild(bar);
});
}
// 初始化应用
init();
</script>
</body>
</html>