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的代码:
-
if (avformat_alloc_output_context2(&Out_FormatContext, NULL, "mp4", NULL) < 0)
-
return false;
-
pb_Buf = (uint8_t*)av_malloc(sizeof(uint8_t)*(D_PB_BUF_SIZE));
-
Out_FormatContext->pb = avio_alloc_context(pb_Buf, D_PB_BUF_SIZE,1,(void*)this,NULL,write_buffer,NULL);
-
if (Out_FormatContext->pb == NULL)
-
{
-
avformat_free_context(Out_FormatContext);
-
Out_FormatContext = NULL;
-
sendWSString("fail");
-
return false;
-
}
-
Out_FormatContext->pb->write_flag = 1;
-
Out_FormatContext->pb->seekable = 1;
-
Out_FormatContext->flags=AVFMT_FLAG_CUSTOM_IO;
-
Out_FormatContext->flags |= AVFMT_FLAG_FLUSH_PACKETS;
-
Out_FormatContext->flags |= AVFMT_NOFILE;
-
Out_FormatContext->flags |= AVFMT_FLAG_AUTO_BSF;
-
Out_FormatContext->flags |= AVFMT_FLAG_NOBUFFER;
这里需要注意的是pb不仅write_flag要设置成1,seekable也要设置成1,seekable这个很容易就忽略了,然而这个如果不是1,那么创建会失败。ffmpeg写数据输出到内存部分,参考avio_alloc_context的回调函数用法。
获取mime的方法:
-
static std::string GetMIME(uint8_t* data, int len)
-
{
-
int n = 0;
-
if (data[0] == 0)
-
{
-
while (n + 3 < len)
-
{
-
if ((data[n] == 0) & (data[n + 1] == 0) & (data[n + 2] == 1))
-
{
-
n += 3;
-
break;
-
}
-
else
-
n++;
-
}
-
}
-
n += 1;
-
if (n + 3 > len)
-
return "";
-
char mime[10] = {0};
-
sprintf(mime,"%.2x%.2x%.2x",data[n], data[n + 1], data[n + 2]);
-
return std::string(mime);
-
}
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数据后的处理
-
if(typeof(evt.data)=="string") //服务器传过来的可能是字符串,判断是不是
-
{
-
var str = evt.data;
-
console.log(str);
-
var strs = new Array(); //定义一数组
-
strs = str.split(":"); //字符分割
-
if (strs[0] == "open")
-
{
-
var mimestr = strs[1];
-
this.playurl(mimestr);
-
}
-
}
-
else
-
{
-
var result = new Uint8Array(evt.data);
-
this.queue.push(result);
-
if (this.needsend == true)
-
{
-
this.loadvideo();
-
}
-
}
字符串数据这里只用了很简单的定义,如果ws网关打开rtsp失败,那么返回的是“fail”,如果返回成功,则是“open:mime”,通过分隔符将mime取出,就可以初始化mime了;如果是二进制数据,则直接放到队列中。js代码不熟,有需要的各位自己按需求优化。
这套代码对rtsp源有格式要求,必须是h264+aac或纯h264的rtsp数据,因为mse对能播放的fmp4有要求,代码中并未对音视频进行重编码。另外代码中是使用rtp over tcp来传输的,使用udp模式请修改代码。最后,本方案达到的延时极低,但在chrome和firefox上对比,firefox的延时略大一些,估计各个浏览器的缓存策略造成了差异。