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

news2025/6/10 9:54:07

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

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2406475.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

免费批量Markdown转Word工具

免费批量Markdown转Word工具 一款简单易用的批量Markdown文档转换工具&#xff0c;支持将多个Markdown文件一键转换为Word文档。完全免费&#xff0c;无需安装&#xff0c;解压即用&#xff01; 官方网站 访问官方展示页面了解更多信息&#xff1a;http://mutou888.com/pro…

以太网PHY布局布线指南

1. 简介 对于以太网布局布线遵循以下准则很重要&#xff0c;因为这将有助于减少信号发射&#xff0c;最大程度地减少噪声&#xff0c;确保器件作用&#xff0c;最大程度地减少泄漏并提高信号质量。 2. PHY设计准则 2.1 DRC错误检查 首先检查DRC规则是否设置正确&#xff0c;然…

若依项目部署--传统架构--未完待续

若依项目介绍 项目源码获取 #Git工具下载 dnf -y install git #若依项目获取 git clone https://gitee.com/y_project/RuoYi-Vue.git项目背景 随着企业信息化需求的增加&#xff0c;传统开发模式存在效率低&#xff0c;重复劳动多等问题。若依项目通过整合主流技术框架&…

华为云Flexus+DeepSeek征文 | 基于Dify构建具备联网搜索能力的知识库问答助手

华为云FlexusDeepSeek征文 | 基于Dify构建具备联网搜索能力的知识库问答助手 一、构建知识库问答助手引言二、构建知识库问答助手环境2.1 基于FlexusX实例的Dify平台2.2 基于MaaS的模型API商用服务 三、构建知识库问答助手实战3.1 配置Dify环境3.2 创建知识库问答助手3.3 使用知…

MeanFlow:何凯明新作,单步去噪图像生成新SOTA

1.简介 这篇文章介绍了一种名为MeanFlow的新型生成模型框架&#xff0c;旨在通过单步生成过程高效地将先验分布转换为数据分布。文章的核心创新在于引入了平均速度的概念&#xff0c;这一概念的引入使得模型能够通过单次函数评估完成从先验分布到数据分布的转换&#xff0c;显…

【Vue】scoped+组件通信+props校验

【scoped作用及原理】 【作用】 默认写在组件中style的样式会全局生效, 因此很容易造成多个组件之间的样式冲突问题 故而可以给组件加上scoped 属性&#xff0c; 令样式只作用于当前组件的标签 作用&#xff1a;防止不同vue组件样式污染 【原理】 给组件加上scoped 属性后…

构建Docker镜像的Dockerfile文件详解

文章目录 前言Dockerfile 案例docker build1. 基本构建2. 指定 Dockerfile 路径3. 设置构建时变量4. 不使用缓存5. 删除中间容器6. 拉取最新基础镜像7. 静默输出完整示例 docker runDockerFile 入门syntax指定构造器FROM基础镜像RUN命令注释COPY复制ENV设置环境变量EXPOSE暴露端…

从0开始学习R语言--Day17--Cox回归

Cox回归 在用医疗数据作分析时&#xff0c;最常见的是去预测某类病的患者的死亡率或预测他们的结局。但是我们得到的病人数据&#xff0c;往往会有很多的协变量&#xff0c;即使我们通过计算来减少指标对结果的影响&#xff0c;我们的数据中依然会有很多的协变量&#xff0c;且…

ABAP设计模式之---“Tell, Don’t Ask原则”

“Tell, Don’t Ask”是一种重要的面向对象编程设计原则&#xff0c;它强调的是对象之间如何有效地交流和协作。 1. 什么是 Tell, Don’t Ask 原则&#xff1f; 这个原则的核心思想是&#xff1a; “告诉一个对象该做什么&#xff0c;而不是询问一个对象的状态再对它作出决策。…

虚拟机网络不通的问题(这里以win10的问题为主,模式NAT)

当我们网关配置好了&#xff0c;DNS也配置好了&#xff0c;最后在虚拟机里还是无法访问百度的网址。 第一种情况&#xff1a; 我们先考虑一下&#xff0c;网关的IP是否和虚拟机编辑器里的IP一样不&#xff0c;如果不一样需要更改一下&#xff0c;因为我们访问百度需要从物理机…

