1. 程式人生 > >Android 音視頻深入 十八 FFmpeg播放視頻,有聲音(附源碼下載)

Android 音視頻深入 十八 FFmpeg播放視頻,有聲音(附源碼下載)

音視頻 Android FFmpeg

項目地址
https://github.com/979451341/AudioVideoStudyCodeTwo/tree/master/FFmpegv%E6%92%AD%E6%94%BE%E8%A7%86%E9%A2%91%E6%9C%89%E5%A3%B0%E9%9F%B3%EF%BC%8C%E6%9A%82%E5%81%9C%EF%BC%8C%E9%87%8A%E6%94%BE%E3%80%81%E5%BF%AB%E8%BF%9B%E3%80%81%E9%80%80%E5%90%8E

這個項目是簡書2012lc大神寫的,播放沒問題就是其他功能都有點卡過頭了。。。
哎,自己也沒能寫出一個優秀的播放器,

回到正題

首先這個代碼是生產者和消費者的模式,生成者就是不斷地解碼mp4將一幀的數據給消費者,消費者就是音頻播放類和視頻播放類,也就說生成者一個,消費者兩個,都是通過pthread開啟線程,通過互斥鎖和條件信息來維持這個關系鏈

1.生產者—輸出一幀幀的數據

開始就是初始化各類組件和測試視頻文件是否能夠打開,並獲得視頻相關信息為後來代碼做準備工作

void init() {
LOGE("開啟解碼線程")
//1.註冊組件
av_register_all();
avformat_network_init();
//封裝格式上下文
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", "獲取視頻信息失敗");
}

//得到播放總時間
if (pFormatCtx->duration != AV_NOPTS_VALUE) {
    duration = pFormatCtx->duration;//微秒
}

}

初始化音頻類和視頻類,並將SurfaceView給視頻類

ffmpegVideo = new FFmpegVideo;
ffmpegMusic = new FFmpegMusic;
ffmpegVideo->setPlayCall(call_video_play);

開啟生成者線程

pthread_create(&p_tid, NULL, begin, NULL);//開啟begin線程

從視頻信息裏獲取視屏流和音頻流,將各自的×××上下文復制分別給與兩個消費者類,並將流在哪個位置、還有時間單位給與兩個消費者類

//找到視頻流和音頻流
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();

然後開始一幀一幀的解碼出數據給兩個消費者類的用來存儲數據的矢量,如果矢量裏的數據還有那就沒有播放玩,繼續播放

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 (ffmpegVideo->queue.empty() && ffmpegMusic->queue.empty()) {
                break;
            }
            // LOGE("等待播放完成");
            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);
pthread_exit(0);

2.消費者—音頻類

開啟線程

  pthread_create(&playId, NULL, MusicPlay, this);//開啟begin線程

就下來就是配置OpenSL ES來播放音頻,而這個數據的來源是這一段代碼決定的

(*bqPlayerBufferQueue)->RegisterCallback(bqPlayerBufferQueue, bqPlayerCallback, this);

我們再來看看bqPlayerCallback,數據是從getPcm函數得到的

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());
}

然後這個getPcm函數裏,通過get函數來完成獲取一幀數據

agrs->get(avPacket);

如果有矢量裏有數據它將矢量裏的數據取出,如果沒有就等待生產者通過條件變量

//將packet彈出隊列
int FFmpegMusic::get(AVPacket avPacket) {
LOGE("取出隊列")
pthread_mutex_lock(&mutex);
while (isPlay){
LOGE("取出對壘 xxxxxx")
if(!queue.empty()&&isPause){
LOGE("ispause %d",isPause);
//如果隊列中有數據可以拿出來
if(av_packet_ref(avPacket,queue.front())){
break;
}
//取成功了,彈出隊列,銷毀packet
AVPacket
packet2 = queue.front();
queue.erase(queue.begin());
av_free(packet2);
break;
} else{
LOGE("音頻執行wait")
LOGE("ispause %d",isPause);
pthread_cond_wait(&cond,&mutex);

    }
}
pthread_mutex_unlock(&mutex);
return 0;

}

註意這個獲取的數據是AVPacket,我們需要將他解碼為AVFrame才行

    if (avPacket->pts != AV_NOPTS_VALUE) {
        agrs->clock = av_q2d(agrs->time_base) * avPacket->pts;
    }
    //            解碼  mp3   編碼格式frame----pcm   frame
    LOGE("解碼")
    avcodec_decode_audio4(agrs->codec, avFrame, &gotframe, avPacket);
    if (gotframe) {

        swr_convert(agrs->swrContext, &agrs->out_buffer, 44100 * 2, (const uint8_t **) avFrame->data, avFrame->nb_samples);

// 緩沖區的大小
size = av_samples_get_buffer_size(NULL, agrs->out_channer_nb, avFrame->nb_samples,
AV_SAMPLE_FMT_S16, 1);
break;
}

回到OpenSL ES的回調函數,取到數據後將數據壓入播放器裏讓他播放

    //第一針所需要時間采樣字節/采樣率
    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());

3.消費者—視頻類

這兩者的運行過程很像,我這裏就省略的說說

開啟線程

//申請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);

獲取一幀數據

ffmpegVideo->get(packet);

然後從矢量裏得到數據

調節視頻和音頻的播放速度

    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);

播放視頻

video_call(rgb_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.erase(ffmpegVideo->queue.begin());
}
LOGE("VIDEO EXIT");
pthread_exit(0);

結束了,以後哎,盡量自己寫出一個播放器,要那種暫停不卡的

Android 音視頻深入 十八 FFmpeg播放視頻,有聲音(附源碼下載)