Java 视频直播JavaCV(ffmpeg h264)+RTSP实现低延时1秒推流

上一篇文章介绍了通用协议onvif获取到rtsp地址Java onvif协议通用协议获取rtsp地址 

当然也有很多其他的方式获取rtsp地址

首先还是引入包:


        
            org.bytedeco
            javacv-platform
            1.5.4
        

这里我是使用的rtsp砖udp (h264)的方式推流,还有其他方式例如rtmp 或者rtp 实现方式差不多只用修改一些参数

需要用到的测试软件:VLC

这里是利用转封装的方式进行转码(由于rtsp本身就支持h264编码格式,有两种方式:1.转码2.转封装(转封装消耗的资源更少))

详细解释就不多说了,注释里面有详细说明:

/**
 * rtsp转 udp(转封装方式)
 * @author zf
 */
public class RecordVideo {
    private FFmpegFrameGrabber grabber = null;
    private FFmpegFrameRecorder recorder = null;

    // 视频参数

    /**
     * 选择视频源
     * @param src
     * @author eguid
     * @throws Exception
     */
    public RecordVideo sourcesRtsp(String src) throws Exception {
        // 采集/抓取器
        grabber = MediaUtils.createGrabber(src);
        grabber.start();// 开始之后ffmpeg会采集视频信息,之后就可以获取音视频信息
        return this;
    }

    /**
     * 选择输出
     * @param out
     * @author eguid
     * @throws IOException
     */
    public RecordVideo target(String out) throws IOException {
        // 流媒体输出地址,分辨率(长,高),是否录制音频(0:不录制/1:录制) ?overrun_nonfatal=1&fifo_size=50000000
        //这里udp地址增加参数扩大udp缓存
        recorder = new FFmpegFrameRecorder(out + "?overrun_nonfatal=1&fifo_size=50000000", MediaUtils.FRAME_WIDTH, MediaUtils.FRAME_HEIGHT, 0);
        // 直播流格式
        recorder.setVideoCodec(avcodec.AV_CODEC_ID_H264);
        // 降低编码延时
        recorder.setVideoOption("tune", "zerolatency");
        recorder.setMaxDelay(500);
        recorder.setGopSize(10);
        // 提升编码速度
        recorder.setVideoOption("preset", "ultrafast");
        // 录制的视频格式 flv(rtmp格式) h264(udp格式) mpegts(未压缩的udp) rawvideo
        recorder.setFormat("h264");
        // 帧数
        double frameLength = grabber.getLengthInFrames();
        long frameTime = grabber.getLengthInTime();
        double v = frameLength * 1000 * 1000 / frameTime;
        recorder.setFrameRate(v);
        //百度翻译的比特率,默认400000
        recorder.setVideoBitrate(200000);
//        recorder.setAudioOption("crf", "23");
        // 建议从grabber获取AudioChannels
//        recorder.setAudioChannels(grabber.getAudioChannels());
//        recorder.setInterleaved(true);
        // yuv420p
        recorder.setPixelFormat(avutil.AV_PIX_FMT_YUV420P);
        recorder.setAudioCodec(avcodec.AV_CODEC_ID_AAC);
        recorder.start(grabber.getFormatContext());
        return this;
    }
    
    /**
     * 转封装
     * @author eguid
     * @throws IOException
     */
    public RecordVideo go() throws IOException {
        System.out.println("开始推送...");
        long err_index = 0;//采集或推流导致的错误次数
        // 释放探测时缓存下来的数据帧,避免pts初始值不为0导致画面延时
        grabber.flush();
        //错误采集判断
        for(int no_frame_index = 0; no_frame_index < 10 || err_index > 1;) {
            AVPacket pkt;
            try {
                pkt = grabber.grabPacket();
                if(pkt == null || pkt.size() <= 0 || pkt.data() == null) {
                    //空包记录次数跳过
                    no_frame_index ++;
                    continue;
                }
                //不需要编码频帧推出去
                err_index += (recorder.recordPacket(pkt) ? 0 : 1);//如果失败err_index自增1
                av_packet_unref(pkt);
            } catch (IOException e) {//推流失败
                err_index++;
            }
        }
        return this;
    }

