Android使用FFmpeg(六)--ffmpeg实现音视频同步播放

关于

Android使用FFmpeg(一)--编译ffmpeg
Android使用FFmpeg(二)--Android Studio配置ffmpeg
Android使用FFmpeg(三)--ffmpeg实现视频播放
Android使用FFmpeg(四)--ffmpeg实现音频播放(使用AudioTrack进行播放)
Android使用FFmpeg(五)--ffmpeg实现音频播放(使用openSL ES进行播放)
Android使用FFmpeg(六)--ffmpeg实现音视频同步播放
Android使用FFmpeg(七)--ffmpeg实现暂停、快退快进播放

准备工作

Android使用FFmpeg(三)--ffmpeg实现视频播放
Android使用FFmpeg(五)--ffmpeg实现音频播放(使用openSL ES进行播放)
同步原理介绍

正文

依旧依照流程图来逐步实现同步播放:


音视频同步播放大概流程图.png

从流程图可以看出,实现同步播放需要三个线程,一个开启解码器得到packet线程,然后分别是播放音频和视频的线程。这篇简书是以音频播放为基准来进行播放,也就是音频一直不停的播放,视频根据音频播放来调整延迟时间。
1.开启play线程,在这个线程中,注册组件,得到音视频的解码器并将packet压入队列。这里和前面的音视频分开播放并没有多大差别,也就多了一个队列。

void *begin(void *args) {
    LOGE("开启解码线程")
    //1.注册组件
    av_register_all();
    avformat_network_init();
    //封装格式上下文
    AVFormatContext *pFormatCtx = avformat_alloc_context();

    //2.打开输入视频文件
    if (avformat_open_input(&pFormatCtx, inputPath, NULL, NULL) != 0) {
        LOGE("%s", "打开输入视频文件失败");
    }
    //3.获取视频信息
    if (avformat_find_stream_info(pFormatCtx, NULL) < 0) {
        LOGE("%s", "获取视频信息失败");
    }
    //找到视频流和音频流
    int i=0;
    for (int i = 0; i < pFormatCtx->nb_streams; ++i) {
        //获取解码器
        AVCodecContext *avCodecContext = pFormatCtx->streams[i]->codec;
        AVCodec *avCodec = avcodec_find_decoder(avCodecContext->codec_id);

        //copy一个解码器,
        AVCodecContext *codecContext = avcodec_alloc_context3(avCodec);
        avcodec_copy_context(codecContext,avCodecContext);
        if(avcodec_open2(codecContext,avCodec,NULL)<0){
            LOGE("打开失败")
            continue;
        }
        //如果是视频流
        if(pFormatCtx->streams[i]->codec->codec_type==AVMEDIA_TYPE_VIDEO){
            ffmpegVideo->index=i;
            ffmpegVideo->setAvCodecContext(codecContext);
            ffmpegVideo->time_base=pFormatCtx->streams[i]->time_base;
            if (window) {
                ANativeWindow_setBuffersGeometry(window, ffmpegVideo->codec->width, ffmpegVideo->codec->height,
                                                 WINDOW_FORMAT_RGBA_8888);
            }
        }//如果是音频流
        else if(pFormatCtx->streams[i]->codec->codec_type==AVMEDIA_TYPE_AUDIO){
            ffmpegMusic->index=i;
            ffmpegMusic->setAvCodecContext(codecContext);
            ffmpegMusic->time_base=pFormatCtx->streams[i]->time_base;
        }
    }
//开启播放
    ffmpegVideo->setFFmepegMusic(ffmpegMusic);
    ffmpegMusic->play();
    ffmpegVideo->play();
    isPlay=1;
    //读取packet,并压入队列中
    AVPacket *packet = (AVPacket *)av_mallocz(sizeof(AVPacket));
    int ret;
    while (isPlay){
        ret = av_read_frame(pFormatCtx,packet);
        if(ret ==0){
            if(ffmpegVideo&&ffmpegVideo->isPlay&&packet->stream_index==ffmpegVideo->index){
                //将视频packet压入队列
               ffmpegVideo->put(packet);
            } else if(ffmpegMusic&&ffmpegMusic->isPlay&&packet->stream_index==ffmpegMusic->index){
                ffmpegMusic->put(packet);
            }
            av_packet_unref(packet);
        } else if(ret ==AVERROR_EOF){
 while (isPlay) {
                if (vedio->queue.empty() && audio->queue.empty()) {
                    break;
                }
//                LOGI("等待播放完成");
                av_usleep(10000);
            }
        }
    }

    //读取完过后可能还没有播放完
    isPlay=0;
    if(ffmpegMusic && ffmpegMusic->isPlay){
        ffmpegMusic->stop();
    }
    if(ffmpegVideo && ffmpegVideo->isPlay){
        ffmpegVideo->stop();
    }
    //释放
    av_free_packet(packet);
    avformat_free_context(pFormatCtx);
}

