Android FFmpeg音视频解码播放示例详解

手机APP/开发
377
0
0
2023-03-21
标签   Android
目录
  • 前言
  • 一丶FFmpeg简介
  • 1.简介
  • 2.FFmpeg两个强大功能
  • 2.1 命令功能
  • 常用参数说明:
  • 二丶FFmpeg音视频解码播放
  • 前言
  • 1.FFmpeg 音视频解码流程
  • 2.FFmpeg 音视频解码原理
  • 2.1.解协议
  • 2.2.解封装
  • 2.3.解码
  • 2.4.音视频同步
  • 2.5.FFmpeg音视频解码
  • 3.FFmpeg接口使用
  • 三丶Clang编译FFmpeg常见问题
  • 1、命令找不到
  • 2.xmakefile 文件没有生成
  • 3.arm-linxu-androideabi-gcc is unable to
  • 4./android_config.sh: line 32: xxxxx No such file or directory
  • 5、static declaration of 'xxx' follows non-static declaration
  • 6、xxxxxxxxxx error: expected ')'
  • 7、arm-linux-androideabi-ld -Wl,- soname,libavutil.so unknown option

前言

看到很多都对音视频这块非常的感兴趣,接下来就长篇赘述一下音视频的前前后后,应该从明天开始从音视频的初中高三个层次展开浅谈🤣

本文分别为:

  • FFmpeg简介
  • FFmpeg音视频解码播放
  • Clang编译FFmpeg常见问题

今天来说一下关于今天先说一下FFmpeg一些内容

内容如下:

1.2022最新Android11位大厂面试专题,128道附答案

2.音视频大合集,从初中高到面试应有尽有

3.Android车载应用大合集,从零开始一起学

4.性能优化大合集,告别优化烦恼

5.Framework大合集,从里到外分析的明明白白

6.Flutter大合集,进阶Flutter高级工程师

7.compose大合集,拥抱新技术

8.Jetpack大合集,全家桶一次吃个够

9.架构大合集,轻松应对工作需求

10.Android基础篇大合集,根基稳固高楼平地起

