关于海思3536编解码与FFmpeg的相关问题——推流相关

在我们利用海思3536完成对摄像头视频流的拉取、(ffmpeg解协议头)解码、叠加osd、HDMI本地显示、编码、转存等任务后,我们开始利用ffmpeg将编码后的视频流进行推流,采用的是ZLM的流媒体服务器。然而出师不利,一开始就遇到了很多问题。下面是我们解决的一些思路...

在我们进行推流之前,我们就尝试过直接将摄像头的视频流进行拉取推流,并且是成功了的,所以一开始我们依葫芦画瓢,直接搬的之前的代码,然而。。。是我们想多了,报了很多错。

首先是我们没有创建一个新的输出流,因为之前是直接拿到流推出去的,现在是从海思的硬解码器中取流进行推流,所以我们需要使用ffmpeg提供的API:

pOutStream = avformat_new_stream(AVFormatContext *s, const AVCodec *c);

创建一个新的流,然后在流信息中手动设定好编码器的相关信息。

下一步是利用ffmpeg提供的API:

ret = avformat_write_header(outputContext, NULL);

在输出流中写入头信息,值得注意的是,我们一开始是在前面的流信息中写入了时间基的,但是后面发现似乎没这个必要,因为在写入头时间基发生了变化(我们自己设置的时间基反而不对),我们将经过写头后的时间基打印出来是1/90000。

前面的准备工作完成后,开始下一步,从海思的编码器中取流,将流里的数据地址传给海思的AVpacket结构体中,然后利用ffmpeg的API:

ret = av_interleaved_write_frame(outputContext, packet);

将视频包写入进行推流,一些细节上的问题就不在进行过多的赘述,直接讲问题所在。

完成这一步后,我们的程序并没有报错,且流媒体服务器也没有报错,这说明我们的代码并没有问题,然而在使用vlc和ffplay播放时并没有任何画面,连一帧图像都没有。没法只能使用 wireshark 抓包看看。

(抓包是成功的,说明链接是建立成功了的。协议是没问题的)

我们将之前未进行海思编解码且推流成功抓的视频包与我们编码后推流的视频包进行了对比,发现了少了一部分东西,主要是两个东西,pps和I帧。关于pps和sps主要就是存的一些编码器信息,在拉取到流时服务器或者播放器需要根据这两个东西来配置解码器才能播放视频,I帧是关键帧,缺少就不能播放,关于sps,pps,i帧的详细信息可以在CSDN上查阅,有很多文章进行了解释,这里就不再赘述。

所以这就很奇怪了,我们之前转存是成功的,而且本地播放是没问题的,说明我们的264文件是存在sps,pps,i帧的。我们用notpad++打开264文件采用十六进制进行了查看,确认编码器出来的流是没问题的,该有的东西都有的。

这时候我首先怀疑的是av_interleaved_write_frame(outputContext, packet);这个函数,我在想的是不是他没有将我们的数据完全写入,在再次了解了一下这个函数后,我打消了这个疑虑,这个函数是应该是将packet里的所有数据进行了写入才对。

而后,我想索性将我们赋了stream里面数据后的给packet->data里的数据全部打印出来得了,诶这里就有一个问题了,packet->data是个数组的话,我循环打印,该打多长呢?当时我们并没有想太多,只想着看看里面有没有PPS,所以就随便定义了个长度进行打印,发现是有pps的。后面我就想pps过后的I帧是什么样的呢?或者说第一个包里面的全部数据有哪些东西呢,sps/pps和I帧是不是分开的呢?这时候就不得不确定一下packet的长度了,因为在将编码器的去除的视频流赋给ffmpeg的AVpacket的时候里面有个stStream.pstPack->u32Len需要与packet里面的len进行对接的,而这个长度按理说就是一个包里面数据的长度。我首先就以这个长度来打印视频数据(注意这个长度值得是字节数,所以应该用十六进制打印),同时我也将这个长度进行了打印,重点来了!!!

我发现第一个包打印出来的长度只有15个字节,所以视频数据总共打印出来只有15个字节,而我将这15个字节与前面我们抓取的第一个包(第一个包本应该携带sps/pps)进行了对照,刚好就是前面所提到的包含sps而没有pps的15个字节,所以问题还是出在了

ret = av_interleaved_write_frame(outputContext, packet);

他的原理是根据packet里面设定的长度来决定写多少数据进行的推流,我们的长度取出来只有15,那当然会有数据丢失咯。

