ffmpeg

  • libavformat:用于各种音视频封装格式的生成和解析,包括获取解码所需信息以生成解码上下文结构
  • 和读取音视频帧等功能;
  • libavcodec:用于各种类型声音/图像编解码;
  • libavutil:包含一些公共的工具函数;
  • libswscale:用于视频场景比例缩放、色彩映射转换;
  • libavfilter:在 FFmpeg 中有多种多样的滤镜,你可以把他们当成一个个小工具,专门用于处理视频和音频数据,以便实现一定的目的。如 overlay 这个滤镜,可以将一个图画覆盖到另一个图画上;transport 这个滤镜可以将图画做旋转等等。
  • libswresample: 对音频的采样频率转换,如将采样率从44.1K转换到48K,注意从高采样频率到低采样频率的音频转换是一个有损的过程。

Prepare流程

1、 申请formatContext上下文

    formatContext = avformat_alloc_context();
 // 字典(键值对)
    AVDictionary *dictionary = nullptr;
    //设置超时(5秒)
    av_dict_set(&dictionary, "timeout", "5000000", 0); // 单位微妙

2、 打开文件

    av_register_all();
    int r = avformat_open_input(&formatContext, data_source, nullptr, &dictionary);

3、 查找媒体中的音视频流的信息

 r = avformat_find_stream_info(formatContext, nullptr);

4、进行流解码操作并获取解码器,将参数给解码器

