美文网首页
实时音视频同步总结

实时音视频同步总结

作者: FingerStyle | 来源:发表于2023-06-24 12:13 被阅读0次

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来控制。

相关文章

  • 实现实时同步备份总结

    实现实时同步备份总结 \ 一. 实时同步备份原理1.inotify实时监控2.rsync实时传输3.sersy...

  • day 35综合架构实时同步

    课程介绍部分 (补充扩展) 课程总结 一:网站实时同步服务 二.数据监控软件 inotify 三.实现实时同步数据...

  • Linux sersync day35

    什么是实时同步为什么要实时同步实时同步的原理实时同步的场景实时同步工具选择实时同步案例演示 一、什么是实时同步 实...

  • 备份服务之实时同步备份(sersync)

    实时同步备份方法1)利用脚本实现实时同步2)利用软件实现实时同步 实时同步备份原理 存储服务器 --...

  • 白板技术干货:在线教育平台如何保障课件数据安全

    上周,我们介绍了即构新推出的自研互动白板,依托成熟的亿级用户实时信令网络,即构互动白板具备“音视频实时同步、百人实...

  • 技术分享:教学白板跨国低时延互动技术实现指南

    上周,我们介绍了即构新推出的自研互动白板,依托成熟的亿级用户实时信令网络,即构互动白板具备“音视频实时同步、百人实...

  • FFmpeg命令行工具ffplay

    插播下音画同步的知识: 其中的音频为基准进行音视频同步: ffplay设置音视频同步方式: 播放封装好的音视频: ...

  • 音视频开源网站

    18个实时音视频开发中会用到开源项目 实时音视频的开发学习有很多可以参考的开源项目。一个实时音视频应用共包括几个环...

  • 综合架构实时同步详解

    day35 综合架构实时同步 课程介绍: 1.实时同步原理概念2.实现实时同步方式3.实现实时同步方式1)利用脚本...

  • WebRTC(四)流媒体传输技术

    WebRTC作为一个实时音视频传输技术,实时性是RTC技术的主要评判标准。在整个实时音视频系统中,对实时性影响最大...

网友评论

      本文标题:实时音视频同步总结

      本文链接:https://www.haomeiwen.com/subject/bvlbydtx.html