一、rtsp 协议读取视频
1.1读取方法ffmpeg
这种方法和opencv是一样的,因为opencv使用的就是ffmpeg,结果不是很好,断线重连不是很好做,有一个好处是不用引入其他库,ffmpeg也用来解码之用。
1.2 live555
速度快,断线重连比较方便,摄像头像海康大华等等时间长了会自动重启内部服务,这时候涉及断线重连。这里使用这种方式。
1.3 自己写接收代码
理解rtsp协议之后使用udp服务接收帧,这种好处是完全控制协议和数据,最大的灵活性,但是也带来代码调试量增多的问题。
二、基础算法分析
注意:实现 rtsp 端口使用 sdp 协议获取图像信息,使用 rtp 协议接收图像,解码后得到 yuv,使用 y 分量得到灰白图像,在需要 rgb 的时候使用 yuv 转化成 rgb。如果算法需要的是灰度图,那就直接解码成yuv并取分量,如果一定要彩色图,那就直接使用ffmpeg解码成彩色。如果使用opencv的mat 矩阵,那就直接解码到mat的内存中,不要拷贝来拷贝去。
1 清晰度算法
梯度函数常被用来提取边缘信息,聚焦良好的图像,具有更尖锐的边缘,应有
更大的梯度函数值。采用 sobel 算子分别提取水平和垂直方向的梯度值,基于
tenengrad 梯度函数清晰度定义如下:
2 色偏度和色偏方向
输入 BGR 颜色后转化成 lab 颜色表示方式
Lab 是由一个亮度通道(channel)和两个颜色通道组成的。在 Lab 颜色空间中,每个
颜色用 L、a、b 三个数字表示,各个分量的含义是这样的:
- L*代表亮度
- a*代表从绿色到红色的分量
- b*代表从蓝色到黄色的分量
*函数描述: calcCast 计算并返回一幅图像的色偏度以及,色偏方向
*函数参数: InputImg 需要计算的图片,BGR 存放格式,彩色(3 通道),灰度图无效
- cast 计算出的偏差值,小于 1 表示比较正常,大于 1 表示存在色偏
- da 红/绿色偏估计值,da 大于 0,表示偏红;da 小于 0 表示偏绿
- db 黄/蓝色偏估计值,db 大于 0,表示偏黄;db 小于 0 表示偏蓝
*函数返回值: 返回值通过 cast、da、db 三个应用返回,无显式返回值
3 亮度异常
转成灰度图以后,计算颜色的直方图,计算分布在 0-255 颜色的值,设定灰度
图平均值为亮度均值如 128,求取每个像素点的与平均值差为 d1,并求取均值
da ,求取 256 种灰度值颜色与均值的差并求取平均值 Ma
求取 K = abs(da)/ abs(Ma) 为亮度系数
cast 计算出的偏差值,小于 1 表示比较正常,大于 1 表示存在亮度异常;当 cast 异常时,
da 大于 0 表示过亮,da 小于 0 表示过暗
for (int i = 0; i < width; i++)
{
for (int j = 0; j < height; j++)
{
a += float(value[i, j]) - 128);
int x = value[i, j];
Hist[x]++; //直方图
}}
da = a / float(imageGray.rows*imageGray.cols);
float D = abs(da);
float Ma = 0;
for (int i = 0; i < 256; i++)
{
Ma += abs(i - 128 - da)*Hist[i];
}
Ma /= float((imageGray.rows*imageGray.cols));
float M = abs(Ma);
float K = D / M;
4 马赛克
马赛克出现时,大量正方形边出现,求取图像边缘,边缘检测之 Canny 检测
Canny 检测是最优的边缘检测算法:
1.消除噪声:运用高斯内核进行卷积降噪。
2.计算梯度幅值和方向。
3.非极大值抑制:这一步排除非边缘像素,仅仅保留了一些细线条(候选边
缘)
4.滞后阈值:滞后阈值需要两个阈值(高阈值和低阈值):若某一个像素位置
的幅值超过高阈值,该像素被保留。
:若某一个像素位置的幅值小于低阈值,该像素被排除。
:若某个像素位置的幅值在两者之间,该像素仅仅在连接到一个高于高阈值的
像素时被保留。
求取边缘后,在边缘中寻找方形,如果找到,则为有马赛克
5 雪花噪声
雪花噪声为高斯噪声和椒盐噪声,使用中值滤波算法尝试去除,如果原图和
去除后的图有较大的区别,则判断有雪花。
//准备 0°,45°,90°,135°4 个方向的卷积模板。
//用图像先和四个模板做卷积,用四个卷积绝对值最小值 Min 来检测噪声点。
//求灰度图 gray 与其中值滤波图 median。
//判断噪声点:fabs(median - gray)>10 && min>0.1。
//噪声点占整幅图像的比较即为雪花噪声率。
三、进行分析并返回图像结果到web
两种方式,解码接收到h264 -》sps pps,h265-》 vps,sps,pps,h265解码在后端传图像到前端
3.1 使用ws返回图像jpg 叠加数据 返到web上
这里使用这种模式
function init() {
canvas = document.createElement('canvas');
content = canvas.getContext('2d');
canvas.width = 320;
canvas.height = 240;
content.scale(1, -1);
content.translate(0, -240);
document.body.appendChild(canvas);
// container.appendChild( canvas );
img = new Image();
img.src = "bg1.jpg";
canvas.style.position = 'absolute';
img.onload = function () {
content.drawImage(img, 0, 0, canvas.width, canvas.height);
// imgDate = content.getImageData(0, 0, canvas.width, canvas.height);
//createPotCloud(); //创建点云
};
}
function WebSocketTest() {
if ("WebSocket" in window) {
// alert("您的浏览器支持 WebSocket!");
// 打开一个 web socket
var ws = new WebSocket("ws://127.0.0.1:2268");
console.log(ws);
ws.onopen = function (evt) {
console.log("connected");
/*let obj = JSON.stringify({
test:"qianbo0423"
})
ws.send(obj);*/
};
ws.onmessage = function (evt) {
if (typeof (evt.data) == "string") {
//textHandler(JSON.parse(evt.data));
} else {
var reader = new FileReader();
reader.onload = function (evt) {
if (evt.target.readyState == FileReader.DONE) {
var url = evt.target.result;
// console.log(url);
img.src = url;
//img.src = url;// "bg1.jpg";
//var imga = document.getElementById("imgDiv");
//imga.innerHTML = "<img src = " + url + " />";
}
}
reader.readAsDataURL(evt.data);
}
};
ws.onclose = function () {
alert("连接已关闭...");
};
} else {
// 浏览器不支持 WebSocket
alert("您的浏览器不支持 WebSocket!");
}
}
3.2 使用flv.js返回数据
这里使用这种模式返回h264数据
flv.js 本身没有追帧功能,修改里面的部分函数可以达到较为实时的播放,页面切换时像flv.js或者hls等会停止播放,这些可以用追帧跳帧解决。
四、websocket server
使用c++来写这个server,因为本身里面调用了视频的许多功能,编解码,分析,使用c++比较划算,自己实现一个websocketserver可以根据RFC6455文档,这里使用了boost库的协程模式写出一个websocket的server。规格如下:
RFC 6455
0 1 2 3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-------+-+-------------+-------------------------------+
|F|R|R|R| opcode|M| Payload len | Extended payload length |
|I|S|S|S| (4) |A| (7) | (16/64) |
|N|V|V|V| |S| | (if payload len==126/127) |
| |1|2|3| |K| | |
+-+-+-+-+-------+-+-------------+ - - - - - - - - - - - - - - - +
| Extended payload length continued, if payload len == 127 |
+ - - - - - - - - - - - - - - - +-------------------------------+
| |Masking-key, if MASK set to 1 |
+-------------------------------+-------------------------------+
| Masking-key (continued) | Payload Data |
+-------------------------------- - - - - - - - - - - - - - - - +
: Payload Data continued ... :
+ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +
| Payload Data continued ... |
+---------------------------------------------------------------+
*/
websocket server 最重要的时握手过程,前面时http协议,然后时sha1算法进行摘要算法,返回前端,前端浏览器会验证,其实自己写websocket client一般会省略这个过程,但是浏览器的websocket 不会省略。以下为握手过程:
bool func_hand_shake(boost::asio::yield_context &yield)
{
DEFINE_EC
asio::streambuf content_;
size_t length = asio::async_read_until(v_socket, content_, "\r\n\r\n", yield[ec]);
ERROR_RETURN_FALSE
asio::streambuf::const_buffers_type bufs = content_.data();
std::string lines(asio::buffers_begin(bufs), asio::buffers_begin(bufs) + length);
//c_header_map hmap;
//fetch_head_info(lines, hmap, v_app_stream);
//the url length not over 1024;
char buf[1024];
fetch_head_get(lines.c_str(), buf, 1023);
//v_app_stream = buf;
cout << "get:" << buf<< endl; //like this--> live/1001 rtmp server must like this
std::string response, key, encrypted_key;
//find the get
//std::string request;
size_t n = lines.find_first_of('\r');
//find the Sec-WebSocket-Key
size_t pos = lines.find("Sec-WebSocket-Key");
if (pos == lines.npos)
return false;
size_t end = lines.find("\r\n", pos);
key = lines.substr(pos + 19, end - pos - 19) + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
//get the base64 encode string with sha1
#if 1
boost::uuids::detail::sha1 sha1;
sha1.process_bytes(key.c_str(), key.size());
#endif
#if 0
SHA1 sha;
unsigned int message_digest[5];
sha.Reset();
sha << server_key.c_str();
sha.Result(message_digest);
#endif
unsigned int digest[5];
sha1.get_digest(digest);
for (int i = 0; i < 5; i++) {
digest[i] = htonl(digest[i]);
}
encrypted_key = base64_encode(reinterpret_cast<const uint8_t*>(&digest[0]), 20);
/*
The handshake from the server looks as follows :
HTTP / 1.1 101 Switching Protocols
Upgrade : websocket
Connection : Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK + xOo =
Sec-WebSocket-Protocol: chat
*/
//set the response text
response.append("HTTP/1.1 101 WebSocket Protocol Handshake\r\n");
response.append("Upgrade: websocket\r\n");
response.append("Connection: Upgrade\r\n");
response.append("Sec-WebSocket-Accept: " + encrypted_key + "\r\n\r\n");
//response.append("Sec-WebSocket-Protocol: chat\r\n");
//response.append("Sec-WebSocket-Version: 13\r\n\r\n");
size_t ret = boost::asio::async_write(v_socket, boost::asio::buffer(response), yield[ec]);
ERROR_RETURN_FALSE
//calculate the hash key
v_key = hash_add(buf, HASH_PRIME_MIDDLE);
c_flvhubs::instance()->push_session(v_key, shared_from_this());
return true;
}
发送:
bool func_set_head_send(uint8_t * frame, int len /*payloadlen*/, int framelen, asio::yield_context &yield)
{
*frame = 0x81;//0x81; 1000 0001 text code ; // 1000 0010 binary code
//*frame = 0x82;
if (len <= 125) {
//数据长度小于1个字节
//mask bit is 0
*(frame + 1) = (uint8_t)len;
}
else if (len <= 0xFFFF) { //65535
//数据长度小于2个字节
*(frame + 1) = 126;
*(frame + 2) = len & 0x000000FF;
*(frame + 3) = (len & 0x0000FF00) >> 8;
}
else {
//数据长度为8个字节
*(frame + 1) = 127;
*(frame + 2) = len & 0x000000FF;
*(frame + 3) = (len & 0x0000FF00) >> 8;
*(frame + 4) = (len & 0x00FF0000) >> 16;
*(frame + 5) = (len & 0xFF000000) >> 24;
*(frame + 6) = 0;
*(frame + 7) = 0;
*(frame + 8) = 0;
*(frame + 9) = 0;
}
DEFINE_EC
//send the data
asio::async_write(v_socket, asio::buffer(frame, framelen), yield[ec]);
if (ec)
{
return false;
}
return true;
}
boost的协程模式开发服务器效率挺高的,我用这个做了rtmpserver和其他udpserver,另外一个选择使用go语言,可以看我的其他文章。
五、问题和如何分布式
可以选用这种模式,flv.js自动播放总是要点一下,edge下没有这个问题,可以用技术解决这种小问题,看量,http 的flv有个数限制,http协议限制6个连接数,一个chrome等浏览器最多呈现6个链接,是并发最大数,ws限制为255,所以使用websocket了这种模式,
5.2 分布式推送
准备使用rtp over udp方式进行推送,好处良多,请看我其他文章。