//TODO 第三步 根据流的信息获取流
    for (int stream_index = 0; stream_index < formatContext->nb_streams; ++stream_index) {
        // TODO 第四步 获取媒体流 音 视
        AVStream *stream = formatContext->streams[stream_index];
        //TODO 第五步 从上面的流中 获取编码的参数
        AVCodecParameters *parameters = stream->codecpar;
        //TODO 第六步 根据上面的参数获取解码器
        AVCodec *codec = avcodec_find_decoder(parameters->codec_id);
        //error  ......
        //TODO 第七步 编解码器的上下文 没有内容
        codecContext = avcodec_alloc_context3(codec);
         //error  ......
         //TODO 第八步 将参数给avCodecContext
        r = avcodec_parameters_to_context(codecContext,parameters);
        //TODO 第九步打开解码器
        r = avcodec_open2(codecContext,codec, nullptr);

5、分发流处理

   if (parameters->codec_type == AVMediaType::AVMEDIA_TYPE_VIDEO){
   // 虽然是视频类型,但是只有一帧封面
            if (stream->disposition & AV_DISPOSITION_ATTACHED_PIC) {
                continue;
            }
            //视频
            // 虽然是视频类型,但是只有一帧封面
            if (stream->disposition & AV_DISPOSITION_ATTACHED_PIC) {
                continue;
            }
            video_channel = new VideoChannel(stream_index, codecContext, time_base, fps);
   } else {
         audio_channel = new AudioChannel(stream_index, codecContext,time_base);
   }

start流程

1、 解决帧之间的延迟问题

 if (video_channel && video_channel->packets.size() >100){
            av_usleep(10 * 1000); //微秒
            continue;
        }
        if (audio_channel && audio_channel->packets.size() >100){
            av_usleep(10 * 1000); //微秒
            continue;
}

2、 将AVPacket包入队

 AVPacket *packet = av_packet_alloc();
        int ret = av_read_frame(formatContext,packet);
        if (!ret){
            //音频
            if (audio_channel && audio_channel->stream_index == packet->stream_index){
                audio_channel->packets.insertToQueue(packet);
            //视频
            } else if (video_channel && video_channel->stream_index == packet->stream_index){
                video_channel->packets.insertToQueue(packet);
            }
        } else if (ret == AVERROR_EOF){//end of file 文件末尾
            //表示读完了 并没有播放完
            //TODO  内存泄漏点2
            if (video_channel->frames.empty() && audio_channel->frames.empty()){
                if (helper){
                    helper->onFinish(THREAD_CHILD);
                }
                break;//队列的数据被播放完成 再退出
            }
        } else{
            break; // 出现了错误
        }

解析出原始包流程

//视频
int ret = packets.getQueueAndDel(pkt);
        if (!isWorking){ //如果关闭播放  就跳出去
            break;
        }
        if (!ret) { // ret == 0
            continue; // 哪怕是没有成功,也要继续(假设:你生产太慢(压缩包加入队列),我消费就等一下你)
        }
        //最新版本FFmpeg 差别  1、发送pkt给缓冲区  2、从缓冲区拿原始包
        ret = avcodec_send_packet(codecContext,pkt);
        //由于ffmpeg缓存了一份 可以大胆释放
        //releaseAVPacket(&pkt);

        if (ret){
            break; //出现错误了
        }

        //下面是获取帧
        AVFrame *frame = av_frame_alloc();
        ret = avcodec_receive_frame(codecContext,frame);
        if (ret == AVERROR(EAGAIN)){ // b帧参考前面成功  后面却失败了 可能是p帧没出来 需要再取数据
            continue;
        } else if (ret != 0){
            //TODO 内存泄漏点4 解码出错的时候
            if(frame){
                releaseAVFrame(&frame);
            }
            break; // 错误了
        }
        //拿到了原始包
        frames.insertToQueue(frame);
        // TODO 内存泄漏点5
        //安心释放packet pkt有默认FFmpeg帮你开辟的堆区
        av_packet_unref(pkt);// 减引用  自动释放 释放成员的空间
        releaseAVPacket(&pkt);//释放本身的空间
//音频
 ret = avcodec_send_packet(codecContext,packet);
        releaseAVPacket(&packet);
        if (ret){
            break;
        }
        AVFrame *frame = av_frame_alloc();
        ret = avcodec_receive_frame(codecContext,frame);
        if (ret == AVERROR(EAGAIN)){
            continue;
        } else if (ret != 0){
            //TODO 内存泄漏点4 解码出错的时候
            if(frame){
                releaseAVFrame(&frame);
            }
            break;
        }
        frames.insertToQueue(frame);
        // TODO 内存泄漏点5
        //安心释放packet pkt有默认FFmpeg帮你开辟的堆区
        if (packet){
            av_packet_unref(packet);// 减引用  自动释放 释放成员的空间
            releaseAVPacket(&packet);//释放本身的空间
        }

视频play流程

void VideoChannel::video_play() {
    // SWS_FAST_BILINEAR == 很快 可能会模糊
    // SWS_BILINEAR 适中算法

    AVFrame *frame = 0;
    uint8_t *dst_data[4]; // RGBA
    int dst_linesize[4]; // RGBA
    // 原始包(YUV数据)  ---->[libswscale]   Android屏幕(RGBA数据)

    //给 dst_data 申请内存   width * height * 4 xxxx
    av_image_alloc(dst_data, dst_linesize,
                   codecContext->width, codecContext->height, AV_PIX_FMT_RGBA, 1);

    // yuv -> rgba
    LOGD("width:%d,height:%d",codecContext->width,codecContext->height)

    SwsContext *sws_ctx = sws_getContext(
            // 下面是输入环节
            codecContext->width,
            codecContext->height,
            codecContext->pix_fmt, // 自动获取 xxx.mp4 的像素格式  AV_PIX_FMT_YUV420P // 写死的

            // 下面是输出环节
            codecContext->width,
            codecContext->height,
            AV_PIX_FMT_RGBA,
            SWS_BILINEAR,//像素算法适中的 线性差值
            NULL,
            NULL,
            NULL
    );
    while (isWorking){
        if (!isPlay){
            av_usleep(10*1000);
            continue;
        }
        int ret = frames.getQueueAndDel(frame);
        if (!isWorking){
            break; //关闭了播放器 跳出 并释放releaseFrame
        }

        if (!ret){
            continue; //原始包产生太慢 需要消费等待下
        }
        //格式转换 yuv-->rgba


        sws_scale(sws_ctx,
                  //输入环节 yuv数据
                  frame->data,
                  frame->linesize,//大小 一行一行渲染
                  0,
                  codecContext->height,

                  //下面是输出环节 成果:RGBA数据
                  dst_data,
                  dst_linesize
                  );
        //TODO 音视频同步 3 根据fps来进行休眠 FPS间隔时间加入
        double extra_delay = frame->repeat_pict / (2*fps); //额外延时时间
        double fps_delay = 1.0 / fps; // 0.04 (每一帧的延时时间)
        double video_delay = extra_delay + fps_delay;
        //根据视频来处理的 没有和音频相关
        //av_usleep((extra_delay+fps_delay) * 1000000);

        double video_time = frame->best_effort_timestamp * av_q2d(time_base);
        double audio_time = audioChannel->audio_time;
        //判断两个时间插值  你追我赶
        double time_diff = video_time - audio_time;
        if (time_diff > 0){
            //视频 睡眠
            if (time_diff > 1){//差距较大  稍微睡一下
                av_usleep(video_delay * 2 * 1000000);
            } else { //差距不大 当前时间延迟时间 + 音视频差值
                av_usleep((video_delay + time_diff) * 1000000);
            }
        }
        if (time_diff < 0){
            //视频时间 < 音频时间 丢包
            //不能丢I帧
            //可以在frame 和 packets中去丢

            //经验值 0.05
            if (fabs(time_diff) <= 0.05){ //对浮点数取绝对值
                frames.sync();
                continue;
            }

        } else {
            LOGI("百分百同步了")
        }



        //ANativeWindows 渲染工作 surfaceview --- ANativeWindows
        // 渲染一帧图片  由于拿不到surfaceview  只能回调给native-lib
        renderCallback(dst_data[0],
                     codecContext->width,
                     codecContext->height,
                     dst_linesize[0]
                     );
        //渲染完  数据没用了  释放
        //TODO  内存泄漏点6
       // releaseAVFrame(&frame);
        av_frame_unref(frame); //释放frame中的成员
        releaseAVFrame(&frame); //释放frame
    }
    //releaseAVFrame(&frame); //出现了意外 多要释放
    //TODO  内存泄漏点6
    // releaseAVFrame(&frame);
    av_frame_unref(frame); //释放frame中的成员
    releaseAVFrame(&frame); //释放frame
    isWorking=0;
    av_free(&dst_data[0]);
    //free(sws_ctx); 不用c的释放  需要用ffmpeg的释放
    sws_freeContext(sws_ctx);

}

音频play流程

void AudioChannel::audio_play() {
    SLresult result ;// 用于接收执行成功的返回值
    /**
     * TODO 1、创建引擎并获取引擎接口
     */
     //创建引擎对象
    result = slCreateEngine(&engineObject,0,0,0,0,0);
    if (SL_RESULT_SUCCESS != result){
        LOGE("创建音频引擎 slCreateEngine失败")
        return;
    }
    //初始化引擎
    //SL_BOOLEAN_FALSE : 延时等待创建成功
    result = (*engineObject)->Realize(engineObject,SL_BOOLEAN_FALSE);
    if (SL_RESULT_SUCCESS != result){
        LOGE("创建音频引擎 Realize失败")
        return;
    }
    //获取引擎接口
    result = (*engineObject)->GetInterface(engineObject,SL_IID_ENGINE,&engineInterface);
    if (SL_RESULT_SUCCESS != result){
        LOGE("获取引擎接口 GetInterface失败")
        return;
    }
    if (engineInterface){
        LOGD("AUDIO SUCCESS")
    } else {
        LOGD("AUDIO FAILURE")
    }

    /**
     * TODO 2、混音器
     */
     //0:环境特效,混响特效 ...
    result = (*engineInterface)->CreateOutputMix(engineInterface,&outputMixObject,0,0,0);
    if (SL_RESULT_SUCCESS != result){
        LOGE("初始化混音器 CreateOutputMix失败")
        return;
    }
    //初始化混音器
    result = (*outputMixObject)->Realize(outputMixObject,SL_BOOLEAN_FALSE);

    if (SL_RESULT_SUCCESS != result){
        LOGE("初始化混音器 Realize失败")
        return;
    }

    // 不启用混响可以不用获取混音器接口 【声音的效果】
    // 获得混音器接口
    /*
    result = (*outputMixObject)->GetInterface(outputMixObject, SL_IID_ENVIRONMENTALREVERB,
                                             &outputMixEnvironmentalReverb);
    if (SL_RESULT_SUCCESS == result) {
    // 设置混响 : 默认。
    SL_I3DL2_ENVIRONMENT_PRESET_ROOM: 室内
    SL_I3DL2_ENVIRONMENT_PRESET_AUDITORIUM : 礼堂 等
    const SLEnvironmentalReverbSettings settings = SL_I3DL2_ENVIRONMENT_PRESET_DEFAULT;
    (*outputMixEnvironmentalReverb)->SetEnvironmentalReverbProperties(
           outputMixEnvironmentalReverb, &settings);
    }
    */

    /**
     * TODO 3、创建播放器
     */
    /**
   * TODO 3.创建播放器
   */
    // 创建buffer缓存类型的队列  2的队列大小
    SLDataLocator_AndroidSimpleBufferQueue loc_bufq = {SL_DATALOCATOR_ANDROIDSIMPLEBUFFERQUEUE,
                                                       10};

    // pcm数据格式 == PCM是不能直接播放,mp3可以直接播放(参数集),人家不知道PCM的参数
    //  SL_DATAFORMAT_PCM:数据格式为pcm格式
    //  2:双声道
    //  SL_SAMPLINGRATE_44_1:采样率为44100
    //  SL_PCMSAMPLEFORMAT_FIXED_16:采样格式为16bit
    // SL_PCMSAMPLEFORMAT_FIXED_16:数据大小为16bit
    // SL_SPEAKER_FRONT_LEFT | SL_SPEAKER_FRONT_RIGHT:左右声道(双声道)
    // SL_BYTEORDER_LITTLEENDIAN:小端模式
    SLDataFormat_PCM format_pcm = {SL_DATAFORMAT_PCM, // PCM数据格式
                                   2, // 声道数
                                   SL_SAMPLINGRATE_44_1, // 采样率(每秒44100个点)
                                   SL_PCMSAMPLEFORMAT_FIXED_16, // 每秒采样样本 存放大小 16bit
                                   SL_PCMSAMPLEFORMAT_FIXED_16, // 每个样本位数 16bit
                                   SL_SPEAKER_FRONT_LEFT | SL_SPEAKER_FRONT_RIGHT, // 前左声道  前右声道
                                   SL_BYTEORDER_LITTLEENDIAN}; // 字节序(小端) 例如:int类型四个字节(到底是 高位在前 还是 低位在前 的排序方式,一般我们都是小端)

    // 数据源 将上述配置信息放到这个数据源中
    // audioSrc最终配置音频信息的成果,给后面代码使用
    SLDataSource audioSrc = {&loc_bufq, &format_pcm};
    // audioSrc 给后面代码用的
    // 独立声卡:24bit  集成声卡16bit

    // 3.2 配置音轨(输出)
    // 设置混音器
    SLDataLocator_OutputMix loc_outmix = {SL_DATALOCATOR_OUTPUTMIX, outputMixObject}; // SL_DATALOCATOR_OUTPUTMIX:输出混音器类型
    SLDataSink audioSnk = {&loc_outmix, NULL}; // outmix最终混音器的成果,给后面代码使用
    // 需要的接口 操作队列的接口
    const SLInterfaceID ids[1] = {SL_IID_BUFFERQUEUE};
    const SLboolean req[1] = {SL_BOOLEAN_TRUE};

    // 3.3 创建播放器 SLObjectItf bqPlayerObject
    result = (*engineInterface)->CreateAudioPlayer(engineInterface, // 参数1:引擎接口
                                                   &bqPlayerObject, // 参数2:播放器
                                                   &audioSrc, // 参数3:音频配置信息
                                                   &audioSnk, // 参数4:混音器

            // TODO 下面代码都是 打开队列的工作
                                                   1, // 参数5:开放的参数的个数
                                                   ids,  // 参数6:代表我们需要 Buff
                                                   req // 参数7:代表我们上面的Buff 需要开放出去
    );

    if (SL_RESULT_SUCCESS != result) {
        LOGD("创建播放器 CreateAudioPlayer failed!");
        return;
    }

    // 3.4 初始化播放器:SLObjectItf bqPlayerObject
    result = (*bqPlayerObject)->Realize(bqPlayerObject, SL_BOOLEAN_FALSE);  // SL_BOOLEAN_FALSE:延时等待你创建成功
    if (SL_RESULT_SUCCESS != result) {
        LOGD("实例化播放器 CreateAudioPlayer failed!");
        return;
    }
    LOGD("创建播放器 CreateAudioPlayer success!");

    // 3.5 获取播放器接口 【以后播放全部使用 播放器接口去干(核心)】
    result = (*bqPlayerObject)->GetInterface(bqPlayerObject, SL_IID_PLAY, &bqPlayerPlay); // SL_IID_PLAY:播放接口 == iplayer
    if (SL_RESULT_SUCCESS != result) {
        LOGD("获取播放接口 GetInterface SL_IID_PLAY failed!");
        return;
    }
    LOGI("3、创建播放器 Success");
    /**
        * TODO 4.设置回调函数
        */
    // 4.1 获取播放器队列接口:SLAndroidSimpleBufferQueueItf bqPlayerBufferQueue  // 播放需要的队列
    result = (*bqPlayerObject)->GetInterface(bqPlayerObject, SL_IID_BUFFERQUEUE, &bqPlayerBufferQueue);
    if (result != SL_RESULT_SUCCESS) {
        LOGD("获取播放队列 GetInterface SL_IID_BUFFERQUEUE failed!");
        return;
    }

    // 4.2 设置回调 void bqPlayerCallback(SLAndroidSimpleBufferQueueItf bq, void *context)
    (*bqPlayerBufferQueue)->RegisterCallback(bqPlayerBufferQueue,  // 传入刚刚设置好的队列
                                             bqPlayerCallback,  // 回调函数
                                             this); // 给回调函数的参数
    LOGI("4、设置播放回调函数 Success");

    /**
     * TODO 5、设置播放器状态为播放状态
     */
    (*bqPlayerPlay)->SetPlayState(bqPlayerPlay, SL_PLAYSTATE_PLAYING);
    LOGI("5、设置播放器状态为播放状态 Success");

    // 我手动激活调用
    /**
   * TODO 6、手动激活回调函数
   */
    bqPlayerCallback(bqPlayerBufferQueue, this);
    LOGI("6、手动激活回调函数 Success");
}

void bqPlayerCallback(SLAndroidSimpleBufferQueueItf bq, void * args) {

    auto *audio_channel = static_cast<AudioChannel *>(args);

    int pcm_size = audio_channel->getPCM();

    // 添加数据到缓冲区里面去
    (*bq)->Enqueue(
            bq, // 传递自己,为什么(因为没有this,为什么没有this,因为不是C++对象,所以需要传递自己) JNI讲过了
            audio_channel->out_buffers, // PCM数据
            pcm_size); // PCM数据对应的大小,缓冲区大小怎么定义?(复杂)
}

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,444评论 6 496
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,421评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 160,036评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,363评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,460评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,502评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,511评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,280评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,736评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,014评论 2 328
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,190评论 1 342
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,848评论 5 338
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,531评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,159评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,411评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,067评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,078评论 2 352

推荐阅读更多精彩内容