封装一个rtsp转rtmp的插件

本文介绍如何通过Java实现RTSP到RTMP的服务转换,包括nginx环境搭建、配置文件详解及核心代码实现。

RTSP转RTMP服务

环境搭建

nginx
  1. nginx 需要安装rtmp模块

  2. 配置文件

    rtmp {
        server {
        	# rtmp 监听端口
            listen 1935;
            chunk_size 4000;
            application live {
                 live on;
     
                 # record first 1K of stream
                 #record all;
                 #record_path /tmp/av;
                 #record_max_size 1K;
     
                 # append current timestamp to each flv
                 #record_unique on;
     
                 # publish only from localhost
                 # 推流地址
                 allow publish 127.0.0.1;
                 deny publish all;
     
                 #allow play all;
            }
        }
    }
    

java 核心代码

public class Rtsp2RtmpService {

    private static final Logger logger = LoggerFactory.getLogger(Rtsp2RtmpService.class);

    private static final Map<String, Rtsp2RtmpPusher> PUSHERS_HOLDER = new ConcurrentHashMap<>();

    private static final Map<String, String> RTSP_MAPPING = new ConcurrentHashMap<>();

    @Autowired
    private WsdRtmpProperties wsdRtmpProperties;

    @Autowired
    private Executor wsdExecutor;

    /**
     * 叫号器
     */
    private static final AtomicInteger ai = new AtomicInteger(0);

    @PostConstruct
    public void init() {
        WsdRtmpProperties.AutoClose autoClose = wsdRtmpProperties.getAutoClose();
        if (autoClose.isEnabled()) {
            logger.info("开启自动检测线程...");
            // 如果开启了自动关闭那么启动一个回收线程
            ScheduledThreadPoolExecutor executor = new ScheduledThreadPoolExecutor(1, (r) -> {
                Thread th = new Thread();
                th.setDaemon(true);
                th.setPriority(10);
                th.setName("Rtsp2RtmpService.autoCloseThread");
                return th;
            });
            executor.scheduleAtFixedRate(() -> {
                logger.info("执行定时清理任务:");
                // 需要销毁的任务
                List<Rtsp2RtmpPusher> badPushers = new ArrayList<>();
                long maxTime = System.currentTimeMillis() - autoClose.getMaxTime() * 1000;
                // 获取当前所有的任务
                for (Rtsp2RtmpPusher value : PUSHERS_HOLDER.values()) {
                    if (maxTime > value.activeTime) {
                        badPushers.add(value);
                    }
                }
                if (!badPushers.isEmpty()) {
                    // 回收资源
                    for (Rtsp2RtmpPusher badPusher : badPushers) {
                        // 直接关闭 不检查是否存在观众
                        badPusher.release();
                    }
                }
            }, 0, autoClose.getCheckCircle(), TimeUnit.SECONDS);
        }
    }

    /**
     * 播放视频
     *
     * @param rtspUrl
     * @return
     */
    public Rtsp2RtmpData playLive(String rtspUrl) {
        // 先看当前的rtsp有没有被映射
        String id = RTSP_MAPPING.get(rtspUrl);
        Rtsp2RtmpPusher pusher = null;
        if (StringUtils.isNotEmpty(id)) {
            pusher = PUSHERS_HOLDER.get(id);
        }
        if (pusher == null) {
            int num = ai.incrementAndGet();
            // 进行流的转换
            String rtmpUrl = wsdRtmpProperties.getRtmpBaseUrl() + "live/" + num;
            // 进行流的转换
            pusher = rtsp2rtmp(rtspUrl, rtmpUrl);
            pusher.rtmpUrl = wsdRtmpProperties.getRtmpBaseUrl() + "live/" + num;
        } else {
            // 观众数+1
            pusher.addViewer();
        }
        RTSP_MAPPING.put(rtspUrl, pusher.id);
        Rtsp2RtmpData data = new Rtsp2RtmpData();
        data.setId(pusher.id);
        data.setRtmpUrl(pusher.rtmpUrl);
        return data;
    }


