文档开发者中心
我的

开贴讲讲-Android 音视频流媒体

作者:HW-HBB ///纯解决问题过程中总结的个人对这块的理解,难免有些错误,请各自判断


第一章:Android RTSP流媒体时间戳计算方式


Android音视频同步方式采用的是比较音视频当前帧的时间戳,音视频同步的具体方案将在后面几个章节介绍,这里先把时间戳的概念和计算方式介绍一遍


时间戳:以视频为例,视频在解码显示的时候是以帧为单位进行的,帧里面包含的数据首先是视频本身,如要显示的画面数据,但是仅有这些没有一些控制信息是不行的,以时间戳为例,如果帧不带有任何时间信息的话,我们就不知道在什么时刻去处理这一帧,这就是时间戳的作用了,当然还有一些其他信息也非常重要,这里不介绍。每一个音视频帧都会有一个属性叫时间戳(TimeStamp),这个时间戳参考某一个基准点,然后以某种算法计算出来,这个时间戳用来表明这一帧被显示的时间点。

有了时间戳的概念之后接下来我们分析其作用,我们知道时间戳在音视频同步中起着核心作用,没有时间戳或者不正确的时间戳都可能导致音视频不同步、有音频无视频和有视频无音频等现象,鉴于时间戳的重要性,下面以RTSP流媒体这个经典的例子作为线索,跟踪一下RTSP流媒体是如何通过RTCP/RTP包里面的信息计算时间戳的(本地视频的时间戳都是直接编码在视频帧里面,直接取出来就行)

根据服务器是否发送SR类型的RTCP包,时间戳的计算方式分为两种

1:服务器发送SR类型RTCP包

在ARTPConnection的receive函数中,会监听前面建立的四个套接字端口(两个音频的,两个视频的,其中一个是监听RTCP包,一个是监听RTP包),如果监听端口收到了数据会记录是RTCP还是RTP包,并且将数据存放在buffer中,供后面解析。

在parseRTP函数中会对收到的buffer进行逐字节解析,buffer中第一个字节是版本信息,第二个字节表明RTCP的类型,包括SR、BYE和RR等,SR的标记为是200

下面就是调用SR的解析函数,取出SR包中两个重要的参数ntpTime和rtpTime,这两个参数在后面计算时间戳的公式中介绍其作用。

时间戳的主要逻辑是在MyHandler这个RTSP核心类中进行,SR类型中的这两个参数通过ARTPSource的timeUpdate函数post到MyHandler中

最后是在MyHandler类中的onTimeUpdate函数中做保存处理

这样当针对音视频轨的两个SR类型的RTCP包都收到后,就会两次进入到onTimeUpdate这个函数中,进行相关参数的设置,其中下面这四个参数最重要了

       track->mRTPAnchor = rtpTime;

       track->mNTPAnchorUs = ntpTimeUs;

       mNTPAnchorUs = ntpTimeUs;

       mMediaAnchorUs = mLastMediaTimeUs;

所谓Anchor也就是描点也就是一个参考点的意思,后续来的音视频帧都会携带一个时间点,然后相对这个描点进行计算,计算该帧的时间戳

下面看给音视频帧计算时间戳的方法,这个是在addMediaTimestamp函数中

正如代码中的分析,用于携带音视频数据的RTP包本身会携带一个rtpTime信息,我们直接取出来用于计算这一帧的时间戳,取出后的下面那部分代码就是利用这个rtpTime和SR中的rtpTime和ntpTime计算时间戳,计算公式如下:

mediaTimeUs =

 (((int64_t)rtpTime - (int64_t)track->mRTPAnchor) * 1000000ll)

track->mNTPAnchorUs +  ---------------------------------------------------------------------------------------------  +mMediaAnchorUs - mNTPAnchorUs

                        track->mTimeScale

其中:

track->mNTPAnchorUs 对应的音视频的SR类型包中的ntpTime时间点

track->mRTPAnchor对应的音视频的SR类型包中的ntpTime时间点

track->mTimeScale对应当前音视频的时间幅度,在ffmpeg里面是一个比例转化

正常情况下track->mNTPAnchorUs - mNTPAnchorUs = mMediaAnchorUs  = 0,所以公式简化为:

  (((int64_t)rtpTime - (int64_t)track->mRTPAnchor) * 1000000ll)

