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實現暫停、快退快進播放html

準備工做

Android使用FFmpeg(三)--ffmpeg實現視頻播放
Android使用FFmpeg(五)--ffmpeg實現音頻播放(使用openSL ES進行播放)
同步原理介紹緩存

正文

依舊依照流程圖來逐步實現同步播放:安全

從流程圖能夠看出,實現同步播放須要三個線程,一個開啓解碼的裝置獲得packet線程,而後分別是播放音頻和視頻的線程。這篇簡書是以音頻播放爲基準來進行播放,也就是音頻一直不停的播放,視頻根據音頻播放來調整延遲時間。
1.開啓play線程,在這個線程中,註冊組件,獲得音視頻的解碼的裝置並將packet壓入隊列。這裏和前面的音視頻分開播放並無多大差異,也就多了一個隊列。ide

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.視頻播放線程,由於視頻是根據播放的時間來進行一個加速或者延遲播放的,因此對時間的計算是很重要的。ui

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);這段代碼中能夠看出,咱們真正須要的就是一個真正須要延遲時間,因此相比於單獨播放視頻,就是計算出真正須要延遲的時間。spa

小結

由於有單獨的音視頻播放做爲基礎,因此實現同步播放也不是很難,搞清楚如下幾點就行:
1.同步播放並無完美的同步播放,保持在一個合理的範圍便可;
2.由於人對聲音比較敏感,因此同步播放以音頻播放爲基礎,視頻播放以追趕或者延遲形式進行播放;
3.在音頻播放時計算出從第一幀開始的播放時間並矯正,用於視頻播放的延遲時間的計算;
4.計算出視頻播放真正須要延遲的時間,視頻播放。線程

相關文章
相關標籤/搜索