# rtsp2httpflv **Repository Path**: chengxuworld/rtsp2httpflv ## Basic Information - **Project Name**: rtsp2httpflv - **Description**: No description available - **Primary Language**: Unknown - **License**: Not specified - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2025-02-07 - **Last Updated**: 2025-02-11 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README https://blog.csdn.net/weixin_47914635/article/details/129691433 # 一、下载nginx ## 1、更新相关依赖 ```shell yum -y install gcc zlib zlib-devel pcre-devel openssl openssl-devel ``` ## 2、下载nginx ```shell cd /usr/local mkdir nginx cd nginx wget http://nginx.org/download/nginx-1.19.3.tar.gz tar -zxvf nginx-1.19.3.tar.gz ``` # 二、下载nginx-http-flv-module 下载地址:https://gitcode.com/gh_mirrors/ng/nginx-http-flv-module?utm_source=csdn_github_accelerator&isLogin=1 直接下载最新的代码上传到服务器/usr/local/下 # 三、配置环境支持http2.0 注:配置http2.0是为了解决同源策略的限制,同一源下只能同时创建6个请求,播放6路视频。若无此需求,可跳第一步,直接使用http即可。 ## 1、更新openssl版本 openssl version openssl 版本在1.0.2以上则不需要更新,否则需要升级版本,如下是升级为1.1.1n ```shell cd /usr/local/ mkdir openssl cd openssl wget https://www.openssl.org/source/openssl-1.1.1n.tar.gz tar -zxvf openssl-1.1.1n.tar.gz cd openssl-1.1.1n ./config make && make install ``` 备份原有的openssl ```shell mv /usr/bin/openssl /usr/bin/openssl.bak mv /usr/include/openssl /usr/include/openssl.bak ``` 添加新的软链接 ```shell ln -s /usr/local/bin/openssl /usr/bin/openssl ln -s /usr/local/include/openssl/ /usr/include/openssl echo "/usr/local/lib64" >> /etc/ld.so.conf ldconfig -v ``` ## 2、编译nginx ```shell cd /usr/local/nginx-1.19.3 ./configure --with-http_ssl_module --with-openssl=/usr/local/openssl/openssl-1.1.1n --with-http_v2_module --add-module=/usr/local/nginx-http-flv-module ./configure --with-http_ssl_module --with-openssl=/root/openssl/openssl-1.1.1w --with-http_v2_module --add-module=/usr/local/nginx-http-flv-module 其中--with-openssl 是openssl的路径,--add-module 是nginx-http-flv-module的路径 make -j4 make install ``` ## 3、生成https测试证书 ```shell cd /usr/local/nginx mkdir certs cd /certs openssl genrsa -des3 -out server.key 2048 openssl rsa -in server.key -out server.key openssl req -new -key server.key -out server.csr openssl x509 -req -days 365 -in server.csr -signkey server.key -out server.crt ``` ## 4、配置nginx.conf ``` worker_processes 1; error_log logs/error.log error; # load_module modules/ngx_http_flv_live_module.so; events { worker_connections 4096; } http { include mime.types; default_type application/octet-stream; keepalive_timeout 65; server { listen 18071; location /live { flv_live on; chunked_transfer_encoding on; add_header 'Access-Control-Allow-Origin' '*'; add_header 'Access-Control-Allow-Credentials' 'true'; add_header 'Access-Control-Allow-Headers' 'X-Requested-With'; add_header 'Access-Control-Allow-Methods' 'GET,POST,OPTIONS'; add_header 'Cache-Control' 'no-cache'; } location /hls { types { application/vnd.apple.mpegurl m3u8; video/mp2t ts; } root /tmp; # hls方式m3u8访问的文件路径 } location /stat { rtmp_stat all; rtmp_stat_format json; } } server { listen 18181 ssl http2; server_name 192.168.3.122; ssl_certificate "/usr/local/nginx/certs/server.crt"; # server公钥证书的路径 ssl_certificate_key "/usr/local/nginx/certs/server.key"; # server私钥的路径 ssl_session_cache shared:SSL:1m; ssl_session_timeout 5m; ssl_ciphers HIGH:!aNULL:!MD5; ssl_prefer_server_ciphers on; proxy_set_header Host $http_host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header REMOTE-HOST $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; location /{ proxy_pass http://192.168.3.122:18071/; } } } rtmp_auto_push on; rtmp_auto_push_reconnect 1s; rtmp_socket_dir /tmp; rtmp { out_queue 4096; out_cork 8; max_streams 128; timeout 15s; drop_idle_publisher 15s; log_interval 5s; #log 模块在 access.log 中记录日志的间隔时间,对调试非常有用 log_size 1m; #log 模块用来记录日志的缓冲区大小 server { listen 18072; server_name 192.168.3.122; application transition { live on; gop_cache off; # on_play http://192.168.3.13:18061/flv/on_play; } } } ``` 指定配置文件启动 ```shell ../sbin/nginx -s stop ../sbin/nginx -c /usr/local/nginx/conf/nginx.conf ``` # 四、安装ffmpeg ```shell cd /usr/local mkdir ffmpeg cd ffmpeg ``` ## 1、升级yasm(yasm和nasm选一即可) ```shell cd /usr/local/ffmpeg mkdir yasm cd yasm/ wget http://www.tortall.net/projects/yasm/releases/yasm-1.3.0.tar.gz tar -zxvf yasm-1.3.0.tar.gz cd yasm-1.3.0/ ./configure make && make install yasm --version ln -s /usr/local/ffmpeg/yasm/yasm-1.3.0/yasm /usr/bin/yasm ``` ## 2、升级nasm ```shell cd /usr/local/ffmpeg mkdir nasm cd nasm/ wget https://www.nasm.us/pub/nasm/releasebuilds/2.14.02/nasm-2.14.02.tar.bz2 tar -jxvf nasm-2.14.02.tar.bz2 cd nasm-2.14.02 ./configure make && make install nasm --version make && make install ln -s /usr/local/ffmpeg/nasm/nasm-2.14.02/nasm /usr/bin/nasm ``` ## 3、安装x264视频编码库 ```shell cd /usr/local/ffmpeg mkdir x264 cd x264/ wget https://code.videolan.org/videolan/x264/-/archive/master/x264-master.tar.gz tar -zxvf x264-master.tar.gz cd x264-master ./configure make && make install ``` ## 4、安装ffmpeg ```shell cd /usr/local/ffmpeg wget http://www.ffmpeg.org/releases/ffmpeg-6.0.tar.gz tar -zxvf ffmpeg-6.0.tar.gz cd ffmpeg-6.0 ./configure --prefix=/usr/local/ffmpeg make && make install ``` 配置环境变量 ```shell vim /etc/profile ``` 在最后添加 ```shell export PATH=$PATH:/usr/local/ffmpeg/bin # 指向自己安装的ffmpeg下的bin目录 source /etc/profile #设置生效 ffmpeg -version # 查看版本 ``` # 五、转码测试 ## 1、转码为http-flv直接访问 ```shell # 执行指令,转换rtsp转为rtmp ffmpeg -rtsp_transport tcp -i "rtsp视频流地址" -vcodec libx264 -vprofile baseline -an -tune zerolatency -preset ultrafast -g 25 -f flv "rtmp://192.168.3.122:18072/transition/home" # 其中 18072为nginx下配置的rtmp端口,transition为nginx配置的location,home是自定义房间号 ``` 出现如下表示成功 vlc下载地址:http://www.videolan.org/vlc/ 使用vlc直接访问,注意其中的18181是https配置的端口,port对应上方的18072,app对应上方的location,stream对应上方的home 点击播放查看,楼主测试的延迟大概在1s内,符合预期,直接采用该方案 ## 2、转码为hls通过m3u8文件访问 hls保存了最新的视频文件,可自定义保存时限,可通过保存的文件实现回放功能,测试的延迟在2-3秒 ```shell # 执行指令,转为m3u8文件 ffmpeg -rtsp_transport tcp -i "rtsp视频流地址" -vcodec libx264 -vprofile baseline -an -f hls -hls_time 5 -hls_list_size 2 -hls_flags 2 "/tmp/hls/22222.m3u8" /tmp/hls/测试.m3u8 # 注意这里配置的nginx中fls下root 配置的 /tmp,根据自己的配置修改 -hls_time n: # 设置每片的长度,默认值为2。单位为秒 -hls_list_size n: # 设置播放列表保存的最多条目,设置为0会保存有所片信息,默认值为5 -hls_flags n:# 设置多少片之后开始覆盖,如果设置为0则不会覆盖,默认值为0.这个选项能够避免在磁盘上存储过多的片,而且能够限制写入磁盘的最多的片的数量 -hls_start_number n:# 设置播放列表中sequence number的值为number,默认值为0 ``` 查看文件 cd /tmp/hls 使用vlc直接访问 http://192.168.3.122.1807/hls/测试.m3u8 # 六、通过Java下发指令 ## 1、配置文件yml ```yaml #flv视频地址 camera: #转码服务器账号 username: root #转码服务器密码 password: 123456 #转码服务器ip host: 192.168.3.122 #播放视频流服务器地址 flv-url: https://192.168.3.122:18181/live?port=18072&app=transition&stream=%s stat-url: http://192.168.3.122:18071/stat #接收流服务器地址 rtmp-url: rtmp://192.168.3.122:18072/transition/ #关闭指定进程 close-command: ps -ef | grep 'ffmpeg.*%s' | grep -v grep | awk '{print $2}' | sed -e "s/^/kill -9 /g" | sh - & #开始转码推流指令 open-command: nohup /usr/local/ffmpeg/bin/ffmpeg -rtsp_transport tcp -i "%s" -codec:v libx264 -vprofile baseline -codec:a aac -tune zerolatency -preset ultrafast -ar 24000 -ac 1 -g 25 -s 1600*900 -f flv "${camera.rtmp-url}%s" 1>/dev/null 2>&1 & ``` ## 2、远程执行linux指令工具类 ```java package com.gongl.common.utils; import com.jcraft.jsch.*; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; import javax.annotation.PostConstruct; import java.io.*; @Service public class ExecuteShellUtil { private static final Logger log = LoggerFactory.getLogger(ExecuteShellUtil.class); @Value("${camera.username}") private String username; @Value("${camera.password}") private String password; @Value("${camera.host}") private String host; private Session session; public ExecuteShellUtil() { } @PostConstruct private void connect() throws JSchException { JSch jsch = new JSch(); session = jsch.getSession(username, host); session.setPassword(password); java.util.Properties config = new java.util.Properties(); config.put("StrictHostKeyChecking", "no"); session.setConfig(config); session.connect(); log.info("连接到SFTP成功。host: " + host); } /** * * @param command * @return 0--成功,1--失败 * @throws Exception */ public int execCmd(String command) throws Exception { int returnCode = -1; Channel channel = session.openChannel("exec"); ((ChannelExec) channel).setCommand(command); channel.setInputStream(null); ((ChannelExec) channel).setErrStream(System.err); BufferedReader reader = new BufferedReader(new InputStreamReader(channel.getInputStream())); channel.connect(); StringBuffer buf = new StringBuffer(); int c = -1; while ((c = reader.read()) != -1) { buf.append((char) c); } reader.close(); if (channel.isClosed()) { returnCode = channel.getExitStatus(); } log.info("---给: " + host + " 发送指令:" + command + " 。result:" + returnCode); channel.disconnect(); return returnCode; } } ``` ## 3、RestTemplate工具类,用于维护不需要转码的rtsp ### 3.1 添加依赖 支持http携带body请求,不需要可以不添加 ```xml org.apache.httpcomponents httpclient 4.5.6 ``` 3.2 添加RestTemplateConfig配置类 https://blog.csdn.net/weixin_47914635/article/details/123206595 ```java import org.apache.http.client.methods.HttpEntityEnclosingRequestBase; import org.apache.http.client.methods.HttpUriRequest; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.http.HttpMethod; import org.springframework.http.client.HttpComponentsClientHttpRequestFactory; import org.springframework.http.client.SimpleClientHttpRequestFactory; import org.springframework.http.converter.StringHttpMessageConverter; import org.springframework.web.client.RestTemplate; import javax.net.ssl.*; import java.io.IOException; import java.net.HttpURLConnection; import java.net.InetAddress; import java.net.Socket; import java.net.URI; import java.nio.charset.StandardCharsets; import java.security.cert.X509Certificate; /** * RestTemplate 配置 * * @author gongl */ @Configuration public class RestTemplateConfig { @Bean public RestTemplate restTemplate() { RestTemplate restTemplate = new RestTemplate(); //修改restTemplate的RequestFactory使其支持Get携带body参数 restTemplate.setRequestFactory(new HttpComponentsClientRestfulHttpRequestFactory()); restTemplate.getMessageConverters().set(1, new StringHttpMessageConverter(StandardCharsets.UTF_8)); return restTemplate; } //http支持body-----------------start public static final class HttpComponentsClientRestfulHttpRequestFactory extends HttpComponentsClientHttpRequestFactory { @Override protected HttpUriRequest createHttpUriRequest(HttpMethod httpMethod, URI uri) { if (httpMethod == HttpMethod.GET) { return new HttpGetRequestWithEntity(uri); } return super.createHttpUriRequest(httpMethod, uri); } } private static final class HttpGetRequestWithEntity extends HttpEntityEnclosingRequestBase { public HttpGetRequestWithEntity(final URI uri) { super.setURI(uri); } @Override public String getMethod() { return HttpMethod.GET.name(); } } //http支持body-----------------end //支持https public static class HttpsClientRequestFactory extends SimpleClientHttpRequestFactory { @Override protected void prepareConnection(HttpURLConnection connection, String httpMethod) { try { if (!(connection instanceof HttpsURLConnection)) { throw new RuntimeException("An instance of HttpsURLConnection is expected"); } HttpsURLConnection httpsConnection = (HttpsURLConnection) connection; TrustManager[] trustAllCerts = new TrustManager[]{ new X509TrustManager() { @Override public X509Certificate[] getAcceptedIssuers() { return null; } @Override public void checkClientTrusted(X509Certificate[] certs, String authType) { } @Override public void checkServerTrusted(X509Certificate[] certs, String authType) { } } }; SSLContext sslContext = SSLContext.getInstance("TLS"); sslContext.init(null, trustAllCerts, new java.security.SecureRandom()); httpsConnection.setSSLSocketFactory(new MyCustomSSLSocketFactory(sslContext.getSocketFactory())); httpsConnection.setHostnameVerifier(new HostnameVerifier() { @Override public boolean verify(String s, SSLSession sslSession) { return true; } }); super.prepareConnection(httpsConnection, httpMethod); } catch (Exception e) { e.printStackTrace(); } } // SSLSocketFactory用于创建 SSLSockets private static class MyCustomSSLSocketFactory extends SSLSocketFactory { private final SSLSocketFactory delegate; public MyCustomSSLSocketFactory(SSLSocketFactory delegate) { this.delegate = delegate; } // 返回默认启用的密码套件。除非一个列表启用,对SSL连接的握手会使用这些密码套件。 // 这些默认的服务的最低质量要求保密保护和服务器身份验证 @Override public String[] getDefaultCipherSuites() { return delegate.getDefaultCipherSuites(); } // 返回的密码套件可用于SSL连接启用的名字 @Override public String[] getSupportedCipherSuites() { return delegate.getSupportedCipherSuites(); } @Override public Socket createSocket(final Socket socket, final String host, final int port, final boolean autoClose) throws IOException { final Socket underlyingSocket = delegate.createSocket(socket, host, port, autoClose); return overrideProtocol(underlyingSocket); } @Override public Socket createSocket(final String host, final int port) throws IOException { final Socket underlyingSocket = delegate.createSocket(host, port); return overrideProtocol(underlyingSocket); } @Override public Socket createSocket(final String host, final int port, final InetAddress localAddress, final int localPort) throws IOException { final Socket underlyingSocket = delegate.createSocket(host, port, localAddress, localPort); return overrideProtocol(underlyingSocket); } @Override public Socket createSocket(final InetAddress host, final int port) throws IOException { final Socket underlyingSocket = delegate.createSocket(host, port); return overrideProtocol(underlyingSocket); } @Override public Socket createSocket(final InetAddress host, final int port, final InetAddress localAddress, final int localPort) throws IOException { final Socket underlyingSocket = delegate.createSocket(host, port, localAddress, localPort); return overrideProtocol(underlyingSocket); } private Socket overrideProtocol(final Socket socket) { if (!(socket instanceof SSLSocket)) { throw new RuntimeException("An instance of SSLSocket is expected"); } ((SSLSocket) socket).setEnabledProtocols(new String[]{"TLSv1.2"}); return socket; } } } } ``` ### 3.3 添加公共类RestTemplateUtils ```java package com.haxk.common.utils.http; import com.alibaba.fastjson.JSON; import org.springframework.http.*; import org.springframework.web.client.RestTemplate; import java.util.HashMap; import java.util.Map; /** * RestTemplate 远程调用工具类 * * @author gongl */ public class RestTemplateUtils { private static final RestTemplate restTemplate = new RestTemplate(); private static final RestTemplate sslRestTemplate = new RestTemplate(new RestTemplateConfig.HttpsClientRequestFactory()); /** * GET请求调用方式 * * @param url 请求URL * @param responseType 返回对象类型 * @return ResponseEntity 响应对象封装类 */ public static ResponseEntity get(String url, Class responseType) { return restTemplate.getForEntity(url, responseType); } /** * GET请求调用方式 * * @param url 请求URL * @param responseType 返回对象类型 * @param uriVariables URL中的变量,按顺序依次对应 * @return ResponseEntity 响应对象封装类 */ public static ResponseEntity get(String url, Class responseType, Object... uriVariables) { return restTemplate.getForEntity(url, responseType, uriVariables); } /** * GET请求调用方式 * * @param url 请求URL * @param responseType 返回对象类型 * @param uriVariables URL中的变量,与Map中的key对应 * @return ResponseEntity 响应对象封装类 */ public static ResponseEntity get(String url, Class responseType, Map uriVariables) { return restTemplate.getForEntity(url, responseType, uriVariables); } /** * 带请求头的GET请求调用方式 * * @param url 请求URL * @param headers 请求头参数 * @param responseType 返回对象类型 * @param uriVariables URL中的变量,按顺序依次对应 * @return ResponseEntity 响应对象封装类 */ public static ResponseEntity get(String url, Map headers, Class responseType, Object... uriVariables) { HttpHeaders httpHeaders = new HttpHeaders(); httpHeaders.setAll(headers); return get(url, httpHeaders, responseType, uriVariables); } /** * 带请求头的GET请求调用方式 * * @param url 请求URL * @param headers 请求头参数 * @param responseType 返回对象类型 * @param uriVariables URL中的变量,按顺序依次对应 * @return ResponseEntity 响应对象封装类 */ public static ResponseEntity get(String url, HttpHeaders headers, Class responseType, Object... uriVariables) { HttpEntity requestEntity = new HttpEntity<>(headers); return exchange(url, HttpMethod.GET, requestEntity, responseType, uriVariables); } /** * 带请求头的GET请求调用方式 * * @param url 请求URL * @param headers 请求头参数 * @param responseType 返回对象类型 * @param uriVariables URL中的变量,与Map中的key对应 * @return ResponseEntity 响应对象封装类 */ public static ResponseEntity get(String url, Map headers, Class responseType, Map uriVariables) { HttpHeaders httpHeaders = new HttpHeaders(); httpHeaders.setAll(headers); return get(url, httpHeaders, responseType, uriVariables); } /** * 带请求头的GET请求调用方式 * * @param url 请求URL * @param headers 请求头参数 * @param responseType 返回对象类型 * @param uriVariables URL中的变量,与Map中的key对应 * @return ResponseEntity 响应对象封装类 */ public static ResponseEntity get(String url, HttpHeaders headers, Class responseType, Map uriVariables) { HttpEntity requestEntity = new HttpEntity<>(headers); return exchange(url, HttpMethod.GET, requestEntity, responseType, uriVariables); } /** * body携带参数的GET请求调用方式 * * @param url 请求URL * @param params 请求参数 * @return ResponseEntity 响应对象封装类 */ public static ResponseEntity get(String url, Map params, Class responseType) { HttpHeaders headers = new HttpHeaders(); headers.add("Content-Type", "application/json"); HttpEntity httpEntity = new HttpEntity<>(JSON.toJSONString(params), headers); return restTemplate.exchange(url, HttpMethod.GET, httpEntity, responseType); } /** * POST请求调用方式 * * @param url 请求URL * @param responseType 返回对象类型 * @return ResponseEntity 响应对象封装类 */ public static ResponseEntity post(String url, Class responseType) { return restTemplate.postForEntity(url, HttpEntity.EMPTY, responseType); } /** * POST请求调用方式 * * @param url 请求URL * @param requestBody 请求参数体 * @param responseType 返回对象类型 * @return ResponseEntity 响应对象封装类 */ public static ResponseEntity post(String url, Object requestBody, Class responseType) { return restTemplate.postForEntity(url, requestBody, responseType); } /** * POST请求调用方式 * * @param url 请求URL * @param requestBody 请求参数体 * @param responseType 返回对象类型 * @param uriVariables URL中的变量,按顺序依次对应 * @return ResponseEntity 响应对象封装类 */ public static ResponseEntity post(String url, Object requestBody, Class responseType, Object... uriVariables) { return restTemplate.postForEntity(url, requestBody, responseType, uriVariables); } /** * POST请求调用方式 * * @param url 请求URL * @param requestBody 请求参数体 * @param responseType 返回对象类型 * @param uriVariables URL中的变量,与Map中的key对应 * @return ResponseEntity 响应对象封装类 */ public static ResponseEntity post(String url, Object requestBody, Class responseType, Map uriVariables) { return restTemplate.postForEntity(url, requestBody, responseType, uriVariables); } /** * 带请求头的POST请求调用方式 * * @param url 请求URL * @param headers 请求头参数 * @param requestBody 请求参数体 * @param responseType 返回对象类型 * @param uriVariables URL中的变量,按顺序依次对应 * @return ResponseEntity 响应对象封装类 */ public static ResponseEntity post(String url, Map headers, Object requestBody, Class responseType, Object... uriVariables) { HttpHeaders httpHeaders = new HttpHeaders(); httpHeaders.setAll(headers); return post(url, httpHeaders, requestBody, responseType, uriVariables); } /** * 带请求头的POST请求调用方式 * * @param url 请求URL * @param headers 请求头参数 * @param requestBody 请求参数体 * @param responseType 返回对象类型 * @param uriVariables URL中的变量,按顺序依次对应 * @return ResponseEntity 响应对象封装类 */ public static ResponseEntity post(String url, HttpHeaders headers, Object requestBody, Class responseType, Object... uriVariables) { HttpEntity requestEntity = new HttpEntity(requestBody, headers); return post(url, requestEntity, responseType, uriVariables); } /** * 带请求头的POST请求调用方式 * * @param url 请求URL * @param headers 请求头参数 * @param requestBody 请求参数体 * @param responseType 返回对象类型 * @param uriVariables URL中的变量,与Map中的key对应 * @return ResponseEntity 响应对象封装类 */ public static ResponseEntity post(String url, Map headers, Object requestBody, Class responseType, Map uriVariables) { HttpHeaders httpHeaders = new HttpHeaders(); httpHeaders.setAll(headers); return post(url, httpHeaders, requestBody, responseType, uriVariables); } /** * 带请求头的POST请求调用方式 * * @param url 请求URL * @param headers 请求头参数 * @param requestBody 请求参数体 * @param responseType 返回对象类型 * @param uriVariables URL中的变量,与Map中的key对应 * @return ResponseEntity 响应对象封装类 */ public static ResponseEntity post(String url, HttpHeaders headers, Object requestBody, Class responseType, Map uriVariables) { HttpEntity requestEntity = new HttpEntity(requestBody, headers); return post(url, requestEntity, responseType, uriVariables); } /** * 自定义请求头和请求体的POST请求调用方式 * * @param url 请求URL * @param requestEntity 请求头和请求体封装对象 * @param responseType 返回对象类型 * @param uriVariables URL中的变量,按顺序依次对应 * @return ResponseEntity 响应对象封装类 */ public static ResponseEntity post(String url, HttpEntity requestEntity, Class responseType, Object... uriVariables) { return restTemplate.exchange(url, HttpMethod.POST, requestEntity, responseType, uriVariables); } /** * 自定义请求头和请求体的POST请求调用方式 * * @param url 请求URL * @param requestEntity 请求头和请求体封装对象 * @param responseType 返回对象类型 * @param uriVariables URL中的变量,与Map中的key对应 * @return ResponseEntity 响应对象封装类 */ public static ResponseEntity post(String url, HttpEntity requestEntity, Class responseType, Map uriVariables) { return restTemplate.exchange(url, HttpMethod.POST, requestEntity, responseType, uriVariables); } // ----------------------------------PUT------------------------------------------------------- /** * PUT请求调用方式 * * @param url 请求URL * @param responseType 返回对象类型 * @param uriVariables URL中的变量,按顺序依次对应 * @return ResponseEntity 响应对象封装类 */ public static ResponseEntity put(String url, Class responseType, Object... uriVariables) { return put(url, HttpEntity.EMPTY, responseType, uriVariables); } /** * PUT请求调用方式 * * @param url 请求URL * @param requestBody 请求参数体 * @param responseType 返回对象类型 * @param uriVariables URL中的变量,按顺序依次对应 * @return ResponseEntity 响应对象封装类 */ public static ResponseEntity put(String url, Object requestBody, Class responseType, Object... uriVariables) { HttpEntity requestEntity = new HttpEntity(requestBody); return put(url, requestEntity, responseType, uriVariables); } /** * PUT请求调用方式 * * @param url 请求URL * @param requestBody 请求参数体 * @param responseType 返回对象类型 * @param uriVariables URL中的变量,与Map中的key对应 * @return ResponseEntity 响应对象封装类 */ public static ResponseEntity put(String url, Object requestBody, Class responseType, Map uriVariables) { HttpEntity requestEntity = new HttpEntity(requestBody); return put(url, requestEntity, responseType, uriVariables); } /** * 带请求头的PUT请求调用方式 * * @param url 请求URL * @param headers 请求头参数 * @param requestBody 请求参数体 * @param responseType 返回对象类型 * @param uriVariables URL中的变量,按顺序依次对应 * @return ResponseEntity 响应对象封装类 */ public static ResponseEntity put(String url, Map headers, Object requestBody, Class responseType, Object... uriVariables) { HttpHeaders httpHeaders = new HttpHeaders(); httpHeaders.setAll(headers); return put(url, httpHeaders, requestBody, responseType, uriVariables); } /** * 带请求头的PUT请求调用方式 * * @param url 请求URL * @param headers 请求头参数 * @param requestBody 请求参数体 * @param responseType 返回对象类型 * @param uriVariables URL中的变量,按顺序依次对应 * @return ResponseEntity 响应对象封装类 */ public static ResponseEntity put(String url, HttpHeaders headers, Object requestBody, Class responseType, Object... uriVariables) { HttpEntity requestEntity = new HttpEntity(requestBody, headers); return put(url, requestEntity, responseType, uriVariables); } /** * 带请求头的PUT请求调用方式 * * @param url 请求URL * @param headers 请求头参数 * @param requestBody 请求参数体 * @param responseType 返回对象类型 * @param uriVariables URL中的变量,与Map中的key对应 * @return ResponseEntity 响应对象封装类 */ public static ResponseEntity put(String url, Map headers, Object requestBody, Class responseType, Map uriVariables) { HttpHeaders httpHeaders = new HttpHeaders(); httpHeaders.setAll(headers); return put(url, httpHeaders, requestBody, responseType, uriVariables); } /** * 带请求头的PUT请求调用方式 * * @param url 请求URL * @param headers 请求头参数 * @param requestBody 请求参数体 * @param responseType 返回对象类型 * @param uriVariables URL中的变量,与Map中的key对应 * @return ResponseEntity 响应对象封装类 */ public static ResponseEntity put(String url, HttpHeaders headers, Object requestBody, Class responseType, Map uriVariables) { HttpEntity requestEntity = new HttpEntity(requestBody, headers); return put(url, requestEntity, responseType, uriVariables); } /** * 自定义请求头和请求体的PUT请求调用方式 * * @param url 请求URL * @param requestEntity 请求头和请求体封装对象 * @param responseType 返回对象类型 * @param uriVariables URL中的变量,按顺序依次对应 * @return ResponseEntity 响应对象封装类 */ public static ResponseEntity put(String url, HttpEntity requestEntity, Class responseType, Object... uriVariables) { return restTemplate.exchange(url, HttpMethod.PUT, requestEntity, responseType, uriVariables); } /** * 自定义请求头和请求体的PUT请求调用方式 * * @param url 请求URL * @param requestEntity 请求头和请求体封装对象 * @param responseType 返回对象类型 * @param uriVariables URL中的变量,与Map中的key对应 * @return ResponseEntity 响应对象封装类 */ public static ResponseEntity put(String url, HttpEntity requestEntity, Class responseType, Map uriVariables) { return restTemplate.exchange(url, HttpMethod.PUT, requestEntity, responseType, uriVariables); } // ----------------------------------DELETE------------------------------------------------------- /** * DELETE请求调用方式 * * @param url 请求URL * @param responseType 返回对象类型 * @param uriVariables URL中的变量,按顺序依次对应 * @return ResponseEntity 响应对象封装类 */ public static ResponseEntity delete(String url, Class responseType, Object... uriVariables) { return delete(url, HttpEntity.EMPTY, responseType, uriVariables); } /** * DELETE请求调用方式 * * @param url 请求URL * @param responseType 返回对象类型 * @param uriVariables URL中的变量,与Map中的key对应 * @return ResponseEntity 响应对象封装类 */ public static ResponseEntity delete(String url, Class responseType, Map uriVariables) { return delete(url, HttpEntity.EMPTY, responseType, uriVariables); } /** * DELETE请求调用方式 * * @param url 请求URL * @param requestBody 请求参数体 * @param responseType 返回对象类型 * @param uriVariables URL中的变量,按顺序依次对应 * @return ResponseEntity 响应对象封装类 */ public static ResponseEntity delete(String url, Object requestBody, Class responseType, Object... uriVariables) { HttpEntity requestEntity = new HttpEntity(requestBody); return delete(url, requestEntity, responseType, uriVariables); } /** * DELETE请求调用方式 * * @param url 请求URL * @param requestBody 请求参数体 * @param responseType 返回对象类型 * @param uriVariables URL中的变量,与Map中的key对应 * @return ResponseEntity 响应对象封装类 */ public static ResponseEntity delete(String url, Object requestBody, Class responseType, Map uriVariables) { HttpEntity requestEntity = new HttpEntity(requestBody); return delete(url, requestEntity, responseType, uriVariables); } /** * 带请求头的DELETE请求调用方式 * * @param url 请求URL * @param headers 请求头参数 * @param responseType 返回对象类型 * @param uriVariables URL中的变量,按顺序依次对应 * @return ResponseEntity 响应对象封装类 */ public static ResponseEntity delete(String url, Map headers, Class responseType, Object... uriVariables) { HttpHeaders httpHeaders = new HttpHeaders(); httpHeaders.setAll(headers); return delete(url, httpHeaders, responseType, uriVariables); } /** * 带请求头的DELETE请求调用方式 * * @param url 请求URL * @param headers 请求头参数 * @param responseType 返回对象类型 * @param uriVariables URL中的变量,按顺序依次对应 * @return ResponseEntity 响应对象封装类 */ public static ResponseEntity delete(String url, HttpHeaders headers, Class responseType, Object... uriVariables) { HttpEntity requestEntity = new HttpEntity(headers); return delete(url, requestEntity, responseType, uriVariables); } /** * 带请求头的DELETE请求调用方式 * * @param url 请求URL * @param headers 请求头参数 * @param responseType 返回对象类型 * @param uriVariables URL中的变量,与Map中的key对应 * @return ResponseEntity 响应对象封装类 */ public static ResponseEntity delete(String url, Map headers, Class responseType, Map uriVariables) { HttpHeaders httpHeaders = new HttpHeaders(); httpHeaders.setAll(headers); return delete(url, httpHeaders, responseType, uriVariables); } /** * 带请求头的DELETE请求调用方式 * * @param url 请求URL * @param headers 请求头参数 * @param responseType 返回对象类型 * @param uriVariables URL中的变量,与Map中的key对应 * @return ResponseEntity 响应对象封装类 */ public static ResponseEntity delete(String url, HttpHeaders headers, Class responseType, Map uriVariables) { HttpEntity requestEntity = new HttpEntity(headers); return delete(url, requestEntity, responseType, uriVariables); } /** * 带请求头的DELETE请求调用方式 * * @param url 请求URL * @param headers 请求头参数 * @param requestBody 请求参数体 * @param responseType 返回对象类型 * @param uriVariables URL中的变量,按顺序依次对应 * @return ResponseEntity 响应对象封装类 */ public static ResponseEntity delete(String url, Map headers, Object requestBody, Class responseType, Object... uriVariables) { HttpHeaders httpHeaders = new HttpHeaders(); httpHeaders.setAll(headers); return delete(url, httpHeaders, requestBody, responseType, uriVariables); } /** * 带请求头的DELETE请求调用方式 * * @param url 请求URL * @param headers 请求头参数 * @param requestBody 请求参数体 * @param responseType 返回对象类型 * @param uriVariables URL中的变量,按顺序依次对应 * @return ResponseEntity 响应对象封装类 */ public static ResponseEntity delete(String url, HttpHeaders headers, Object requestBody, Class responseType, Object... uriVariables) { HttpEntity requestEntity = new HttpEntity(requestBody, headers); return delete(url, requestEntity, responseType, uriVariables); } /** * 带请求头的DELETE请求调用方式 * * @param url 请求URL * @param headers 请求头参数 * @param requestBody 请求参数体 * @param responseType 返回对象类型 * @param uriVariables URL中的变量,与Map中的key对应 * @return ResponseEntity 响应对象封装类 */ public static ResponseEntity delete(String url, Map headers, Object requestBody, Class responseType, Map uriVariables) { HttpHeaders httpHeaders = new HttpHeaders(); httpHeaders.setAll(headers); return delete(url, httpHeaders, requestBody, responseType, uriVariables); } /** * 带请求头的DELETE请求调用方式 * * @param url 请求URL * @param headers 请求头参数 * @param requestBody 请求参数体 * @param responseType 返回对象类型 * @param uriVariables URL中的变量,与Map中的key对应 * @return ResponseEntity 响应对象封装类 */ public static ResponseEntity delete(String url, HttpHeaders headers, Object requestBody, Class responseType, Map uriVariables) { HttpEntity requestEntity = new HttpEntity(requestBody, headers); return delete(url, requestEntity, responseType, uriVariables); } /** * 自定义请求头和请求体的DELETE请求调用方式 * * @param url 请求URL * @param requestEntity 请求头和请求体封装对象 * @param responseType 返回对象类型 * @param uriVariables URL中的变量,按顺序依次对应 * @return ResponseEntity 响应对象封装类 */ public static ResponseEntity delete(String url, HttpEntity requestEntity, Class responseType, Object... uriVariables) { return restTemplate.exchange(url, HttpMethod.DELETE, requestEntity, responseType, uriVariables); } /** * 自定义请求头和请求体的DELETE请求调用方式 * * @param url 请求URL * @param requestEntity 请求头和请求体封装对象 * @param responseType 返回对象类型 * @param uriVariables URL中的变量,与Map中的key对应 * @return ResponseEntity 响应对象封装类 */ public static ResponseEntity delete(String url, HttpEntity requestEntity, Class responseType, Map uriVariables) { return restTemplate.exchange(url, HttpMethod.DELETE, requestEntity, responseType, uriVariables); } // ----------------------------------通用方法------------------------------------------------------- /** * 通用调用方式 * * @param url 请求URL * @param method 请求方法类型 * @param requestEntity 请求头和请求体封装对象 * @param responseType 返回对象类型 * @param uriVariables URL中的变量,按顺序依次对应 * @return ResponseEntity 响应对象封装类 */ public static ResponseEntity exchange(String url, HttpMethod method, HttpEntity requestEntity, Class responseType, Object... uriVariables) { return restTemplate.exchange(url, method, requestEntity, responseType, uriVariables); } /** * 通用调用方式 * * @param url 请求URL * @param method 请求方法类型 * @param requestEntity 请求头和请求体封装对象 * @param responseType 返回对象类型 * @param uriVariables URL中的变量,与Map中的key对应 * @return ResponseEntity 响应对象封装类 */ public static ResponseEntity exchange(String url, HttpMethod method, HttpEntity requestEntity, Class responseType, Map uriVariables) { return restTemplate.exchange(url, method, requestEntity, responseType, uriVariables); } //--------------------------------SSL---------------------------- /** * ssl请求的get * * @param url 请求URL * @param responseType 返回对象类型 * @param uriVariables URL中的变量,与Map中的key对应 * @return ResponseEntity 响应对象封装类 */ public static ResponseEntity getSsl(String url, Class responseType, Map uriVariables) { return sslRestTemplate.getForEntity(url, responseType, uriVariables); } /** * ssl请求的post * * @param url 请求URL * @param headers 请求头 * @param requestBody 请求体 * @param responseType 返回对象类型 * @param uriVariables URL中的变量,与Map中的key对应 * @return ResponseEntity 响应对象封装类 */ public static ResponseEntity postSsl(String url, Map headers, Object requestBody, Class responseType, Map uriVariables) { HttpHeaders httpHeaders = new HttpHeaders(); httpHeaders.setAll(headers); HttpEntity requestEntity = new HttpEntity(requestBody, httpHeaders); return sslRestTemplate.exchange(url, HttpMethod.POST, requestEntity, responseType, uriVariables); } /** * @param url 请求URL * @param requestBody 请求体 * @param responseType 返回对象类型 * @return ResponseEntity 响应对象封装类 */ public static ResponseEntity postSsl(String url, Object requestBody, Class responseType) { return sslRestTemplate.postForEntity(url, requestBody, responseType); } } ``` ## 4、定义播放鉴权 该内容对应nginx中rtmp配置的on_play配置,on_play_done根据自己的需要同理实现即可。 可通过nginx直接配置鉴权或定义到项目中去鉴权 ```java package com.gongl.web.controller.base; import com.gongl.common.core.domain.AjaxResult; import com.gongl.quartz.task.FlvMonitorTask; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import javax.servlet.http.HttpServletRequest; /** * @author gongl * @date 2023-03-22 */ @RestController @RequestMapping("/flv") public class FlvController { @Autowired private FlvMonitorTask flvMonitorTask; @RequestMapping("/on_play") public AjaxResult on_play(HttpServletRequest request) { //鉴权,根据自己系统需要进行鉴权。返回200成功,返回其他失败 //播放 String stream = request.getParameter("stream"); flvMonitorTask.openStream(stream); return AjaxResult.success(); } } ``` ## 5、编写定时检测无人观看流以及发送转换流的方法 ```java package com.gongl.quartz.task; import com.alibaba.fastjson.JSONArray; import com.alibaba.fastjson.JSONObject; import com.gongl.common.utils.ExecuteShellUtil; import com.gongl.common.utils.http.RestTemplateUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Component; /** * @author gongl * @date 2023-03-22 */ @Component public class FlvMonitorTask { private static final Logger log = LoggerFactory.getLogger(FlvMonitorTask.class); @Autowired private ExecuteShellUtil executeShellUtil; @Value("${camera.stat-url}") private String stat; @Value("${camera.close-command}") private String closeCommand; @Value("${camera.open-command}") private String openCommand; //定时关闭无人观看的视频转码流 @Scheduled(cron = "0 0/5 * * * ?") public void doTask() { JSONArray streams = getBody(); streams.forEach(x -> { JSONObject object = (JSONObject) x; String name = object.getString("name");//摄像头编号,即转换配置的房间号 JSONArray clients = object.getJSONArray("clients"); if (clients.size() <= 1) { try { executeShellUtil.execCmd(String.format(closeCommand, name)); } catch (Exception e) { log.error("关闭视频流异常---->" + e); } } }); } //需要播放的视频是否已经在转码 public void openStream(String stream) { JSONArray streams = getBody(); boolean flag = true; for (Object x : streams) { JSONObject object = (JSONObject) x; String name = object.getString("name"); if (stream.equals(name)) { flag = false; break; } } //若没有转码,则开始转码 if (flag) { try { //根据stream摄像头编号得到摄像头的rtsp流地址 executeShellUtil.execCmd(String.format(openCommand,"rtsp地址" , stream)); } catch (Exception e) { log.error("启动视频流转换异常---->" + e); } } } //用于获取转码服务器在线的视频流及实时在线连接 private JSONArray getBody() { String body = RestTemplateUtils.get(stat, String.class).getBody(); JSONObject jsonObject = JSONObject.parseObject(body); JSONObject httpFlv = jsonObject.getJSONObject("http-flv"); JSONObject server = httpFlv.getJSONArray("servers").getJSONObject(0); JSONObject application = server.getJSONArray("applications").getJSONObject(0); JSONObject live = application.getJSONObject("live"); return live.getJSONArray("streams"); } } ``` # 七、flv.js做追帧、断线重连等 http://news.558idc.com/396049.html ## 1、修改config配置 ```json { enableWorker: true, // 启用分离的线程进行转换 enableStashBuffer: false, // 关闭IO隐藏缓冲区 stashInitialSize: 128, // 减少首帧显示等待时长 } ``` 开启 flv.js 的 Worker,多线程运行 flv.js 提升解析速度可以优化延迟 关闭 buffer 缓存,这个选项可以明显地降低延迟,缺点就是由于关闭了 buffer 缓存,网络不好的时候可能会出现 loading 加载 调低 IO 缓冲区的初始尺寸,减少首帧显示的等待时长 ## 2、追帧设置 解决延时累加最有效的方式就是进行追帧设置 追帧,就是去判断缓冲区末尾的 buffer 值与当前播放时间的差值,如果大于某个值,就进行追帧设置,具体的思路如下: 首先,在 progress 事件,或者定时器中进行追帧逻辑 判断 buffer 的差值 delta ```javascript let end = this.player.buffered.end(0); //获取当前buffered值(缓冲区末尾) let delta = end - this.player.currentTime; //获取buffered与当前播放位置的差值 ``` 如果 delta 值大于某个设定的值,则进行追帧操作 追帧有两种方式 1)一种是直接更新当前的时间:this.player.currentTime = this.player.buffered.end(0) - 1,缺点是如果频繁触发会导致跳帧,观感差; 2)一种是调快播放速度的方式来慢慢追帧: this.videoElement.playbackRate = 1.1,优点是稳定,缺点是如果 delta 值过大,通过这种方式追得太慢 在实际使用中两种方式可以结合起来。 代码实现: ```javascript videoElement.addEventListener("progress", () => { let end = player.buffered.end(0); //获取当前buffered值(缓冲区末尾) let delta = end - player.currentTime; //获取buffered与当前播放位置的差值 // 延迟过大,通过跳帧的方式更新视频 if (delta > 10 || delta < 0) { this.player.currentTime = this.player.buffered.end(0) - 1; return; } // 追帧 if (delta > 1) { videoElement.playbackRate = 1.1; } else { videoElement.playbackRate = 1; } }); ``` ## 3、断流重连 断流重连即在flvjs播放失败的回调中,进行重建视频的操作 代码实现: ```javascript this.player.on(flvjs.Events.ERROR, (e) => { // destroy this.player.pause(); this.player.unload(); this.player.detachMediaElement(); this.player.destroy(); this.player = null; // 进行重建的逻辑,这里不再展开 this.init(); }); ``` ## 4、实时更新 直播需要保证视频的实时性,以下两种操作都会导致视频的实时性得不到保证: 用户点击了暂停,过一段时间后再点播放,这时候的直播视频不是最新的 网页切到后台,再重新切换回前台,视频不是最新的 所以需要根据这两种情况来实时更新视频 代码实现: ```javascript // 点击播放按钮后,更新视频 videoElement.addEventListener("play", () => { let end = player.buffered.end(0) - 1; this.player.currentTime = end; }); // 网页重新激活后,更新视频 window.onfocus = () => { let end = player.buffered.end(0) - 1; this.player.currentTime = end; }; ``` ## 5、解决stuck问题 有的时候,视频在播放的过程中会突然卡住,或者控制台有时会报错 “Playback seems stuck at 0, seek to 1.1”。 我们需要判断视频是否卡住了,然后重建视频实例 思路就是判断 decodedFrames 是否产生变化,如果视频是播放状态并且该值没有产生变化,则可以判断视频卡住了。 代码实现: ```javascript function handleStuck() { let lastDecodedFrames = 0; let stuckTime = 0; this.interval && clearInterval(this.interval); this.interval = setInterval(() => { const decodedFrames = this.player.statisticsInfo.decodedFrames; if (!decodedFrames) return; if (lastDecodedFrames === decodedFrames && !this.videoElement.paused) { // 可能卡住了,重载 stuckTime++; if (stuckTime > 1) { console.log(`%c 卡住,重建视频`, "background:red;color:#fff"); // 先destroy,再重建视频实例 this.rebuild(); } } else { lastDecodedFrames = decodedFrames; stuckTime = 0; } }, 800); } ``` ## 6、封装插件 flvExtend.js 我将这些优化方案封装成了一个插件 flvExtend.js,它相当于是 flv.js 的一个功能扩展 插件地址:https://github.com/shady-xia/flvExtend 使用起来是这个样子: ```javascript import FlvExtend from "flv-extend"; // 配置需要的功能 const flv = new FlvExtend({ element: videoElement, // *必传 frameTracking: true, // 开启追帧设置 updateOnStart: true, // 点击播放后更新视频 updateOnFocus: true, // 获得焦点后更新视频 reconnect: true, // 开启断流重连 reconnectInterval: 2000, // 断流重连间隔 }); // 调用 init 方法初始化视频 // init 方法的参数与 flvjs.createPlayer 相同,并返回 flvjs.player 实例 const player = flv.init( { type: "flv", url: "http://192.168.0.11/stream", isLive: true, }, { enableStashBuffer: false, // 如果您需要实时(最小延迟)来进行实时流播放,则设置为false stashInitialSize: 128, // 减少首帧显示等待时长 } ); // 直接调用play即可播放 player.play(); ``` ========================================= ```shell ./configure --with-http_ssl_module --with-openssl=/root/openssl/openssl-1.1.1w --with-http_v2_module --add-module=/usr/local/nginx-http-flv-module ```