那怎么解决呢,我们先将长度固定死了,给了一个比较长的长度,然后进行抓包,这次抓包工具能识别出含有sps和pps

 但是依旧不能播放出视频,因为在抓取的包中并没有I帧,这说明sps/pps以及i帧在海思进行编码后是放在一个包中的,i帧中是含有一帧完整图像,是能够不依靠其他帧进行显示的,所以我们设置的长度远远不够。于是我们就给了一个特别大的长度。

这下有图像了!!!虽然是卡着不动的,但起码能够看见图像了,说明i帧是接收到且识别出来了的,再看抓包工具。

 

 跟想的一样,i帧是能够接收到了,那么就是下一个问题,为什么视频卡着,只是偶尔动一下呢?这个问题先不急着解决,我们先把上面的长度理清楚,毕竟我们不能将长度写死了,因为每个包的字节长度其实是有差异的,如果写死了很可能会影响视频的质量。

首先我们翻看了海思提供的库文件里面对 VENC_STREAM_S stStream;的定义,其中是这样的:

typedef struct hiVENC_STREAM_S
{
    VENC_PACK_S *pstPack;                           /*stream pack attribute*/
    HI_U32      u32PackCount;                       /*the pack number of one frame stream*/
    HI_U32      u32Seq;                             /*the list number of stream*/

    union
    {
        VENC_STREAM_INFO_H264_S  stH264Info;         /*the stream info of h264*/
        VENC_STREAM_INFO_JPEG_S  stJpegInfo;         /*the stream info of jpeg*/
        VENC_STREAM_INFO_MPEG4_S stMpeg4Info;       /*the stream info of mpeg4*/
        VENC_STREAM_INFO_H265_S  stH265Info;        /*the stream info of h265*/
    };
}VENC_STREAM_S;

其中pstPack就是我们要取出视频相关信息并传给ffmpeg的AVpacket的结构体。这次我们注意到了一个参数u32PackCount;其意思是“一帧流的包号”。所以这里让我注意到,我混淆了一个概念,认为从海思编码器去除的一帧流就只有一个包,也就是一帧流对应一个包,所以也就直接将一帧流存储视频流的地址给了ffmpeg的AVpacket(由于他们同一个流里面的几个包的地址是连续的,所以之前打印全部数据,各个包的数据都能打印出来)。后面经过打印,在含有sps/pps以及I帧的一帧流里面是包含了四个pack(u32PackCount=4),sps/pps/i帧各占了一个pack(剩下的一个尚不清楚是什么。有知道的小伙伴还请告知我,谢谢)。对于其他的P帧数据一帧流也就只包含了一个pack(u32PackCount=1),也就是再看VENC_PACK_S *pstPack;  结构体的定义:

typedef struct hiVENC_PACK_S
{
    HI_U32   u32PhyAddr;                         /*the physics address of stream*/
    HI_U8   *pu8Addr;                            /*the virtual address of stream*/
    HI_U32   u32Len;                             /*the length of stream*/
    
    HI_U64   u64PTS;                             /*PTS*/
    HI_BOOL  bFrameEnd;                          /*frame end*/
    
    VENC_DATA_TYPE_U  DataType;                  /*the type of stream*/
    HI_U32   u32Offset;

	HI_U32 u32DataNum;
	VENC_PACK_INFO_S stPackInfo[8];
}VENC_PACK_S;

u32Len是属于pstPack的里面的成员,而每一帧流里面包含的可能pack不止一个,那么也就是说如果是起始流(包含sps/pps等)里面其实是有四个长度的,为此我们只需要将四个长度加起来就是我们最终需要计算的一帧流的长度。以下是代码实现,就算一帧流只有一个包次方法依然适用:

    int j=0 ;

	int64_t packet_size=0;

	for(j=0;j<stStream.u32PackCount;j++){

	packet_size+=stStream.pstPack[j].u32Len;
}

经过测试,抓包数据完整:

下面开始解决的是页面卡的问题。

