Android 利用 FFmpeg 解码音视频数据

这把我C 2021-07-02 15:09:39 7447

Android 利用 FFmpeg 解码音视频数据

FFMpeg解码流程图解

img点击并拖拽以移动

FFMpeg解码代码流程

以下的音频解码的代码实现流程对应于上面的解码图解流程(视频的解码流程跟音频类似)

img点击并拖拽以移动

FFMpeg的处理流程

结合上面的流程得到以下的流程:

得到输入文件 -> 解封格式 -> 得到编码的数据包 -> 解码数据包 -> 得到解码后的数据帧 ->
处理数据帧 -> 编码 -> 得到编码后的数据包 -> 封装格式 -> 输出文件

点击并拖拽以移动

一、本节目标

继上节获取解封装的 AvPacket 数据包之后,我们知道 AvPacket 存储的都是编码后的数据,因此我们需要将数据包进行解码,从而得到原始的数据,而 FFmpeg 使用 AvFrame 这个数据结构来存储解码后的数据。

对于解码后的数据:

  • 视频原始数据一般是用 yuv 表示。
  • 音频原始数据一般用 pcm 表示。

而在开始之前,我们还是来回顾一下 FFmpeg 处理流的整个过程。

FFmeg 处理流程如下:

  • 1、得到输入流,打开输入流
  • 2、解封装格式->得到编码数据包 AvPacket
  • 3、解码数据包->得到解码的原始数据 AvFrame
  • 4、处理数据->例如滤镜处理,重采样,像素格式转化等
  • 5、编码原始数据->得到编码后的数据
  • 6、封装格式
  • 7、得到输出文件

根据本节目标,我们可以知道,我们重点要了解的就是第 3 步,解码数据包得到 AvFrame 数据。

二、解码音视频的步骤

2.1、 获取解码器

  • 0、注册编解码器
avcodec_register_all();

点击并拖拽以移动

  • 1、 获取解码器 AVCodec

因为音频和视频的解码器 AVCodec 是不一样的,而在 FFmpeg 中每一个解码器都会对应的一个codec_id,我们可以通过这个 id 就可以获取对应的解码器了。当前除了通过 codec_id 获取之外,也可以通过 name 来获取,目前先不考虑这种方式。

下面来看一下如何获取:

//得到视音频解码器
AVCodec *audioCodec = avcodec_find_decoder(
        avFormatContext->streams[audioIndex]->codecpar->codec_id);
//得到视频解码器
AVCodec *vedioCodec = avcodec_find_decoder(
        avFormatContext->streams[videoIndex]->codecpar->codec_id);

点击并拖拽以移动

  • 2、分配解码器上下文空间 AVCodecContext

创建 AVCodecContext 空间

AVCodecContext *ac = avcodec_alloc_context3(audioCodec);
AVCodecContext *vc = avcodec_alloc_context3(vedioCodec);

点击并拖拽以移动

  • 3、初始化解码器上下文

AVCodecParameters 的参数赋值给 AVCodecContext

ret = avcodec_parameters_to_context(ac, avFormatContext->streams[audioIndex]->codecpar);
if (ret < 0) {
    LOGE("avcodec_parameters_to_context audio failed...")
    return;
}

ret = avcodec_parameters_to_context(vc, avFormatContext->streams[videoIndex]->codecpar);
if (ret < 0) {
    LOGE("avcodec_parameters_to_context vedio failed...")
    return;
}

点击并拖拽以移动

  • 4、打开解码器

使用 AVCodec 初始化 AVCodecContext

//打开解码器
ret = avcodec_open2(ac, audioCodec, 0);
if (ret != 0) {
    LOGE("avcodec_open2 audioCodec failed ...");
    return;
}

ret = avcodec_open2(vc, vedieCodec, 0);
if (ret != 0) {
    LOGE("avcodec_open2 vedieCodec failed ...");
    return;
}

点击并拖拽以移动

2.2、开始解码流程

准备好解码器以及解码器上下文就可以开始解码流程了。在上一节中,我们已经通过 av_read_frame解封装获取到对应的编码数据包 AvPacket,下面我们要做的是解码这个数据包。

还是列一下操作步骤:

  • 0、av_read_frame得到解封装后的 AvPacket 。
  • 1、avcodec_send_packet 将 AvPacket 送入解码队列。
  • 2、avcodec_receive_frame 得到解码后的 AvFrame 数据。注意:在 avcodec_send_packet之后,可能有多个 AvFrame 可以读取,因此在读取时需要循环读取。
//临时存储的解码器上下文
AVCodecContext *cc = NULL;
//视频解码器
AVCodecContext *vc = NULL;
//视频解码器
AVCodecContext *ac = NULL;

//得到解码器并初始化解码器上下文
...