--------------------------------------------------------------------------

   track->mTimeScale

这个公式也就很明朗了,就是用这一帧中携带的rtp时间减去sr包中携带的参考的rtp时间,除以时间幅度就是我们需要的值。

2:服务器不发送SR类型的RTCP包

并不是所有的流媒体服务器都会按照标准去实现,在一些流媒体服务器中就不会发送用于同步的SR类型包,我们客户端在解析各种流媒体时就必须考虑到各种异常情况,对这些异常做处理,使这些服务器上的流媒体能够正常播放,当然客户端也不能考虑到所有的异常,所以总有那么一些服务器上的流媒体是无法正常播放的。

没有SR包就省了前面的所有处理,公式也将变得非常简单,如下:

    (int64_t)rtpTime    -   0

0  + -------------------------------------------------------------   +  0  -  0

      track->mTimeScal

这些零是怎么来的呢?过程如下:

每接收到一帧的时候到会发送一个timeout的消息,检查10秒后有没有接受到数据,上面这个条件就是判断最初的10秒内有没有收到SR类型的rtcp包,如果没有就调用下面这个函数:

Fake的意思不言而喻吧,没有收到SR包就没有rtpTime和ntpTime两个参数信息,那么我们就假造一个,fakeTimestamps函数中两次调用了onTimeUpdate这个函数,并且传入的两个参数为0,我们上面公式中的那些0就是这里传入的。

自此,无论是哪一种情况,在收到任一个RTP帧都会打好时间戳,供后面处理,但是Android这块并没有结束,对于直博流媒体和点播流媒体(I版本),点播流媒体(J版本)会有个时间戳的重新计算过程。

前面的计算方式采用的是当前帧的时间相对于SR里面的时间,这样计算出来的时间戳非常有可能不是从零开始的,甚至在某些情况下,视频开始的第一帧的时间戳就是多少多少个小时,对于点播流媒体这种情况肯定是不容易接受的,所以无论是在I版本还是在J版本中,对于点播流媒体时间戳的计算方式都会采用另一种计算方式进行覆盖,而对于直博流媒体是否是从0开始播放就没有那么重要了,所以I版本做了重新计算,而J版本中就没有。

下面就跟踪一下重新计算时间戳的方式:

在MyHandler的onAccessUnitComplete函数中,调用postQueueAccessUnit函数会将当前帧发送给RTSPSource类处理,处理完之后放到AnotherPacketSource的待解码队列中去,供解码器取出解码使用,不过在放入待解码队列前会做一些时间戳的处理。

这里不跟踪具体跟踪每个变量值是怎么赋值的,直接给出时间戳的计算方式,与MyHandler中打时间戳相对于SR中的时间描点进行计算不同,这里是根据音视频到来的第一帧作为参考描点,也就是第一帧相对于自己计算时间戳,第二、三及后面的所有帧都相对于第一帧计算时间戳。

公式:

    ((double)rtpTime - (double)info->mRTPTime)

------------------------------------------------------------------* 1000000ll + info->mNormalPlaytimeUs

    info->mTimeScale

这样算出来的音视频第一帧的时间戳肯定是0,后续音视频帧的时间戳也是从0开始慢慢增大。而MyHandler中相对SR计算出来的时间戳就不一定是(而且大概率)不是从0开始的,而且测试发现,对于直播流媒体,由于没有给出视频的总时间,进度条右端会采用默认的一个小时,而有时间通过SR类型的包计算出来的时间戳最开始就超过了1个小时,这样就导致进度条直接超过了屏幕的显示范围。


点赞
收藏
回复
10
分享
举报
浏览3373 编辑于2020-07-31 08:15未知归属地
全部评论
最多点赞
最新发布
最早发布
写回答
新增插入模板功能
一键使用模板,快速填写内容,轻松发帖~
知道了
  • 为了保障您的信息安全,请勿上传您的敏感个人信息(如您的密码等信息)和您的敏感资产信息(如关键源代码、签名私钥、调试安装包、业务日志等信息),且您需自行承担由此产生的信息泄露等安全风险。
  • 如您发布的内容为转载内容,请注明内容来源。

我要发帖子

目录 收起
1:服务器发送SR类型RTCP包
2:服务器不发送SR类型的RTCP包
了解社区公约,与您携手共创和谐专业的开发者社区。