使用nginx-rtmp-module搭建HLS直播服务器 用FFmpeg推送,前端实时展示(亲测可用,踩坑记录)
需求
对接某某机构的视频,视频播放需要配置ip白名单才行。给的是固定流地址,但是用前端播放IP不固定,只能用后端获取后转码发给前端播放了。
nginx搭建
1.下载nginx
或者 wget http://nginx.org/download/nginx-1.18.0.tar.gz
2.wget https://github.com/arut/nginx-rtmp-module/archive/master.zip
3.tar -zxvf nginx-1.18.0.tar.gz
4. unzip nginx-rtmp-module-master.zip
5. cd nginx-1.18.0
6. ./configure --add-module=/data/app/nginx/nginx-rtmp-module-master
7. make && make install
nginx安装完成
安装ffmpeg
我是用conda 安装的,需要先安装这个环境
wget -c https://repo.continuum.io/miniconda/Miniconda3-latest-Linux-x86_64.sh
chmod 777 Miniconda3-latest-Linux-x86_64.sh
bash Miniconda3-latest-Linux-x86_64.sh
设置环境变量
vim ~/.bashrc
export PATH=$PATH:/root/miniconda3/bin
source ~/.bashrc
一键安装
conda install ffmpeg
nginx配置
1.下面的/data/app/nginx/use/www/hls/ 是随便写的,就是推流后产生m3u8的文件,放在哪
2. /data/app/nginx/nginx-rtmp-module-master/ 是组件的位置
worker_processes 2;
error_log logs/error.log debug;
events {
worker_connections 1024;
}
rtmp {
server {
listen 1935;
chunk_size 4000;
application rtmplive{
live on;
max_connections 1024;
}
application hls{
live on;
hls on;
hls_path /data/app/nginx/use/www/hls/;
hls_fragment 5s;
}
}
}
http {
server {
listen 8080;
location /hls {
types{
application/vnd.apple.mpegurl m3u8;
video/mp2t ts;
}
root /data/app/nginx/use/www;
add_header Cache-Control no-cache;
add_header Access-Control-Allow-Origin *;
add_header Access-Control-Allow-Headers *;
}
location /stat {
rtmp_stat all;
rtmp_stat_stylesheet stat.xsl;
}
location /stat.xsl {
root /data/app/nginx/nginx-rtmp-module-master/;
}
location /control {
rtmp_control all;
}
#location /publish {
# return 201;
#}
#location /play {
# return 202;
#}
#location /record_done {
# return 203;
#}
location /rtmp-publisher {
root /data/app/nginx/nginx-rtmp-module-master/test;
}
location / {
root /data/app/nginx/nginx-rtmp-module-master/test/www;
}
}
}
推流与获取
ffmpeg -re -i xxx -vcodec libx264 -vprofile baseline -acodec aac -ar 44100 -strict -2 -ac 1 -f flv -s 1280x720 -q 10 rtmp://localhost:1935/hls/test
需要开放端口,然后就可以获取到了
rtmp://xx:1935/hls/test10
http://xx:8080/hls/test10.m3u8
Java代码
通过调用这个来推流,因为推流很消耗CPU,先开3台,控制下数量。
如果返回-1,就是是资源被占完了,要等会再调用
import io.swagger.models.auth.In;
import lombok.extern.log4j.Log4j2;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.SetOperations;
import org.springframework.util.CollectionUtils;
import javax.security.auth.login.CredentialNotFoundException;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.util.Random;
import java.util.Set;
import java.util.concurrent.Callable;
import java.util.concurrent.TimeUnit;
@Log4j2
public class RTSPCallableTool implements Callable<Integer> {
private final RedisTemplate redisTemplate;
SetOperations setOperations;
private final String key = "nginx:push";
Set<Object> set;
{
redisTemplate = (RedisTemplate) SpringUtil.getBean("redisTemplate");
SetOperations setOperations = redisTemplate.opsForSet();
set= setOperations.members(key);
if (CollectionUtils.isEmpty(setOperations.members(key))){
setOperations.add(key,100001);
set= setOperations.members(key);
}
random = new Random();
}
volatile String line = "";
private final String equUrl;
private int number = 0;
String flag = "Already publishing";
private Process process;
private final Random random;
// ffmpeg位置,最好写在配置文件中
private final String ffmpegPath = "ffmpeg -re -i ";
private final String param = " -vcodec libx264 -vprofile baseline -acodec aac -ar 44100 -strict -2 -ac 1 -f flv -s 1280x720 -q 10 ";
private final String rtmpUrl = " rtmp://localhost:1935/hls/test";
public RTSPCallableTool(String equUrl, int number) {
super();
this.equUrl = equUrl;
this.number = number;
}
@Override
public Integer call() throws Exception {
number = numerCheck(number);
if (-1 == number) return -1;
String command = ffmpegPath; // ffmpeg位置
command += equUrl; // ffmpeg开头,-re代表按照帧率发送,在推流时必须有
command += param;
command += rtmpUrl; // 指定推送服务器,-f:指定格式
command += number;
try {
// 运行cmd命令,获取其进程
process = Runtime.getRuntime().exec(command);
// 输出ffmpeg推流日志
BufferedReader br = new BufferedReader(new InputStreamReader(process.getErrorStream()));
int len = 0;
while ((line = br.readLine()) != null) {
len++;
if (line.contains(flag)) {
//说明已经有在看了,需要换个地址
process.destroy();
}
if (len > 32) break;
}
new Thread(() -> {
try {
Thread.sleep(10 * 60 * 1000);
} catch (InterruptedException e) {
log.error(e);
}
set.remove(number);
process.destroy();
}).start();
} catch (Exception e) {
log.error("推流报错,请求{},{}", command, e);
}
return number;
}
private int numerCheck(int number) {
if (set.size() < 3) {
number = random.nextInt(100000);
for (int j = 0; j < 100; j++) {
set.add(number);
redisTemplate.expire(key,600, TimeUnit.SECONDS);
return number;
}
}
return -1;
}
}
核心调用
Random random = new Random();
//说明是有ip白名单的,那只能nginx转发了
FutureTask<Integer> futureTask = new FutureTask<>(new RTSPCallableTool(playUrl,random.nextInt(100000)));
new Thread(futureTask).start();
// playUrlFixed.put("playUrl","http://xx:8080/hls/test"+futureTask.get()+".m3u8");
// rtmp://xx:1935/hls/test10
playUrlFixed.put("playUrl","https://223.4.64.195:8080/hls/test"+futureTask.get()+".m3u8");
from origin ‘http://…‘ has been blocked by CORS policy: No ‘Access-Control-Allow-Origin‘ 跨域问题
因为涉及到从nginx代理到服务器端,地址会不一样,涉及到了跨域。
定义:浏览器从一个域名的网页去请求另一个域名的资源时,域名、端口、协议任一不同,都是跨域
代码层面控制
import org.apache.commons.lang3.StringUtils;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.core.Ordered;
import org.springframework.stereotype.Component;
import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* @author LiaoShengQiu
*/
@Component
public class CorsFilter implements Filter {
@Override
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {
HttpServletResponse response = (HttpServletResponse) res;
HttpServletRequest request = (HttpServletRequest) req;
// 不使用*,自动适配跨域域名,避免携带Cookie时失效
response.setHeader("Access-Control-Allow-Origin", "*");
// 自适应所有自定义头
String headers = request.getHeader("Access-Control-Request-Headers");
if(StringUtils.isNotBlank(headers)) {
response.setHeader("Access-Control-Allow-Headers", headers);
response.setHeader("Access-Control-Expose-Headers", headers);
}
// 允许跨域的请求方法类型
response.setHeader("Access-Control-Allow-Methods", "*");
// 预检命令(OPTIONS)缓存时间,单位:秒
response.setHeader("Access-Control-Max-Age", "3600");
// 明确许可客户端发送Cookie,不允许删除字段即可
response.setHeader("Access-Control-Allow-Credentials", "true");
chain.doFilter(request, response);
}
@Override
public void init(FilterConfig filterConfig) {
}
@Override
public void destroy() {
}
@Bean
public FilterRegistrationBean registerFilter() {
FilterRegistrationBean<CorsFilter> bean = new FilterRegistrationBean<>();
// bean.addUrlPatterns("/*");
bean.addUrlPatterns("/getData/*");
bean.setFilter(new CorsFilter());
// 过滤顺序,从小到大依次过滤
bean.setOrder(Ordered.HIGHEST_PRECEDENCE);
return bean;
}
}
nginx
在/location中加
location /hls {
types{
application/vnd.apple.mpegurl m3u8;
video/mp2t ts;
}
root /data/app/nginx/use/www;
add_header Cache-Control no-cache;
add_header Access-Control-Allow-Origin *;
add_header Access-Control-Allow-Headers *;
}
Mixed Content:‘xxx’ was loaded over HTTPS, but requested an insecure XMLHttpRequest endpoint ‘xxx’ 协议问题
跨域解决后,发现因为流推过去是HTTP格式的,而页面是HTTPS格式的,就报了上面的错。
解决办法一:
页面的head中加入:
试了下没用。
解决办法二
用nginx反向代理HTTPS,需要安装插件 http_ssl_module,然后配置证书什么的。
操作地址
最终解决
后面想起来公司有两个HTTPS证书,通过SLV代理HTTPS,监听指定服务器和端口,推流出去的时候用把域名换成ip,就可以。
最终的流地址是
https://xx/hls/test17726.m3u8
nginx(解决)
后来仔细想想,我这个其实就是绑定了ip白名单,转nginx转发就可以了
https下访问http,多一层转发就可以
比如xx公司的 有绑定白名单的,访问路径是这个 http://xx:7086/live/cameraid/1000105%2445/substream/1.m3u8
nginx在server 中配置一个转发
#xxx
location /iot/video/xxx/ {
proxy_pass http://xxx:7086/live/cameraid/;
add_header Cache-Control no-cache;
add_header Access-Control-Allow-Origin *;
add_header Access-Control-Allow-Headers *;
}
然后在https服务器下,配置一个转发。
#反向代理,解决白名单、https
location /iot/video/ {
proxy_pass http://10.145.240.207:80/iot/video/;
add_header Cache-Control no-cache;
add_header Access-Control-Allow-Origin *;
add_header Access-Control-Allow-Headers *;
proxy_set_header X-Forwarded-Proto $scheme;
}
然后就可以访问了,如果后续如果有添加
location /iot/video/aaa/ {
proxy_pass http://aaa:10016/hls2/;
add_header Cache-Control no-cache;
add_header Access-Control-Allow-Origin *;
add_header Access-Control-Allow-Headers *;
proxy_set_header X-Forwarded-Proto $scheme;
}
都可以通过这个转发