//开始解码
for(;;){
    //得到解封装后的 AvPacket
    ret = av_read_frame(avFormatContext, pkt);
    if(ret!=0){
        continue;
    }

    if(pkt->stream_index == audioIndex){//当前解码音频数据
        cc = ac;
    }else if(pkt->stream_index == videoIndex){//当前解码视频帧
        cc = vc;
    }

    //将 AvPacket 送入给解码队列
    ret = avcodec_send_packet(cc, pkt);

    //得到解码后的 AvFrame 数据
    //发送一个 avpacket 之后可能可以收到多个 avframe
    for(;;){
        ret = avcodec_receive_frame(cc, avFrame);
        if (ret != 0) {
            break;
        }
        //TODO 在这里可以处理解码后的数据拉,例如滤镜操作,像素格式转化,重采样等。
    }
}

//释放资源
avcodec_free_context(&ac);
avcodec_free_context(&vc);

点击并拖拽以移动

注意:

  • avcodec_send_packetavcodec_receive_frame应该是异步操作的,avcodec_send_packet 会将 AvPacket 放入到缓存队列中去解码,avcodec_receive_frame初次被调用时因为异步的原因可能没有获取到,也有可能可以获取多个 AvFrame,主要还是依赖解码的速度,因此通过循环去调用 avcodec_receive_frame 是比较妥当的做法。

    接下来,对照着上面的流程,使用代码来实现 FFmpeg 的解码流程。

    3.1 开启线程

    • 调用 prepared() 方法,开启线程。
    • 在 callbackDecode 中执行 decodeFFmpegThread 方法。
    extern "C"
    JNIEXPORT void JNICALL
    Java_com_example_audioplayer_player_AudioPlayer__1prepare(JNIEnv *env, jobject instance,
                                                            jstring source_) {
      const char *source = env->GetStringUTFChars(source_, 0);
    
      if (ffmpeg == NULL) {
          if (callJava == NULL) {
              callJava = new CallJava(env, jvm, &instance);
          }
          //自己定义的一个类,用于解码音频数据
          ffmpeg = new FFmpeg(callJava, source);
          //1.调用准备方法
          ffmpeg->prepare();
      }
    }
    
    //2.准备方法
    void FFmpeg::prepare() {
      pthread_create(&decodeThread, NULL, callbackDecode, this);
    }
    //构造方法
    FFmpeg::FFmpeg(CallJava *callJava, const char *url) {
      this->callJava = callJava;
      this->url = url;
    }
    //3.线程执行体
    void *callbackDecode(void *data) {
      FFmpeg *ffmpeg = (FFmpeg *) data;
    
      ffmpeg->decodeFFmpegThread();
    
      pthread_exit(&ffmpeg->decodeThread);
    }

    点击并拖拽以移动

    接下来,解码流程会在 decodeFFmpegThread 方法中执行。

    3.2 准备阶段

    下面是 decodeFFmpegThread 方法的内容:

    • 注册
    //注册
    av_register_all();
    avformat_network_init();

    点击并拖拽以移动

    • 打开文件或网络流
    avFormatContext = avformat_alloc_context();
    if (avformat_open_input(&avFormatContext, url, NULL, NULL) != 0) {
      LOGE("avformat_open_input failed...");
      return;
    }

    点击并拖拽以移动

    • 获取流信息
    if (avformat_find_stream_info(avFormatContext, NULL) < 0) {
      LOGE("avformat_find_stream_info failed...");
      return;
    }

    点击并拖拽以移动

    • 获取音频流

    这里只解码音频,因此只需要找到 codec_typeAVMEDIA_TYPE_AUDIO 流信息即可。

    for (int i = 0; i < avFormatContext->nb_streams; i++) {
      //找到对应的音频流信息
      if (avFormatContext->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_AUDIO) {
          if (audioInfo == NULL) {
              //创建 AudioInfo 保存音频相关信息
              audioInfo = new AudioInfo();
              audioInfo->streamIndex = i;
              audioInfo->avCodecParameters = avFormatContext->streams[i]->codecpar;
              break;
          }
      }
    }

    点击并拖拽以移动

    • 根据 AVCodecID 获取解码器
    const AVCodec *avCodec = avcodec_find_decoder(audioInfo->avCodecParameters->codec_id);
    if (!avCodec) {
      LOGE("avcodec_find_decoder failed...");
      return;
    }

    点击并拖拽以移动

    • 利用解码器创建解码器上下文
    audioInfo->avCodecContext = avcodec_alloc_context3(avCodec);
    if (!audioInfo->avCodecContext) {
      LOGE("avcodec_alloc_context3 failed...");
      return;
    }
    if (avcodec_parameters_to_context(audioInfo->avCodecContext, audioInfo->avCodecParameters) <
      0) {
      LOGE("avcodec_parameters_to_context failed...");
      return;
    }

    点击并拖拽以移动

    • 打开解码器

    至此,打开解码器之后,音频准备工作已经完成,接下来就可以解析每一个 AvPacket 数据了

    if (avcodec_open2(audioInfo->avCodecContext, avCodec, 0) != 0) {
      LOGE("avcodec_open2 failed...");
      return;
    }

    点击并拖拽以移动

    3.3 解码 AvPacket 阶段

    解码 AvPacket 阶段就是解码每一帧音频数据,AvPacket 存放了每一帧的音频数据。

    AVPacket *avPacket = av_packet_alloc();
    av_read_frame(avFormatContext, avPacket)

    点击并拖拽以移动

    下面这个写一个 start() 函数,负责解码音频数据。

    void FFmpeg::start() {
      //判断
      if (audioInfo == NULL) {
          LOGE("start failed audio info is null.")
          return;
      }
    
      int count = 0;
      //死循环判断
      while (1) {
          AVPacket *avPacket = av_packet_alloc();
    
          if (av_read_frame(avFormatContext, avPacket) == 0) {
    
              if (avPacket->stream_index == audioInfo->streamIndex) {
                  count++;
                  LOGD("当前解码第%d帧", count);
                  av_packet_free(&avPacket);
                  av_free(avPacket);
              } else {
                  av_packet_free(&avPacket);
                  av_free(avPacket);
              }
          } else {
              LOGD("解码完成,总共解码%d帧", count);
              av_packet_free(&avPacket);
              av_free(avPacket);
              break;
          }
      }
    }

    点击并拖拽以移动

    示例

    img点击并拖拽以移动

    示例

    11-25 22:45:52.752 27636-27868/example.com.jniexample I/MainActivity: onPrepared
    11-25 22:45:52.754 27636-27870/example.com.jniexample D/liaoweijian: 当前解码第1帧
    11-25 22:45:52.754 27636-27870/example.com.jniexample D/liaoweijian: 当前解码第2帧
    11-25 22:45:52.754 27636-27870/example.com.jniexample D/liaoweijian: 当前解码第3帧
    11-25 22:45:52.754 27636-27870/example.com.jniexample D/liaoweijian: 当前解码第4帧
    11-25 22:45:52.754 27636-27870/example.com.jniexample D/liaoweijian: 当前解码第5帧
    11-25 22:45:52.754 27636-27870/example.com.jniexample D/liaoweijian: 当前解码第6帧
    11-25 22:45:52.754 27636-27870/example.com.jniexample D/liaoweijian: 当前解码第7帧
    ...
    11-25 22:45:57.530 27636-27870/example.com.jniexample D/liaoweijian: 当前解码第8403帧
    11-25 22:45:57.530 27636-27870/example.com.jniexample D/liaoweijian: 当前解码第8404帧
    11-25 22:45:57.530 27636-27870/example.com.jniexample D/liaoweijian: 解码完成,总共解码8404帧

    点击并拖拽以移动

