java+vue推rtsp流实现视频播放(由javacv+ffmpg转为vlcj)

一、vlcj

vlcj是一个强大的Java框架,专为VLC媒体播放器设计,让开发者能够轻松地在Java应用中集成专业的媒体播放功能。无论是构建简单的音频播放器还是复杂的视频处理应用,vlcj都提供了丰富的API和工具,帮助你快速实现各种媒体播放需求

二、为什么替换掉javacv+ffmpeg

1、起因

开发过程中的需求,需要调用海康isc平台,海康硬盘录像机、宇视平台、宇视硬盘录像机的接口获取其返回的rtsp流地址,然后在播放出来这个回放流。

2、难点

查阅资料后发现需要本地搭建一个流媒体服务器,然后给流媒体暴露出一个端口,通过websocket+rtsp播放地址实现画面播放,但是整理的各种流媒体播放类以及接口比较多需要安装ffmpeg环境依赖,服务搭建完成后发现本地确实可以运行,但是放到线上后因为现场环境和本地环境差异,效果很差,有很多流播放不了。而且还需要适配各种因播放流格式为h264,或h265转换,如果分辨率不同还是需要再次适配不同的分辨率,比较繁琐,现场也因为环境问题不能升级ffmpeg,所以还是只能用ffmpeg4.x的版本,bug还是比较多的。然后才想换一种方式

三、实现步骤

1、引入vlcj和jna依赖

java 复制代码
        <dependency>
            <groupId>uk.co.caprica</groupId>
            <artifactId>vlcj</artifactId>
            <version>3.12.1</version>
        </dependency>
        <dependency>
            <groupId>net.java.dev.jna</groupId>
            <artifactId>jna</artifactId>
            <version>5.12.1</version>
        </dependency>

注意:使用vlcj依赖为3.x的版本,对应服务器安装的vlc也要是3.x的版本

2、安装vlc

2.1 windows安装

官网地址,进入下载,如下页面

点击下载安装即可,像安装普通软件一样

2.2Linux下载

如果是有桌面操作系统那么就和windows无异,如果是无桌面版本需要执行命令,以Ubuntu为例

bash 复制代码
sudo apt update

sudo apt install vlc vlc-plugin-base vlc-plugin-video-output libvlc-dev -y

安装完成输入命令查看版本

bash 复制代码
vlc --version

如图就是安装好了

注意:如果是root用户因为vlc权限问题是执行不了的,建议不要用root用户

3、代码集成

3.1创建VlcjRtspService

java 复制代码
package com.ruoyi.web.controller.util;

import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import uk.co.caprica.vlcj.discovery.NativeDiscovery;
import uk.co.caprica.vlcj.player.MediaPlayerFactory;
import uk.co.caprica.vlcj.player.headless.HeadlessMediaPlayer;

import javax.annotation.PreDestroy;
import java.util.concurrent.ConcurrentHashMap;

@Slf4j
@Component
public class VlcjRtspService {

    static {
        // 自动发现 VLC 安装路径
        //new NativeDiscovery().discover();

        // 可选:手动指定 VLC 路径(如果自动发现失败)
         System.setProperty("jna.library.path", "C:\\Program Files\\VideoLAN\\VLC");
        //System.setProperty("jna.library.path", "/usr/lib64/vlc");

    }

    private final ConcurrentHashMap<String, HeadlessMediaPlayer> activePlayers = new ConcurrentHashMap<>();
    private final ConcurrentHashMap<String, MediaPlayerFactory> activeFactories = new ConcurrentHashMap<>();

    /**
     * 启动 RTSP 推流
     */
    public boolean startStream(String streamId, String rtspUrl, int flvPort, String flvPath) {
        if (activePlayers.containsKey(streamId)) {
            log.warn("流 {} 已在运行", streamId);
            return false;
        }

        try {
            // 3.x 版本:直接 new MediaPlayerFactory()
            MediaPlayerFactory factory = new MediaPlayerFactory();
            activeFactories.put(streamId, factory);

            // 3.x 版本:直接 newHeadlessMediaPlayer()
            HeadlessMediaPlayer mediaPlayer = factory.newHeadlessMediaPlayer();
            activePlayers.put(streamId, mediaPlayer);

            // 转推选项:添加 keyint=25, bframes=0 强制 I 帧间隔和禁用 B 帧
            String sout = String.format(
                    ":sout=#transcode{vcodec=h264,acodec=none,noaudio,keyint=25,bframes=0}:http{mux=flv,dst=0.0.0.0:%d%s}",
                    flvPort, flvPath
            );

// 播放选项
            String[] options = {
                    "--live-caching=0",
                    "--network-caching=300",
                    "--clock-synchro=0",
                    "--no-audio",
                    "--vout", "dummy",
                    "--demux=ffmpeg",
                    "--codec=avcodec",
                    "--avcodec-threads=2",
                    "--avcodec-hw=none",
                    "--input-repeat=65535",
                    "--rtsp-tcp",
                    "--rtsp-timeout=30",
                    "--sout-keep",
                    "--sout-mux-caching=10000",           // 增加缓存
                    "--fflags=+genpts",                   // 强制生成 PTS
                    "--use-wallclock-as-timestamps",      // 使用系统时间作为时间戳
                    "--no-clock",                         // 禁用内部时钟同步
                    "--verbose=2",
                    sout
            };
            log.info("启动 VLCJ 推流: {} -> http://10.30.127.93:{}{}", rtspUrl, flvPort, flvPath);

            // 3.x 版本:playMedia 方法签名
            mediaPlayer.playMedia(rtspUrl, options);

            // 等待启动
            Thread.sleep(2000);
            if (!mediaPlayer.isPlaying()) {
                log.error("推流启动失败: {}", streamId);
                stopStream(streamId);
                return false;
            }

            log.info("推流启动成功: {}", streamId);
            return true;

        } catch (Exception e) {
            log.error("启动推流失败: {}", e.getMessage(), e);
            stopStream(streamId);
            return false;
        }
    }

