JavaCV 通过rtsp拉流并推流到rtmp

该博客介绍了如何使用JavaCV库从RTSP流中抓取视频并推送到RTMP服务器的过程。通过Spring Boot应用进行定时任务管理,监控rtmp服务状态,确保推流的稳定性和效率。主要涉及的技术包括FFmpegFrameGrabber和FFmpegFrameRecorder,以及线程池管理。
摘要由CSDN通过智能技术生成

nginx-rtmp服务搭建

目录结构

在这里插入图片描述

pom.xml

<dependency>
    <groupId>org.bytedeco</groupId>
    <artifactId>javacv</artifactId>
    <version>1.5.1</version>
</dependency>

<dependency>
    <groupId>org.bytedeco</groupId>
    <artifactId>ffmpeg-platform</artifactId>
    <version>4.1.3-1.5.1</version>
</dependency>

VideoRealPlayApplication

@SpringBootApplication
@EnableScheduling
public class VideoRealPlayApplication {

    public static void main(String[] args) {
        // 服务启动执行FFmpegFrameGrabber和FFmpegFrameRecorder的tryLoad(),以免导致第一次推流时耗时。
        try {
            FFmpegFrameGrabber.tryLoad();
            FFmpegFrameRecorder.tryLoad();
        } catch (org.bytedeco.javacv.FrameRecorder.Exception e) {
            e.printStackTrace();
        } catch (Exception e) {
            e.printStackTrace();
        }

        SpringApplication.run(VideoRealPlayApplication.class, args);
    }

    @PreDestroy
    public void destroy() {
        ThreadPoolUtil.shutdownAndAwaitTermination();
    }

}

IndexController

@Slf4j
@RestController
@RequestMapping("/video")
public class IndexController {

    @Reference
    private IRoadsideDeviceService iRoadsideDeviceService;
    @Reference
    private IDeviceInfoService iDeviceInfoService;

    /**
     * 获取设备信息,并执行拉流、推流任务,并返回rtmp地址
     * @return
     */
    @GetMapping("/rtmp")
    public BaseRespEntity rtmp(@RequestParam String deviceIp, @RequestParam String factory) {
        /*if (StrUtil.isBlank(deviceno)) {
            return BaseRespEntity.error("设备序列号不能为空!");
        }

        DeviceInfoBO device = iDeviceInfoService.getOne(DeviceInfoBO.builder().deviceno(deviceno).build());
        if (ObjectUtil.isNull(device)) {
            return BaseRespEntity.error("设备信息异常!");
        }*/

        // 如果设备已经存在拉流,直接返回rtmp
        VideoDTO video = VideoDataCache.VIDEO_MAP.get(deviceIp);
        if (ObjectUtil.isNotNull(video) && StrUtil.isNotBlank(video.getRtmp())) {
            return BaseRespEntity.ok(video.getRtmp());
        }
        String rtsp;
        if (factory.equals("DH")) {
            rtsp = StrUtil.format(VideoConsts.DAHUA_RTSP_URL, VideoConsts.DAHUA_USERNAME,
                    VideoConsts.DAHUA_PASSWORD, deviceIp);
        } else {
            rtsp = StrUtil.format(VideoConsts.YUSHI_RTSP_URL, VideoConsts.YUSHI_USERNAME,
                    VideoConsts.YUSHI_PASSWORD, deviceIp);
        }
        String rtmp = StrUtil.format(VideoConsts.RTMP_URL, VideoConsts.RTMP_PUSH_IP,
                VideoConsts.RTMP_PORT, deviceIp.hashCode());

        video = new VideoDTO()
                .setDeviceIp(deviceIp)
                .setRtsp(rtsp)
                .setRtmp(rtmp)
                .setOpentime(LocalDateTime.now());
        VideoStreamService videoStreamService = new VideoStreamService(video);
        Future<?> task = ThreadPoolUtil.POOL.submit(() -> {
            try {
                videoStreamService.from().to().go(Thread.currentThread());
            } catch (BaseException e) {
                log.error("BaseException error {}", e.getMsg());
                videoStreamService.close();
                e.printStackTrace();
            } catch (FrameGrabber.Exception e) {
                log.error("FrameGrabber error {}", e.getMessage());
                videoStreamService.close();
                e.printStackTrace();
            } catch (FrameRecorder.Exception e) {
                log.error("FrameRecorder error {}", e.getMessage());
                videoStreamService.close();
                e.printStackTrace();
            }
        });
        // 缓存信息
        video.setRtmp(StrUtil.format(VideoConsts.RTMP_URL, VideoConsts.RTMP_ACCESS_IP,
                VideoConsts.RTMP_PORT, deviceIp.hashCode()));
        String key = String.valueOf(video.getDeviceIp().hashCode());
        VideoDataCache.VIDEO_MAP.put(key, video);
        VideoDataCache.RTMP_MAP.put(key, videoStreamService);
        VideoDataCache.TASK_MAP.put(key, task);
        return BaseRespEntity.ok(video.getRtmp());
    }
}

VideoDataCache

