DoubleLi

qq: 517712484 wx: ldbgliet

  博客园 :: 首页 :: 博问 :: 闪存 :: 新随笔 :: 联系 :: 订阅 订阅 :: 管理 ::

eb端是无法直接播放rtsp流的,目前常用的解决方案是如jsmpeg、flv.js等。这些方案都是要推送流到服务端,之后才能在web上播放视频,相对比较麻烦。我采用websocket结合mse的方式,实现了一个websocket网关,及其对应的js播放器,在这里做下说明,具体代码参考github上我的源码。

这套方案的原理是,ws网关在拉到rtsp流后,取得mime,将其发送给web端,然后将rtsp流转为fmp4格式,以二进制数据格式发给web端;web端用其初始化mse,然后将websocket收到的二进制数据扔给mse,实现视频的播放。

ws网关有两个关键的问题需要解决,一是封装成fmp4后,输出要到内存而不是文件,二是要能取得mime。如果以网上以回调函数作为ffmpeg输出的例子来写,会发现创建失败。mime对应的是编码类型,需要解析流才能得到,具体怎么解决这两个问题,看看下面的说明。

创建输出的AVFormatContext的代码:

  1.  
    if (avformat_alloc_output_context2(&Out_FormatContext, NULL, "mp4", NULL) < 0)
  2.  
    return false;
  3.  
    pb_Buf = (uint8_t*)av_malloc(sizeof(uint8_t)*(D_PB_BUF_SIZE));
  4.  
    Out_FormatContext->pb = avio_alloc_context(pb_Buf, D_PB_BUF_SIZE,1,(void*)this,NULL,write_buffer,NULL);
  5.  
    if (Out_FormatContext->pb == NULL)
  6.  
    {
  7.  
    avformat_free_context(Out_FormatContext);
  8.  
    Out_FormatContext = NULL;
  9.  
    sendWSString("fail");
  10.  
    return false;
  11.  
    }
  12.  
    Out_FormatContext->pb->write_flag = 1;
  13.  
    Out_FormatContext->pb->seekable = 1;
  14.  
    Out_FormatContext->flags=AVFMT_FLAG_CUSTOM_IO;
  15.  
    Out_FormatContext->flags |= AVFMT_FLAG_FLUSH_PACKETS;
  16.  
    Out_FormatContext->flags |= AVFMT_NOFILE;
  17.  
    Out_FormatContext->flags |= AVFMT_FLAG_AUTO_BSF;
  18.  
    Out_FormatContext->flags |= AVFMT_FLAG_NOBUFFER;

这里需要注意的是pb不仅write_flag要设置成1,seekable也要设置成1,seekable这个很容易就忽略了,然而这个如果不是1,那么创建会失败。ffmpeg写数据输出到内存部分,参考avio_alloc_context的回调函数用法。

获取mime的方法:

  1.  
    static std::string GetMIME(uint8_t* data, int len)
  2.  
    {
  3.  
    int n = 0;
  4.  
    if (data[0] == 0)
  5.  
    {
  6.  
    while (n + 3 < len)
  7.  
    {
  8.  
    if ((data[n] == 0) & (data[n + 1] == 0) & (data[n + 2] == 1))
  9.  
    {
  10.  
    n += 3;
  11.  
    break;
  12.  
    }
  13.  
    else
  14.  
    n++;
  15.  
    }
  16.  
    }
  17.  
    n += 1;
  18.  
    if (n + 3 > len)
  19.  
    return "";
  20.  
    char mime[10] = {0};
  21.  
    sprintf(mime,"%.2x%.2x%.2x",data[n], data[n + 1], data[n + 2]);
  22.  
    return std::string(mime);
  23.  
    }

mime可以通过spspps取得,ffmpeg在创建AVStream后,264的spspps可以从codecpar->extradata取得,在extradata中跳过264的分隔符后,接下来的第2、3、4个字节就可以拼出264的mime。

mime的音频部分可以参考https://wiki.multimedia.cx/index.php?title=MPEG-4_Audio,以"mp4a.40.2"举例,mp4a.40表示音频解码器为aac,.2表示AAC LC,对比ffmpeg的定义可以发现,这个值就是codecpar的profile加1.

另外说明一下movflags,fmp4需要设置成frag_keyframe+empty_moov,这样就是fmp4,我设置的值是frag_keyframe+empty_moov+omit_tfhd_offset+faststart+separate_moof+disable_chpl+default_base_moof+dash,其中omit_tfhd_offset这个设置是针对chrome浏览器的,如果不设置的该项,chrome上是会播放失败的,faststart是为了将moov移动到mdat前面,separate_moof如果不加上,chrome处理音频时会有问题,尚未找出是视频源问题还是共性问题。

js播放器这边很简单,需要说明一下的是收到ws数据后的处理

  1.  
    if(typeof(evt.data)=="string") //服务器传过来的可能是字符串,判断是不是
  2.  
    {
  3.  
    var str = evt.data;
  4.  
    console.log(str);
  5.  
    var strs = new Array(); //定义一数组
  6.  
    strs = str.split(":"); //字符分割
  7.  
    if (strs[0] == "open")
  8.  
    {
  9.  
    var mimestr = strs[1];
  10.  
    this.playurl(mimestr);
  11.  
    }
  12.  
    }
  13.  
    else
  14.  
    {
  15.  
    var result = new Uint8Array(evt.data);
  16.  
    this.queue.push(result);
  17.  
    if (this.needsend == true)
  18.  
    {
  19.  
    this.loadvideo();
  20.  
    }
  21.  
    }

字符串数据这里只用了很简单的定义,如果ws网关打开rtsp失败,那么返回的是“fail”,如果返回成功,则是“open:mime”,通过分隔符将mime取出,就可以初始化mime了;如果是二进制数据,则直接放到队列中。js代码不熟,有需要的各位自己按需求优化。

这套代码对rtsp源有格式要求,必须是h264+aac或纯h264的rtsp数据,因为mse对能播放的fmp4有要求,代码中并未对音视频进行重编码。另外代码中是使用rtp over tcp来传输的,使用udp模式请修改代码。最后,本方案达到的延时极低,但在chrome和firefox上对比,firefox的延时略大一些,估计各个浏览器的缓存策略造成了差异。

posted on 2021-02-23 16:23  DoubleLi  阅读(1623)  评论(0编辑  收藏  举报