    public void stopStream(String streamId) {
        HeadlessMediaPlayer player = activePlayers.remove(streamId);
        MediaPlayerFactory factory = activeFactories.remove(streamId);

        if (player != null) {
            player.stop();
            player.release();
            log.info("停止推流: {}", streamId);
        }

        if (factory != null) {
            factory.release();
        }
    }

    @PreDestroy
    public void destroy() {
        for (String streamId : activePlayers.keySet()) {
            stopStream(streamId);
        }
    }
}

可以直接复制粘贴使用,注意下图:

这个是vlc安装路径所在,我的是国产统信息UOS操作系统所以和Ubuntu存在地址还是不太一样,输入命令查看安装所在地址

bash 复制代码
whereis vlc

输入这个地址就行了

3.2调用方法

java 复制代码
 @GetMapping("getVlcFlv")
    public AjaxResult getVlcFlv(String rtspUrl) {
        // 生成唯一流ID
        String streamId = UUID.randomUUID().toString().replace("-", "");
        //String flvPath = "/" + streamId + ".flv";
        String flvPath = "/1.flv";
        int flvPort = 8088; // 从配置读取

        //String rtspUrl = "rtsp://admin:abcd1234@192.168.10.xx:554/Streaming/Channels/101";

        // 启动 VLCJ 拉流
        boolean started = vlcjRtspService.startStream("1", rtspUrl, flvPort, flvPath);
        if (!started) {
            return AjaxResult.error("启动回放流失败");
        }

        // 返回 HTTP-FLV 地址
        String flvUrl = "http://localhost:" + flvPort + flvPath;
        return AjaxResult.success(flvUrl);
    }

我写了一个测试方法,直接传入rtsp地址,然后可以推流就行了。使用uuid做流地址的一部分是因为摄像仪可能比较多,流也比较多,用来做区分,停止推流的时候制醋要传入已经推来流的uuid就行了,例如

java 复制代码
 @GetMapping("stopVlcFlv")
    public AjaxResult stopVlcFlv(String streamId) {
        // 停止 VLCJ 拉流
        vlcjRtspService.stopStream(streamId);
        return AjaxResult.success("停止回放流成功");

    }

3.3前端页面

我这写了一个简单的html页面如果能打开视频就是播放成功了,可以直接复制测试

html 复制代码
<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <title>FLV测试</title>
    <script src="https://cdn.bootcdn.net/ajax/libs/flv.js/1.6.2/flv.min.js"></script>
</head>
<body>
    <video id="video" controls autoplay style="width: 800px;"></video>
    <script>
        const url = "http://192.168.10.x/flv/1.flv";
        if (flvjs.isSupported()) {
            const player = flvjs.createPlayer({ type: 'flv', url: url });
            player.attachMediaElement(document.getElementById('video'));
            player.load();
            player.play();
        } else {
            alert("浏览器不支持flv.js");
        }
    </script>
</body>
</html>

四:注意项

1、跨域

如上所述,我后端开了一个端口为8088,然后用前端html页面测试发现有跨域问题,会播放失败例如错误提示:

html 复制代码
3.html:1 Access to fetch at 'http://10.30.127.93:8088/1.flv' from origin 'null' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource. If an opaque response serves your needs, set the request's mode to 'no-cors' to fetch the resource with CORS disabled.

2、解决方案

使用nginx代理,修改nginx配置文件添加以下选项

bash 复制代码
location /flv/ {
        proxy_pass http://192.168.10.x:8088/;

        add_header Access-Control-Allow-Origin *;
        add_header Access-Control-Allow-Credentials true;
         }

所以我在html测试页面才写url = "http://192.168.10.x/flv/1.flv"

五、效果

点击开始播放,后端开始推流

推流成功,看到画面,隐私问题,我故意打的马赛克

停止推流

相关推荐
XiYang-DING3 小时前
【Java SE】泛型(Generics)
java·windows·python
云霄IT3 小时前
安卓apk逆向之crc32检测打补丁包crc32_patcher.py
java·前端·python
小句3 小时前
Java Web 技术演进:Servlet → Spring → Spring Boot
java·前端·spring
波波0073 小时前
每日一题:请解释 .NET中的内存模型是什么
开发语言·c#·.net
kyle~3 小时前
JNI与JNA ---打通Java服务端与C++机器人系统的通信链路
java·c++·机器人
XiYang-DING3 小时前
【Java SE】缓存池和常量池的区别
java·spring·缓存
Code blocks3 小时前
Firms-Java:NASA火灾卫星数据Java客户端开源
java·spring boot·后端·开源软件
敏编程3 小时前
一天一个Python库:soupsieve - CSS 选择器在 Beautiful Soup 中的力量
开发语言·css·python
马士兵教育3 小时前
AI大模型教程【LangChainV1.0+LangGraph V1.0】企业级Agent全集开发实战!
开发语言·人工智能·考研·面试·职场和发展