Android使用FFmpeg(六)--ffmpeg實現(xiàn)音視頻同步播放

關(guān)于

Android使用FFmpeg(一)--編譯ffmpeg
Android使用FFmpeg(二)--Android Studio配置ffmpeg
Android使用FFmpeg(三)--ffmpeg實現(xiàn)視頻播放
Android使用FFmpeg(四)--ffmpeg實現(xiàn)音頻播放(使用AudioTrack進(jìn)行播放)
Android使用FFmpeg(五)--ffmpeg實現(xiàn)音頻播放(使用openSL ES進(jìn)行播放)
Android使用FFmpeg(六)--ffmpeg實現(xiàn)音視頻同步播放
Android使用FFmpeg(七)--ffmpeg實現(xiàn)暫停、快退快進(jìn)播放

準(zhǔn)備工作

Android使用FFmpeg(三)--ffmpeg實現(xiàn)視頻播放
Android使用FFmpeg(五)--ffmpeg實現(xiàn)音頻播放(使用openSL ES進(jìn)行播放)
同步原理介紹

正文

依舊依照流程圖來逐步實現(xiàn)同步播放:


音視頻同步播放大概流程圖.png

從流程圖可以看出,實現(xiàn)同步播放需要三個線程,一個開啟解碼器得到packet線程,然后分別是播放音頻和視頻的線程。這篇簡書是以音頻播放為基準(zhǔn)來進(jìn)行播放,也就是音頻一直不停的播放,視頻根據(jù)音頻播放來調(diào)整延遲時間。
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.因為音頻播放就是一直播放,所以音頻播放和單獨(dú)的音頻播放并沒有多大差別,只是多了一個時間的記錄。其中,因為pakcet是開始壓入隊列,然后再從隊列中取出,當(dāng)注意到線程安全,此處使用生產(chǎn)者消費(fèi)者模式,也就是說當(dāng)隊列中有了pakcet過后才能從中取出,不然的話就等待。

//生產(chǎn)者
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("壓入一幀音頻數(shù)據(jù)  隊列%d ",queue.size());
//    給消費(fèi)者解鎖
    pthread_cond_signal(&cond);
    pthread_mutex_unlock(&mutex);
    return 1;
}
//消費(fèi)者
int FFmpegAudio::get(AVPacket *packet) {
    pthread_mutex_lock(&mutex);
    while (isPlay) {
        if (!queue.empty()) {
//            從隊列取出一個packet   clone一個 給入?yún)ο?            if (av_packet_ref(packet, queue.front())) {
                break;
            }
//            取成功了  彈出隊列  銷毀packet
            AVPacket *pkt = queue.front();
            queue.pop();
            LOGE("取出一 個音頻幀%d",queue.size());
            av_free(pkt);
            break;
        } else {
//            如果隊列里面沒有數(shù)據(jù)的話  一直等待阻塞
            pthread_cond_wait(&cond, &mutex);
        }
    }
    pthread_mutex_unlock(&mutex);
    return 0;
}
//時間計算
//回調(diào)函數(shù)
void bqPlayerCallback(SLAndroidSimpleBufferQueueItf bq, void *context){
    //得到pcm數(shù)據(jù)
    LOGE("回調(diào)pcm數(shù)據(jù)")
    FFmpegMusic *musicplay = (FFmpegMusic *) context;
    int datasize = getPcm(musicplay);
    if(datasize>0){
        //第一針?biāo)枰獣r間采樣字節(jié)/采樣率
        double time = datasize/(44100*2*2);
        //
        musicplay->clock=time+musicplay->clock;
        LOGE("當(dāng)前一幀聲音時間%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.視頻播放線程,因為視頻是根據(jù)播放的時間來進(jìn)行一個加速或者延遲播放的,所以對時間的計算是很重要的。

void *videoPlay(void *args){
    FFmpegVideo *ffmpegVideo = (FFmpegVideo *) args;
    //申請AVFrame
    AVFrame *frame = av_frame_alloc();//分配一個AVFrame結(jié)構(gòu)體,AVFrame結(jié)構(gòu)體一般用于存儲原始數(shù)據(jù),指向解碼后的原始幀
    AVFrame *rgb_frame = av_frame_alloc();//分配一個AVFrame結(jié)構(gòu)體,指向存放轉(zhuǎn)換成rgb后的幀
    AVPacket *packet = (AVPacket *) av_mallocz(sizeof(AVPacket));
    //輸出文件
    //FILE *fp = fopen(outputPath,"wb");


    //緩存區(qū)
    uint8_t  *out_buffer= (uint8_t *)av_mallocz(avpicture_get_size(AV_PIX_FMT_RGBA,
                                                                  ffmpegVideo->codec->width,ffmpegVideo->codec->height));
    //與緩存區(qū)相關(guān)聯(lián),設(shè)置rgb_frame緩存區(qū)
    avpicture_fill((AVPicture *)rgb_frame,out_buffer,AV_PIX_FMT_RGBA,ffmpegVideo->codec->width,ffmpegVideo->codec->height);


    LOGE("轉(zhuǎn)換成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             //當(dāng)前幀的播放時間
    ,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;
        }
        //轉(zhuǎn)換為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);這段代碼中可以看出,我們真正需要的就是一個真正需要延遲時間,所以相比于單獨(dú)播放視頻,就是計算出真正需要延遲的時間。

小結(jié)

因為有單獨(dú)的音視頻播放作為基礎(chǔ),所以實現(xiàn)同步播放也不是很難,搞清楚以下幾點就行:
1.同步播放并沒有完美的同步播放,保持在一個合理的范圍即可;
2.因為人對聲音比較敏感,所以同步播放以音頻播放為基礎(chǔ),視頻播放以追趕或者延遲形式進(jìn)行播放;
3.在音頻播放時計算出從第一幀開始的播放時間并矯正,用于視頻播放的延遲時間的計算;
4.計算出視頻播放真正需要延遲的時間,視頻播放。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

推薦閱讀更多精彩內(nèi)容

  • 教程一:視頻截圖(Tutorial 01: Making Screencaps) 首先我們需要了解視頻文件的一些基...
    90后的思維閱讀 4,754評論 0 3
  • 最近學(xué)習(xí)播放器的一些東西,所以接觸了ffmpeg,看源碼的過程中,就想了解一下ffplay是怎么處理音視頻同步的,...
    smm987閱讀 4,441評論 0 5
  • Android 自定義View的各種姿勢1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 173,124評論 25 708
  • 視頻是一幀一幀播放的,音頻也是一幀一幀(20ms)播放的。播放器都是按照它們的每一幀的PTS來作為參考進(jìn)行同步的。...
    TinyTina閱讀 4,287評論 0 6
  • 這個世界越來越浮躁了,我們只是小小的塵埃,在這個世界里越來越慌張。 不知從何時開始,心中莫名的悸動,再也靜不下心來...
    七月流殤閱讀 449評論 1 1