linux+springboot+nginx转rtsp流为hls流

一、下载安装FFmpeg
1、下载(自定义版本:http://www.ffmpeg.org/releases/)

wget http://www.ffmpeg.org/releases/ffmpeg-5.1.tar.gz

2、解压

tar -zxvf ffmpeg-5.1.tar.gz

3、编译安装(耗时较长)
为自己指定的安装目录

/data/ioc/ffmpeg
cd ffmpeg-5.1
./configure --prefix=/data/ioc/ffmpeg
make && make install

4、配置环境变量(方便在任意地方使用)

vi /etc/profile

在最后PATH添加环境变量:

export PATH=$PATH:$JAVA_HOME/bin:/data/ioc/ffmpeg/bin
source /ect/profile

5、验证是否正确安装

ffmpeg -version

二、 安装nginx
1、安装依赖

yum -y install gcc zlib zlib-devel pcre-devel openssl openssl-devel

2、下载编译安装
下载

 wget http://nginx.org/download/nginx-1.21.5.tar.gz

解压缩

tar -zxvf nginx-1.21.5.tar.gz
cd nginx-1.21.5/

#执行配置并加载ssl模块

./configure --prefix=/usr/local/nginx --with-http_ssl_module

#编译安装(默认安装在/usr/local/nginx)

make
make install

3、配置nginx,因后端服务器只开通了一个端口,故将两个服务代理至一个端口
1)events部分配置

events {
	use epoll;
    worker_connections  1024;	
	multi_accept on;
}

2)http部分配置

http {
    include       mime.types;
    default_type  application/octet-stream;
    sendfile        on;
	# 开启 tcp_nopush,适用于大文件传输 
    #tcp_nopush     on;
	# 开启 tcp_nodelay,适用于小数据包传输
	tcp_nodelay 		on;
    keepalive_timeout  65;
    gzip  on;

3)server 部分配置

server {
		listen     8099;
        location /ffmpeg-video/{
            types{
                application/vnd.apple.mpegurl m3u8;
                video/mp2t ts;
            }
			#路径改为转码后的视频文件夹路径
            alias /data/ioc/ffmpeg-video/; 
            add_header Cache-Control no-cache;
            add_header Access-Control-Allow-Origin *;
        }
		location / {
            proxy_pass   http://x.x.6.21:8098/;
        }
	}

4、启动

/usr/local/nginx/sbin/nginx -c /usr/local/nginx/conf/nginx.conf

进入:/usr/local/nginx/sbin
1 启动 ./nginx
2 停止 ./nginx -s stop
3 重启 ./nginx -s reload

三、手动转码测试

ffmpeg -rtsp_transport tcp -t 02:00:00 -i rtsp://x.x.x.10:554/openUrl/jughzeo -c copy -s 216x384 -f hls -hls_time 2 -hls_list_size 6 -hls_flags delete_segments /data/ioc/ffmpeg-video/37040421001310484694/video.m3u8

参数解释:

-rtsp_transport tcp 使用tcp协议接收,减少延迟
-t 02:00:00 转码时长2小时,2小时后进程自动停止
-i 输入流地址
-c copy 复制视频格式不进行编码
-s 216x384 设置帧的大小
-f hls 输出文件格式
-hls_time 2 分片文件时长秒
-hls_list_size 6 播放列表中保存的最大分片数量
-hls_flags delete_segments 删除过时的分段,这样ts文件数将不会占用过多磁盘

四、播放测试
使用vlc播放器播放,打开媒体,输入网络地址:
http://x.x.x.212:8099/ffmpeg-video/37040421001310484694/video.m3u8
五、java后台进行多线程转码控制
1、基本逻辑为前端请求时获取新的rtsp流地址,调用一次rtsp2Hls转码方法,将生成新的hls切片文件(不用考虑旧文件,将会覆盖),转码时先停止当前转码线程,再创建新的线程。
2、线程监控:开启一个定时任务,每10分钟查看一次,若有异常线程,则重新获取rtsp流地址进行转码,在晚上8点后则停止所有转码任务。

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.io.FileUtils;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;

import java.io.BufferedReader;
import java.io.File;
import java.io.InputStreamReader;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.util.concurrent.ConcurrentHashMap;

/**
 * rtsp 转 hlv协议
 */
@Service
@Slf4j
@EnableScheduling
public class RtspConvert  implements InitializingBean {
    //转换map
    private static ConcurrentHashMap<String, CoverThread> coverMap = new ConcurrentHashMap<>();
    //每次转换2小时,2小时之后自动停止
//    private static final String rtsp2hls = "ffmpeg -rtsp_transport tcp -t 02:00:00 -i %s -c copy -s 216x384 -f hls -hls_time 2 -hls_list_size 6 -hls_flags delete_segments %s 2>/data/null";
    @Value("${ffmpeg.rtsp2hls}" )
    private String rtsp2hls;

    @Value("${ffmpeg.m3u8Path}" )
    private String m3u8Path;

@Value("${ffmpeg.stopCoverHour}" )
    private int stopCoverHour;


