Android 利用 FFmpeg 解码音视频数据
5854 打赏
不完整教程 2021-07-02 15:09:39

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帧

    点击并拖拽以移动

声明:本文内容由易百纳平台入驻作者撰写,文章观点仅代表作者本人,不代表易百纳立场。如有内容侵权或者其他问题,请联系本站进行删除。
评论
0个
时间排序
内容存在敏感词
    0 条记录 第 0 /
    相关专栏
    打赏作者
    易百纳技术社区
    不完整教程
    您的支持将鼓励我继续创作!
    打赏金额:
    ¥1 易百纳技术社区
    ¥5 易百纳技术社区
    ¥10 易百纳技术社区
    ¥50 易百纳技术社区
    ¥100 易百纳技术社区
    支付方式:
    微信支付
    支付宝支付
    易百纳技术社区 微信支付
    易百纳技术社区
    打赏成功!

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

    举报反馈

    举报类型

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

    详细说明

    审核成功

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

    审核失败

    失败原因
    备注
    Loading...
    易百纳技术社区
    确定要删除此文章、专栏、评论吗?
    确定
    取消
    易百纳技术社区
    易百纳技术社区
    在专栏模块发布专栏,可获得其他E友的打赏
    易百纳技术社区
    回答悬赏问答,被题主采纳后即可获得悬赏金
    易百纳技术社区
    在上传资料时,有价值的资料可设置为付费资源
    易百纳技术社区
    达到一定金额,收益即可提现~
    收益也可用来充值ebc,下载资料、兑换礼品更容易
    易百纳技术社区
    活动规则
    • 1.周任务为周期性任务,每周周一00:00刷新,上周完成的任务不会累计到本周,本周需要从头开始任务,当前任务完成后才可以完成下一个任务
    • 2.发布的专栏与资料需要与平台的板块有相关性,禁止注水,专栏/资料任务以审核通过的篇数为准,专栏需为原创文章且首次在社区发布
    • 3.任务完成后,现金奖励直接打款到微信账户;EBC/收益将自动发放到个人账户,可前往“我的钱包”查看;其他奖励请联系客服兑换
    易百纳技术社区
    升级提醒
    升级

    恭喜您的社区称号由 升级为 “社区游民”

    同时为了感谢您对社区的支持,我们将送出xxx礼品一份, 记得领取哦~

    升级提醒
    易百纳技术社区