2.因为音频播放就是一直播放,所以音频播放和单独的音频播放并没有多大差别,只是多了一个时间的记录。其中,因为pakcet是开始压入队列,然后再从队列中取出,当注意到线程安全,此处使用生产者消费者模式,也就是说当队列中有了pakcet过后才能从中取出,不然的话就等待。

//生产者
int FFmpegAudio::put(AVPacket *packet) {
    AVPacket *packet1 = (AVPacket *) av_mallocz(sizeof(AVPacket));
    if (av_packet_ref(packet1, packet)) {
//        克隆失败
        return 0;
    }
    pthread_mutex_lock(&mutex);
    queue.push(packet1);
    LOGE("压入一帧音频数据  队列%d ",queue.size());
//    给消费者解锁
    pthread_cond_signal(&cond);
    pthread_mutex_unlock(&mutex);
    return 1;
}
//消费者
int FFmpegAudio::get(AVPacket *packet) {
    pthread_mutex_lock(&mutex);
    while (isPlay) {
        if (!queue.empty()) {
//            从队列取出一个packet   clone一个 给入参对象
            if (av_packet_ref(packet, queue.front())) {
                break;
            }
//            取成功了  弹出队列  销毁packet
            AVPacket *pkt = queue.front();
            queue.pop();
            LOGE("取出一 个音频帧%d",queue.size());
            av_free(pkt);
            break;
        } else {
//            如果队列里面没有数据的话  一直等待阻塞
            pthread_cond_wait(&cond, &mutex);
        }
    }
    pthread_mutex_unlock(&mutex);
    return 0;
}
//时间计算
//回调函数
void bqPlayerCallback(SLAndroidSimpleBufferQueueItf bq, void *context){
    //得到pcm数据
    LOGE("回调pcm数据")
    FFmpegMusic *musicplay = (FFmpegMusic *) context;
    int datasize = getPcm(musicplay);
    if(datasize>0){
        //第一针所需要时间采样字节/采样率
        double time = datasize/(44100*2*2);
        //
        musicplay->clock=time+musicplay->clock;
        LOGE("当前一帧声音时间%f   播放时间%f",time,musicplay->clock);
        (*bq)->Enqueue(bq,musicplay->out_buffer,datasize);
        LOGE("播放 %d ",musicplay->queue.size());
    }
}
//时间矫正
        if (avPacket->pts != AV_NOPTS_VALUE) {
            agrs->clock = av_q2d(agrs->time_base) * avPacket->pts;
        }

3.视频播放线程,因为视频是根据播放的时间来进行一个加速或者延迟播放的,所以对时间的计算是很重要的。

