RTP时间戳
RTP固定报头中,带有4字节大小的时间戳。
1 2 3 4 5 6 7 8 9 10 11 12 |
0 1 2 3 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ |V=2|P|X| CC |M| PT | sequence number | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | timestamp | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | synchronization source (SSRC) identifier | +=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+ | contributing source (CSRC) identifiers | | .... | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ |
RTP时间戳一般以采样频率为单位。相邻两帧的时间戳增量为:
\[时间差(ms)* 采样率 / 1000 \tag{1}\]
RTP时间戳分为绝对时间戳跟相对时间戳。本文主要介绍这个时间戳是如何产生的。
视频RTP时间戳产生
由于视频每帧间隔也就是时间差不是稳定的,无法使用固定的时间戳增量计算时间戳,所以视频使用当前帧采集的NTP时间作为时间戳计算基础,也就是使用绝对时间戳表示。
采集阶段
调用set_timestamp_ms(rtc::TimeMillis())
设置当前时间。
1 2 3 4 5 6 7 8 9 |
int32_t VideoCaptureImpl::IncomingFrame() { VideoFrame captureFrame = VideoFrame::Builder() .set_video_frame_buffer(buffer) .set_timestamp_rtp(0) .set_timestamp_ms(rtc::TimeMillis()) .set_rotation(!apply_rotation ? _rotateFrame : kVideoRotation_0) .build(); } |
编码阶段
这里将NTP时间戳转换为RTP时间戳,计算方法为:
RTP时间:NTP时间(ms) * 90000(采样率) / 1000
,对应代码如下:
1 2 3 4 5 6 7 8 9 10 11 |
void VideoStreamEncoder::OnFrame() { // delta_ntp_internal_ms_ = clock_->CurrentNtpInMilliseconds() - clock_->TimeInMilliseconds() // 用来进行内部时间跟NTP时间的转换,从而得到采集的NTP时间 // video_frame.render_time_ms()就是前一步设置的时间 capture_ntp_time_ms = video_frame.render_time_ms() + delta_ntp_internal_ms_; incoming_frame.set_ntp_time_ms(capture_ntp_time_ms); // Convert NTP time, in ms, to RTP timestamp. const int kMsToRtpTimestamp = 90; incoming_frame.set_timestamp( kMsToRtpTimestamp * static_cast<uint32_t>(incoming_frame.ntp_time_ms())); } |
根据rfc3551,5.Video章节,之所以使用90kHz作为采样频率是因为,90kHz跟MPEG呈现时间戳频率相同。同时90kHz差不多是24(HDTV),25(PAL),29.97(NTSC)和30Hz(HDTV)这些电视信号制式帧率以及50,59.94和60帧率的整数倍。同时使用90kHz能够满足一些场景的时间精度需要。例如:
- 音视频同步,需要将RTP时间戳转为NTP时间戳
- 抖动计算,参考:WebRTC研究:统计参数之抖动
RTP打包发送阶段
这里会在之前设置的RTP时间戳基础上加上一个随机偏移,加上这个偏移是为了防止时间戳冲突,假如同一个时刻有好几个视频源同时产生视频,它们就可能有完全一样的时间戳,对应代码如下:
1 2 3 4 5 |
EncodedImageCallback::Result RtpVideoSender::OnEncodedImage() { uint32_t rtp_timestamp = encoded_image.Timestamp() + rtp_streams_[stream_index].rtp_rtcp->StartTimestamp(); } |
至此得到了最后视频RTP包中的时间戳。
音频RTP时间戳产生
RTC中音频默认是10ms采集一次。由于采样间隔也就是时间差是稳定的,根据公式(1),相邻采集帧时间戳增量为:
10ms * 采样率 / 1000 = 采样率 / 100
,也就是每帧单声道的样本数。所以音频时间戳计算无需获取当前NTP时间,从0开始,按固定时间戳增量不断增加即可,也就是使用相对时间戳表示。
采集阶段
直接取每帧单道样本数作为时间戳增量,计算代码如下:
1 2 3 |
void ChannelSend::ProcessAndEncodeAudio() { _timeStamp += static_cast<uint32_t>(audio_frame->samples_per_channel_); } |
这样得到了每一个采集帧的时间戳。
编码阶段
Opus编码器默认设置20ms编码帧长,一个Opus编码帧包含20ms音频数据。也就是每两个10ms音频采集帧编码为一个opus帧,喂给编码器两帧10ms数据,最后编码出一帧,这时编码帧时间戳增量为:2 * 采样率 / 100
,Opus编码器内部处理代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
AudioEncoder::EncodedInfo AudioEncoderOpusImpl::EncodeImpl() { // 记录input_buffer_第一帧的时间戳 if (input_buffer_.empty()) first_timestamp_in_buffer_ = rtp_timestamp; input_buffer_.insert(input_buffer_.end(), audio.cbegin(), audio.cend()); // 如果in_buffer大小不满足 2 * 10ms样本数 if (input_buffer_.size() < (Num10msFramesPerPacket() * SamplesPer10msFrame())) { return EncodedInfo(); } const size_t max_encoded_bytes = SufficientOutputBufferSize(); EncodedInfo info; info.encoded_bytes = encoded->AppendData( max_encoded_bytes, [&](rtc::ArrayView<uint8_t> encoded) { int status = WebRtcOpus_Encode( inst_, &input_buffer_[0], rtc::CheckedDivExact(input_buffer_.size(), config_.num_channels), rtc::saturated_cast<int16_t>(max_encoded_bytes), encoded.data()); return static_cast<size_t>(status); }); input_buffer_.clear(); // 送到RTP打包发送阶段的RTP时间戳 info.encoded_timestamp = first_timestamp_in_buffer_; } |
Opus编码完一帧后,会将此时的RTP时间戳通过EncodedInfo
返回给上层,然后送到RTP打包发送模块,对应处理代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
int32_t AudioCodingModuleImpl::Encode() { AudioEncoder::EncodedInfo encoded_info; encoded_info = encoder_stack_->Encode( rtp_timestamp, rtc::ArrayView<const int16_t>( input_data.audio, input_data.audio_channel * input_data.length_per_channel), &encode_buffer_); if (packetization_callback_) { // 送到打包发送模块的RTP时间戳:encoded_info.encoded_timestamp packetization_callback_->SendData( frame_type, encoded_info.payload_type, encoded_info.encoded_timestamp, encode_buffer_.data(), encode_buffer_.size(), absolute_capture_timestamp_ms.value_or(-1)); } } |
RTP打包发送阶段
跟视频RTP时间戳处理一样,也需要加上一个随机偏移:
1 2 3 4 5 6 7 8 9 10 |
int32_t ChannelSend::SendRtpAudio() { // 最后RTP包的时间戳:rtp_timestamp + rtp_rtcp_->StartTimestamp() if (!rtp_sender_audio_->SendAudio( frameType, payloadType, rtp_timestamp + rtp_rtcp_->StartTimestamp(), payload.data(), payload.size(), absolute_capture_timestamp_ms)) { RTC_DLOG(LS_ERROR) << "ChannelSend::SendData() failed to send data to RTP/RTCP module"; return -1; } } |
总结
RTP时间戳在组帧,抖动计算,音视频同步等起到了重要作用,通过本文相信对RTP时间戳有了更深刻认识。
参考
[1]RFC3550.https://www.rfc-editor.org/rfc/rfc3550.
[2]RFC3551.https://www.rfc-editor.org/rfc/rfc3551.
文章评论