Android直播APP源码开发是如何实现音视频同步的(二)

时间:2024-03-15 18:14:30

三、NuPlayer 的 avsync 逻辑梳理

下面的时序图简单的展示了一下NuPlayer在音视频同步这块的基本流程: 

Android直播APP源码开发是如何实现音视频同步的(二)

NuPlayerDecoder 拿到解码后的音视频数据后 queueBuffer给NuPlayerRenderer,在NuPlayerRenderer中通过postDrainAudioQueue_l方法调用AudioSink进行写入,并且获取“Audio当前播放的时间”,可以看到这里也调用了AudioTrack的getTimeStamp和getPosition方法,和ExoPlayer中类似,同时会利用MediaClock类记录一些锚点时间戳变量。NuPlayerRenderer中调用postDrainVideoQueue方法对video数据进行处理,包括计算实际送显时间,利用vsync信号调整送显时间等,这里的调整是利用VideoFrameScheduler类完成的。需要注意的是,实际上NuPlayerRenderer方法中只进行了avsync的调整,真正的播放还要通过onRendereBuffer调用到NuPlayerDecoder中,进而调用MediaCodec的release方法进行播放。

Video部分

1、利用pts和系统时间计算realTimeUs

NuPlayer::Renderer::postDrainVideoQueue
int64_t nowUs = ALooper::GetNowUs();
BufferItem *bufferItem = &*mBufferItems.begin();
int64_t itemMediaUs = bufferItem->mTimestamp / 1000;
//这里就是调用MediaClock的getRealTimeFor方法,得到“视频帧应该显示的时间”
int64_t itemRealUs = getRealTime(itemMediaUs, nowUs);
realTimeUs = PTS - nowMediaUs + nowUs = PTS - (mAnchorTimeMediaUs + (nowUs - mAnchorTimeRealUs)) + nowUs 

mAnchorTimeMediaUs代表锚点媒体时间戳,可以理解为最开始播放的时候记录下来的第一个媒体时间戳。
mAnchorTimeRealUs代表锚点real系统时间戳。

nowUs - mAnchorTimeRealUs即为从开始播放到现在,系统时间经过了多久。
再加上mAnchorTimeMediaUs,即为“在当前系统时间下,对应的媒体时间戳”
用PTS减去这个时间,表示“还有多久该播放这一帧”
最后再加上一个系统时间,即为这一帧应该显示的时间。

2、利用vsync信号调整realTimeUs

NuPlayer::Renderer::postDrainVideoQueue realTimeUs = mVideoScheduler->schedule(realTimeUs * 1000) / 1000;

schedule方法非常复杂,我们难以完全理解,但也看到了计算ns精度的视频帧间隔的代码,这也与exoplayer的做法相同。

3、提前2倍vsync duration进行render

Android直播APP源码开发是如何实现音视频同步的(二)

NuPlayer::Renderer::postDrainVideoQueue
//2倍vsync duration
int64_t twoVsyncsUs = 2 * (mVideoScheduler->getVsyncPeriod() / 1000);
//利用调整后的realTimeUs再计算一次“还有多久该播放这一帧”
delayUs = realTimeUs - nowUs;
// post 2 display refreshes before rendering is due
//如果delayUs大于两倍vsync duration,则延迟到“距离显示时间两倍vsync duration之前的时间点”再发消息进入后面的流程,否则立即走后面的流程
msg->post(delayUs > twoVsyncsUs ? delayUs - twoVsyncsUs : 0);

Android直播APP源码开发是如何实现音视频同步的(二)

4、丢帧与送显

Android直播APP源码开发是如何实现音视频同步的(二)

NuPlayer::Renderer::onDrainVideoQueue

//取出pts值
CHECK(entry->mBuffer->meta()->findInt64("timeUs", &mediaTimeUs));
nowUs = ALooper::GetNowUs();
//考虑到中间发消息等等会有耗时,所以这里重新利用pts计算一次realTimeUs
realTimeUs = getRealTimeUs(mediaTimeUs, nowUs);
//如果nowUs>realTimeUs,即代表视频帧来晚了
setVideoLateByUs(nowUs - realTimeUs);
//如果晚了40ms,即认为超过了门限值
tooLate = (mVideoLateByUs > 40000);
//把realTimeUs赋值给timestampNs,通过消息发出去
entry->mNotifyConsumed->setInt64("timestampNs", realTimeUs * 1000ll);

Android直播APP源码开发是如何实现音视频同步的(二)

5、视频部分总结

a). 计算video送显时间的核心公式如下

realTimeUs = PTS - nowMediaUs + nowUs = PTS - (mAnchorTimeMediaUs + (nowUs - mAnchorTimeRealUs)) + nowUs

b). 比较exoplayer和nuplayer

相同点包括
a.都是比较系统时间与视频帧的送显时间来判断要不要丢帧,丢帧门限值固定为40ms;
b.都会在计算送显时间时考虑函数调用与消息传递的耗时;
c.计算送显在计算送显时间时,都利用到了vsync信号来对送显时间进行校准

