nginx+ffmpeg+springboot+vue+西瓜视频-播放海康摄像头

        前端采用vue3+字节开源的西瓜视频播放FLV视频,后端使用ffmpeg+nginx对海康摄像头提供的rtsp流进行推拉流。

1.Nginx配置

        针对不同系统环境可以分别去下载编译nginx-rtmp-module或者nginx-http-flv-module

        windows编译相对复杂需要装一大堆软件,可以用下面地址进行下载:

链接: https://pan.baidu.com/s/1ND7DI16X4x3PUPnlWCDfuA?pwd=6rt8 提取码: 6rt8 

        linux比较交单,百度搜一搜,找个教程,按照教程进行编译就行了

下面贴一下nginx.conf的配置:

worker_processes  1;#error_log  logs/error.log;
#error_log  logs/error.log  notice;
#error_log  logs/error.log  info;
#error_log  logs/error.log  debug;#pid        logs/nginx.pid;events {worker_connections  1024;
}# 添加RTMP服务
rtmp {server {listen 1935; # 监听端口chunk_size 4000;application live {live on;gop_cache on;}}
}# HTTP服务
http {include       mime.types;default_type  application/octet-stream;#access_log  logs/access.log  main;server {listen       8090; # 监听端口#http-flv的相关配置location /flv {flv_live on; #打开HTTP播放FLV直播流功能chunked_transfer_encoding  on;add_header 'Access-Control-Allow-Origin' '*';add_header 'Access-Control-Allow-Credentials' 'true';}location /stat.xsl {root html;}location /stat {rtmp_stat all;rtmp_stat_stylesheet stat.xsl;}location / {root html;}}
}

        上面的配置只是针对flv格式播放,如果需要hls格式播放,需要增加hls的配置,配置如下:

worker_processes 1;events {worker_connections 1024;
}rtmp {out_queue		4096;out_cork	        8;max_streams 	128;timeout		15s;drop_idle_publisher 15s;log_interval	5s; #interval used by log module to log in access.log, it is very useful for debuglog_size		1m; #buffer size used by log module to log in access.logserver {listen 1935;chunk_size 4096;server_name localhost; #for suffix wildcard matching of virtual hostapplication live {live on;gop_cache on; #open GOP cache for reducing the wating time for the first picture of video#live on;hls on;hls_path D:\video;hls_fragment 1s;hls_playlist_length 3s;# hls_playlist_length 60s;#hls_continuous on;#hls_cleanup on;#hls_nested on;wait_key on;#meta off;#allow play all;}}
}http {include       mime.types;default_type  application/octet-stream;sendfile        on;keepalive_timeout  65;server {listen       88;#charset utf-8;#access_log  logs/host.access.log  main;server_name  localhost;# 拉http-flv的配置location /flv {	flv_live on;chunked_transfer_encoding on;add_header 'Access-Control-Allow-Origin' '*';add_header 'Access-Control-Allow-Credentials' 'true';}#配置hls点播location /hls {types{application/vnd.apple.mpegurl m3u8;video/mp2t ts;}alias D:\video;   # 读取文件的位置,应和上面rtmp中的配置一样expires -1;add_header 'Cache-Control' 'no-cache'; add_header 'Access-Control-Allow-Origin' '*' always;add_header 'Access-Control-Expose-Headers' 'Content-Length,Content-Range';add_header 'Access-Control-Allow-Headers' 'Range';#add_header Cache-Control no-cache;#add_header 'Access-Control-Allow-Origin' '*';#add_header 'Access-Control-Allow-Credentials' '*';#add_header 'Access-Control-Allow-Methods' 'GET, HEAD, OPTIONS';#add_header 'Access-Control-Expose-Headers' 'Server,range,Content-Length,Content-Range';}# 定义状态的访问URIlocation /stat {          rtmp_stat all;rtmp_stat_stylesheet stat.xsl;}# 定义状态文件路径location /stat.xsl {       root html;}# 定义播放器网页访问的URI和根目录location / {               root html;}error_page   500 502 503 504  /50x.html;location = /50x.html {root   html;}}
}

我是用了两个nginx进行的测试,所以有的端口写的不一样,这个端口不重复就行,配置好了直接启动可以使用nginx -t验证一下配置文件是否正确。没问题直接启动nginx。

注意:端口要和配置文件的一样

# windows 启动
start nginx
# linux 启动
./nginx# 重启 windows/linux
nginx -s reload# 停止 windows/linux
nginx -s stop

2.下载解压FFMPEG

下载地址:

https://github.com/BtbN/FFmpeg-Builds/releases

        windows环境下载解压就行了,linux我用centos7出现了缺少包的情况,我升级了gcc和glibc解决了这个问题。

注意:glibc没有一定经验不要乱动,有系统崩溃的风险。

3.测试使用FFMPEG进行直播推流

ffmpeg.exe -rtsp_transport tcp -buffer_size 4096000 
-i "rtsp://admin:hc@123456@192.168.1.70:554/Streaming/Channels/1?videoCodecType=H.264" 
-vcodec copy -acodec copy -f flv "rtmp://127.0.0.1:1935/live/88"

上面的rtsp地址中的用户名、密码、IP、端口需要替换成自己的海康摄像头

然后使用VLC播放器播放。

播放地址:

注意: ip和port是否对应

rtmp: rtmp://127.0.0.1:1935/live/88

flv: http://127.0.0.1:88/flv?port=1935&app=live&stream=88

hls: http://127.0.0.1:88/hls/88.m3u8

 4.SpringBoot整合FFMPEG

        本方案采用的方式是使用外部FFMPEG程序,还可以使用javacv的方式,但是javacv方式较为复杂,我实现之后效果一般,遂采用的使用外部MMFPEG,好处是稳定性高,可以使用最新版的FFMPEG。下面贴一下关键代码。

/*** @author cew* @date 2023年7月20日17:26:29*/
@Slf4j(topic = "admin-steam")
public class FfmpegUtil {public static ConcurrentHashMap processMap = new ConcurrentHashMap<>();private final static Integer LIVE_TIME = 15;public static Process getProcess(String videoUrl){String cacheKey = getCacheKey(videoUrl);return processMap.get(cacheKey);}public static String getPlayUrl(String videoUrl){String cacheKey = getCacheKey(videoUrl);return SpringUtils.getBean(RedisCache.class).getCacheObject(cacheKey);}public static boolean push(String videoUrl, String flvUrl, Process process){synchronized (FfmpegUtil.class){String cacheKey = getCacheKey(videoUrl);processMap.put(cacheKey,process);SpringUtils.getBean(RedisCache.class).setCacheObject(cacheKey,flvUrl, LIVE_TIME, TimeUnit.MINUTES);return true;}}public static boolean keepLive(String videoUrl){synchronized (FfmpegUtil.class){String cacheKey = getCacheKey(videoUrl);String cacheVideoUrl = SpringUtils.getBean(RedisCache.class).getCacheObject(cacheKey);if (ObjectUtils.isEmpty(cacheVideoUrl)){log.info("视频流保活失败:{}",videoUrl);return false;}SpringUtils.getBean(RedisCache.class).setCacheObject(cacheKey,cacheVideoUrl,LIVE_TIME,TimeUnit.MINUTES);log.info("视频流保活成功:{}",videoUrl);return true;}}public static boolean exists(String videoUrl){String cacheKey = getCacheKey(videoUrl);return SpringUtils.getBean(RedisCache.class).hasKey(cacheKey);}public static boolean destroy(String videoUrl){synchronized (FfmpegUtil.class){String cacheKey = getCacheKey(videoUrl);Process process = processMap.get(cacheKey);if (process != null){process.destroy();processMap.remove(cacheKey);log.info("视频url:{}",videoUrl);log.info("------------------------推流结束-------------------------");}SpringUtils.getBean(RedisCache.class).deleteObject(cacheKey);return true;}}private static String getCacheKey(String videoUrl){return CacheConstants.CAMERA_KEY + videoUrl;}
}
/*** @author cew*/
@Slf4j(topic = "admin-steam")
@Component
public class StreamGobbler {/*** 异步开启新的线程池* @param is ffmpeg输入的错误流* @param type 类型* @param videoUrl 推流地址*/@Async("threadPoolTaskExecutor")public void run(InputStream is, String type, String videoUrl) {try {InputStreamReader isr = new InputStreamReader(is);BufferedReader br = new BufferedReader(isr);String line = null;while ((line = br.readLine()) != null) {// 增加不续期取消推流if (type.equals("Error")) {if (FfmpegUtil.exists(videoUrl)){log.info(line);} else {// 视频流不存在了跳出循环log.info("缓存过期,跳出循环");break;}} else {log.debug(line);}}// 无论正常还是意外导致跳出循环都应该注销视频流FfmpegUtil.destroy(videoUrl);} catch (IOException e) {log.error("视频流出现异常:",e);}}
}

/*** 转换视频流* @author cew*/
@Slf4j(topic = "admin-steam")
@Component
public class ConversionVideo {@Autowiredprivate StreamGobbler errorGobbler;/*** 推流* @param videoUrl 推流视频地址* @param controllerUrl rtmp地址* @param flvUrl 前端播放地址* @return 播放地址*/public String pushVideoAsRTSP(String videoUrl, String controllerUrl,String flvUrl){//ffmpeg.exe -rtsp_transport tcp -buffer_size 4096000 -i "rtsp://admin:hc@123456@192.168.1.70:554/Streaming/Channels/1?videoCodecType=H.264" -vcodec copy -acodec copy -f flv "rtmp://127.0.0.1:1935/live/88"Process process = FfmpegUtil.getProcess(videoUrl);// 已经在推送了不需要进行操作if(!ObjectUtils.isEmpty(process)){return FfmpegUtil.getPlayUrl(videoUrl);}destroy(videoUrl);// 获取ffmpeg位置 根据操作系统读取配置String ffmpegPath = VideoConfig.getFfmpegPath();try {// cmd命令拼接,注意命令中存在空格StringBuilder command = new StringBuilder(ffmpegPath) ;// ffmpeg开头command .append(" -rtsp_transport tcp");// 减少卡顿或者花屏现象,相当于增加或扩大了缓冲区,给予编码和发送足够的时间。command .append(" -buffer_size 4096000");// 指定要推送的视频command .append(" -i ").append(videoUrl);// fps 设置帧频 缺省25command .append(" -r 30 ");// 音频选项, 一般后面加copy表示拷贝command .append(" -vcodec copy");// 音频选项, 一般后面加copy表示拷贝command .append(" -acodec copy");// 指定推送服务器,-f:指定格式command .append(" -f flv ").append(controllerUrl);log.info("ffmpeg推流命令:{}", command);// 运行cmd命令,获取其进程process = Runtime.getRuntime().exec(command.toString());//这里处理process的信息errorGobbler.run(process.getErrorStream(), "Error",videoUrl);log.info("开始推流:{}",controllerUrl);FfmpegUtil.push(videoUrl,flvUrl,process);}catch (Exception e){log.error("推流出现异常:",e);}return flvUrl;}public boolean keepLive(String videoUrl){return FfmpegUtil.keepLive(videoUrl);}public boolean destroy(String videoUrl){return FfmpegUtil.destroy(videoUrl);}}
/*** @author Administrator*/
public class StreamUtil {/*** 获取rtsp地址* rtsp://admin:hc@123456@10.10.10.200:554/Streaming/Channels/101?videoCodecType=H.264* @param userName 海康用户名* @param password 海康密码* @param channel 海康通道* @param ip 海康ip* @param port 海康端口* @return rtsp 地址*/public static String getRtspUrl(String userName,String password,int channel,String ip,int port){StringBuilder sb = new StringBuilder();sb.append("rtsp://");sb.append(userName);sb.append(":");sb.append(password);sb.append("@");sb.append(ip);sb.append(":");sb.append(port);sb.append("/Streaming/Channels/");sb.append(channel);sb.append("?videoCodecType=H.264");return sb.toString();}/*** 获取rtmp地址* rtmp://10.10.10.233:1935/live/1* @param ip nginx_flv服务 ip地址* @param port nginx_flv服务 端口* @param appName  流标识* @return rtmp地址*/public static String getRtmpUrl(String ip,String port,Integer appName){StringBuilder sb = new StringBuilder();sb.append("rtmp://");sb.append(ip);sb.append(":");sb.append(port);sb.append("/live/");sb.append(appName);return sb.toString();}/*** 获取前端播放地址 前端转发到nginx或其他代里服务器* @param flvPort nginx_flv服务 端口* @param appName 设备id 流标识* @return 前端播放地址*/public static String flvUrl(String flvPort,Integer appName){StringBuilder sb = new StringBuilder();sb.append("/flv?port=");sb.append(flvPort);sb.append("&app=live&stream=");sb.append(appName);return sb.toString();}
}

/*** 摄像头推流 使用外部 ffmpeg* @author cew* @date 2023年7月20日09:35:26*/
public interface IVideoConversionService {/*** 推流* @param imageData 实体类参数*/public String live(ImageData imageData);/*** 保活* @param imageData 实体类参数*/public boolean keepLive(ImageData imageData);/*** 销毁* @param imageData 实体类参数*/public boolean destroy(ImageData imageData);}
/*** @author cew* @date 2023年7月20日10:23:30*/
@Slf4j(topic = "admin-steam")
@Service
public class IVideoConversionServiceImpl implements IVideoConversionService {@Autowiredprivate ConversionVideo conversionVideo;@Overridepublic String live(ImageData imageData) {String rtspUrl = StreamUtil.getRtspUrl(imageData.getUsername(), imageData.getPassword(),imageData.getChannel(), imageData.getIp(), imageData.getPort());String rtmpUrl = StreamUtil.getRtmpUrl(VideoConfig.getFlvIp(), VideoConfig.getFlvPort(),imageData.getDeviceId());String flvUrl = StreamUtil.flvUrl(VideoConfig.getFlvPort(), imageData.getDeviceId());return conversionVideo.pushVideoAsRTSP(rtspUrl, rtmpUrl, flvUrl);}@Overridepublic boolean keepLive(ImageData imageData) {String rtspUrl = StreamUtil.getRtspUrl(imageData.getUsername(), imageData.getPassword(),imageData.getChannel(), imageData.getIp(), imageData.getPort());return conversionVideo.keepLive(rtspUrl);}@Overridepublic boolean destroy(ImageData imageData) {String rtspUrl = StreamUtil.getRtspUrl(imageData.getUsername(), imageData.getPassword(),imageData.getChannel(), imageData.getIp(), imageData.getPort());return conversionVideo.destroy(rtspUrl);}
}
@Data
public class ImageData {/** 连接ip地址 */private String ip;/** 连接端口 */private short port;/** 连接用户名 */private String username;/** 连接密码 */private String password;/** 通道号 */private Integer channel;/** 设备标识 */private Integer deviceId;}

        上面使用Redis是做了一个15分钟的视频流保活(前端正常播放会每十分钟发送一次保活的请求),节省服务器资源,当无人观看时会关闭推流线程。该操作非必要,可根据业务需求是否保留。

 5.使用西瓜播放器+VUE3播放FLV视频

西瓜视频播放器官网:http://h5player.bytedance.com/

相关的教程和配置参数都可以在官网查看。

首先肯定是初始化西瓜视频组件:

# 西瓜视频播放器组件主体
npm install xgplayer
or
yarn add xgplayer# 西瓜视频播放器FLV扩展插件
npm i -S xgplayer-flv
or 
yarn add xgplayer-flv

自定义组件主要分为两个文件,一个是获取播放地址及保活的,另一个是创建视频播放器的具体代码如下:

camera/video.vue

创建video的代码deviceId的作用是同一个页面可以多次创建video,保障id唯一;

移除了部分内置插件,增加了点击全屏,异常3s重连,播放自动跳帧等功能。

camera/index.vue

主要作用就是获取播放地址及视频保活,两个方法就是两个get方法比较简单,后端Controller构建了一个ImageData(参考第二节)对象,然后就是调用service方法或者播放地址或者视频流保活。

使用方法大致如下,具体的获取设备id或者后端或者摄像头地址可以根据自己的业务需求设计:


下面配置是使用代里服务器映射播放地址,前端如使用nginx部署后需要在location增加类似配置,该配置非必须,如不使用代里服务,直接后端返回播放地址全路径类似于:

http://127.0.0.1:88/flv?port=1935&app=live&stream=88

 server: {open: true,host: true,proxy: {// https://cn.vitejs.dev/config/#server-proxy'/flv': {target: 'http://127.0.0.1:8090/flv',ws: true,changeOrigin: true,rewrite: (p) => p.replace(/^\/flv/, '')},}}

6.结语

        大概的代码就这么多了,本身是报着学习分享的精神写的这篇文章,如果有问题及遗漏可是直接评论,有不懂的地方也可以直接留言,没有写太多理论,可能需要一定的基础才能看的懂。

        顺便说一下这个方案可能不太适合大型项目,大型项目还得是流媒体服务器。该方案的优点就是不需要搭建流媒体服务器,使用简单,应付一些内网项目或者小型项目应该问题不大。


本文来自互联网用户投稿,文章观点仅代表作者本人,不代表本站立场,不承担相关法律责任。如若转载,请注明出处。 如若内容造成侵权/违法违规/事实不符,请点击【内容举报】进行投诉反馈!

相关文章

立即
投稿

微信公众账号

微信扫一扫加关注

返回
顶部