    /**
     * 进行流的转换
     *
     * @param rtspUrl
     * @return
     */
    public Rtsp2RtmpPusher rtsp2rtmp(String rtspUrl, String rtmpUrl) {
        Rtsp2RtmpPusher pusher = new Rtsp2RtmpPusher(rtspUrl, wsdRtmpProperties);
        try {
            int width = wsdRtmpProperties.getWidth(), height = wsdRtmpProperties.getHeight();
            FFmpegFrameGrabber grabber = FFmpegFrameGrabber.createDefault(rtspUrl);
            // tcp方式防止丢包
            grabber.setOption("rtsp_transport", "tcp");
            grabber.setImageWidth(width);
            grabber.setImageHeight(height);
            grabber.start();
            logger.info("{} grabber start success!", rtspUrl);
            FFmpegFrameRecorder recorder = new FFmpegFrameRecorder(rtmpUrl, width, height, grabber.getAudioChannels());
            pusher.bind(grabber, recorder);
            recorder.setInterleaved(true);
            // 降低编码延时
            recorder.setVideoOption("tune", "zerolatency");
            // 提升编码速度
            recorder.setVideoOption("preset", "ultrafast");
            // 视频质量参数(详见 https://trac.ffmpeg.org/wiki/Encode/H.264)
            recorder.setVideoOption("crf", wsdRtmpProperties.getVideoOptionCrf() + "");
            recorder.setVideoCodec(avcodec.AV_CODEC_ID_H264);
            // 封装格式flv rtmp使用
            recorder.setFormat(wsdRtmpProperties.getFormat());
            // 视频帧率(保证视频质量的情况下最低25,低于25会出现闪屏)
            recorder.setVideoBitrate(wsdRtmpProperties.getVideoBitrate());
            recorder.setFrameRate(25);
            recorder.setPixelFormat(0);
            // 最高质量
            recorder.setAudioQuality(0);
            // 音频比特率
            recorder.setAudioBitrate(192000);
            // 音频采样率
            recorder.setSampleRate(44100);
            // 双通道(立体声) 2(立体声);1(单声道);0(无音频)
            recorder.setAudioChannels(grabber.getAudioChannels());
            // 音频编/解码器
            recorder.setAudioCodec(avcodec.AV_CODEC_ID_AAC);
            logger.info("{} recorder start success!", rtmpUrl);
            // 提交任务
            wsdExecutor.execute(pusher);
        } catch (Exception e) {
            e.printStackTrace();
            logger.error("rtsp 转换为rtmp异常:", e);
            // 如果发生异常直接释放上面的流
            pusher.release();
            throw new RuntimeException("rtsp 转换为rtmp异常:" + e.getMessage());
        }
        return pusher;
    }

    /**
     * 停止播放
     *
     * @param id
     */
    public void stopLive(String id) {
        // 先从容器中获取到数据
        Rtsp2RtmpPusher pusher = PUSHERS_HOLDER.get(id);
        if (pusher != null) {
            // 释放资源
            pusher.releaseWhenViewerIsNone();
        }
    }

    public Rtsp2RtmpData playHistory(String rtspUrl) {
        int num = ai.incrementAndGet();
        // 进行流的转换
        String rtmpUrl = wsdRtmpProperties.getRtmpBaseUrl() + "history/" + num;
        // 进行流的转换
        Rtsp2RtmpPusher pusher = rtsp2rtmp(rtspUrl, rtmpUrl);
        pusher.rtmpUrl = wsdRtmpProperties.getRtmpBaseUrl() + "history/" + num;
        Rtsp2RtmpData data = new Rtsp2RtmpData();
        data.setId(pusher.id);
        data.setRtmpUrl(pusher.rtmpUrl);
        return data;
    }

    public void stopHistory(String id) {
        // 先从容器中获取到数据
        Rtsp2RtmpPusher pusher = PUSHERS_HOLDER.get(id);
        if (pusher != null) {
            // 释放资源
            pusher.releaseWhenViewerIsNone();
        }
    }

    /**
     * 当前正在播放的列表
     *
     * @return
     */
    public List<Rtsp2RtmpData> list() {
        return PUSHERS_HOLDER.values().stream().map(item -> {
            Rtsp2RtmpData data = new Rtsp2RtmpData();
            data.setId(item.id);
            data.setRtmpUrl(item.rtmpUrl);
            return data;
        }).collect(Collectors.toList());
    }

    /**
     * 保活
     *
     * @param id
     */
    public void keepalive(String id) {
        Rtsp2RtmpPusher pusher = PUSHERS_HOLDER.get(id);
        if (pusher != null) {
            // 保活
            pusher.keepalive();
        }
    }