差异:
a.nuplayer会在最开始的时候就先确保音视频保持基本范围的同步
b.nuplayer中会有一个提前两倍vsync时间开始执行releaseOutputbuffer的逻辑,这一点与API注释中的描述一致

Audio部分

1、初始pts的纠正

Android直播APP源码开发是如何实现音视频同步的(二)

NuPlayer::Renderer::onQueueBuffer
int64_t diff = firstVideoTimeUs - firstAudioTimeUs;
...
if (diff > 100000ll) {
// Audio data starts More than 0.1 secs before video.
// Drop some audio.
// 这里是对音视频的第一个pts做一下纠正,保证一开始两者是同步的,但是这里只是考虑了audio提前的情况,而没有考虑video提前的情况
(*mAudioQueue.begin()).mNotifyConsumed->post();
mAudioQueue.erase(mAudioQueue.begin());
return;
}

Android直播APP源码开发是如何实现音视频同步的(二)

2、利用pts更新几个锚点变量 

NuPlayer::Renderer::onDrainAudioQueue

//pts减去“还没播放的时间”,就是当前已经播放的时间,即playedDuration,将其设置为nowMediaUs
int64_t nowMediaUs = mediaTimeUs - getPendingAudioPlayoutDurationUs(nowUs);

Android直播APP源码开发是如何实现音视频同步的(二)

//计算“还没播放的时间”
//计算writtenFrames对应的duration
//writtenDuration = writtenFrames/sampleRate
int64_t writtenAudioDurationUs = 
getDurationUsIfPlayedAtSampleRate(mNumFramesWritten);
//用wriitenDuration - playedDuration,即为“还没播出的时长pendingPlayDuration”
return writtenAudioDurationUs - getPlayedOutAudioDurationUs(nowUs);=

Android直播APP源码开发是如何实现音视频同步的(二)

Android直播APP源码开发是如何实现音视频同步的(二)

//计算playedDuration – 使用getTimeStamp方法
status_t res = mAudioSink->getTimestamp(ts);
//当前播放的framePosition
numFramesPlayed = ts.mPosition;
//framePosition对应的系统时间
numFramesPlayedAt =
ts.mTime.tv_sec * 1000000LL + ts.mTime.tv_nsec / 1000;
int64_t durationUs = getDurationUsIfPlayedAtSampleRate(numFramesPlayed)
+ nowUs - numFramesPlayedAt;

Android直播APP源码开发是如何实现音视频同步的(二)

这里可以说是avsync的核心逻辑了
来简单说说这几个变量,numFramesPlayed代表“从底层获取到的已播放帧数”,需要注意的是,这个并不一定是当前系统时间下已经播放的实时帧数,而numFramesPlayedAt代表“numFramesPlayed对应的系统时间”,所以
durationUs = numFramesPlayed/sampleRate +nowUs - numFramesPlayedAt才是当前系统时间下已经播放的音频时长

Android直播APP源码开发是如何实现音视频同步的(二)

//计算playedDuration – 使用getPosition方法
//与exoplayer中的逻辑一样,如果getTimestamp用不了,再走getposition流程
res = mAudioSink->getPosition(&numFramesPlayed);
numFramesPlayedAt = nowUs;
//当前系统时间加上latency才是真正playedOut的时间,这里取了latency/2,可以看做是一种平均,因为latency方法返回值可能并不准
numFramesPlayedAt += 1000LL * mAudioSink->latency() / 2; 
int64_t durationUs = getDurationUsIfPlayedAtSampleRate(numFramesPlayed)
+ nowUs - numFramesPlayedAt;

Android直播APP源码开发是如何实现音视频同步的(二)

//利用当前系统时间,当前播放的媒体时间戳,pts,更新锚点
mMediaClock->updateAnchor(nowMediaUs, nowUs, mediaTimeUs);

Android直播APP源码开发是如何实现音视频同步的(二)

void MediaClock::updateAnchor(
int64_t anchorTimeMediaUs,
int64_t anchorTimeRealUs,
int64_t maxTimeMediaUs) {
…
int64_t nowUs = ALooper::GetNowUs();
int64_t nowMediaUs =
anchorTimeMediaUs + (nowUs - anchorTimeRealUs) * (double)mPlaybackRate;
...
mAnchorTimeRealUs = nowUs;
mAnchorTimeMediaUs = nowMediaUs;
}

Android直播APP源码开发是如何实现音视频同步的(二)

3、音频部分总结

整个逻辑核心的公式就是如何计算已经播出的audio时长:

durationUs = numFramesPlayed/sampleRate +nowUs - numFramesPlayedAt

与exoplayer一样,可以通过getTimeStamp或者getPosition方法来获取

不同的地方有几点:首先是调用getTimeStamp的间隔不同,exoplayer中是500ms间隔,
而nuplayer中的间隔是pendingPlayedOutDuration/2,没有取定值;
其次是调用getPosition方法时,加上的是latency/2。

至于那些锚点变量的计算,看似复杂,其中心思想也大同小异。 

四、MediaSync的使用与原理

MedaiSync是android M新加入的API,可以帮助应用视音频的同步播放。

1、MediaSync的基本用法