对于视频的卡顿,主要就是看I帧的数据完整性,以及时间戳是否有问题,我们经过抓包工具在三确认了I帧是完整的。娜美问题就很有可能出现在了时间戳上。此前我们的时间戳是从一帧流里面的数据包里面取的:

 packet->dts = stStream.pstPack->u64PTS;
 packet->pts = stStream.pstPack->u64PTS;

 经过打印,发现为一直为0,这样写显然是不对的。经过上面我们对海思结构体的了解可知,一帧流里面是分包的,我们需要指定是哪个包的时间戳。于是我们做了修改如下:

 packet->dts = stStream.pstPack[0].u64PTS;
 packet->pts = stStream.pstPack[0].u64PTS;

至于为什么取第一个pstPack的时间戳,我们经过打印在同一帧流里面的所有包的时间戳是一样的。这样修改后视频终于能动起来了,但是很卡,非常卡,好消息是说明就是时间戳的问题。对于一帧视频流的时间戳实际是前面在解码前传进去的时间戳,这看样子是不能用前面解码部分的时间戳。

这时候我参考了

(25条消息) 视频帧数据用硬件编码输出,仅仅使用FFMPEG将硬件编码出的数据推流RTSP数据后严重花屏且画面卡住不动_雨夜和阳晨的博客-CSDN博客

这篇文章所提到的时间戳的计算方法,为保险起见,先是按照文章作者的方式,将时间戳写死,然后递增,这时候的视频就像ppt一样一帧一帧的显示,但是间隔时间很均匀,这进一步说明方向是对的。于是乎,我直接采用了文章作者最终给出的计算方法:

packet->duration = 9000;
pts+= av_rescale_q( 9000 ,(AVRational){ 1, 1000000}, pOutStream->time_base);

av_rescale_q( 9000 ,(AVRational){ 1, 1000000}, pOutStream->time_base); 这里使用 av_rescale_q() 函数将 9000 这个时间单位转换到输出流的时间基下,生成一个对应的 pts 值。av_rescale_q() 函数的作用是将时间从一个时间基转换到另一个时间基

 最终在播放器上拉取并显示出了完整且流畅的画面:

至此板子的推流功能基本走通。

修改:

一开始我是同时用的VLC和ffplay进行播放,前面都还好,都播放播出来,在我完善时间戳后,奇怪的事情发生了,VLC播放画面一闪一闪的,而ffplay却没有任何问题。这让我一度认为时间戳还有问题。后面尝试了其他播放器(potplayer),跟方法play一样完全没有问题,所以是vlc的设置问题。后面了解到VLC的渲染图像时有GPU参与的,其设置里有一个硬件加速的功能,目的时减少卡顿和延时,然而根据网上查找的资料,VLC的版本可能会存在显卡或者驱动兼容不好导致闪屏。后面测试中,我将此功能直接禁用了,VLC也能播放了,只不过相较于其他的播放器还是有卡顿,起初以为是VLC需要设置一些缓冲,我将缓冲设置长了一些,依旧没有改善卡顿,所还是回到了时间戳上,对时间戳的计算做了进一步了解,并重新梳理了一下。

在从海思编码器获取视频流后,需要通过调用FFmpeg的API进行推流。在这个过程中,需要将码流数据封装成AVPacket结构体并给AVPacket结构体赋值时间戳,时间戳的单位为AVStream的time_base。因此,在设置AVPacket的时间戳时,我们需要首先确定码流中每帧的时间戳信息,然后根据AVStream的time_base来计算AVPacket的时间戳。如果你的时间基是1/9000,那么你需要将每帧的时间戳乘以9000,再将结果转换为AVRational类型并通过av_rescale_q()函数进行单位转换。而上面那篇文章作者采用的是自己定义一帧的时长,然后手动对时间戳进行的递增操作,按理说这个操作可行,毕竟在potplayer和ffplay上是没问题的,但是并不是最好的选择,所以结合之前的理解,修改后的代码如下:

	//packet->duration = 9000;

	pts= av_rescale_q( stStream.pstPack[0].u64PTS ,(AVRational){ 1, 1000000}, pOutStream->time_base);

所以最终归根结底就是时间戳要进行转换,将一帧流的时间戳转换成我们推流的时间戳。

最终呈现效果(硬件加速还是要关闭)。 

小计几个ffmpeg的命令:

ffprobe rtsp://192.168.3.55/live/mystream1 -show_streams -select_streams v -print_format json

这个命令会使用ffprobe工具来获取指定RTSP流的视频流信息,并以JSON格式输出

ffplay rtsp://192.168.3.3/live/mystream1

这是一个RTSP流的播放命令,使用FFplay工具播放RTSP流。

  • 4
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 7
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 7
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值