void *videoPlay(void *args){
    FFmpegVideo *ffmpegVideo = (FFmpegVideo *) args;
    //申请AVFrame
    AVFrame *frame = av_frame_alloc();//分配一个AVFrame结构体,AVFrame结构体一般用于存储原始数据,指向解码后的原始帧
    AVFrame *rgb_frame = av_frame_alloc();//分配一个AVFrame结构体,指向存放转换成rgb后的帧
    AVPacket *packet = (AVPacket *) av_mallocz(sizeof(AVPacket));
    //输出文件
    //FILE *fp = fopen(outputPath,"wb");


    //缓存区
    uint8_t  *out_buffer= (uint8_t *)av_mallocz(avpicture_get_size(AV_PIX_FMT_RGBA,
                                                                  ffmpegVideo->codec->width,ffmpegVideo->codec->height));
    //与缓存区相关联,设置rgb_frame缓存区
    avpicture_fill((AVPicture *)rgb_frame,out_buffer,AV_PIX_FMT_RGBA,ffmpegVideo->codec->width,ffmpegVideo->codec->height);


    LOGE("转换成rgba格式")
    ffmpegVideo->swsContext = sws_getContext(ffmpegVideo->codec->width,ffmpegVideo->codec->height,ffmpegVideo->codec->pix_fmt,
                                            ffmpegVideo->codec->width,ffmpegVideo->codec->height,AV_PIX_FMT_RGBA,
                                            SWS_BICUBIC,NULL,NULL,NULL);



    LOGE("LC XXXXX  %f",ffmpegVideo->codec);

    double  last_play  //上一帧的播放时间
    ,play             //当前帧的播放时间
    ,last_delay    // 上一次播放视频的两帧视频间隔时间
    ,delay         //两帧视频间隔时间
    ,audio_clock //音频轨道 实际播放时间
    ,diff   //音频帧与视频帧相差时间
    ,sync_threshold
    ,start_time  //从第一帧开始的绝对时间
    ,pts
    ,actual_delay//真正需要延迟时间
    ;

    //两帧间隔合理间隔时间
    start_time = av_gettime() / 1000000.0;
    int frameCount;
    int h =0;
    LOGE("解码 ")
    while (ffmpegVideo->isPlay) {
        ffmpegVideo->get(packet);
        LOGE("解码 %d",packet->stream_index)
        avcodec_decode_video2(ffmpegVideo->codec, frame, &frameCount, packet);
        if(!frameCount){
            continue;
        }
        //转换为rgb格式
        sws_scale(ffmpegVideo->swsContext,(const uint8_t *const *)frame->data,frame->linesize,0,
                  frame->height,rgb_frame->data,
                  rgb_frame->linesize);
        LOGE("frame 宽%d,高%d",frame->width,frame->height);
        LOGE("rgb格式 宽%d,高%d",rgb_frame->width,rgb_frame->height);

        if((pts=av_frame_get_best_effort_timestamp(frame))==AV_NOPTS_VALUE){
            pts=0;
        }

        play = pts*av_q2d(ffmpegVideo->time_base);
        //纠正时间
        play = ffmpegVideo->synchronize(frame,play);
        delay = play - last_play;
        if (delay <= 0 || delay > 1) {
            delay = last_delay;
        }
        audio_clock = ffmpegVideo->ffmpegMusic->clock;
        last_delay = delay;
        last_play = play;
//音频与视频的时间差
        diff = ffmpegVideo->clock - audio_clock;
//        在合理范围外  才会延迟  加快
        sync_threshold = (delay > 0.01 ? 0.01 : delay);

        if (fabs(diff) < 10) {
            if (diff <= -sync_threshold) {
                delay = 0;
            } else if (diff >=sync_threshold) {
                delay = 2 * delay;
            }
        }
        start_time += delay;
        actual_delay=start_time-av_gettime()/1000000.0;
        if (actual_delay < 0.01) {
            actual_delay = 0.01;
        }
        av_usleep(actual_delay*1000000.0+6000);
        LOGE("播放视频")
        video_call(rgb_frame);
//        av_packet_unref(packet);
//        av_frame_unref(rgb_frame);
//        av_frame_unref(frame);
    }
    LOGE("free packet");
    av_free(packet);
    LOGE("free packet ok");
    LOGE("free packet");
    av_frame_free(&frame);
    av_frame_free(&rgb_frame);
    sws_freeContext(ffmpegVideo->swsContext);
    size_t size = ffmpegVideo->queue.size();
    for (int i = 0; i < size; ++i) {
        AVPacket *pkt = ffmpegVideo->queue.front();
        av_free(pkt);
        ffmpegVideo->queue.pop();
    }
    LOGE("VIDEO EXIT");
    pthread_exit(0);
    }

从av_usleep(actual_delay*1000000.0+6000);这段代码中可以看出,我们真正需要的就是一个真正需要延迟时间,所以相比于单独播放视频,就是计算出真正需要延迟的时间。

小结

因为有单独的音视频播放作为基础,所以实现同步播放也不是很难,搞清楚以下几点就行:
1.同步播放并没有完美的同步播放,保持在一个合理的范围即可;
2.因为人对声音比较敏感,所以同步播放以音频播放为基础,视频播放以追赶或者延迟形式进行播放;
3.在音频播放时计算出从第一帧开始的播放时间并矫正,用于视频播放的延迟时间的计算;
4.计算出视频播放真正需要延迟的时间,视频播放。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,271评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,275评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,151评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,550评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,553评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,559评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,924评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,580评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,826评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,578评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,661评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,363评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,940评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,926评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,156评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,872评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,391评论 2 342

推荐阅读更多精彩内容

  • 教程一:视频截图(Tutorial 01: Making Screencaps) 首先我们需要了解视频文件的一些基...
    90后的思维阅读 4,648评论 0 3
  • 最近学习播放器的一些东西,所以接触了ffmpeg,看源码的过程中,就想了解一下ffplay是怎么处理音视频同步的,...
    smm987阅读 4,390评论 0 5
  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 171,389评论 25 707
  • 视频是一帧一帧播放的,音频也是一帧一帧(20ms)播放的。播放器都是按照它们的每一帧的PTS来作为参考进行同步的。...
    TinyTina阅读 4,146评论 0 6
  • 这个世界越来越浮躁了,我们只是小小的尘埃,在这个世界里越来越慌张。 不知从何时开始,心中莫名的悸动,再也静不下心来...
    七月流殇阅读 435评论 1 1