    public static void main(String[] args) throws Exception, IOException {
        //运行,设置视频源和推流地址
        new RecordVideo().sourcesRtsp("rtsp://{username}:{password}@{ip}:{port}/Streaming/Unicast/channels/1602")
        .target("udp://{ip}:{port}")
        .go();
    }
}

Java 视频直播JavaCV(ffmpeg h264)+RTSP实现低延时1秒推流_第1张图片

package com.onvif.java.utils;

import com.onvif.java.common.RrException;
import com.onvif.java.model.OnvifCredentials;
import com.onvif.java.service.OnvifDevice;
import org.bytedeco.ffmpeg.avcodec.AVPacket;
import org.bytedeco.ffmpeg.global.avcodec;
import org.bytedeco.ffmpeg.global.avutil;
import org.bytedeco.javacv.FFmpegFrameGrabber;
import org.bytedeco.javacv.FFmpegFrameRecorder;
import org.bytedeco.javacv.FrameGrabber;
import org.onvif.ver10.schema.GetRecordingsResponseItem;
import org.onvif.ver10.schema.Profile;
import org.onvif.ver10.schema.TransportProtocol;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import org.springframework.stereotype.Component;

import javax.xml.soap.SOAPException;
import java.io.IOException;
import java.net.ConnectException;
import java.net.MalformedURLException;
import java.net.URL;
import java.text.SimpleDateFormat;
import java.time.LocalDateTime;
import java.time.ZoneOffset;
import java.util.Date;
import java.util.List;
import java.util.concurrent.Executor;

import static org.bytedeco.ffmpeg.global.avcodec.av_packet_unref;

/**
 * @program: javaOnvif
 * @description: 获取rtsp地址
 * @author: zf
 * @create: 2020-09-08 10:50
 **/
@Component
public class MediaUtils {

    @Autowired
    ThreadPoolTaskExecutor taskExecutor;
    /**
     * 视频帧率
     */
    public static final int FRAME_RATE = 25;
    /**
     * 视频宽度
     */
    public static final int FRAME_WIDTH = 480;
    /**
     * 视频高度
     */
    public static final int FRAME_HEIGHT = 270;
    /**
     * 流编码格式
     */
    public static final int VIDEO_CODEC = avcodec.AV_CODEC_ID_H264;
    /**
     * 编码延时 zerolatency(零延迟)
     */
    public static final String TUNE = "zerolatency";
    /**
     * 编码速度 ultrafast(极快)
     */
    public static final String PRESET = "ultrafast";
    /**
     * 录制的视频格式 flv(rtmp格式) h264(udp格式) mpegts(未压缩的udp) rawvideo
     */
    public static final String FORMAT = "h264";
    /**
     * 比特率
     */
    public static final int VIDEO_BITRATE = 200000;

    private static FFmpegFrameGrabber grabber = null;
    private static FFmpegFrameRecorder recorder = null;


    /**
     * 构造视频抓取器
     * @param rtsp 拉流地址
     * @return
     */
    public static FFmpegFrameGrabber createGrabber(String rtsp) {
        // 获取视频源
        FFmpegFrameGrabber grabber = new FFmpegFrameGrabber(rtsp);
        grabber.setOption("rtsp_transport","tcp");
        //设置帧率
        grabber.setFrameRate(FRAME_RATE);
        //设置获取的视频宽度
        grabber.setImageWidth(FRAME_WIDTH);
        //设置获取的视频高度
        grabber.setImageHeight(FRAME_HEIGHT);
        //设置视频bit率
        grabber.setVideoBitrate(2000000);
        return grabber;
    }

    /**
     * 选择视频源
     * @param src
     * @author eguid
     * @throws FrameGrabber.Exception
     */
    public MediaUtils from(String src) throws FrameGrabber.Exception {
        start = System.currentTimeMillis();
        // 采集/抓取器
        grabber = createGrabber(src);
        // 开始之后ffmpeg会采集视频信息
        grabber.start();
        grabber.flush();
        form = src.substring(src.indexOf("@") + 1);
        return this;
    }

}

udp的地址是udp/h264://@{ip}:{port}

这里格式说明下:

 // 录制的视频格式 flv(rtmp格式) h264(udp格式) mpegts(未压缩的udp) rawvideo
        recorder.setFormat("h264");