整理不易,关注一下吧。开始进入正题,ღ( ´・ᴗ・` ) 🤔

一丶FFmpeg简介

1.简介

FFmpeg(FastForward Mpeg)是一款遵循GPL的开源软件,在音视频处理方面表现十分优秀,几乎囊括了现存所有的视音频格式的编码,解码、转码、混合、过滤及播放。同时也是一款跨平台的软件,完美兼容Linux、Windows、Mac OSX等平台。其实它由3大部件组成:

FFmpeg:由命令行组成,用于多媒体格式转换 FFplay:基于FFmpeg开源代码库libraries做的多媒体播放器 FFprobe:基于FFmpeg做的多媒体流分析器

源码下载

2.FFmpeg两个强大功能

1.命令功能
2.api功能

2.1 命令功能

应用程序使用方法

上图实例是将一个bus.avi中视频分离出来,利用三个应用程序中的ffmpeg.exe

ffmpeg –y –i input –vcodeccopy –an output.avi

其中-y表示覆盖同名文件,-i表示输入文件即bus.avi,-vcodec表示编码方式,后面的copy表示用原来的编码方式,即不重新编码,-an表示去除音频,后面的busv.avi表示分离出的视频文件。

同理将视频中的音频文件分离出来的命令行为:

ffmpeg -ibus.avi -acodec copy -vn busa.wav

上面举例说明了应用程序的用法,应用程序的命令行相对代码要简单很多,也能实现例如音视频分离、转码、播放等各种功能,如视频转码的命令行为:

ffmpeg -y -i input.mp4 -vcodec libx264 -acodec copy output.mp4

这个命令用于剪切视频,-ss表示从第几秒开始,如上实例为从第5秒开始,-t代表剪持续几秒长度的视频,如上实例就是剪10秒长度的视频,copy表示视频编码格式和音频编码格式与原视频统一。

ffmpeg -ss 0:0:5 -t 0:0:10 -i input.avi -vcodec copy -acodec copy output.avi

分离视频音频流

ffmpeg -i input_file -vcodec copy -an output_file_video //分离视频流
ffmpeg -i input_file -acodec copy -vn output_file_audio //分离音频流

视频解复用

ffmpeg –i test.mp4 –vcodec copy –an –f m4v test.264
ffmpeg –i test.avi –vcodec copy –an –f m4v test.264

视频转码

ffmpeg –i test.mp4 –vcodec h264 –s 352*278 –an –f m4v test.264 //转码为码流原始文件
ffmpeg –i test.mp4 –vcodec h264 –bf 0 –g 25 –s 352*278 –an –f m4v test.264 //转码为码流原始文件
ffmpeg –i test.avi -vcodec mpeg4 –vtag xvid –qsame test_xvid.avi //转码为封装文件

视频封装

ffmpeg –i video_file –i audio_file –vcodec copy –acodec copy output_file

视频剪切

ffmpeg –i test.avi –r 1 –f image2 image-%3d.jpeg //提取图片
ffmpeg -ss 0:1:30 -t 0:0:20 -i input.avi -vcodec copy -acodec copy output.avi
//剪切视频
//-r 提取图像的频率,-ss 开始时间,-t 持续时间

视频录制

ffmpeg –i rtsp://192.168.3.205:5555/test –vcodec copy out.avi

YUV序列播放

ffplay -f rawvideo -video_size 1920x1080 input.yuv

YUV序列转AVI

ffmpeg –s w*h –pix_fmt yuv420p –i input.yuv –vcodec mpeg4 output.avi

常用参数说明:

主要参数:

  • -i 设定输入流
  • -f 设定输出格式
  • -ss 开始时间 视频参数:
  • -b 设定视频流量,默认为200Kbit/s
  • -r 设定帧速率,默认为25
  • -s 设定画面的宽与高 -aspect 设定画面的比例
  • -vn 不处理视频
  • -vcodec 设定视频编解码器,未设定时则使用与输入流相同的编解码器 音频参数:
  • -ar 设定采样率 -ac 设定声音的Channel数
  • -acodec 设定声音编解码器,未设定时则使用与输入流相同的编解码器 -an 不处理音频

二丶FFmpeg音视频解码播放

前言

通常情况下,媒体文件以如MP4,MKV、FLV等等格式存在我们的计算机,手机等设备中,而这些文件格式都属于封装格式,就是把音视频数据按照相应的规范,打包成文件。

1.FFmpeg 音视频解码流程

平常我们播放媒体文件时,通常需要经过以下几个步骤

2.FFmpeg 音视频解码原理

2.1.解协议

将流媒体协议的数据,解析为标准的相应的封装格式数据。视音频在网络上传播的时候,常常采用各种流媒体协议,例如 HTTP,RTMP,或是 MMS 等等。这些协议在传输视音频数据的同时,也会传输一些信令数据。这些信令数据包括对播放的控制(播放,暂停,停止),或者对网络状态的描述等。

解协议的过程中会去除掉信令数据而只保留视音频数据。例如,采用 RTMP 协议传输的数据,经过解协议操作后,输出 FLV 格式的数据。

2.2.解封装

将输入的封装格式的数据,分离成为音频流压缩编码数据和视频流压缩编码数据。封装格式种类很多,例如 MP4,MKV,RMVB,TS,FLV,AVI 等等,它的作用就是将已经压缩编码的视频数据和音频数据按照一定的格式放到一起。例如,FLV 格式的数据,经过解封装操作后,输出 H.264 编码的视频码流和AAC 编码的音频码流。

2.3.解码

将视频/音频压缩编码数据,解码成为非压缩的视频/音频原始数据。解码是整个系统中最重要也是最复杂的一个环节。通过解码,压缩编码的视频数据输出成为非压缩的颜色数据,例如 YUV420P,RGB 等等;

2.4.音视频同步

根据解封装模块处理过程中获取到的参数信息,同步解码出来的视频和音频数据,并将视频音频数据送至系统的显卡和声卡播放出来。

2.5.FFmpeg音视频解码

通过前文,我们知道每一个媒体文件在被终端播放前主要经过了两个关键步骤,分别是解封装和解码。而在ffmpeg中,使用相关接口实现解封装和解码流程如下图:

由上图可知,我们需要重点关注下面这些FFmpeg的API接口:

  • av_register_all():注册所有组件。
  • avformat_open_input():打开输入视频文件。
  • avformat_find_stream_info():获取视频文件信息。
  • avcodec_find_decoder():查找解码器。
  • avcodec_open2():打开解码器。
  • av_read_frame():从输入文件读取一帧压缩数据。
  • avcodec_decode_video2():解码一帧压缩数据。

3.FFmpeg接口使用

在使用FFmpeg解码媒体文件之前,首先需要注册了容器和编解码器有关的组件。

av_register_all()

如果我们需要播放网络多媒体,则可以加载socket库以及网络加密协议相关的库,为后续使用网络相关提供支持。

avformat_network_init();

我们通过avformat_open_input()来打开一个媒体文件,并获得媒体文件封装格式的上下文

  //打开一个文件并解析。可解析的内容包括:视频流、音频流、视频流参数、音频流参数、视频帧索引
  int res = avformat_open_input(&pAVFormatCtx, url, NULL, NULL);
  LOGI("avformat_open_input %s %d", url, res);
  if(res != 0){
      LOGE("can not open url :%s", url);
      callJava->onCallError(CHILD_THREAD, 1001, "can not open url");
      exit = true;
      pthread_mutex_unlock(&init_mutex);
      return;
  }

通过avformat_find_stream_info()获取媒体文件中,提取流的上下文信息,分离出音视频流。

  //解码时,作用是从文件中提取流信,将所有的Stream的MetaData信息填充好,先read_packet一段数据解码分析流数据
    if(avformat_find_stream_info(pAVFormatCtx, NULL) < 0){
       LOGE("can not find streams from %s", url);
       callJava->onCallError(CHILD_THREAD, 1002,"can not find streams from url");
       exit = true;
       pthread_mutex_unlock(&init_mutex);
       return;
  }

通过遍历找出文件中的音频流或视频流

  for(int i = 0; i < pAVFormatCtx->nb_streams; i++){
          if(pAVFormatCtx->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_AUDIO) {
             //得到音频流
             if(audio == NULL){
                audio = new FFAudio(playstatus, pAVFormatCtx->streams[i]->codecpar->sample_rate, callJava);
                audio->streamIndex = i;
                audio->codecpar = pAVFormatCtx->streams[i]->codecpar;
                audio->duration = pAVFormatCtx->duration / AV_TIME_BASE;
                audio->time_base = pAVFormatCtx->streams[i]->time_base;
                duration = audio->duration;
                //av_q2d(time_base)=每个刻度是多少秒
                LOGI("audio stream_info[%d], duration:%d, time_base den:%d,sample_rate:%d",i, audio->duration, audio->time_base.den, pAVFormatCtx->streams[i]->codecpar->sample_rate);
                LOGI("audio stream_info[%d], duration %lld", i, pAVFormatCtx->duration);
             }
          } else if (pAVFormatCtx->streams[i]->codecpar->codec_type ==AVMEDIA_TYPE_VIDEO){
            //得到视频流
            if (video == NULL){
                video = new FFVideo(playstatus, callJava);
                video->streamIndex = i;
                video->codecpar = pAVFormatCtx->streams[i]->codecpar;
                video->time_base = pAVFormatCtx>streams[i]->time_base;
                int num = pAVFormatCtx->streams[i]->avg_frame_rate.num;
                int den = pAVFormatCtx->streams[i]->avg_frame_rate.den;
                LOGI("video stream_info[%d], frame_rate num %d,den %d", i, num,den);
                if(num != 0 && den != 0){
                    int fps = num / den;//[25 / 1]
                    video->defaultDelayTime = 1.0 / fps;
                }
                LOGI("video stream_info[%d], defaultDelayTime is %f", i, video->defaultDelayTime);
           }
      }
  }

分离出音视频流之后,可以找到对应AVCodecContext,即编解码器的上下文,用来寻找对应的解码器并设置。

  //查找对应的解码器 存储编解码器信息的结构体
     AVCodec *avCodec = avcodec_find_decoder(codecpar->codec_id);// 软解
     //avCodec = avcodec_find_decoder_by_name("mp3_mediacodec"); // 硬解
     if (!avCodec){
         LOGE("MFFmpeg::getCodecContext can not find decoder!");
         callJava->onCallError(CHILD_THREAD, 1003, "can not find decoder");
         exit = true;
         pthread_mutex_unlock(&init_mutex);
         return -1;
     }
     LOGI("getCodecContext codecpar-> 解码类型:%d 编码格式:%s" , codecpar->codec_type, avCodec->name);
     //配置解码器
     *avCodecContext = avcodec_alloc_context3(avCodec);
     if (!*avCodecContext){
         LOGE("can not alloc new decodecctx");
         callJava->onCallError(CHILD_THREAD, 1004, "can not alloc new decodecctx");
         exit = true;
         pthread_mutex_unlock(&init_mutex);
         return -1;
  }

通过avcodec_open2()打开解码器,解码媒体文件。

   //打开编解码器
 if(avcodec_open2(*avCodecContext, avCodec, 0) != 0){
   LOGE("cant not open strames");
   callJava->onCallError(CHILD_THREAD, 1006, "cant not open strames");
   exit = true;
   pthread_mutex_unlock(&init_mutex);
   return -1;
 }

所以第2,3,4,5四个步骤使用的关系如下图

打开解码器之后,通过av_read_frame()一帧一帧读取压缩数据。

  AVPacket \*avPacket = av\_packet\_alloc();
    //读取具体的音/视频帧数据
    int ret = av_read_frame(pAVFormatCtx, avPacket);
    if (ret==0){
        //stream_index:标识该AVPacket所属的视频/音频流
        if(avPacket->stream_index == audio->streamIndex){
           //LOGI("audio 解码第 %d 帧 DTS:%lld PTS:%lld", count, avPacket->dts,avPacket->pts);
          audio->queue->putAVpacket(avPacket);
        } else if(avPacket->stream_index == video->streamIndex){
          //LOGI("video 解码第 %d 帧 DTS:%lld PTS:%lld", count, avPacket->dts,avPacket->pts);
          count++;
          video->queue->putAVpacket(avPacket);
        } else{
          av_packet_free(&avPacket);
          av_free(avPacket);
          avPacket = NULL;
        }
  }

通过avcodec_decode_video2()/avcodec_decode_audio4解码一帧视频或者音压缩数据,通过AVPacket->AVFrame得到视频像素数据。

  //解码AVPacket->AVFrame
  ret = avcodec_decode_audio4(pCodeCtx, frame, &got_frame, packet);
  //解码一帧视频压缩数据,得到视频像素数据
  ret = avcodec_decode_video2(pCodecCtx, pFrame, &got_picture, packet);

三丶Clang编译FFmpeg常见问题

1、命令找不到

错误信息:

./build_android.sh: line 18: --enable-shared: command not found
./build_android.sh: line 20: --disable-static: command not found
./build_android.sh: line 22: --disable-doc: command not found
./build_android.sh: line 24: --disable-ffmpeg: command not found
./build_android.sh: line 26: --disable-ffplay: command not found
./build_android.sh: line 28: --disable-ffprobe: command not found
./build_android.sh: line 30: --disable-ffserver: command not found
./build_android.sh: line 32: --disable-avdevice: command not found

解决: 如果是直接copy网上的shell脚本,可能会是dos格式,请使用dos2unix build_android.sh 转换一下,删掉多余空格(这一点非常重要dos2unix 是一个工具,如果没有安装的话请先安装一下:brew install dos2unix ,很快就完事。

2.xmakefile 文件没有生成

错误信息:

./android_config.sh: line 36: --enable-shared: command not found
Makefile:2: ffbuild/config.mak: No such file or directory
Makefile:40: /tools/Makefile: No such file or directory
Makefile:41: /ffbuild/common.mak: No such file or directory
Makefile:91: /libavutil/Makefile: No such file or directory
Makefile:91: /ffbuild/library.mak: No such file or directory
Makefile:93: /fftools/Makefile: No such file or directory
Makefile:94: /doc/Makefile: No such file or directory
Makefile:95: /doc/examples/Makefile: No such file or directory
Makefile:160: /tests/Makefile: No such file or directory
make: *** No rule to make target `/tests/Makefile'. Stop.
Makefile:2: ffbuild/config.mak: No such file or directory
Makefile:40: /tools/Makefile: No such file or directory
Makefile:41: /ffbuild/common.mak: No such file or directory
Makefile:91: /libavutil/Makefile: No such file or directory
Makefile:91: /ffbuild/library.mak: No such file or directory
Makefile:93: /fftools/Makefile: No such file or directory
Makefile:94: /doc/Makefile: No such file or directory
Makefile:95: /doc/examples/Makefile: No such file or directory
Makefile:160: /tests/Makefile: No such file or directory

解决: 执行 ./configure --disable-x86asm 生成config.mak文件

3.arm-linxu-androideabi-gcc is unable to

create an executable file

错误信息:

/Users/aria/dev/android/sdk/ndk-bundle/toolchains/arm-linux-androideabi-
4.9/prebuilt/darwin-x86_64/bin/arm-linux-androideabi-gcc is unable to create an
executable file.

原因: 检查ndk版本,android官方从 r18b 开始,已经移除了gcc这个编译工具详情见ndk r18b修订内容 解决: 使用clang进行编译

4./android_config.sh: line 32: xxxxx No such file or directory

原因: .configure 后面的命令不能有注释

解决: 删除注释的哪一行代码

5、static declaration of 'xxx' follows non-static declaration

解决: config.h 搜索 lrint、lrintf、round、roundf 等对于的字符,将0修改为1

#define HAVE_LLRINT 1
#define HAVE_LLRINTF 1
#define HAVE_LRINT 1
#define HAVE_LRINTF 1
#define HAVE_ROUND 1
#define HAVE_ROUNDF 1
#define HAVE_CBRT 1
#define HAVE_CBRTF 1
#define HAVE_COPYSIGN 1
#define HAVE_TRUNC 1
#define HAVE_TRUNCF 1
#define HAVE_RINT 1
#define HAVE_HYPOT 1
#define HAVE_ERF 1

或直接使用 sed 来修改 config.h 文件

sed -i -e 's/#define HAVE_LLRINT 0/#define HAVE_LLRINT 1/g' config.h
sed -i -e 's/#define HAVE_LLRINTF 0/#define HAVE_LLRINTF 1/g' config.h
sed -i -e 's/#define HAVE_LRINT 0/#define HAVE_LRINT 1/g' config.h
sed -i -e 's/#define HAVE_LRINTF 0/#define HAVE_LRINTF 1/g' config.h
sed -i -e 's/#define HAVE_ROUND 0/#define HAVE_ROUND 1/g' config.h
sed -i -e 's/#define HAVE_ROUNDF 0/#define HAVE_ROUNDF 1/g' config.h
sed -i -e 's/#define HAVE_CBRT 0/#define HAVE_CBRT 1/g' config.h
sed -i -e 's/#define HAVE_CBRTF 0/#define HAVE_CBRTF 1/g' config.h
sed -i -e 's/#define HAVE_COPYSIGN 0/#define HAVE_COPYSIGN 1/g' config.h
sed -i -e 's/#define HAVE_TRUNC 0/#define HAVE_TRUNC 1/g' config.h
sed -i -e 's/#define HAVE_TRUNCF 0/#define HAVE_TRUNCF 1/g' config.h
sed -i -e 's/#define HAVE_RINT 0/#define HAVE_RINT 1/g' config.h
sed -i -e 's/#define HAVE_HYPOT 0/#define HAVE_HYPOT 1/g' config.h
sed -i -e 's/#define HAVE_ERF 0/#define HAVE_ERF 1/g' config.h
sed -i -e 's/#define HAVE_GMTIME_R 0/#define HAVE_GMTIME_R 1/g' config.h
sed -i -e 's/#define HAVE_LOCALTIME_R 0/#define HAVE_LOCALTIME_R 1/g' config.h
sed -i -e 's/#define HAVE_INET_ATON 0/#define HAVE_INET_ATON 1/g' config.h

6、xxxxxxxxxx error: expected ')'

错误信息:

#define getenv(x) NULL
^
/home/cd008/diska/android-ndk-r9/platforms/android-18/archarm/usr/include/stdlib.h:54:14: note: in expansion of macro 'getenv'
extern char *getenv(const char *);
^
./config.h:17:19: error: expected ')' before numeric constant
#define getenv(x) NULL
^
/home/cd008/diska/android-ndk-r9/platforms/android-18/archarm/usr/include/stdlib.h:54:14: note: in expansion of macro 'getenv'
extern char *getenv(const char *);

解决: 在config.h中注释掉#define getenv(x) NULL /#define getenv(x) NULL/

sed -i -e 's/#define getenv(x) NULL/\/\*#define getenv(x) NULL\*\//g' config.h

7、arm-linux-androideabi-ld -Wl,- soname,libavutil.so unknown option

错误信息:

Users/aria/dev/android/sdk/ndk-bundle/toolchains/arm-linux-androideabi-
4.9/prebuilt/darwin-x86_64/bin/arm-linux-androideabi-ld -Wl,-soname

原因: gcc 构建 .so 的命令是 -shared -wl,soname,xxxx.so 而 clang 的是 -shared -soname xxx.so

解决: 修改 ffbuild/config.mak 文件,将 SHFLAGS=-shared -Wl,-soname,$(SLIBNAME) 修改为 SHFLAGS=- shared -soname $(SLIBNAME)