public class VideoDataCache {
    /**
     * 保存已经开始推的流
     */
    public static final ConcurrentHashMap<String, VideoStreamService> RTMP_MAP = new ConcurrentHashMap();

    /**
     * 保存正在推送的设备信息
     */
    public static final ConcurrentHashMap<String, VideoDTO> VIDEO_MAP = new ConcurrentHashMap();

    /**
     * 保存正在推送的任务
     */
    public static final ConcurrentHashMap<String, Future<?>> TASK_MAP = new ConcurrentHashMap<>();


    public static void remove(String key) {
        // 终止线程
        ThreadPoolUtil.cancelTask(VideoDataCache.TASK_MAP.get(key));

        // 清除缓存
        VideoDataCache.TASK_MAP.remove(key);
        VideoDataCache.VIDEO_MAP.remove(key);
        VideoDataCache.RTMP_MAP.remove(key);
    }
}

ThreadPoolUtil

@Slf4j
public class ThreadPoolUtil {

    private static final int CORE_POOL_SIZE = (int) (Runtime.getRuntime().availableProcessors() / (1 - 0.5f));
    private static final int MAX_POOL_SIZE = 35;
    private static final long KEEP_LIVE_TIME = 60L;
    private static final BlockingQueue<Runnable> BLOCKING_QUEUE = new LinkedBlockingQueue<>();
    public static final ThreadPoolExecutor POOL;

    static{
        POOL = new ThreadPoolExecutor(CORE_POOL_SIZE,
                MAX_POOL_SIZE,
                KEEP_LIVE_TIME,
                TimeUnit.MILLISECONDS,
                BLOCKING_QUEUE,
                Executors.defaultThreadFactory(),
                new ThreadPoolExecutor.DiscardOldestPolicy());
    }
    
    /**
     * 取消当前正在执行的任务
     */
    public static void cancelTask(Future<?> future) {
        // 终止正在执行的任务
        if (ObjectUtil.isNotNull(future) && !future.isDone() && !future.isCancelled()) {
            future.cancel(true);
        }
    }

    /**
     * 释放线程池
     */
    public static void shutdownAndAwaitTermination() {
        if (POOL != null && !POOL.isShutdown()) {
            POOL.shutdown();
            try {
                if (!POOL.awaitTermination(120, TimeUnit.SECONDS)) {
                    POOL.shutdownNow();
                    if (!POOL.awaitTermination(120, TimeUnit.SECONDS)) {
                        log.info("pool did not termination");
                    }
                }
            } catch (InterruptedException e) {
                POOL.shutdownNow();
                Thread.currentThread().interrupt();
            }
        }
    }
}

VideoTimer

/**
 * 请求rtmp服务状态地址,获取nclients在线客户端数量,等于1时表示没有被预览
 *
 */
@Slf4j
@Component
public class VideoTimer {

	/** */
    private static final String RTMP_STAT_URL = "http://***.***.***.***/stat";
    private static final int TIME_OUT = 3000;

    @Scheduled(cron = "0/5 * * * * ?")
    public void configureTasks() {
        List<String> rtmpStatList = getRtmpStat();
        if (CollUtil.isNotEmpty(rtmpStatList)) {
            for (String key : rtmpStatList) {
                VideoDTO video = VideoDataCache.VIDEO_MAP.get(key);
                if (ObjectUtil.isNotNull(video) && video.getOpentime().plusMinutes(1).isBefore(LocalDateTime.now())) {
                    log.info("Video Streaming Stop ={}", video);
                    VideoDataCache.remove(key);
                }
            }
        }
    }

    private static List<String> getRtmpStat() {
        try {
            String body = HttpRequest.get(RTMP_STAT_URL)
                    .timeout(TIME_OUT)
                    .execute()
                    .body();
            return xmlToObj(body);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }

    private static List<String> xmlToObj(String xmlStr) {
        List<String> resList = new ArrayList<>();
        if (StrUtil.isBlank(xmlStr) && !xmlStr.contains("<live>")) {
            return resList;
        }

        String live = StrUtil.subBetween(xmlStr, "<live>", "</live>");
        if (!live.contains("<stream>")) {
            return resList;
        }
        String[] split = StrUtil.split(live, "</stream>");
        for (int i = 0; i < split.length; i++) {
            String s = split[i];
            if (s.contains("<name>") && s.contains("<nclients>")) {
                Integer nclients = Integer.valueOf(StrUtil.subBetween(s, "<nclients>", "</nclients>"));
                if (nclients == 1) {
                    String name = StrUtil.subBetween(s, "<name>", "</name>");
                    resList.add(name);
                }
            }
        }
        return resList;
    }
}

VideoStreamService

@Slf4j
public class VideoStreamService {

    private VideoDTO videoDTO;
    /**
     * 解码器
     */
    private FFmpegFrameGrabber grabber = null;

    /**
     * 编码器
     */
    private FFmpegFrameRecorder recorder = null;

    /**
     * 帧率
     */
    private double FRAMERATE;

    /**
     * 比特率
     */
    private int BITRATE;