udp 设置的是h264 其他的根据注释选择就可以输出不同的协议,上门代码中注释部分是音频相关的如果有需要可以使用

下面是对比的海康摄像头提供的原始sdk获取和我们推流转码后的时间对比 延时在1-2秒范围内,推流稳定

再提醒下,详细参数可以根据实际情况调整,根据所需调整 例如清晰度

setVideoBitrate设置比特率 设置画面连续性setFrameRate等

Java 视频直播JavaCV(ffmpeg h264)+RTSP实现低延时1秒推流_第2张图片

排坑指南:

视频的长宽只能设置4的倍数,不然会强制使用默认

比特率的设置则需要视频源的清晰度压缩,有一个极限值越过值了再小也没用了

更新日志:

2020年11月27日

目前的所有方式中,在拉取流的时候比较耗时大约需要2到3秒的加载

在项目启动的时候提前加载一次会有比较好的效果,但是garber.start依然会消耗一定时间,根据询问大佬这个应该目前java上没有一个好的解决方案,使用c效果阔能比较好

//提前加载资源,解决第一次推流慢
        FFmpegFrameGrabber.tryLoad();
        FFmpegFrameRecorder.tryLoad();

推流方式二

直接推送图片方式(更低延迟,验证控制在1秒以内,当然也会提高一定的cpu占用量,目前i5-4590 最多推送7到8路视频流就会占用cpu80%)

ps 如果追求超低延迟可以参考

这种方式是直接抓取流帧 转换成图片直接websocket推送出去

 /**
     * 推送图片流
     * @throws Exception
     */
    public MediaUtils startPush(String ip, Integer port) throws Exception {
        Long end = System.currentTimeMillis();
        System.out.println(form + " 开始推送 耗时:" + (end - start) + "ms");
        Java2DFrameConverter java2DFrameConverter = new Java2DFrameConverter();
        try {
            Frame frame;
            while ((frame = grabber.grabImage()) != null) {
                //线程控制中断
                if (Thread.currentThread().isInterrupted()) {
                    System.out.println(form + " 停止推送...");
                    return this;
                }
                BufferedImage bufferedImage = java2DFrameConverter.getBufferedImage(frame);
                byte[] bytes = imageToBytes(bufferedImage, "jpg");
                //使用udp发送图片数据
                udpService.sendMessageBytes(bytes, ip, port);
                //使用websocket发送数据
//                MyWebSocket.sendAll(channel, bytes);
            }
        } catch (Exception e){
            Thread.currentThread().interrupt();
        }finally {
            if (grabber != null) {
                grabber.stop();
            }
        }
        return this;
    }

调用方式是首先调用上面的

new MediaUtils().from(from).startPush(ip, port);

然后直接websocket推送到前端,前端只能不断替换图片即可,目前测试海康,大华和宇视延迟都在1秒以内

2021年2月7日

测试vlc 软件:

链接:百度网盘 请输入提取码 
提取码:z7ox 

udpSever(Netty实现的udp服务端):

package com.onvif.java.service;

import com.alibaba.fastjson.JSONObject;
import io.netty.bootstrap.Bootstrap;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.*;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.DatagramPacket;
import io.netty.channel.socket.nio.NioDatagramChannel;
import io.netty.handler.codec.DelimiterBasedFrameDecoder;
import io.netty.handler.codec.MessageToMessageEncoder;
import io.netty.util.CharsetUtil;
import lombok.Data;
import lombok.experimental.Accessors;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;

import java.net.InetSocketAddress;
import java.nio.charset.StandardCharsets;
import java.util.List;

/**
 * UDP
 *
 * @author zf
 * @since 2019/8/16
 */
@Service
public class UdpService {
    private static final Logger LOG = LoggerFactory.getLogger(UdpService.class);

    private Channel channel;
    private NioEventLoopGroup group;


    @Data
    public static class Net{
        private String type;
        private Integer tcpPort;
        private String udpPort;
        private String webPort;
        private String ip;
    }