    @Override
    public void afterPropertiesSet() throws Exception {

    }

    /**
     * 转换rtsp并获取hls文件路径
     */
    public String rtsp2Hls(String code, String rtspUrl) {
        // 需要从新地址重新开始拉流,故注释掉
        /*if (coverMap.containsKey(code)) {
            CoverThread thread = coverMap.get(code);
            if (thread == null || thread.getTaskState() != CoverThread.running) {
            } else {
                return thread.getM3U8File();
            }
        }*/
        String m3u8File =String.format(m3u8Path, code);
        startTransform(code, rtspUrl, m3u8File);
        CoverThread thread = coverMap.get(code);
        if (thread != null) {
            return thread.getM3U8File();
        }
        return null;
    }
    /**
     * 开启转换
     */
    private void startTransform(String code, String rtspUrl, String m3u8Path) {
        log.info("转换rtsp, {},{},{}", code, rtspUrl, m3u8Path);
        String memKey = "startLive" + code;
        synchronized (memKey.intern()) {
            if (coverMap.containsKey(code)) {
                stopTransform(code);
            }
            CoverThread thread = new CoverThread(code, rtspUrl, m3u8Path);
            coverMap.put(code, thread);
            thread.start();
        }
    }

    /**
     * 停止转换
     */
    public void stopTransform(String code) {
        String memKey = "startLive" + code;
        synchronized (memKey.intern()) {
            if (coverMap.containsKey(code)) {
                CoverThread thread = coverMap.get(code);
                if (thread != null && thread.getTaskState() != CoverThread.fail) {
                    thread.stopTask();
                    log.info("结束{}转换线程", code);
                }
            }
        }
    }

    /**
     * 监控所有的转换线程
     */
    @Scheduled(cron = "0 0/10 * * * ?")
    public synchronized void monitorThreads() {
        for (String code : coverMap.keySet()) {
            CoverThread thread = coverMap.get(code);
            // 晚上8点后停止拉流
            LocalDateTime now = LocalDateTime.now();
            LocalTime twentyOclock = LocalTime.of(stopCoverHour
, 0);
            if (now.toLocalTime().isAfter(twentyOclock)) {
                stopTransform(code);
                continue;
            }
            if (thread != null && thread.getTaskState() != CoverThread.running) {
                //线程出现异常,重新获取视频流地址
                String response = FdcjspService.getCamerasUrlByCode(code);
                JSONObject jsonObject = JSON.parseObject(response);
                if ("0".equals(jsonObject.get("code"))) {
                    JSONObject parse = (JSONObject) jsonObject.get("data");
                    String rtspUrl = (String) parse.get("url");
                    rtsp2Hls(code, rtspUrl);
                }
            }
        }
    }

    /**
     * 执行命令线程
     */
    private class CoverThread extends Thread {
        private String code;
        private String rtspUrl;
        private String m3u8File;
        private int taskState = 0; //运行状态 0未开始 1进行中 -1失败
        private static final int notStart = 0;
        private static final int running = 1;
        private static final int fail = -1;
        private Process process = null;

        CoverThread(String code, String rtspUrl, String m3u8File) {
            this.code = code;
            this.rtspUrl = rtspUrl;
            this.m3u8File = m3u8File;
            setName("m3u8-" + code);
            this.taskState = notStart;
        }

        @Override
        public void run() {
            try {
                FileUtils.forceMkdir(new File(m3u8File).getParentFile());
                String command = String.format(rtsp2hls, rtspUrl, m3u8File);
                this.taskState = running;

                // 操作系统是linux
                String[] comds = new String[]{"/bin/sh", "-c", command};
                ProcessBuilder builder = new ProcessBuilder(comds);

                // 开始执行命令
                log.info("执行命令:" + command);
                process = builder.start();
                // 启动线程读取输出流
                /*BufferedReader inputStreamReader = new BufferedReader(new InputStreamReader(process.getInputStream()));
                inputStreamReader.lines().forEach(System.out::println);

                BufferedReader errorStreamReader = new BufferedReader(new InputStreamReader(process.getErrorStream()));
                errorStreamReader.lines().forEach(System.err::println);*/
                // 命令中加入 2>/data/null 不输出日志,故不用读取日志
                int flag = process.waitFor();
                log.info("结束{}", code);
            } catch (Exception e) {
                log.error("出现异常" + e.getMessage(), e);
                this.taskState = fail;
            } finally {
                if (process != null) {
                    try {
                        process.exitValue();
                    } catch (Exception e) {
                    }
                    try {
                        process.destroyForcibly();
                    } catch (Exception e) {
                    }
                }
            }
        }

        /**
         * 获取任务执行状态
         */
        public int getTaskState() {
            return taskState;
        }

        public String getM3U8File() {
            return this.m3u8File;
        }

        public String getRtspUrl() {
            return this.rtspUrl;
        }

        /**
         * 立即停止任务
         */
        public void stopTask() {
            if (process != null) {
                try {
                    process.destroy();
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }
    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

k0933

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值