boost asio 实现http-flv rtsp hls流媒体服务器

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行。


streamserver代码行数
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,117评论 4 360
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,963评论 1 290
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 107,897评论 0 240
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,805评论 0 203
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,208评论 3 286
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,535评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,797评论 2 311
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,493评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,215评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,477评论 2 244
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,988评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,325评论 2 252
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,971评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,055评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,807评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,544评论 2 271
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,455评论 2 266

推荐阅读更多精彩内容