声明:本文内容由易百纳平台入驻作者撰写,文章观点仅代表作者本人,不代表易百纳立场。如有内容侵权或者其他问题,请联系本站进行删除。
红包 95 收藏 评论 打赏
评论
0个
内容存在敏感词
手气红包
    易百纳技术社区暂无数据
相关专栏
置顶时间设置
结束时间
删除原因
  • 广告/SPAM
  • 恶意灌水
  • 违规内容
  • 文不对题
  • 重复发帖
打赏作者
易百纳技术社区
这把我C
您的支持将鼓励我继续创作!
打赏金额:
¥1易百纳技术社区
¥5易百纳技术社区
¥10易百纳技术社区
¥50易百纳技术社区
¥100易百纳技术社区
支付方式:
微信支付
支付宝支付
易百纳技术社区微信支付
易百纳技术社区
打赏成功!

感谢您的打赏,如若您也想被打赏,可前往 发表专栏 哦~

举报反馈

举报类型

  • 内容涉黄/赌/毒
  • 内容侵权/抄袭
  • 政治相关
  • 涉嫌广告
  • 侮辱谩骂
  • 其他

详细说明

审核成功

发布时间设置
发布时间:
是否关联周任务-专栏模块

审核失败

失败原因
备注
拼手气红包 红包规则
祝福语
恭喜发财,大吉大利!
红包金额
红包最小金额不能低于5元
红包数量
红包数量范围10~50个
余额支付
当前余额:
可前往问答、专栏板块获取收益 去获取
取 消 确 定

小包子的红包

恭喜发财,大吉大利

已领取20/40,共1.6元 红包规则

    易百纳技术社区