承接上一节
udp服务器接流转html5播放
效果
先看效果:
服务器拉rtsp流,直接转成flv,不经过推流,浏览器使用ws直接连接写得我们写得服务器播放,因为websocket本身是跨域得,可以直接使用文件进行播放。http协议支持得时候注意要启动http服务支持,否则会跨域截断。
rtsp,rtp截流hack
上一章我们使用了udpserver来接流,也就是接得纯rtp,拼接成帧就行,而rtsp,和GB28181都使用rtp协议,【GB28181会使用ps流】,原理都一样,同样获取rtp流以后,可以直接转化成单帧再进行html5传输,如fmp4,或者转化成flv格式,使用flv.js来直接播放,flv内部也是转成了fmp4,区别不是很大。
1.1 截流描述
上一次我们使用了live555来接rtsp,live555接rtsp流,这样加上udp接就比较实用了,可以使用ffmpeg发送测试,也可以使用rtsp直接接入,rtsp连接写入配置文件中:in.txt
#id(int):address(varchar):width(int):height(int)
#418511899@qq.com author:钱波 qianbo 2021-02-28
1 rtsp://admin:hk123456@192.168.0.55/h264/ch1/sub/av_stream 1280 720 live/1001
#2 rtsp://127.0.0.1/2.264 1280 720 live/1001
#3 rtsp://127.0.0.1/1.264 640 360 live/1002
带# 得是注释,文件引入了一路海康流,输出流为live/1001
1.2流程
流程为:
1 线程rtsp client 拉到流后发送给flv服务器
2 flv服务器接收到连接后将连接放入到hub中,待接到rtsp sps pps等信息后开始发送meta信息,启动推流协程,并同步每个人得时间戳。注意每个人得时间戳是不一样得。
3 ws浏览器播放端退出在推流协程里面同步判断,退出则在hub中删除该端。
1.3 读配置文件类
struct s_file
{
std::vector<s_source> v_sources;
int func_read()
{
std::ifstream infile("in.txt", std::ios::in); //以文本模式打开in.txt备读
if (!infile) { //打开失败
std::cout << "error opening source file." << std::endl;
return -1;
}
v_sources.clear();
while (infile.good()) {
string temp;
s_source s;
std::getline(infile, temp);
if (temp.length() > 3 /*0xEF 0xBB 0xBF*/)
{
uint8_t *pos = (uint8_t*)temp.c_str();
if (*pos == 0xEF)
{
temp = temp.substr(3);
printf("%02x %02x %02x \n", *(pos), *(pos + 1), *(pos + 2));
cout << "utf8 and BOM" << endl;
}
}
//0xEF, 0xBB, 0xBF
/*注意如果文件直接使用移入字符串,必须判断是否为结束,否则会多一行*/
//infile >> temp;
//if (infile.eof())
// break;
trim(temp);
if (!ifnote(temp) && !temp.empty())
{
istringstream istr(temp);
istr >>s.id >> s.v_address >> s.d_width >> s.d_height >>s.v_liveflv;
v_sources.push_back(s);
}
else
{
//数据库解析字段
}
}
return 0;
}
};
isnote函数只是判断是否带有注释,注释则不加入其中,比较简单,读者可以自行写出。
1.4 websocket服务器
websocket服务器我们自行编写,使用协程方式来制作,这个在我得其他文章里面也提供了全部源代码,读者可以找一下。
1.5 flv服务器
显然flv服务器建立在websocket服务器之上,加了flv格式得封装,class 图如下图所示
class c_flvserver
{
private:
//websocket or http
asio::io_context &v_io_con_webs;
//calculate the number of coroutine
std::atomic<uint32_t> v_coroutine_num = 0;
public:
/*int process_spspps(uint8_t * sps, uint32_t spslen,
uint8_t * pps,
uint32_t ppslen,
uint32_t timestamp,
int width, int height, int fps,
uint32_t key
);*/
#if 0
//关键帧再次同时写入sps和pps,特殊场合使用
int process_h264_spspps(
uint8_t *sps, int spslen,
uint8_t *pps, int ppslen,
uint8_t * nal,
uint32_t nal_len,
uint32_t timestamp,
bool IsIframe);
#endif
void storage_meta(uint32_t key,
ptr_s_memory mem);
int create_metadata(uint8_t* buf, int width, int height, int frameRate);
int process_spspps(uint8_t * sps, uint32_t spslen,
uint8_t * pps,
uint32_t ppslen,
uint32_t timestamp,
uint32_t key
);
int process_spspps_sp(uint8_t * sps, uint32_t spslen,
uint8_t * pps,
uint32_t ppslen,
uint32_t timestamp,
uint32_t key
);
int process_h264(uint8_t * nal,
uint32_t nal_len,
uint32_t timestamp,
bool IsIframe,
uint32_t key
);
int process_aac(uint8_t* data,
uint32_t data_len,
uint32_t timestamp);
c_flvserver(asio::io_context &iow) :v_io_con_webs(iow)
{}
void start(uint16_t port);
protected:
#ifdef SEND_META_SCRIPT
int create_metadata(uint8_t* buf, int width, int height, int frameRate);
#endif
protected:
void storage_header(uint32_t key, int av, ptr_s_memory mem);
void start_write_spawn(uint32_t key);
bool func_set_head_send(tcp::socket &sock,
uint8_t * buf,
size_t buflen /*payloadlen*/,
asio::yield_context &yield);
//we must add the websockets head,use the func_set_head_send
int func_send_2session_webs(s_flvhub &flvhub,
ptr_session_webs ptr,
ptr_s_memory mem,
asio::yield_context &yield);
//we do not add ,just send the flv data
int func_send_2session_http(s_flvhub &flvhub,
ptr_session_webs ptr,
ptr_s_memory mem,
asio::yield_context &yield);
};
1.6 下面给出关键代码
只有一个流容易,只有一个客户也容易,我们面对得一定是多路流和多个客户并发连接,所以要注意得事项比较多,下面看具体代码:
int c_flvserver::func_send_2session_webs(s_flvhub &flvhub,ptr_session_webs ptr, ptr_s_memory mem,
asio::yield_context &yield)
{
if (!ptr->v_socket.is_open())
return -2;
DEFINE_EC
uint8_t * data = mem->v_data;
size_t len = mem->v_len;
#if 1
if (!ptr->v_has_send_meta){
uint8_t * _data = flvhub.v_meta->v_data;
size_t _len = flvhub.v_meta->v_len;
if (func_set_head_send(ptr->v_socket, _data, _len, yield) == false)
return -1;
ptr->v_has_send_meta = 1;
}
#endif
if (flvhub.v_need_video && !ptr->v_has_send_video_head)
{
uint8_t * _data = flvhub.v_cache_headerv->v_data;
size_t _len = flvhub.v_cache_headerv->v_len;
if (func_set_head_send(ptr->v_socket, _data, (int)_len, yield) == false)
return -1;
ptr->v_has_send_video_head = 1;
}
if (flvhub.v_need_audio && !ptr->v_has_send_audio_head)
{
uint8_t * _data = flvhub.v_cache_headera->v_data;
size_t _len = flvhub.v_cache_headera->v_len;
if (func_set_head_send(ptr->v_socket, _data, (int)_len, yield) == false)
return -1;
ptr->v_has_send_audio_head = 1;
}
if (!ptr->v_has_sent_key_frame)
{
switch (*data) //the first byte
{
case 8://audio
{
cout << "audio waiting key frame." << endl;
return 0;
}
break;
case 9: //video
{
//fix me ,we must storage the key frame to the client
if (*(data+11 )== 0x17) //0x27 not keyframe
{
cout << "find the keyframe" << endl;
ptr->v_has_sent_key_frame = 1;
//开始时间戳计算,meevery
ptr->v_start_ts = GetTimestamp32();
}
else {
cout << "...";
return 0;
}
}
break;
default:
return -1;
break;
}
}
#if 1
//change the timestamp,get the real stamp;
uint32_t ts = GetTimestamp32() - ptr->v_start_ts;
*(data + 4) = (uint8_t)(ts >> 16);
*(data + 5) = (uint8_t)(ts >> 8);
*(data + 6) = (uint8_t)(ts);
*(data + 7) = (uint8_t)(ts >> 24);
#endif
//flv_const_buffer bb(&header[0], 11, _data, _len);
if (func_set_head_send(ptr->v_socket, data, (int)len, yield) == false)
return -1;
return 0;
}
以上是关键代码,下面给出一个代码结构,读者可以参考:
整个工程得代码并不多,比较简单,包含了flvserver,websocketserver,httpserver, rtp over udpserver,都使用协程制作,这样流程简单。包含rtsp得拉流客户端,比较实用,包含了analyse分析模块,可以对流进行图像分析等等,读者可以自行制作。
时间戳问题
如下图所示,明显vlc和ws连接得时间显然是不一样,ws连接时间很长,显示得时间竟然和vlc 相差20秒,这就是时间戳得问题了,所以要进一步处理该问题
如图,修正以后,时间戳趋于正常,flv播放比vlc rtsp 相差无几,快1秒左右,应该还有余地,可以在进行优化。
总结
本文并不使用拉流后再推流到服务端,而是将服务端直接集成,减少了部署内容,这是优点之一,第二个优点是并没有使用rtmp协议,而是直接转化成浏览器可播放得流,这是优点之二,缺点也很明显,读者可以提出讨论,或者有不明白得地方也可以讨论,知无不言言无不尽,第三篇我们可以加上rtsp服务,待续…