    /**
     * 持有转换流的2个插件对象
     */
    private static class Rtsp2RtmpPusher implements Runnable {

        /**
         * 资源id
         */
        private String id;

        private FFmpegFrameGrabber grabber;

        private FFmpegFrameRecorder recorder;

        private String rtspUrl;

        private String rtmpUrl;

        /**
         * 激活时间
         */
        private long activeTime;

        /**
         * 运行状态
         */
        private volatile boolean running = true;

        private boolean grabber_starting = false;

        /**
         * 当前观众数量
         */
        private final AtomicInteger viewerCount = new AtomicInteger(1);

        private WsdRtmpProperties wsdRtmpProperties;

        public Rtsp2RtmpPusher(String rtspUrl, WsdRtmpProperties wsdRtmpProperties) {
            this.id = UUID.randomUUID().toString();
            this.rtspUrl = rtspUrl;
            this.wsdRtmpProperties = wsdRtmpProperties;
            this.keepalive();
            PUSHERS_HOLDER.put(id, this);
        }

        public void bind(FFmpegFrameGrabber grabber, FFmpegFrameRecorder recorder) {
            this.grabber = grabber;
            this.recorder = recorder;
        }

        /**
         * 增加一个观众
         */
        public void addViewer() {
            viewerCount.incrementAndGet();
            this.keepalive();
            logger.info("复用资源:{}", this);
        }

        public void keepalive() {
            // 重新设置激活时间
            this.activeTime = System.currentTimeMillis();
        }

        /**
         * 释放资源
         */
        public void release() {
            running = false;
        }

        /**
         * 当没有观众的时候释放掉当前资源
         */
        public void releaseWhenViewerIsNone() {
            // 如果当前观看人数减1 依旧大于0 那么
            if (viewerCount.decrementAndGet() > 0) {
                return;
            }
            running = false;
        }

        /**
         * 执行释放资源
         */
        public void doRelease() {
            logger.info("开始释放当前资源:{}", this);
            if (grabber != null) {
                try {
                    grabber.stop();
                } catch (FrameGrabber.Exception e) {
                    e.printStackTrace();
                }
                try {
                    grabber.release();
                } catch (FrameGrabber.Exception e) {
                    e.printStackTrace();
                }
                try {
                    grabber.close();
                } catch (FrameGrabber.Exception e) {
                    e.printStackTrace();
                }
            }
            if (recorder != null) {
                try {
                    recorder.stop();
                } catch (FrameRecorder.Exception e) {
                    e.printStackTrace();
                }
                try {
                    recorder.release();
                } catch (FrameRecorder.Exception e) {
                    e.printStackTrace();
                }
                try {
                    recorder.close();
                } catch (FrameRecorder.Exception e) {
                    e.printStackTrace();
                }
            }

            PUSHERS_HOLDER.remove(id);
            logger.info("当前资源释放完毕:{}", this);
        }

        /**
         * 判断当前任务是否继续执行
         *
         * @return 当前任务是否继续执行
         */
        public boolean isRunning() {
            return running;
        }

        @Override
        public void run() {
            // 失败计数
            int failCount = 0;
            logger.info("资源开始转换:{}", this);
            while (isRunning()) {
                try {
                    Frame frame = grabber.grabImage();
                    if (frame == null) {
                        grabber.start();
                        continue;
                    }
                    if (!grabber_starting) {
                        recorder.start();
                        grabber_starting = true;
                    }
                    recorder.record(frame);
                    // 只要有1次成功那么就恢复初始值
                    failCount = 0;
                } catch (Exception e) {
                    failCount++;
                    // 当前线程休眠1秒后再重试
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException interruptedException) {
                        interruptedException.printStackTrace();
                    }
                    e.printStackTrace();
                    logger.error("推流发生异常:" + this + "\n", e);
                }
                if (failCount == wsdRtmpProperties.getRetryNum()) {
                    // 发生异常次数达到最大值的时候直接关闭
                    release();
                }
            }
            logger.info("资源停止转换:{}", this);
            // 释放资源
            doRelease();
        }

        @Override
        public String toString() {
            return "Rtsp2RtmpPusher{" +
                    "id='" + id + '\'' +
                    ",rtspUrl='" + rtspUrl + '\'' +
                    ",rtmpUrl='" + rtmpUrl + '\'' +
                    ",activeTime=" + activeTime +
                    '}';
        }
    }
}
评论 1
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值