    /**
     * 视频像素宽
     */
    public int WIDTH;
    /**
     * 视频像素高
     */
    public int HEIGHT;

    public VideoStreamService(VideoDTO videoDTO) {
        this.videoDTO = videoDTO;
    }

    /**
     * 视频源
     */
    public VideoStreamService from() throws FrameGrabber.Exception {
        grabber = new FFmpegFrameGrabber(videoDTO.getRtsp());
        // tcp用于解决丢包问题
        grabber.setOption("rtsp_transport", "tcp");

        // 设置采集器构造超时时间
        grabber.setOption("stimeout", "3000");
        // 开始之后ffmpeg会采集视频信息,之后就可以获取音视频信息
        grabber.start();

        WIDTH = grabber.getImageWidth();
        HEIGHT = grabber.getImageHeight();
        FRAMERATE = grabber.getVideoFrameRate();
        BITRATE = grabber.getVideoBitrate();
        // 若视频像素值为0,说明采集器构造超时,程序结束
        if (WIDTH == 0 && HEIGHT == 0) {
            log.error("Streaming Exception ...");
            return null;
        }
        return this;
    }

    /**
     * 输出
     *
     */
    public VideoStreamService to() throws FrameRecorder.Exception {
        recorder = new FFmpegFrameRecorder(videoDTO.getRtmp(), WIDTH, HEIGHT);
        // 画面质量参数,0~51;18~28是一个合理范围
        recorder.setVideoOption("crf", "28");
        // 该参数用于降低延迟
        recorder.setVideoOption("tune", "zerolatency");
        /**
         ** 权衡quality(视频质量)和encode speed(编码速度) values(值): *
         * ultrafast(终极快),superfast(超级快), veryfast(非常快), faster(很快), fast(快), *
         * medium(中等), slow(慢), slower(很慢), veryslow(非常慢) *
         * ultrafast(终极快)提供最少的压缩(低编码器CPU)和最大的视频流大小;而veryslow(非常慢)提供最佳的压缩(高编码器CPU)的同时降低视频流的大小
         */
        recorder.setVideoOption("preset", "ultrafast");
        recorder.setGopSize(2);
        recorder.setFrameRate(FRAMERATE);
        recorder.setVideoBitrate(BITRATE);
        AVFormatContext fc = null;
        if (videoDTO.getRtmp().indexOf("rtmp") >= 0 || videoDTO.getRtmp().indexOf("flv") > 0) {
            // 封装格式flv
            recorder.setFormat("flv");
            recorder.setAudioCodecName("aac");
            fc = grabber.getFormatContext();
        }
        recorder.start(fc);
        log.info("Push Stream Device Info:\ndeviceIp:{}  \nrtsp:{} \nrtmp:{}",
                videoDTO.getDeviceIp(), videoDTO.getRtsp(), videoDTO.getRtmp());
        return this;
    }

    public VideoStreamService go(Thread nowThread) throws FrameGrabber.Exception, FrameRecorder.Exception {
        // 采集或推流导致的错误次数
        long errIndex = 0;
        // 连续五次没有采集到帧则认为视频采集结束,程序错误次数超过5次即中断程序
        
        // 将探测时留下的数据帧释放掉,以免因为dts,pts的问题对推流造成影响
        grabber.flush();

        for (int noFrameIndex = 0; noFrameIndex < 5 || errIndex < 5; ) {
            try {
                // 用于中断线程时,结束该循环
                nowThread.sleep(1);
                // 获取没有解码的音视频帧
                AVPacket pkt = grabber.grabPacket();
                if (pkt == null || pkt.size() <= 0 || pkt.data() == null) {
                    // 空包记录次数跳过
                    noFrameIndex++;
                    errIndex++;
                    continue;
                }
                // 不需要编码直接把音视频帧推出去
                errIndex += (recorder.recordPacket(pkt) ? 0 : 1);
                avcodec.av_packet_unref(pkt);
            } catch (InterruptedException e) {
                // 当需要结束推流时,调用线程中断方法,中断推流的线程。当前线程for循环执行到
                // nowThread.sleep(1);这行代码时,因为线程已经不存在了,所以会捕获异常,结束for循环
                log.info("Device interrupt push stream succeeded...");
                break;
            } catch (FrameGrabber.Exception e) {
                errIndex++;
            } catch (FrameRecorder.Exception e) {
                errIndex++;
            }
        }
        // 程序正常结束释放资源
        this.close();
        log.info("The device streaming is stop...");
        return this;
    }

    public void close() {
        try {
            if (recorder != null) {
                recorder.close();
                log.info("Recorder close success!");
            }
        } catch (FrameRecorder.Exception e) {
            e.printStackTrace();
        }
        try {
            if (grabber != null) {
                grabber.close();
                log.info("Grabber close success!");
            }
        } catch (FrameGrabber.Exception e) {
            e.printStackTrace();
        }
    }
}

VideoDTO

@Data
@Accessors(chain = true)
public class VideoDTO {

    private String rtsp;

    private String rtmp;

    private String deviceIp;

    /**
     * 打开时间
     */
    private LocalDateTime opentime;
}
评论 12
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值