1. 视频向音频同步
- 优点:逻辑简单,不需要记录开始播放的系统时间,只需要根据音频的每一帧的播放时间计算视频每一帧的播放时间即可。 当音频和视频都出现丢帧时,用户感知不明显。
- 缺点:没有音频时无法使用。
2. 视频向系统时钟同步
- 优点:没有音频时也可正常使用。
- 缺点:逻辑比较复杂,需要记录视频开始播放的时间,并计算每一帧解码后相对于开始播放的时间,将其与pts对比,大于pts需要延时,小于pts需要丢帧。 当App退后台后由于系统时钟不会停止,而连接可能会断开导致视频停止播放,因此需要在回前台时重置开始播放的时间与pts,重新对比。
音频部分代码
- (void)decodePacket:(AVPacket*)pkt {
if (_abort) {
return;
}
[[MGVideoPerformanceTool sharedManager] markDecodeStartIsVideo:NO];
if (pkt) {
int ret = avcodec_send_packet(codec_ctxt, pkt);
free(pkt->data);
av_packet_free(&pkt);
if (ret < 0) {
av_log(NULL, AV_LOG_ERROR, "audio avcodec_send_frame failed,ret=%d\n",ret);
if (ret != AVERROR(EAGAIN))
{
[[MGVideoPerformanceTool sharedManager] markDecodeEndIsVideo:NO displayResult:VideoDecodeFail];
return;
}
}
AVFrame *frame = av_frame_alloc();
while (!_abort) {
ret = avcodec_receive_frame(codec_ctxt, frame);
if (ret == 0) {
//获取音频的相对时间
AVRational timebase;
if (self.stream) {
timebase = self.stream->time_base;
self.videoContext.audio_pts_second = frame->pts * av_q2d(timebase);
}else{
timebase = codec_ctxt->time_base;
double durationPerPacket = (double)frame->nb_samples / (double)frame->sample_rate ;
self.videoContext.audio_pts_second += durationPerPacket;
}
printf("audio_pts_second:%f\n", self.videoContext.audio_pts_second);
// 声道数
int inChs = av_get_channel_layout_nb_channels(codec_ctxt->channel_layout);
if (inChs>1 || (codec_ctxt->sample_fmt != AV_SAMPLE_FMT_S16)) {
ret = [self resampleAudioFrame:frame ];
if (ret> 0) {
if (self.isWriteDataToFile) {
fwrite((const char *)frame->data[0], 1, frame->linesize[0], fp_pcm);
}
if (self.delegate && [self.delegate respondsToSelector:@selector(decoder:didDecodeAudioFrame:)]){
[self.delegate decoder:self didDecodeAudioFrame:frame];
}
}
// av_log(NULL, AV_LOG_INFO, "release outData memory\n");
if (frame->data[0]) {
av_freep(&(frame->data[0]));
}
}
else {
if (self.isWriteDataToFile) {
fwrite(frame->data[0],1,frame->linesize[0], fp_pcm);
}
if (self.delegate && [self.delegate respondsToSelector:@selector(decoder:didDecodeAudioFrame:)]){
[self.delegate decoder:self didDecodeAudioFrame:frame];
}
}
[[MGVideoPerformanceTool sharedManager] markDecodeEndIsVideo:NO displayResult:VideoNoFail];
}else if (AVERROR(EAGAIN)) {
break;
}else{
// _abort = 1;
// av_log(NULL, AV_LOG_ERROR, "avcodec_receive_frame failed,ret=%d\n",ret);
break;
}
}
av_frame_free(&frame);
}else{
av_log(NULL, AV_LOG_INFO, "decodeThread not got packet\n");
}
}
视频部分代码
- (void)decodePacket:(AVPacket*)pkt{
if (_abort){
av_log(NULL, AV_LOG_TRACE, "vidoe decoder is stoped, but still receive package\n");
return;
}
[[MGVideoPerformanceTool sharedManager] markDecodeStartIsVideo:YES];
if (pkt) {
av_log(NULL, AV_LOG_TRACE,"video packet pts =%llu\n", pkt->pts);
if (!_videoStartTime) {
_videoStartTime = [NSDate date];
}
int ret = avcodec_send_packet(codec_ctxt, pkt);
if (ret < 0) {
av_log(NULL, AV_LOG_ERROR, "video avcodec_send_frame failed,%s\n",av_err2str(ret));
if (ret != AVERROR(EAGAIN))
{
[[MGVideoPerformanceTool sharedManager] markDecodeEndIsVideo:YES displayResult:VideoDecodeFail];
if (self.delegate && [self.delegate respondsToSelector:@selector(decoder:didFailedToDecodePacket:)]) {
[self.delegate decoder:self didFailedToDecodePacket:pkt];
}
free(pkt->data);
av_packet_free(&pkt);
return;
}
}
free(pkt->data);
av_packet_free(&pkt);
int fps = self.stream ? av_q2d(self.stream->avg_frame_rate) : self.fps;
double frame_delay = 1.0 / fps;
AVFrame *frame = av_frame_alloc();
while (!_abort) {
ret = avcodec_receive_frame(codec_ctxt, frame);
self.frameNum ++;
if (ret == 0) {
if (self.isWriteDataToFile) {
[self recordFrameToFile:frame];
}
//判断当前帧是否有效,如果isValidFrame为false,则需要跳帧
BOOL isValidFrame = [self scheduleVideoFrame:frame fps:fps frame_delay:frame_delay];
if (!isValidFrame) {
NSLog(@"视频太慢,跳过该帧");
[[MGVideoPerformanceTool sharedManager] markDecodeEndIsVideo:YES displayResult:VideoSyncSkip];
continue;
}
//原始frame的数据(包括data、linesize和buffer)在切换线程后会被释放,所以这里需要增加引用计数来确保其不被释放
//如果是传递转换后的frame则不需要,因为outBuffer没有释放
CVPixelBufferRef buffer = [self convertFrameToPixelBuffer:frame];
dispatch_async(dispatch_get_main_queue(), ^{
if (buffer && self.delegate && [self.delegate respondsToSelector:@selector(decoder:didDecodeVideoBuffer:)]){
[self.delegate decoder:self didDecodeVideoBuffer:buffer];
}
if (buffer) {
CVPixelBufferRelease(buffer);
}
});
[[MGVideoPerformanceTool sharedManager] markDecodeEndIsVideo:YES displayResult:VideoNoFail];
continue;
}else if (AVERROR(EAGAIN)) {
break;
}else{
break;
}
}
av_frame_free(&frame);
}else{
av_log(NULL, AV_LOG_INFO, "decodeThread not got packet\n");
}
}
- (BOOL)scheduleVideoFrame:(AVFrame*)avFrame fps:(double)fps frame_delay:(double)frame_delay{
//获取当前画面的相对播放时间 , 相对 : 即从播放开始到现在的时间
// 该值大多数情况下 , 与 pts 值是相同的
// 该值比 pts 更加精准 , 参考了更多的信息
// 转换成秒 : 这里要注意 pts 需要转成 秒 , 需要乘以 time_base 时间单位
// 其中 av_q2d 是将 AVRational 转为 double 类型
double video_best_effort_timestamp_second;
if (self.stream) {
AVRational timebase = self.stream->time_base;
video_best_effort_timestamp_second = avFrame->best_effort_timestamp * av_q2d(timebase);
}else{
video_best_effort_timestamp_second = (double)avFrame->best_effort_timestamp /AV_TIME_BASE;
// video_best_effort_timestamp_second = frame_delay * pkt_num;
}
// printf("video packet Num:%d video_best_effort_timestamp_second:%f\n ",self.videoContext.video_packetNum,video_best_effort_timestamp_second);
//解码时 , 该值表示画面需要延迟多长时间在显示
// 需要使用该值 , 计算一个额外的延迟时间
// 这里按照文档中的注释 , 计算一个额外延迟时间
double extra_delay = avFrame->repeat_pict / ( fps * 2 );
//计算总的帧间隔时间 , 这是真实的间隔时间
double total_frame_delay = frame_delay + extra_delay;
//将 total_frame_delay ( 单位 : 秒 ) , 转换成 微秒值 , 乘以 100 万
unsigned microseconds_total_frame_delay = total_frame_delay * AV_TIME_BASE;
if(video_best_effort_timestamp_second == 0 ){
//如果播放的是第一帧 , 或者当前音频没有播放 , 就要正常播放
//休眠 , 单位微秒 , 控制 FPS 帧率
av_usleep(microseconds_total_frame_delay);
}else{
//如果不是第一帧 , 要开始考虑音视频同步问题了
double second_delta;
//优先视频向音频对齐,如果没有音频,则向系统时钟对齐
if (self.videoContext.syncMode == VideoSyncModeAudioClock) {
//音频的相对播放时间 , 这个是相对于播放开始的相对播放时间
double audio_pts_second = self.videoContext.audio_pts_second;
//使用视频相对时间 - 音频相对时间
second_delta = video_best_effort_timestamp_second - audio_pts_second;
// printf("差距:%f秒 ",second_delta);
}else{
//当前时间,videoStartTime是视频相对于播放开始的相对时间
// double currentTime = av_gettime_relative() / 1000000.0 - videoStartTime;
double currentTime = [[NSDate date] timeIntervalSinceDate:_videoStartTime];
// printf("currentTime:%f\n",currentTime);
//视频帧的时间
double pts = video_best_effort_timestamp_second;
//计算时间差,大于0则late,小于0则early。
second_delta = pts - currentTime;
printf("差距:%f秒 ",second_delta);
//没有音频的情况下,调整起始时间,确保下一帧播放时pts和currentTime是差不多的,减少后续丢帧
// if(![VideoContext sharedInstance].audio_pts_second && second_delta <0) {
// videoStartTime = [NSDate dateWithTimeInterval:-second_delta sinceDate:videoStartTime];
// }
}
//将相对时间转为 微秒单位
unsigned microseconds_delta = second_delta * AV_TIME_BASE;
//如果 second_delta 大于 0 , 说明视频播放时间比较长 , 视频比音频快或者比系统时钟快
//如果 second_delta 小于 0 , 说明视频播放时间比较短 , 视频比音频慢或者比系统时钟慢
if(second_delta > 0){
//视频快处理方案 : 增加休眠时间
//休眠 , 单位微秒 , 控制 FPS 帧率
if (second_delta > 0.1) second_delta = 0.1;
printf("视频太快,休眠%f秒,其中microseconds_delta为%f秒\n", (double)(microseconds_total_frame_delay + microseconds_delta)/1000000, (double)microseconds_delta/1000000 );
av_usleep(microseconds_total_frame_delay + microseconds_delta);
}else if(second_delta < 0){
//视频慢处理方案 :
// ① 方案 1 : 减小休眠时间 , 甚至不休眠
// ② 方案 2 : 视频帧积压太多了 , 这里需要将视频帧丢弃 ( 比方案 1 极端 )
if(fabs(second_delta) >= 2 * frame_delay){
//丢弃解码后的视频帧
//终止本次循环 , 继续下一次视频帧绘制
return false;
}else{
//如果音视频之间差距低于 0.05 秒 , 不操作 ( 50ms )
}
}
}
return true;
}
由于人对声音的停顿比视频感知更强,所以以上两种方法都是对视频做延迟或丢帧处理,而音频不做处理,接收到就播放。
为了减少数据发送过快导致播放太快的问题,发送端需要通过延迟或丢帧的方式来控制发送速率,确保每一帧都是40ms左右发送(fps=25)。
这里有个坑,就是音频和视频如果放在同一个线程里去发送,会造成互相影响(sleep线程)从而降低发送速率的问题,所以音视频需要分两个线程分开发送。
如果发送端没有做延迟处理(比如直接从文件中读取这种情况),则接收端需要通过数据包队列来缓存接收到的数据,并定时从队列中取数据进行解码播放。
关于定时读取,iOS上可以使用CADisplayLink来实现(设置preferredFramesPerSecond),但准确率并不是很高,更好的做法是单独开一个线程,通过sleep来控制。