gihub:https://github.com/wangdxh/Desert-Eagle/
只实现了视频的处理。rtsp只支持rtp over rtsp
简单说下使用asio的原因,一开始使用go实现了http-flv流媒体服务器的功能,总共大约300行的代码,生产率非常高的,本机测试基本没有问题,但是当局域网内测试的时候,因为buffer的回收机制,导致一对多时chan丢消息,内存使用也是很感人。
也考虑了使用libevent和自己进行内存计数管理,可行是可行的,但是生产率低,维护成本高。后来采用asio的proactor网络模型,加上智能指针对码流内存计数自动释放,以及c11的lambda函数支持,使得最终的流媒体服务器代码有一种同步routine编程的错觉。整个服务器c++的代码大约1650行,生产率上来讲不输go,内存和效率上完胜。
在一对多的实时转发上,go不是那么合适,所以采用asio。但是go有go的长处,流媒体协议的实现终归还是技术层面的东西,没有太多的花头,选择采用的语言实现时,更多的考量的是生产率,elegance。(维护性并不在考量范围内,一般生产率高的语言,代码更少,很多功能是封装在语言内的,语言使用越广泛,功能越稳定,更易维护。)
从一对多的转发来比较go和asio,go的chan相当于一个stl的deque队列,routine调度其实都是通过网络出发的,内存自动回收可以通过智能指针完成,c++里过多的new可以通过jemalloc进行优化,这样来看,能不能设计一种dsl,语法像go一样简洁,但是执行的时候,先将dsl翻译成boost asio的函数调用,然后编译执行?
(感觉有门,dsl的翻译过程可以使用python来实现,请参考pyparsing实现letrec语法函数递归调用)
boost asio基础
asio 异步accept
async_accept指定socket,和一个lambda函数,发起一次,成功之后,函数被执行,socket被赋予正确的值,进行处理,然后再发起另一次异步accept。
asio 异步读写
异步读取网络数据使用到了2个读取的函数:
- boost::asio::async_read,需要指定需要读取的buffer的指针和长度,和lambda函数,只有当buffer的长度读取满了之后,lambda函数才会被执行。
- boost::asio::async_read_until 需要指定streambuf和一个特殊的字符,只有当读取的数据里面包含指定的字符时,指定的lambda函数才会被执行,lambda函数执行,streambuf里面的数据包含指定的字符,但指定字符可能在中间。在分析http和rtsp的时候,需要使用“\r\n\r\n”,来检测信令结束。
asio写数据async_write也是异步的,有几个注意项:
- 一次只能发起一个写请求,buffer完整写完之后,回调函数才会被执行,所以就需要一个发送队列来保存将要发送的数据。
- 写数据的时候,为了提高发送效率,支持了ConstBufferSequence类似writev支持多个buffer,多个buffer通过ConstBufferSequence接口来获取。
shared_const_buffer_flv
shared_const_buffer_flv 实现了ConstBufferSequence接口:
// Implement the ConstBufferSequence requirements.
typedef boost::asio::const_buffer value_type;
typedef const boost::asio::const_buffer* const_iterator;
const boost::asio::const_buffer* begin() const { return m_abuffer; }
const boost::asio::const_buffer* end() const { return m_abuffer + FLV_ASIO_BUFFER; }
boost::asio::const_buffer m_abuffer[FLV_ASIO_BUFFER];
FLV_ASIO_BUFFER的值是3,有三个const_buffer:
- 默认情况下要发送的数据保存在第二个buffer内,当要发送http-chunked的数据格式时,在第一个buffer内生成长度信息,在第三个buffer内生成结尾信息,这样一次写请求,会把3个数据全部进去,类似writev。
- 当第一个和第三个buffer为空的时候,异步写的时候,只会把第二个buffer内的数据发送出去,并不会受到影响。
- 第二个buffer内的数据,当构造的时候,分配在shared_ptr内,这样一对多当码流转发的时候,每个客户端tcp保存一个shared_const_buffer_flv的备份,但是真正的码流数据只有一份,通过shared_ptr进行计数,当所有的客户端都发送完成的时候,引用计数为0,码流数据释放。
m_streamdata = std::shared_ptr<uint8_t>(new uint8_t[dwtotallen],
[]( uint8_t *p ) { delete[] p; });
m_abuffer[1] = boost::asio::buffer(m_streamdata.get(), dwtotallen);
- 码流转发的时候,每个tcp端都有一个发送队列,队列都有最大数限制,超过限制后,就不往队列里面添加,直接丢弃。
typedef std::deque<shared_const_buffer_flv> stream_message_queue;
streampushclient 码流推送
代码在streampushclient文件夹内
- 码流推送读取本地的h264文件,然后合成flv的文件格式,然后发往服务端
- h264的文件内并不是h264裸码流 ,而是4个字节的小端编码的长度,跟着h264帧长度。
- 向服务器发送数据时,先发送4个字节的长度,然后再发送相应长度的数据,4个字节的长度信息使用小端编码。
- 在发送flv数据之前,先发送本次推流的字符串名称,用于rtsp,hls等协议进行点播。
- H264Frame会对h264裸码流进行分析,解析出sps,pps,是否为关键帧,并把nalu单元独立拆出来。
- CFlv 会对码流进行flv合成,当发现sps和pps都出现之后,会合成flv的头信息,并生成flv的帧信息,发送flv头和flv帧到服务器端。
- flv的头信息是标准的头信息,但是flv的帧并不是完整的flv帧,完整的帧是11个字节的tag头,5个字节的关键帧信息和CompositionTime,h264帧数据,最后是4个字节长度(表示前面数据的总长11+5+h264帧长),11个字节的tag头里面保存了,这个tag的时间戳信息,但是当服务器使用http-flv推流给客户端的时候,不论客户端什么时候连接上来,每个客户端的时间戳都应该从0开始,所以为了服务器转发方便和更好地利用shared_const_buffer_flv ,当转发flv给每个客户端的时候,tag头的11个字节再重新打。
- flv里面的h264数据不是0001分割的nalu单元,而是将0001 4个字节替换成对应的nalu单元的长度,使用大端编码表示。
- client 推送码流使用的是阻塞推送,测试时运行和server运行在一起,后续改为异步的推送。
tcp server
代码在streamserver文件夹内
boost::asio::io_service io_service;
tcp_server<stream_rtsp_to> server_rtsp_to(io_service, tcp::endpoint(tcp::v4(), 554));
tcp_server<stream_httpflv_to> server_Httpflv_to(io_service, tcp::endpoint(tcp::v4(), 1984));
tcp_server<stream_flv_from> server_flv_from(io_service, tcp::endpoint(tcp::v4(), 1985));
io_service.run();
- 服务器进行tcp端口监听,rtsp使用标准的554,httpflv的请求端口使用1984,码流推送端口使用1985.
- tcp_server 是模板类,当类构造时,执行一次do_accept,发起一次异步accept,当accept执行成功时,回调函数被执行,创建一个模板参数的类,将accept成功的socket,move到新的类中,执行模板参数的start()函数,然后继续发起一次异步accept。
void do_accept()
{
//a new connection
acceptor_.async_accept(socket_,
[this](boost::system::error_code ec)
{
if (!ec)
{
std::make_shared<T>(std::move(socket_))->start();//session
}
do_accept();
});
}
stream_hub
- 根据客户端的推流的名称,会创建全局的stream_hub map。
typedef std::shared_ptr<stream_hub> stream_hub_ptr;
std::map< std::string, stream_hub_ptr > g_map_stream_hubs;
- stream_hub相当于一个流的中转站,每个stream_hub 保存了:流的名称,这条流的flv头信息,请求这条流的rtsp客户端,请求这条流的http-flv客户端,加入和退出hub,向hub内输入数据进行分发。
std::set<stream_session_ptr> http_flv_sessions_;
std::set<stream_session_ptr> rtsp_sessions_;
copyed_buffer m_buf_header;
std::string m_strname;
void join_http_flv(stream_session_ptr participant)
void leave_http_flv(stream_session_ptr participant)
void join_rtsp(stream_session_ptr participant)
void leave_rtsp(stream_session_ptr participant)
void deliver(const boost::asio::mutable_buffer& msg, bool isheader = false)
- 后续在每个协议内,会对这些函数进行详解;对hls切片文件的生成也是在stream_hub内,因为每个实时流对应一个hls切片组,所以在hub实现。
- stram_session
class stream_session
{
public:
virtual ~stream_session() {}
virtual void deliver(const shared_const_buffer_flv& msg) = 0;
//participant should deliver message
};
stream_session 定义了向请求码流的客户端发送码流的统一接口。
- 服务器从推流的客户端接收到码流,通过stream_hub的deliver函数,将码流发送给hub,hub再通过各个接收码流客户端的deliver接口,将码流分发出去。
stream_flv_from
- stream_flv_from 处理客户端推送上来的码流,起始处理函数是start()
class stream_flv_from : public std::enable_shared_from_this<stream_flv_from>
{
void start()
{
do_read_header();
}
}
- 首先读取4个字节的长度,读取成功之后,读取相应字节的数据,进行处理,完成之后,开始起始循环,再发起读取四个字节长度的请求...
- 数据处理,
- 先获取推送的流的名称,成功之后根据名称创建一个stream_hub。
- 然后获取flv的头信息(flv的头信息要保存起来,每个请求http-flv码流的客户端都要首先发送flv的头信息),设置到stream_hub内。
- 后续到来的数据都是flv的帧数据,获取到之后,直接deliver到stream_hub内,供分发。
boost::asio::mutable_buffer steambuf (m_bufmsg, length);
if (false == m_bget_stream_name)
{
m_bget_stream_name = true;
m_bufmsg[length] = '\0';
m_streamname = (char*)m_bufmsg;
room_ = create_stream_hub(m_streamname);
}
else if(false == m_bget_flv_header)
{
m_bget_flv_header = true;
room_->setmetadata(steambuf);
}
else
{
room_->deliver(steambuf);
}
stream_httpflv_to
- stream_httpflv_to 从stream_session继承,业务起始函数是start()
class stream_httpflv_to: public stream_session,
void start()
{
do_read_header();
}
- 读取文件头,使用了async_read_until,直到读取到“\r\n\r\n”之后,才会执行回调函数,从url的信息中提取出客户端要浏览的视频流的名称,通过查询参数”deviceid“的值获取到。
- 根据流的名称,在stream_hub的map内查找是否已经有客户推流了,没有返回404,找到后返回
"HTTP/1.1 200 OK\r\n
Connection: close\r\n
Content-Type: video/x-flv\r\n
Transfer-Encoding: chunked\r\n
Access-Control-Allow-Origin: *\r\n\r\n";
Content-Type: video/x-flv 表示码流是flv格式的;Transfer-Encoding: chunked表示具体码流的内容按照chunked格式去发送;Access-Control-Allow-Origin: * 一定要有的,表示支持跨域访问,因为我们的http-flv提供出去的端口是1984,所以必须要增加这个http信息。
将http的回应,加入到发送队列中,发起发送请求。
http chunked 传输方式
每个chunk分为头部和正文两部分
1.头部内容指定下一段正文的字符总数(非零开头的十六进制的数字)和数量单位(一般不写,表示字节).
2.正文部分就是指定长度的实际内容,chunk头和内容后面都跟着一个回车换行(CRLF)。
在最后一个长度为0的chunk中的内容是称为footer的内容,是一些附加的Header信息(通常可以直接忽略)
- 根据请求流的名称,加入到对应的stream_hub内,stream_hub 的函数join_http_flv时,会将新加入的stream_session,插入到http-flv的session列表内,新加入的请求播放的客户端,要马上把flv的头信息,deliver给该
客户端。
room_ = get_stream_hub(m_streamname);
room_->join_http_flv(shared_from_this());
stream_hub::
void join_http_flv(stream_session_ptr participant)
{
http_flv_sessions_.insert(participant);//add a client
if (!m_buf_header.isnull())
{
shared_const_buffer_flv flvheader(m_buf_header.m_buffer, shared_const_buffer_flv::em_http_flv);// send flv header
flvheader.setisflvheader(true);
flvheader.setisflvstream(true);
participant->deliver(flvheader);
}
}
- stream_hub 通过deliver从推流端接收到码流之后,如果发现http-flv的session列表内,有客户端在请求该码流,就将码流封装到shared_const_buffer_flv内,然后deliver给各个客户端。码流数据保存在shared_const_buffer_flv的第二个buffer内,第一个buffer和第二个buffer用户增加http的chunk信息。
stream_hub::
void deliver(const boost::asio::mutable_buffer& msg, bool isheader = false)
{
if (http_flv_sessions_.size() > 0)
{
shared_const_buffer_flv flvbuf(msg, shared_const_buffer_flv::em_http_flv);
flvbuf.setisflvheader(isheader);
flvbuf.setisflvstream(true);
for (auto session: http_flv_sessions_)
session->deliver(flvbuf);
}
}
- stream_httpflv_to deliver码流,这一段是发送的核心处理过程:
- http-flv发送给客户的消息有2种,1是http的回应,2是码流数据,http回应和flv的头是不能丢弃的,只有中间的码流能够丢弃。
- http的回应是不需要进行chunk的,只有码流数据要进行chunk
- 真正的帧数据发送的时候,要先判断是否已经向客户发送了关键帧,如果没有发送关键帧,要等待关键帧到来再进行发送。
- 收到关键帧之后,后续的帧往队列里面增加,如果超过了队列的最大值,不进行码流的发送,并且将接收到关键帧标记置为空,这样后续的非关键帧就无法进行插入队列的操作,直到下一个关键帧到来,这样处理是因为,如果发送队列已经满了,说明现在网络有些阻塞,进行码流丢弃之后,要等到下一个关键帧到来再继续进行发送,减轻网络压力。
- 然后进行发送的操作,如果消息队列为空,说明没有消息在发送,将消息插入队列之后,执行do_write的操作,码流的chunk操作,也是在发送的时候才进行的。
void deliver(const shared_const_buffer_flv& msg)
{
if (msg.isflvstream() && !msg.isflvheader())
{
// all flv info need chunked
if (false == m_bfirstkeycoming )
{
if (!msg.iskeyframe())
{
printf("flvdata keyframe is not coming %s \r\n", m_szendpoint);
return;
}
else
{
m_bfirstkeycoming = true;
}
}
// just drop the stream data but not the head and protocol
if (write_msgs_.size() > MAX_STREAM_BUFFER_NUMS)
{
//buffer is full, do not need p-frame,so wait the I-frame
m_bfirstkeycoming = false;
printf("the buffer over the max number %d, %s\r\n", MAX_STREAM_BUFFER_NUMS, m_szendpoint);
return;
}
}
bool write_in_progress = !write_msgs_.empty();
write_msgs_.push_back(msg);//会将消息先放到write_msgs_中
if (!write_in_progress)
{
//write message
do_write();
}
}
- do_write 发送码流
- 前面的发送队列插入的逻辑能够保证当出现发送阻塞,队列超出的时候,码流总是能够从关键帧进行发送。
- 真正执行发送的时候,要处理2个操作,一是码流帧增加flv的11个字节tag头(flv的头信息不增加,码流帧总是从0x17或者0x27开始的);而是增加http的chunk头和尾信息。
- chunk的长度信息和flv的tag头的11个字节都放在shared_const_buffer_flv的第一个buffer内,chunk的尾”\r\n“信息放在第三个buffer内。通过setchunk函数设置进去。
- 因为每条http-flv的请求流都要求时间戳从0开始进行递增,所以在stream_httpflv_to内保存一个时间戳,从0开始每次成功发送码流之后再递增。
- 发起异步写操作,将shared_const_buffer_flv数据发送完成,发送成功后,回调函数内部会把已经发送的数据,pop出去,如果队列不为空,继续进行do_write操作发送码流。
void do_write()
{
shared_const_buffer_flv& ptagflvbuf = write_msgs_.front();
if (ptagflvbuf.isflvstream())
{
const boost::asio::const_buffer* pbuffer = ptagflvbuf.getstreamdata();
int nsize = boost::asio::buffer_size(*pbuffer);
const char* pdata = boost::asio::buffer_cast<const char*>(*pbuffer);
int nLen;
//memset(m_szchunkbuf, sizeof(m_szchunkbuf), 0);
if (pdata[0] == 0x17 || pdata[0] == 0x27)
{
int ntaglen = nsize -4;
nLen = sprintf(m_szchunkbuf, "%x\r\n", nsize+11);
m_szchunkbuf[nLen+0] = 9; //video
m_szchunkbuf[nLen+1] = (ntaglen >> 16) & 0xff;
m_szchunkbuf[nLen+2] = (ntaglen >> 8) & 0xff;
m_szchunkbuf[nLen+3] = ntaglen & 0xff;
// nb timestamp
m_szchunkbuf[nLen+4] = (m_dwtime>> 16) & 0xff;
m_szchunkbuf[nLen+5] = (m_dwtime>> 8) & 0xff;
m_szchunkbuf[nLen+6] = m_dwtime& 0xff;
m_szchunkbuf[nLen+7] = (m_dwtime>> 24) & 0xff;
m_szchunkbuf[nLen+8] = 0;
m_szchunkbuf[nLen+9] = 0;
m_szchunkbuf[nLen+10] = 0;
nLen += 11;
m_dwtime += 40;
}
else
{
nLen = sprintf(m_szchunkbuf, "%x\r\n", nsize);
}
ptagflvbuf.setchunk(m_szchunkbuf, nLen, m_szchunkend, 2);
}
boost::asio::async_write(socket_,//当前session的socket
ptagflvbuf,
[this, self](boost::system::error_code ec, std::size_t length/*length*/)
{
if (!ec)
{
write_msgs_.pop_front();
if (!write_msgs_.empty())
{
do_write();
}
}
});
}
stream_rtsp_to
rtsp从码流层和http-flv有些区别
- http-flv要求每个客户端收到的码流时间戳要从0开始,然后递增
- rtsp只支持rtp over rtsp,rtp包里面的时间戳起始点可以是随机的,而且当码流丢失时,要能够在rtp的时间戳内反映出来,所以rtsp的码流的时间戳是在stream_hub 内打包完成的,发送给每个rtsp客户端的码流数据都是相同的。
stream_rtsp_to 从stream_session继承,业务起始点也在start()内
class stream_rtsp_to: public stream_session,
void start()
{
do_read_header();
}
- rtsp的信令交互就不细写了,从rtsp的url中提取出查询参数:deviceid 作为要请求的流的名称,如果流名称对应的stream_hub不存在,返回404 错误码;如果流存在,当rtsp客户端发起play的请求的时候,调用hub的join_rtsp函数将该session加入到hub内。rtsp不需要flv的头信息,只要发送rtp的帧信息即可。
stream_hub::
void join_rtsp(stream_session_ptr participant)
{
rtsp_sessions_.insert(participant);
// rtsp do not need the flv header
}
- stream_hub的deliver的时候,创建类型为rtsp的shared_const_buffer_flv,rtp的sequence和时间戳通过引用传递,会在构造函数内部被修改。在构造函数内部会调用get_rtsp_rtp_video_total_len,根据h264帧数据先获取需要分配的rtp数据长度,然后在使用shared_ptr进行分配内存空间,然后调用generate_rtp_info_over_rtsp,生成rtp数据,当shared_const_buffer_flv被deliver到stream_rtsp_to内时,判断关键帧和发送队列满的逻辑和http-flv是一致的,只不过发送的时候更加简单,因为省略了http chunk的步骤。
shared_const_buffer_flv(const boost::asio::const_buffer& buff, em_buffertype etype, uint64_t dwtimestamp, uint16_t& dwsequence)
{
const uint8_t* pData = boost::asio::buffer_cast<const uint8_t*>(buff);
...
int nLen = boost::asio::buffer_size(buff);
uint32_t dwtotallen = nLen;
if (em_rtsp == etype)
{
// 发往每个rtsp客户端的rtp里面的时间戳,并不需要从0开始,而且h264转成rtp时,每个rtp的头,是掺杂在数据中间的,
// 所以时间戳的递增,从hub这里开始,然后递增,客户端拿到什么rtp时间,就是什么开始,并不受影响
if (0x17 == pData[0] || 0x27 == pData[0])
{
uint32_t dwnumnalus;
get_rtsp_rtp_video_total_len(pData, nLen, dwtotallen, dwnumnalus);
m_streamdata = std::shared_ptr<uint8_t>(new uint8_t[dwtotallen], []( uint8_t *p ) { delete[] p; });
bool bret = generate_rtp_info_over_rtsp(pData, nLen, m_streamdata.get(), dwtotallen,
dwnumnalus, dwtimestamp, dwsequence);
}
}
m_abuffer[1] = boost::asio::buffer(m_streamdata.get(), dwtotallen);
}
- 码流相关
- 这里的码流还是有flv的5个字节的tag头,末尾还有4个字节的lasttaglen的长度信息的,所以get_rtsp_rtp_video_total_len和generate_rtp_info_over_rtsp的时候会跳过这9个字节,直接分析h264帧数据。
- rtp over rtsp的时候除了标准的rtp的包数据,还有在每个rtp的包头增加4个字节的额外信息,第一个字节是 $字符,用以代表后面是rtp数据,第二个字节是channel信息,rtp over rtsp的时候,音视频的rtp和rtcp都会在setup的过程中协商出不同的channelid,后面2个字节是跟着的rtp包的总长度,大端序表示。generate_rtp_info_over_rtsp生成的一帧的数据包含了完整一帧h264的rtp数据包和相关的扩展头。直接发送给各个客户端即可。
hls支持
hls的支持是在stream_hub内部完成,每一路流对应一个m3u8的文件和一组ts文件用于实时播放,生成的m3u8文件和ts文件通过web服务器提供http下载,供客户端进行播放。
生成实时流的m3u8文件是这样的:
#EXTM3U
#EXT-X-ALLOW-CACHE:NO
#EXT-X-TARGETDURATION:2
#EXT-X-MEDIA-SEQUENCE:8
#EXTINF:1
http://x.x.x.x/static/123abcdef32153421-8.ts
#EXTINF:2
http://x.x.x.x/static/123abcdef32153421-9.ts
#EXTINF:1
http://x.x.x.x/static/123abcdef32153421-10.ts
#EXTINF:1
http://x.x.x.x/static/123abcdef32153421-11.ts
- #EXT-X-TARGETDURATION 下面的ts文件列表中最大的那个ts文件的时长,单位是秒,因为我们测试文件的并不是固定帧率的,所以切出来的ts文件长度是变化的。
- #EXT-X-MEDIA-SEQUENCE:8 表示ts列表的文件是从sequence为8的ts文件开始播放的。
- #EXTINF:1 紧跟着的ts文件的时长。
- http://x.x.x.x/static/123abcdef32153421-9.ts 表示访问的ts文件的http路径。
- 因为我们的m3u8存放的地方和ts文件相同,所以m3u8文件的访问路径也是类似的:http://x.x.x.x/static/123abcdef32153421.m3u8
- 我们设置的ts切片文件是4个,当实时流当有新的ts切片文件生成时,要更新m3u8文件,将#EXT-X-MEDIA-SEQUENCE设置为9,同时ts列表应该从9开始,跟着10,11,12。
生成m3u8和ts的业务逻辑:
- m_dw_ts_slice_num = 4;表示m3u8文件内最多有4个文件切片
m_m3u8_ts_directory指定了m3u8和ts的生成目录
m_m3u8_ts_prefix = "http://x.x.x.x/static/";指定了m3u8内的ts文件的前缀
m_a_file_duration 每个切片的时长,是通过一个vector循环使用保存的。 - 收到的h264码流是4个字节的nalu长度+nalu数据的格式,ts文件要求h264是0001+nalu数据的格式,所以要先转换下h264帧格式。
- 具体切片
- 我们的ts切片是以1个关键帧为间隔的,0x17表示上来的flv的数据为关键帧,收到关键帧,就把前面的ts文件关闭
- 如果总的生成文件的个数,大于指定的切片文件的上限(总的生成文件的格式,最大为切片文件上限+1),删除原来的要被淘汰的最早的切片文件,m3u8文件内的切片的起始序号增加1
- 计算出最大的ts文件的时长,然后根据根据当前的开始切片序号,将所有的信息写入到m3u8文件内,完成文件更新。
- get_ts_frame_totallen获取一帧h264数据转成ts格式后的长度
generate_ts_frame 生成h264的ts封装,只有在关键帧的时候,才生成pat和pmt。
stream_hub::
m_cur_file_num = 1;
m_cur_hls_sequence = 1;
m_dw_ts_slice_num = 4;
m_m3u8_ts_directory = "D:\\github\\Desert-Eagle\\webserver\\static\\";
//std::string strdirectory = "D:\\nginx-1.10.3\\html\\";
m_m3u8_ts_prefix = "http://x.x.x.x/static/";
---------
uint8_t* pData = boost::asio::buffer_cast<uint8_t*>(msg);
int nLen = boost::asio::buffer_size(msg);
if (isheader == false || pData[0] == 0x17 || pData[0] == 0x27)
{
// generate m3u8 and ts file to the directory
change_flv_h264_buffer_to_0001_buffer(pData+5, nLen-9);
}
if (m_bsupporthls)
{
if (isheader == false)
{
bool bkeyframe = false;
if (pData[0] == 0x17)
{
bkeyframe = true;
if (nullptr != m_file_ts)
{
fflush(m_file_ts);
fclose(m_file_ts);
uint64_t utemp = m_a_file_duration[(m_cur_file_num-1)%3];
m_a_file_duration[(m_cur_file_num-1)%3] = (m_u64timestamp - utemp)/90000;// now is duration second
printf("ts file is %d duration is %llu\r\n", m_cur_file_num, m_a_file_duration[(m_cur_file_num-1)%3]);
// file number add 1
m_cur_file_num++;
if (m_cur_file_num - m_cur_hls_sequence > m_dw_ts_slice_num)
{
char szfilepath[256] = {0};
sprintf(szfilepath, "%s%s-%d.ts", m_m3u8_ts_directory.c_str(), m_strname.c_str(), m_cur_hls_sequence);
m_cur_hls_sequence++;
::remove(szfilepath);
}
uint64_t maxduration = 0;
for(int inx = m_cur_hls_sequence; inx < m_cur_file_num; inx++)
{
maxduration = std::max(maxduration, m_a_file_duration[(inx-1)%3]);
}
printf("update m3u8 file max duration is %llu\r\n", maxduration);
// write m3u8 file
std::stringstream strm3u8;
strm3u8 << "#EXTM3U\r\n"
<< "#EXT-X-ALLOW-CACHE:NO\r\n"
<< "#EXT-X-TARGETDURATION:" <<maxduration <<"\r\n"
<< "#EXT-X-MEDIA-SEQUENCE:" << m_cur_hls_sequence << "\r\n";
for(int inx = m_cur_hls_sequence; inx < m_cur_file_num; inx++)
{
strm3u8 << "#EXTINF:" << m_a_file_duration[(inx-1)%3] <<"\r\n"
<< m_m3u8_ts_prefix << m_strname.c_str() << "-"<< inx << ".ts\r\n" ;
}
std::string strtemp = strm3u8.str();
std::string strm3u8filepath = m_m3u8_ts_directory + m_strname +".m3u8";
FILE* file_m3u8 = fopen(strm3u8filepath.c_str(), "wb");
fwrite(strtemp.c_str(), strtemp.length(), 1, file_m3u8);
fflush(file_m3u8);
fclose(file_m3u8);
printf("after update m3u8 hls_sequence is %d, cur_file_num is %d\r\n", m_cur_hls_sequence, m_cur_file_num);
}
char szfilepath[256] = {0};
sprintf(szfilepath, "%s%s-%d.ts", m_m3u8_ts_directory.c_str(), m_strname.c_str(), m_cur_file_num);
m_file_ts = fopen(szfilepath, "wb");
m_a_file_duration[(m_cur_file_num-1)%3] = m_u64timestamp;// start time
}
uint32_t dwtotallen = 0;
m_ts.get_ts_frame_totallen(pData+5, nLen-9, bkeyframe, dwtotallen);
m_ts.generate_ts_frame(pData+5, nLen-9, m_ts_stream_buff.get(), dwtotallen, bkeyframe, m_u64timestamp);
fwrite(m_ts_stream_buff.get(), dwtotallen, 1, m_file_ts);
}
webserver
webserver使用python提供web服务
- 网页服务提供一个url连接,http://x.x.x.x/flvplay?deviceid=123abcdef32153421 这个页面中请求的deviceid,正好就是推流客户端的中发送的流id,这个请求生成一个使用flv.js请求http-flv进行html5播放的页面,用于http-flv的码流测试。
- 文件下载服务提供hls的m3u8和ts文件的http请求下载。
测试
各种协议测试播放步骤,请查看github的readme文档。
https://github.com/wangdxh/Desert-Eagle/blob/master/README.md
streamserver的代码约么有1650行。