第一步:初始化MediaSync, 初始化mediaCodec和AudioTrack, 将AudioTrack和surface传给MeidaSync

Android直播APP源码开发是如何实现音视频同步的(二)

MediaSync sync = new MediaSync();
sync.setSurface(surface);
Surface inputSurface = sync.createInputSurface();
...
// MediaCodec videoDecoder = ...;
videoDecoder.configure(format, inputSurface, ...);
...
sync.setAudioTrack(audioTrack);

Android直播APP源码开发是如何实现音视频同步的(二)

第二步: MediaSync只会对audiobuffer做操作,一个是代表写入的queueAudio方法,一个是代表写完了的回调方法,也就是下面的

Android直播APP源码开发是如何实现音视频同步的(二)

onAudioBufferConsumed
sync.setCallback(new MediaSync.Callback() {
@Override
public void onAudioBufferConsumed(MediaSync sync, ByteBuffer audioBuffer, int bufferId) {
...
}
}, null);

Android直播APP源码开发是如何实现音视频同步的(二)

第三步:设置播放速度

// This needs to be done since sync is paused on creation.
sync.setPlaybackParams(new PlaybackParams().setSpeed(1.f));

第四步:开始流转音视频buffer,这里就和MediaCodec的基本调用流程一样了,当拿到audioBuffer后,通过queueAudio将buffer给MediaSync,在对应的回调方法中release播放出去,至于video部分,直接releaseOutputBuffer即可

Android直播APP源码开发是如何实现音视频同步的(二)

for (;;) {
...
// send video frames to surface for rendering, e.g., call
videoDecoder.releaseOutputBuffer(videoOutputBufferIx,videoPresentationTimeNs);
...
sync.queueAudio(audioByteBuffer, bufferId, audioPresentationTimeUs); // non-blocking.
// The audioByteBuffer and bufferId will be returned via callback.
}

Android直播APP源码开发是如何实现音视频同步的(二)

第五步:播放完毕

sync.setPlaybackParams(new PlaybackParams().setSpeed(0.f));
sync.release();
sync = null;

如果用的是MediaCodec的异步流程,如下,通过下面的代码可以更好的理解video buffer和audio buffer分别是怎么处理的

Android直播APP源码开发是如何实现音视频同步的(二)

onOutputBufferAvailable(MediaCodec codec, int bufferId, BufferInfo info) {
// ...
if (codec == videoDecoder) {
codec.releaseOutputBuffer(bufferId, 1000 * info.presentationTime);
} else {
ByteBuffer audioByteBuffer = codec.getOutputBuffer(bufferId);
sync.queueAudio(audioByteBuffer, bufferId, info.presentationTime);
}
// ...
}
onAudioBufferConsumed(MediaSync sync, ByteBuffer buffer, int bufferId) {
// ...
audioDecoder.releaseBuffer(bufferId, false);
// ...
}

Android直播APP源码开发是如何实现音视频同步的(二)

2、MediaSync的关键变量与方法

SyncParams:Android M新加入的API,用于控制AV同步的方法,具体包括:

1)、倍速播放时如何处理audio

Android直播APP源码开发是如何实现音视频同步的(二)

int AUDIO_ADJUST_MODE_DEFAULT
System will determine best handling of audio for playback rate adjustments.
Used by default. This will make audio play faster or slower as required by the sync source without changing its pitch; however, system may fall back to some other method (e.g. change the pitch, or mute the audio) if time stretching is no longer supported for the playback rate.

int AUDIO_ADJUST_MODE_RESAMPLE
Resample audio when playback rate must be adjusted.
This will make audio play faster or slower as required by the sync source by changing its pitch (making it lower to play slower, and higher to play faster.)

int AUDIO_ADJUST_MODE_STRETCH
Time stretch audio when playback rate must be adjusted.
This will make audio play faster or slower as required by the sync source without changing its pitch, as long as it is supported for the playback rate.

Android直播APP源码开发是如何实现音视频同步的(二)

2)、选择avsync的基准

Android直播APP源码开发是如何实现音视频同步的(二)

int SYNC_SOURCE_AUDIO
Use audio track for sync source. This requires audio data and an audio track.

int SYNC_SOURCE_DEFAULT
Use the default sync source (default). If media has video, the sync renders to a surface that directly renders to a display, and tolerance is non zero (e.g. not less than 0.001) vsync source is used for clock source. Otherwise, if media has audio, audio track is used. Finally, if media has no audio, system clock is used.

int SYNC_SOURCE_SYSTEM_CLOCK
Use system monotonic clock for sync source.

int SYNC_SOURCE_VSYNC
Use vsync as the sync source. This requires video data and an output surface that directly renders to the display, e.g. SurfaceView

Android直播APP源码开发是如何实现音视频同步的(二)

PlaybackParams Android M 新加入的API,主要用于控制倍速播放

get & setPlaybackParams (PlaybackParams params)
Gets and Sets the playback rate using PlaybackParams.

MediaTimestamp Android M新加入的API

MediaTimestamp getTimestamp ()
Get current playback position.

五、提高&升华

尝试开发一个音视频同步的播放器