java 局域网 rtsp 取流 WebSocket 推送到前端显示 低延迟

发布于:2025-06-09 ⋅ 阅读:(21) ⋅ 点赞:(0)

众所周知 摄像头取流推流显示前端延迟大

传统方法是服务器取摄像头的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>


网站公告

今日签到

点亮在社区的每一天
去签到