關(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)同步播放:
從流程圖可以看出,實現(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.計算出視頻播放真正需要延遲的時間,視頻播放。