直播视频跨浏览器预览方案(ffmpeg+VideoJS+H5)附源码
1.概述
本文主要介绍一种在H5页面中,跨浏览器预览目前主流直播摄像头使用的rtsp流的预览方案。由于本人第一次接触这类需求,所以只是做到功能完整,还有各种不足之处,望各位大神指正。
2.前言
目前直播摄像头主要采用rtsp视频流的方式进行传输,而rtsp流是无法直接被浏览器所加载的。之前常用的方式是通过VLC media player插件来对视频进行预览。但随着时间的推移,VLC已经无法被大多数浏览器所兼容,所以无奈只能上网查找其他可以在浏览器直接浏览直播视频的方式。(VLC兼容性)。学习后做如下总结,留于有该类需求的小伙伴及自己查看使用。
3.思路
既然已经知道直播摄像头所采用的rtsp视频流无法直接被进行加载,那摆在我们面前的就只有几条路可以走。
1.自己独立开发rtsp流的解码及播放功能。
2.将rtsp流想办法处理为可以被浏览器所支持的视频流格式。
3.更换摄像头。。。(完全不现实,甲方不会允许的)
饶小弟才疏学浅,对视频流方面的涉猎真心不多。无奈只能选择第二条路。于是就有了如下的解决方案。
服务端:
第一、需要对rtsp的直播流实时进行转换,将其转换为可以被浏览器播放的其他视频流。
第二、需要在将转换后的视频流推送给客户端。
第三、当客户端关闭视频预览后,需要同时删除转换后的视频流文件及关闭服务端对应的视频转换进程。
客户端:
第一、将视频需要的用户名等信息传递到服务器,用于生成rtsp视频流。(我的程序是从前台读取的登录信息,也可以直接在后台从数据库中查询)
第二、将服务端转换好的视频流加载入页面。
第三、需要考虑视频在页面中加载时对浏览器的通用性。
第四、将关闭视频的信号发送回服务端,使其释放视频资源。
4.详解
4.1.ffmpeg
FFmpeg是一套可以用来记录、转换数字音频、视频,并能将其转化为流的开源计算机程序(下载)。
之所以选择用FFmpeg原因有三:一是因为其开源且免费(特别重要),二是因为功能比较全面,三是因为使用人数较多,网上的贴子也别较多,遇到问题比较容易查找解决。
4.1.1.配置方法
下载好对应版本的ffmpeg应用包,进行解压。解压后包含bin和doc两个文件夹。其中doc文件夹为使用demo及一些说明。bin文件夹则为需要用到的内容,包括ffmpeg.exe、ffplay.exe、ffprobe.exe三个文件。
解压后无需安装,只需要在环境变量中添加好地址即可。方法:我的电脑----右键"属性" —选择 “高级系统属性”----在“环境变量”中的【PATH】追加路径 (刚刚解压bin文件夹路径),重启电脑即可。例如我的就解压在“E:\HKVideoDemo\ffmpegToHLS\ffmpeg-w64\bin;”
4.1.2.验证方法
在运行窗口(Win+R)中,输入CMD。输入ffmpeg回车即可看到如下内容。
4.1.3.启动方法
4.1.3.1.手动启动
在运行窗口(Win+R)中,输入ffmpeg -i 直播摄像头rtsp流信息 其他ffmpeg参数即可。
附:
rtsp直播流获取方式 rtsp://视频用户名:视频密码@视频IP地址:端口号(默认554)/码率/通道号/main/av_stream
ffmpeg完成命令配置拼接 ffmpeg -i rtsp://视频用户名:视频密码@视频IP地址:端口号(默认554)/码率/通道号/main/av_stream -fflags flush_packets -max_delay 1 -an -flags -global_header -hls_time 1 -hls_list_size 3 -hls_wrap 3 -vcodec copy -y 转换后输出hls流的地址
具体ffmpeg的各参数说明在此就不详细叙述了,想了解的同学可以参考(ffmpeg参数说明)
4.1.3.2.代码启动
上述启动方式需要先手动在命令窗口将ffmpeg命令运行起来,如果需要同时查看多个直播视频,则需要运行多个命令窗口,非常不便于操作。于是我通过C#的进程启动方式将其进行了代码化。直接上代码:
/// <summary>
/// 播放
/// </summary>
/// <param name="param">前台传过来的视频用户名等信息</param>
private void videoPlay(string param)
{
if (!string.IsNullOrEmpty(param))
{
CloseProessAndDelFile();//结束正在运行的进程
string _HLSFileName = Guid.NewGuid().ToString(); //转换名称可以随便起,停止的时候带回去用于删除文件
VideoInfo vi = JsonConvert.DeserializeObject<VideoInfo>(param);
vi.HLSFileName = _HLSFileName;
Session["_VideoInfo"] = vi; //Session变量存储视频信息
if (Directory.Exists(vi.HLSPath) == false)//如果不存在就创建file文件夹
{
Directory.CreateDirectory(vi.HLSPath);
}
#region 启动ffmpeg服务
//拼接命令
string strML = "-i";
strML += " rtsp://" + vi.username + ":" + vi.password + "@" + vi.ip + ":" + vi.port + "/H.264/ch1/main/av_stream";
strML += " -fflags flush_packets";
strML += " -max_delay 1";
strML += " -an";
strML += " -flags";
strML += " -global_header";
strML += " -hls_time 1";
strML += " -hls_list_size 3";
strML += " -hls_wrap 3";
strML += " -vcodec copy";
strML += " -y";
strML += " "+vi.HLSPath;
strML += vi.HLSFileName+ ".m3u8";
ExcuteProcess("ffmpeg.exe", strML, (s, e) => Console.WriteLine(e.Data));
#endregion
while (Session["_ProcessID"] != null)
{
Thread.Sleep(3000);//启动转换服务后,形成流文件需要一点时间。所以先暂停。时间根据实际情况设置
//向客户端写回数据
Response.ContentType = "text/plain";
Response.ContentEncoding = System.Text.Encoding.UTF8;
Response.Write(vi.HLSFileName);
Response.End();
}
}
}
/// <summary>
/// 启动进程
/// </summary>
/// <param name="exe">启动程序进程名</param>
/// <param name="arg">启动参数</param>
/// <param name="output">输出</param>
private void ExcuteProcess(string exe, string arg, DataReceivedEventHandler output)
{
var p = new Process();
p.StartInfo.FileName = exe;
p.StartInfo.Arguments = arg;
p.StartInfo.UseShellExecute = false; //输出信息重定向
p.StartInfo.CreateNoWindow = true;
p.StartInfo.RedirectStandardError = true;
p.StartInfo.RedirectStandardOutput = true;
p.OutputDataReceived += output;
p.ErrorDataReceived += output;
p.Start(); //启动线程
p.BeginOutputReadLine();
p.BeginErrorReadLine();
Session["_ProcessID"] = p.Id; //存储启动的进程ID
//p.WaitForExit(); //等待进程结束
}
/// <summary>
/// 判断 关闭进程并删除文件
/// </summary>
private string CloseProessAndDelFile() {
string FileName = "";
#region 判断并关闭已经开启的转换进程
if (Session["_ProcessID"] != null)
{
//根据进程ID,关闭进程
Process[] processes = System.Diagnostics.Process.GetProcesses();
foreach (Process item in processes)
{
if (item.Id == int.Parse(Session["_ProcessID"].ToString()))
{
item.Kill();
break;
}
}
}
#endregion
#region 删除视频流文件
if (Session["_VideoInfo"] != null)
{
VideoInfo vi = Session["_VideoInfo"] as VideoInfo;
//需要删除的文件
string fileFullPath1 = vi.HLSPath + vi.HLSFileName + ".m3u8";//HLS流文件
string fileFullPath2 = vi.HLSPath + vi.HLSFileName + "0.ts";//ts文件
string fileFullPath3 = vi.HLSPath + vi.HLSFileName + "1.ts";//ts文件
string fileFullPath4 = vi.HLSPath + vi.HLSFileName + "2.ts";//ts文件
FileName = vi.HLSFileName;
// 判断文件路径是否存在
if (File.Exists(fileFullPath1))
{
//删除文件
File.Delete(fileFullPath1);
}
// 判断文件路径是否存在
if (File.Exists(fileFullPath2))
{
//删除文件
File.Delete(fileFullPath2);
}
// 判断文件路径是否存在
if (File.Exists(fileFullPath3))
{
//删除文件
File.Delete(fileFullPath3);
}
// 判断文件路径是否存在
if (File.Exists(fileFullPath4))
{
//删除文件
File.Delete(fileFullPath4);
}
}
#endregion
return FileName;
}
/// <summary>
/// 视频信息实体
/// </summary>
public class VideoInfo
{
/// <summary>
/// 用户名
/// </summary>
public string username { get; set; }
/// <summary>
/// 密码
/// </summary>
public string password { get; set; }
/// <summary>
/// IP
/// </summary>
public string ip { get; set; }
/// <summary>
/// 端口(默认554)
/// </summary>
public string port { get; set; }
/// <summary>
/// 转换后HLS地址
/// </summary>
public string HLSPath { get; set; }
/// <summary>
/// 转换后HLS文件名
/// </summary>
public string HLSFileName { get; set; }
}
4.2.前端页面
为了将转换好的hls直播流可以显示出来,我引入了Jquery、VideoJS及videojs-contrib-hlsJS插件(下载地址)。插件均为引入页面即可使用的,同时我还添加了判断用户离开页面的方法。在用户离开当前页面时异步调用后台代码,将视频资源释放。HTML完整代码如下:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta HTTP-EQUIV="pragma" CONTENT="no-cache">
<meta HTTP-EQUIV="Cache-Control" CONTENT="no-cache, must-revalidate">
<meta HTTP-EQUIV="expires" CONTENT="0">
<meta name="viewport" content="initial-scale=1, maximum-scale=1, user-scalable=no, width=device-width">
<meta content="email=no" name="format-detection" />
<meta content="telephone=no" name="format-detection" />
<script type="text/javascript" src="VideoJs/js/jquery-1.10.2.min.js"></script>
<script type="text/javascript" src="VideoJs/js/video.min.js"></script>
<script type="text/javascript" src="VideoJs/js/videojs-contrib-hls.min.js"></script>
<link href="VideoJs/css/video-js.min.css" rel="stylesheet">
<title>demo</title>
</head>
<body>
<div>
<button id="btnPlay">播放</button>||
<a href="http://www.baidu.com" target="_self">测试跳转后清理视频缓存</a>
</div>
<div id="dvVideo">
<video id="my-video" style='margin-top:5px;' poster="b.gif" class="video-js vjs-big-play-centered" controls preload="auto" width="580" height="435" data-setup="{}"></video>
</div>
</body>
</html>
<script type="text/javascript">
//初始化VideoJS
$("#dvVideo").hide();
var videoObj = videojs('my-video');
var option = {};
var myPlayer;
myPlayer = videojs('my-video', option, function onPlayerReady() {
默认加载视频
//var myPlayer = this;
//myPlayer.src({ src: "ffmpeg/1.m3u8", type: "application/x-mpegURL" });
//myPlayer.play();
});
myPlayer.controls = false;
</script>
<script>
$(function () {
//播放按钮
$("#btnPlay").click(function () {
$("#dvVideo").show();
var param = new Object();
param.username="admin"; //摄像头用户名
param.password = "ty123456"; //密码
param.ip = "192.168.16.35"; //IP地址
param.port = "554"; //端口号,默认554
param.HLSPath = "E:/HKVideoDemo/ffmpegToHLS/DataDemo/DataDemo/Demo/ffmpeg/"; //转换后HLS地址,这里可以将项目中文件夹的相对路径转为绝对路径传入
$.ajax({
url: 'DataForm.aspx?param=' + JSON.stringify(param)+'&type=add',
contentType: "application/json",
success: function (data) {
console.log(data);
//注意:必须要引用videojs-contrib-hls.min.js,只引用video.js无效
//这里src中的相对路径根据实际项目设置
myPlayer.src({ src: "ffmpeg/" + data + ".m3u8", type: "application/x-mpegURL" });
myPlayer.play();
}
});
});
//离开页面,释放视频。包括刷新、跳转、关闭网页、关闭浏览器等操作
$(window).bind('beforeunload',function () {
console.log("离开页面,释放视频。");
$.ajax({
url: 'DataForm.aspx?type=stop',
dataType: "json",
success: function (data) {
}
});
});
});
</script>
5.源码
至此,前端和后端的开发就此完毕。由于只是一个简单的DEMO事例,所以功能和用法都略显粗糙。只是为了做一个简单的记录,将思路及方法表述出来。希望可以给以后的小伙伴提供一些思路,以及留待后用。
附源码下载地址:https://download.csdn.net/download/fwl562213140/12919777
6.转载
码字不易,转载望留痕。
原文出处:https://blog.csdn.net/fwl562213140/article/details/109056188
7.填坑
这里顺手记录一下实际使用时遇到比较特殊的坑,帮助他人排雷。
- 在程序部署的时候,甲方设置的摄像头密码中包含“@”符号。。。这就导致在ffmpeg在解析拼接后的rtsp时,会出现两个@,例如
ffmpeg -i rtsp://admin:ty@123456@192.168.16.35:554/H.264/ch1/main/av_stream -fflags flush_packets -max_delay 1 -an -flags -global_header -hls_time 1 -hls_list_size 3 -hls_wrap 3 -vcodec copy -y D:/FF/1.m3u8
从而导致解析失败。解决方法是将用户名和密码中的特殊字符,进行URL的特殊符号转义编码。
“@”转义后的字符对应“%40”,所以上文转义后为ffmpeg -i rtsp://admin:ty%40123456@192.168.16.35:554/H.264/ch1/main/av_stream -fflags flush_packets -max_delay 1 -an -flags -global_header -hls_time 1 -hls_list_size 3 -hls_wrap 3 -vcodec copy -y D:/FF/1.m3u8