    public UdpEntity start(String ip, Integer port) {
        group = new NioEventLoopGroup();
        Bootstrap bootstrap = new Bootstrap();
        bootstrap.group(group)
                .channel(NioDatagramChannel.class)
                .option(ChannelOption.SO_BROADCAST, true)
                .option(ChannelOption.SO_BACKLOG, 1024)
                .option(ChannelOption.SO_RCVBUF, 128 * 1024 * 1024)
                .option(ChannelOption.SO_SNDBUF, 128 * 1024 * 1024)
                //增加发送长度
                .option(ChannelOption.RCVBUF_ALLOCATOR, new FixedRecvByteBufAllocator(128 * 1024))
                .handler(new ChannelInitializer() {
                    @Override
                    protected void initChannel(NioDatagramChannel socketChannel) {
                        //解决粘包和半包问题 接收数据全部要以$next$分割
                        socketChannel.pipeline().addLast(new DelimiterBasedFrameDecoder(128 * 1024,
                                Unpooled.wrappedBuffer("$next$".getBytes())));
                        socketChannel.pipeline().addLast(outboundHandler());
                        socketChannel.pipeline().addLast(inboundHandler());
                    }
                });
        System.out.println("###### Udp ######启动 端口:" + port);
        try {
            channel = bootstrap.bind(ip, port).sync().channel();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return new UdpEntity().setChannel(channel)
                .setGroup(group);

    }

    @Data
    @Accessors(chain = true)
    public static class UdpEntity {
        private Channel channel;
        private NioEventLoopGroup group;
    }

    public static class MsgEvent {
        private final InetSocketAddress inetSocketAddress;

        public InetSocketAddress getInetSocketAddress() {
            return inetSocketAddress;
        }

        public String getMsg() {
            return msg;
        }

        private final String msg;

        public MsgEvent(InetSocketAddress inetSocketAddress, String msg) {
            this.inetSocketAddress = inetSocketAddress;
            this.msg = msg;
        }
    }


    public void sendMessageBytes(byte[] data, String ip, Integer port) {
        if (channel != null) {
            try {
                channel.writeAndFlush(new DatagramPacket(Unpooled.copiedBuffer(data),
                        new InetSocketAddress(ip,port))).sync();
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
//                e.printStackTrace();
            }
        }
    }

    /**
     * 关闭
     */
    public void shutdown(){
        group.shutdownGracefully();
    }

    /**
     * 出参数据
     *
     * @return
     */
    private MessageToMessageEncoder outboundHandler() {
        return new MessageToMessageEncoder() {
            @Override
            protected void encode(ChannelHandlerContext ctx, MsgEvent msgEvent, List out) throws Exception {
                ByteBuf byteBuf = ctx.alloc().buffer(msgEvent.getMsg().length());
                byte[] content = msgEvent.getMsg().getBytes(CharsetUtil.UTF_8);
                byteBuf.writeBytes(content);
                out.add(new DatagramPacket(byteBuf, msgEvent.getInetSocketAddress()));
            }
        };
    }

    /**
     * json 美化
     * @param json
     * @return
     */
    public static String prettyJson(String json){
        if(StringUtils.isBlank(json)){
            return json;
        }
        JSONObject jsonObject;
        try {
            jsonObject = JSONObject.parseObject(json);
        }catch (Exception e){
            return json;
        }
        return JSONObject.toJSONString(jsonObject,true);
    }

    /**
     * 入参数据
     * @return
     */
    private SimpleChannelInboundHandler inboundHandler() {
        return new SimpleChannelInboundHandler() {

            @Override
            protected void channelRead0(ChannelHandlerContext ctx, DatagramPacket packet) throws Exception {
                ByteBuf content = packet.content();
                String req = content.toString(StandardCharsets.UTF_8);
                JSONObject object = JSONObject.parseObject(req);
                System.out.println("收到解析数据:" +  prettyJson(object.toJSONString()));
            }
        };
    }


}

2020-12-21

目前发现windows server 2012R2 无法启动,经过询问javacv作者得到回复

在新版本中,目前我测过最新版本1.5.4 是无法启动,会出现无法找到acode,Could not initialize class org.bytedeco.ffmpeg.global.avutil等各种错误

作者解释是在新版本中新功能使用到了Media Foundation 的功能 且在初始化garber时会加载,若操作系统环境没有该功能则报错

解决方案1:使用1.5.1版本无问题

解决方案2:在没有Media Foundation 的系统中安装该功能,再使用最新版本
 

你可能感兴趣的:(视频,视频服务,java)