1、实际应用
参考资料 1、使用JavaCV实现海康rtsp转rtmp实现无插件web端直播(无需转码,低资源消耗)_banmajio的博客-CSDN博客_java实现rtsp转rtmp
简单理解:转码的安全性更高(因为转封装不匹配的话很容易导致推流器直接崩溃),但是相对的 转码的效率更低,特别是cpu上 可能会有几百倍的差距,建议是能用转封装就直接使用转封装进行使用,必要时可以修改摄像头码流配置来使摄像头和我们的转封装配置相一致,从而实现高效推流
这里只保留代码 详情请仔细查看参考资料内的介绍!
1.1、转封装推流rtmp
import static org.bytedeco.ffmpeg.global.avcodec.av_packet_unref;
import org.bytedeco.ffmpeg.avcodec.AVPacket;
import org.bytedeco.ffmpeg.avformat.AVFormatContext;
import org.bytedeco.javacv.FFmpegFrameGrabber;
import org.bytedeco.javacv.FFmpegFrameRecorder;
import com.junction.pojo.CameraPojo;
/**
* @Title CameraPush.java
* @description 拉流推流
* @time 2019年12月16日 上午9:34:41
* @author wuguodong
**/
public class CameraPush {
protected FFmpegFrameGrabber grabber = null;// 解码器
protected FFmpegFrameRecorder record = null;// 编码器
int width;// 视频像素宽
int height;// 视频像素高
// 视频参数
protected int audiocodecid;
protected int codecid;
protected double framerate;// 帧率
protected int bitrate;// 比特率
// 音频参数
// 想要录制音频,这三个参数必须有:audioChannels > 0 && audioBitrate > 0 && sampleRate > 0
private int audioChannels;
private int audioBitrate;
private int sampleRate;
// 设备信息
private CameraPojo cameraPojo;
public CameraPush(CameraPojo cameraPojo) {
this.cameraPojo = cameraPojo;
}
/**
* 选择视频源
*
* @author wuguodong
* @throws Exception
*/
public CameraPush from() throws Exception {
// 采集/抓取器
System.out.println(cameraPojo.getRtsp());
grabber = new FFmpegFrameGrabber(cameraPojo.getRtsp());
if (cameraPojo.getRtsp().indexOf("rtsp") >= 0) {
grabber.setOption("rtsp_transport", "tcp");// tcp用于解决丢包问题
}
// 设置采集器构造超时时间
grabber.setOption("stimeout", "2000000");
grabber.start();// 开始之后ffmpeg会采集视频信息,之后就可以获取音视频信息
width = grabber.getImageWidth();
height = grabber.getImageHeight();
// 若视频像素值为0,说明采集器构造超时,程序结束
if (width == 0 && height == 0) {
System.err.println("[ERROR] 拉流超时...");
return null;
}
// 视频参数
audiocodecid = grabber.getAudioCodec();
System.err.println("音频编码:" + audiocodecid);
codecid = grabber.getVideoCodec();
framerate = grabber.getVideoFrameRate();// 帧率
bitrate = grabber.getVideoBitrate();// 比特率
// 音频参数
// 想要录制音频,这三个参数必须有:audioChannels > 0 && audioBitrate > 0 && sampleRate > 0
audioChannels = grabber.getAudioChannels();
audioBitrate = grabber.getAudioBitrate();
if (audioBitrate < 1) {
audioBitrate = 128 * 1000;// 默认音频比特率
}
return this;
}
/**
* 选择输出
*
* @author wuguodong
* @throws Exception
*/
public CameraPush to() throws Exception {
// 录制/推流器
record = new FFmpegFrameRecorder(cameraPojo.getRtmp(), width, height);
record.setVideoOption("crf", "28");// 画面质量参数,0~51;18~28是一个合理范围
record.setGopSize(2);
record.setFrameRate(framerate);
record.setVideoBitrate(bitrate);
record.setAudioChannels(audioChannels);
record.setAudioBitrate(audioBitrate);
record.setSampleRate(sampleRate);
AVFormatContext fc = null;
if (cameraPojo.getRtmp().indexOf("rtmp") >= 0 || cameraPojo.getRtmp().indexOf("flv") > 0) {
// 封装格式flv
record.setFormat("flv");
record.setAudioCodecName("aac");
record.setVideoCodec(codecid);
fc = grabber.getFormatContext();
}
record.start(fc);
return this;
}
/**
* 转封装
*
* @author wuguodong
* @throws org.bytedeco.javacv.FrameGrabber.Exception
* @throws org.bytedeco.javacv.FrameRecorder.Exception
* @throws InterruptedException
*/
public CameraPush go(Thread nowThread)
throws org.bytedeco.javacv.FrameGrabber.Exception, org.bytedeco.javacv.FrameRecorder.Exception {
long err_index = 0;// 采集或推流导致的错误次数
// 连续五次没有采集到帧则认为视频采集结束,程序错误次数超过5次即中断程序
//将探测时留下的数据帧释放掉,以免因为dts,pts的问题对推流造成影响
grabber.flush();
for (int no_frame_index = 0; no_frame_index < 5 || err_index < 5;) {
try {
// 用于中断线程时,结束该循环
nowThread.sleep(1);
AVPacket pkt = null;
// 获取没有解码的音视频帧
pkt = grabber.grabPacket();
if (pkt == null || pkt.size() <= 0 || pkt.data() == null) {
// 空包记录次数跳过
no_frame_index++;
err_index++;
continue;
}
// 不需要编码直接把音视频帧推出去
err_index += (record.recordPacket(pkt) ? 0 : 1);
av_packet_unref(pkt);
} catch (InterruptedException e) {
// 当需要结束推流时,调用线程中断方法,中断推流的线程。当前线程for循环执行到
// nowThread.sleep(1);这行代码时,因为线程已经不存在了,所以会捕获异常,结束for循环
// 销毁构造器
grabber.close();
record.close();
System.err.println("设备中断推流成功...");
break;
} catch (org.bytedeco.javacv.FrameGrabber.Exception e) {
err_index++;
} catch (org.bytedeco.javacv.FrameRecorder.Exception e) {
err_index++;
}
}
// 程序正常结束销毁构造器
grabber.close();
record.close();
System.err.println("设备推流完毕...");
return this;
}
}
1.2、转码推流rtmp
package com.baolu.dahua.push;
import lombok.extern.slf4j.Slf4j;
import org.bytedeco.ffmpeg.avcodec.AVCodecParameters;
import org.bytedeco.ffmpeg.avformat.AVFormatContext;
import org.bytedeco.ffmpeg.avformat.AVStream;
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.FFmpegLogCallback;
import org.bytedeco.javacv.Frame;
import java.util.concurrent.TimeUnit;
/**
* @author willzhao
* @version 1.0
* @description 读取指定的mp4文件,推送到SRS服务器
* @date 2021/11/19 8:49
*/@Slf4j
public class PushMp4 {
/**
* 本地MP4文件的完整路径(两分零五秒的视频)
*/// private static final String MP4_FILE_PATH = "/Users/zhaoqin/temp/202111/20/sample-mp4-file.mp4";
private static final String MP4_FILE_PATH = "rtsp://admin:blzn8899@192.168.0.220:554/Streaming/Channels/201";
/**
* SRS的推流地址
*/
private static final String SRS_PUSH_ADDRESS = "rtmp://192.168.11.27:9935/live/weinigb";
/**
* 读取指定的mp4文件,推送到SRS服务器
* @param sourceFilePath 视频文件的绝对路径
* @param PUSH_ADDRESS 推流地址
* @throws Exception
*/ private static void grabAndPush(String sourceFilePath, String PUSH_ADDRESS) throws Exception {
// 实例化帧抓取器对象,将文件路径传入
FFmpegFrameGrabber grabber = new FFmpegFrameGrabber(sourceFilePath);
long startTime = System.currentTimeMillis();
log.info("开始初始化帧抓取器");
grabber.setOption("timeout", "2000000");
grabber.setOption("rtsp_transport", "tcp");
grabber.startUnsafe(true);
// 初始化帧抓取器,例如数据结构(时间戳、编码器上下文、帧对象等),
// 如果入参等于true,还会调用avformat_find_stream_info方法获取流的信息,放入AVFormatContext类型的成员变量oc中
log.info("帧抓取器初始化完成,耗时[{}]毫秒", System.currentTimeMillis()-startTime);
// grabber.start方法中,初始化的解码器信息存在放在grabber的成员变量oc中
AVFormatContext avFormatContext = grabber.getFormatContext();
// 文件内有几个媒体流(一般是视频流+音频流)
int streamNum = avFormatContext.nb_streams();
// 没有媒体流就不 用继续了
if (streamNum<1) {
log.error("文件内不存在媒体流");
return; }
// 取得视频的帧率
// int frameRate = (int)grabber.getVideoFrameRate();
int frameRate =25;
log.info("视频帧率[{}],视频时长[{}]秒,媒体流数量[{}]",
frameRate,
avFormatContext.duration()/1000000,
avFormatContext.nb_streams());
// 遍历每一个流,检查其类型
for (int i=0; i< streamNum; i++) {
AVStream avStream = avFormatContext.streams(i);
AVCodecParameters avCodecParameters = avStream.codecpar();
log.info("流的索引[{}],编码器类型[{}],编码器ID[{}]", i, avCodecParameters.codec_type(), avCodecParameters.codec_id());
}
// 视频宽度
int frameWidth = grabber.getImageWidth();
// 视频高度
int frameHeight = grabber.getImageHeight();
// 音频通道数量
int audioChannels = grabber.getAudioChannels();
log.info("视频宽度[{}],视频高度[{}],音频通道数[{}]",
frameWidth,
frameHeight,
audioChannels);
// 实例化FFmpegFrameRecorder,将SRS的推送地址传入
FFmpegFrameRecorder recorder = new FFmpegFrameRecorder(PUSH_ADDRESS,
frameWidth,
frameHeight,
audioChannels);
// 设置编码格式
recorder.setVideoCodec(avcodec.AV_CODEC_ID_H264);
// 设置封装格式
recorder.setFormat("flv");
// 一秒内的帧数
recorder.setFrameRate(frameRate);
// 两个关键帧之间的帧数
recorder.setGopSize(frameRate);
// 设置音频通道数,与视频源的通道数相等
recorder.setAudioChannels(grabber.getAudioChannels());
startTime = System.currentTimeMillis();
log.info("开始初始化帧抓取器");
// 初始化帧录制器,例如数据结构(音频流、视频流指针,编码器),
// 调用av_guess_format方法,确定视频输出时的封装方式,
// 媒体上下文对象的内存分配,
// 编码器的各项参数设置
recorder.start();
log.info("帧录制初始化完成,耗时[{}]毫秒", System.currentTimeMillis()-startTime);
Frame frame;
startTime = System.currentTimeMillis();
log.info("开始推流");
long videoTS = 0;
int videoFrameNum = 0;
int audioFrameNum = 0;
int dataFrameNum = 0;
// 假设一秒钟15帧,那么两帧间隔就是(1000/15)毫秒
int interVal = 1000/frameRate;
// 发送完一帧后sleep的时间,不能完全等于(1000/frameRate),不然会卡顿,
// 要更小一些,这里取八分之一
interVal/=8;
System.out.println("1"+Thread.currentThread().getName());
// 持续从视频源取帧
while (null!=(frame=grabber.grab())) {
System.out.println("2"+Thread.currentThread().getName());
videoTS = 1000 * (System.currentTimeMillis() - startTime);
// 时间戳
recorder.setTimestamp(videoTS);
// 有图像,就把视频帧加一
if (null!=frame.image) {
videoFrameNum++;
}
// 有声音,就把音频帧加一
if (null!=frame.samples) {
audioFrameNum++;
}
// 有数据,就把数据帧加一
if (null!=frame.data) {
dataFrameNum++;
}
// 取出的每一帧,都推送到SRS
recorder.record(frame);
// 停顿一下再推送
Thread.sleep(interVal);
}
log.info("推送完成,视频帧[{}],音频帧[{}],数据帧[{}],耗时[{}]秒",
videoFrameNum,
audioFrameNum,
dataFrameNum,
(System.currentTimeMillis()-startTime)/1000);
// 关闭帧录制器
recorder.close();
// 关闭帧抓取器
grabber.close();
}
public static void main(String[] args) throws Exception {
// new Thread(()->{
// try {
// grabAndPush(MP4_FILE_PATH, SRS_PUSH_ADDRESS);
// } catch (Exception e) {
// throw new RuntimeException(e);
// }
// }).start();
// new Thread(()->{
// try {
// grabAndPush("rtsp://admin:mtadmin123@192.168.19.224:554/cam/realmonitor?channel=435&subtype=1", "rtmp://172.17.0.227:9935/live/weinigb1");
// } catch (Exception e) {
// throw new RuntimeException(e);
// }
// }).start();
// /**
// * 本地MP4文件的完整路径(两分零五秒的视频)
// */
// Thread.sleep(300000000);
grabAndPush(MP4_FILE_PATH, SRS_PUSH_ADDRESS);
// grabAndPush("rtsp://admin:mtadmin123@192.168.19.224:554/cam/realmonitor?channel=435&subtype=1", "rtmp://172.17.0.227:9935/live/weinigb1");
}
}
```erp