zoukankan      html  css  js  c++  java
  • 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);

    结束了,以后哎,尽量自己写出一个播放器,要那种暂停不卡的

  • 相关阅读:
    移动端1px问题
    js几种数组排序及sort的实现
    从零开始搭建vue移动端项目到上线
    Maven项目常见错误解决方法汇总
    重读《Java编程思想》
    ubuntu开发环境下eclipse的alt+/自动补全功能不能用
    Linux环境下解压rar文件
    Ubuntu 16.04下deb文件的安装
    优化Ubuntu 16.04系统的几件事
    Ubuntu16.04 安装 “宋体,微软雅黑,Consolas雅黑混合版编程字体” 等 Windows 7 下的字体
  • 原文地址:https://www.cnblogs.com/jianpanwuzhe/p/8494261.html
Copyright © 2011-2022 走看看