SOC-ESP32S3部分:30-I2S音频-麦克风扬声器驱动

飞书文档https://x509p6c8to.feishu.cn/wiki/SKZzwIRH3i7lsckUOlzcuJsdnVf I2S简介 I2S&#xff08;Inter-Integrated Circuit Sound&#xff09;是一种用于传输数字音频数据的通信协议&#xff0c;广泛应用于音频设备中。 ESP32-S3 包含 2 个 I2S 外设&#xff0c;通过配置…

break 语句和 continue 语句

break语句和continue语句都具有跳转作用&#xff0c;可以让代码不按既有的顺序执行 break break语句用于跳出代码块或循环 1 2 3 4 5 6 for (var i 0; i < 5; i) { if (i 3){ break; } console.log(i); } continue continue语句用于立即终…

【Linux】使用1Panel 面板让服务器定时自动执行任务

服务器就是一台24小时开机的主机&#xff0c;相比自己家中不定时开关机的主机更适合完成定时任务&#xff0c;例如下载资源、备份上传&#xff0c;或者登录某个网站执行一些操作&#xff0c;只需要编写 脚本&#xff0c;然后让服务器定时来执行这个脚本就可以。 有很多方法实现…

Excel 怎么让透视表以正常Excel表格形式显示

目录 1、创建数据透视表 2、设计 》报表布局 》以表格形式显示 3、设计 》分类汇总 》不显示分类汇总 1、创建数据透视表 2、设计 》报表布局 》以表格形式显示 3、设计 》分类汇总 》不显示分类汇总

LINUX编译vlc

下载 VideoLAN / VLC GitLab 选择最新的发布版本 准备 sudo apt install -y xcb bison sudo apt install -y autopoint sudo apt install -y autoconf automake libtool编译ffmpeg LINUX FFMPEG编译汇总&#xff08;最简化&#xff09;_底部的附件列表中】: ffmpeg - lzip…

WinUI3开发_使用mica效果

简介 Mica(云母)是Windows10/11上的一种现代化效果&#xff0c;是Windows10/11上所使用的Fluent Design(设计语言)里的一个效果&#xff0c;Windows10/11上所使用的Fluent Design皆旨在于打造一个人类、通用和真正感觉与 Windows 一样的设计。 WinUI3就是Windows10/11上的一个…

Python爬虫(52)Scrapy-Redis分布式爬虫架构实战:IP代理池深度集成与跨地域数据采集

目录 一、引言&#xff1a;当爬虫遭遇"地域封锁"二、背景解析&#xff1a;分布式爬虫的两大技术挑战1. 传统Scrapy架构的局限性2. 地域限制的三种典型表现 三、架构设计&#xff1a;Scrapy-Redis 代理池的协同机制1. 分布式架构拓扑图2. 核心组件协同流程 四、技术实…

华为OD机考- 简单的自动曝光/平均像素

import java.util.Arrays; import java.util.Scanner;public class DemoTest4 {public static void main(String[] args) {Scanner in new Scanner(System.in);// 注意 hasNext 和 hasNextLine 的区别while (in.hasNextLine()) { // 注意 while 处理多个 caseint[] arr Array…

Spring是如何实现无代理对象的循环依赖

无代理对象的循环依赖 什么是循环依赖解决方案实现方式测试验证 引入代理对象的影响创建代理对象问题分析 源码见&#xff1a;mini-spring 什么是循环依赖 循环依赖是指在对象创建过程中&#xff0c;两个或多个对象相互依赖&#xff0c;导致创建过程陷入死循环。以下通过一个简…

C++ Saucer 编写Windows桌面应用

文章目录 一、背景二、Saucer 简介核心特性典型应用场景 三、生成自己的项目四、以Win32项目方式构建Win32项目禁用最大化按钮 五、总结 一、背景 使用Saucer框架&#xff0c;开发Windows桌面应用&#xff0c;把一个html页面作为GUI设计放到Saucer里&#xff0c;隐藏掉运行时弹…