RTSP转RTMP服务
环境搭建
nginx
-
nginx 需要安装rtmp模块
-
配置文件
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 +
'}';
}
}
}

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





