一、下载安装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();
}
}
}
}
}