標籤:音視頻 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.消費者—視頻類
這兩者的運行過程很像,我這裡就省略的說說
開啟線程
//申請AVFrameAVFrame *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播放視頻,有聲音(附源碼下載)