【FFmpeg】(一) 音视频相关基础知识以及FFmpeg介绍

一、音视频相关基础知识

1、视频播放器原理

视频播放器就是将一个封装的格式数据进行解封装,得到对应的音频压缩数据和视频压缩数据,再进行相应的音视频解码,得到音频采样数据和视频采样数据,最后将音频采样数据和视频采样数据同时播放,达到音视频同步。

  • 封装格式数据通常有FLV、MKV、MP4、AVI、RMVB 等等
  • 录音、录像的实质就是一个压缩采集到的图像或者音频数据的过程,这个过程又称为编码
  • 播放视频或者音频文件,实质上是一个解压缩的过程,这个过程又称为解码

视频播放器原理

1.1 解封装

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

1.2 解码

1.2.1 音频压缩数据解码

一般我们常见的音频压缩编码标准包含AAC,MP3,AC-3,WMA 等等;通过解码将压缩编码的音频数据输出成为非压缩的音频采样数据,例如PCM数据

  • AAC:数据由大小不固定的ADTS构成
  • PCM:单声道的情况下按照顺序存储每个采样点的数据;双声道的情况下按照 “左右、左右”的顺序存储每个采样点两个声道的数据

采样率:也称为采样速度或者采样率,定义了每秒从连续信号中提取并组成离散信号的采样个数,它用赫兹(Hz)来表示。采样频率的倒数是采样周期或者叫作采样时间,它是采样之间的时间间隔。通俗的讲采样频率是指计算机每秒钟采集多少个信号样本。

1.2.2 视频压缩数据解码

一般我们常见的视频的压缩编码标准则包含H.264,MPEG2,VC-1等等;压缩编码的视频数据输出成为视频像素数据(非压缩的颜色数据),例如YUV(YUV420P,YUV422P,YUV444P;最常见为YUV420P),RGB(RGB24,RGB32)等等。Y:亮度,U:色度,V:浓度

  • H.264:数据由大学不固定的NALU构成,最常见情况下,1个NALU存储了1帧画面的压缩编码后的数据

1.3 音视频同步

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

二、FFmpeg 介绍

1、定义

FFmpeg是一套可以用来记录、转换数字音频、视频,并能将其转化为流的开源计算机程序。采用LGPL或GPL许可证。它提供了录制、转换以及流化音视频的完整解决方案。它包含了非常先进的音频/视频编解码库libavcodec,为了保证高可移植性和编解码质量,libavcodec里很多code都是从头开发的。

2、FFmpeg 命令

2.1 视频格式转换命令

命令参数 说明
-i 源文件
-o 输出文件
```
//进入到FFmpeg 的 bin 目录下执行以下命令
ffmpeg -i d:\input.mp4 -o d:\output.avi
```

2.2 视频转 Gif 命令

命令参数 说明
-i 源文件
-ss 从多少秒开始
-t 到多少秒介绍
-s 图像的尺寸大小
-b:v 码率
//进入到FFmpeg 的 bin 目录下执行以下命令
ffmpeg -ss 5 -t 15 -i d:\\input.mp4 -s 300x200 -b:v 1500K D:\\video_gif.gif

三、使用 visual studio 编译 FFmpeg

*注:项目根目录即创建cpp源文件所在目录

步骤

  1. 【FFmpeg官网】下载 FFmpeg 在windows 下的开发(dev)版本 ffmpeg-XXX-win64-dev
  2. 在 visual studio上创建一个C++的空项目
  3. 将下载好的 FFmpeg 包解压后,复制 include 和 lib 文件夹到刚刚创建好的项目根路径下
  4. 下载 FFmpeg 在windows 下的 Shared 版本ffmpeg-XXX-win64-shared,解压后赋值 bin 目录下的动态库(.dll)文件到项目根目录下
  5. 修改项目的配置管理器中的活动姐姐方案平台为X64(根据自己操作系统位数更改)
  6. 在 visual studio 中项目右键,点击【属性(ALT+Enter)】,选中【C++目录】,右侧的【包含目录】,点击编辑,增加刚刚复制到根目录下的 include 目录

包含目录

  1. 点击【属性】中的【链接器】,在右侧选中【附加库目录】,点击编辑,增加刚刚复制到根目录下的 lib 目录

附加库目录

  1. 点击【属性】中的【链接器】下的【输入】,在右侧选中【附加依赖库】,点击编辑,增加刚刚复制到根目录下的 lib 目录下所有的 .lib 文件
avcodec.lib
avdevice.lib
avfilter.lib
avformat.lib
avutil.lib
postproc.lib
swresample.lib
swscale.lib

附加库目录

  1. 在项目中添加一个.cpp的源文件,如:my_ffmpeg.cpp
#include <stdlib.h>
#include <stdio.h>

//C和C++混编,指示编译器按照C语言进行编译
extern "C"{
//引入ffmpeg的头文件
#include "libavcodec/avcodec.h"
};

void main() {
    //输出 ffmpeg 的配置
    printf("%s\n",avcodec_configuration());

    getchar();
}

四、使用 Linux主机 编译 FFmpeg

1、编译前准备

  • 阿里云主机(Ubuntu/centos)
  • 客户端主机安装XShell,Xftp

2、编译

  • 使用Xftp将NDK、FFmpeg 上传到阿里云主机
  • 安装vim 编辑器
$ apt-get update
$ sudo apt-get install vim-gtk
  • NDK 安装,配置环境变量
//给文件授权可执行
$ chmod 777 -R android-ndk-r10e-linux-x86_64.bin
//执行文件
$ ./android-ndk-r10e-linux-x86_64.bin
//配置环境变量
$ vim ~/.bashrc

//增加环境变量
export NDKROOT=/home/study/ndk/android-ndk-r10e
export PATH=$NDKROOT:$PATH

$ source ~/.bashrc
//查看ndk版本号
$ ndk-build -v
  • 解压 FFmpeg
$ tar -xzvf ffmpeg-4.1.2.tar.gz 
  • 编译 FFmpeg ,实现 Shell 脚本文件android_build.sh
#!/bin/bash
make clean
export NDK=/home/study/ndk/android-ndk-r10e
export SYSROOT=$NDK/platforms/android-9/arch-arm/
export TOOLCHAIN=$NDK/toolchains/arm-linux-androideabi-4.8/prebuilt/linux-x86_64
export CPU=arm
export PREFIX=$(pwd)/android/$CPU
export ADDI_CFLAGS="-marm"

./configure --target-os=linux \
--prefix=$PREFIX --arch=arm \
--disable-doc \
--enable-shared \
--disable-static \
--disable-yasm \
--disable-symver \
--enable-gpl \
--disable-ffmpeg \
--disable-ffplay \
--disable-ffprobe \
--disable-ffserver \
--disable-doc \
--disable-symver \
--cross-prefix=$TOOLCHAIN/bin/arm-linux-androideabi- \
--enable-cross-compile \
--sysroot=$SYSROOT \
--extra-cflags="-Os -fpic $ADDI_CFLAGS" \
--extra-ldflags="$ADDI_LDFLAGS" \
$ADDITIONAL_CONFIGURE_FLAG
make clean
make
make install

执行脚本文件

./android_build.sh

五、使用编译后形成的include和lib目录实现Android NDK 解码功能

步骤:

  1. 在Android studio上创建一个支持C++的Android项目
  2. 将生成的 include 和 lib 复制到 cpp 目录下,更改CMakeLists.txt文件
cmake_minimum_required(VERSION 3.4.1)
set(DISTRIBUTION_DIR ${CMAKE_SOURCE_DIR}/../jniLibs/)

add_library( # Sets the name of the library.
        ffmpeg-video
        SHARED
        ffmpeg_video.c)


# 编解码(最重要的库)
add_library(
        avcodec
        SHARED
        IMPORTED
)

#指定编码库的位置
set_target_properties(
        avcodec
        PROPERTIES IMPORTED_LOCATION
        ${DISTRIBUTION_DIR}/${ANDROID_ABI}/libavcodec-56.so
)

#设备信息
add_library(
        avdevice
        SHARED
        IMPORTED
)

#指定设备信息的位置
set_target_properties(
        avdevice
        PROPERTIES IMPORTED_LOCATION
        ${DISTRIBUTION_DIR}/${ANDROID_ABI}/libavdevice-56.so
)

#滤镜特效处理库
add_library(
        avfilter
        SHARED
        IMPORTED
)

#指定滤镜库位置
set_target_properties(
        avfilter
        PROPERTIES IMPORTED_LOCATION
        ${DISTRIBUTION_DIR}/${ANDROID_ABI}/libavfilter-5.so
)

#封装格式处理库
add_library(
        avformat
        SHARED
        IMPORTED
)

#指定格式库路径
set_target_properties(
        avformat
        PROPERTIES IMPORTED_LOCATION
        ${DISTRIBUTION_DIR}/${ANDROID_ABI}/libavformat-56.so
)

#工具库(大部分库都需要这个库的支持)
add_library(
        avutil
        SHARED
        IMPORTED
)

#指定工具库路径
set_target_properties(
        avutil
        PROPERTIES IMPORTED_LOCATION
        ${DISTRIBUTION_DIR}/${ANDROID_ABI}/libavutil-54.so
)

#后期处理
add_library(
        postproc
        SHARED
        IMPORTED
)

#指定后期处理库路径
set_target_properties(
        postproc
        PROPERTIES IMPORTED_LOCATION
        ${DISTRIBUTION_DIR}/${ANDROID_ABI}/libpostproc-53.so
)

#数据格式转换库
add_library(
        swresample
        SHARED
        IMPORTED
)

#指定库位置
set_target_properties(
        swresample
        PROPERTIES IMPORTED_LOCATION
        ${DISTRIBUTION_DIR}/${ANDROID_ABI}/libswresample-1.so
)

#视频像素数据格式转换
add_library(
        swscale
        SHARED
        IMPORTED
)
#视频像素格式转换库位置
set_target_properties(
        swscale
        PROPERTIES IMPORTED_LOCATION
        ${DISTRIBUTION_DIR}/${ANDROID_ABI}/libswscale-3.so
)


find_library(
        android-lib
        android
)

find_library(
        log-lib
        log
)

find_library(
        jnigraphics-lib
        jnigraphics
)

# 将预构建库与本地库相连
target_link_libraries(
        ffmpeg-video
        avcodec
        avdevice
        avfilter
        avformat
        avutil
        postproc
        swresample
        swscale
        ${android-lib}
        ${jnigraphics-lib}
        ${log-lib}
)
  1. 编写本地 native 方法,编译生成.h头文件
package cn.onestravel.ndk.ffmpegdecodedemo;


public class VideoUtils {
    static {
        System.loadLibrary("avutil-54");
        System.loadLibrary("avcodec-56");
        System.loadLibrary("avdevice-56");
        System.loadLibrary("avfilter-5");
        System.loadLibrary("avformat-56");
        System.loadLibrary("postproc-53");
        System.loadLibrary("swresample-1");
        System.loadLibrary("swscale-3");
        System.loadLibrary("ffmpeg-video");
    }
    public native static void decode(String input,String output);
}
  1. 对编译生成.h头文件进行实现,创建ffmpeg_video.c
//
// Created by Administrator on 2019/3/27.
//

//
// Created by Administrator on 2019/3/26.
//
#include <android/log.h>
#include "cn_onestravel_ndk_ffmpegdecodedemo_VideoUtils.h"
//编码
#include "include/libavcodec/avcodec.h"
//封装格式处理
#include "include/libavformat/avformat.h"
//像素处理
#include "include/libswscale/swscale.h"
#include "include/libavutil/avutil.h"
#include "include/libavutil/frame.h"

#define LOGI(FORMAT,...) __android_log_print(ANDROID_LOG_INFO,"FFMPEG",FORMAT,##__VA_ARGS__);
#define LOGE(FORMAT,...) __android_log_print(ANDROID_LOG_ERROR,"FFMPEG",FORMAT,##__VA_ARGS__);


/*
 * Class:     cn_onestravel_ndk_ffmpegdecode_VoideUtils
 * Method:    decode
 * Signature: (Ljava/lang/String;Ljava/lang/String;)V
 */
JNIEXPORT void JNICALL Java_cn_onestravel_ndk_ffmpegdecodedemo_VideoUtils_decode
  (JNIEnv * env, jclass jcls, jstring input_jstr, jstring output_jstr){
      //需要转码的视频文件(输入的视频文件)
      const char* input_cstr = (*env)->GetStringUTFChars(env,input_jstr,NULL);
      const char* output_cstr = (*env)->GetStringUTFChars(env,output_jstr,NULL);

      //1.注册所有组件
      av_register_all();

      //封装格式上下文,统领全局的结构体,保存了视频文件封装格式的相关信息
      AVFormatContext *pFormatCtx = avformat_alloc_context();

      //2.打开输入视频文件
      if (avformat_open_input(&pFormatCtx, input_cstr, NULL, NULL) != 0)
      {
          LOGE("%s","无法打开输入视频文件");
          return;
      }

      //3.获取视频文件信息
      if (avformat_find_stream_info(pFormatCtx,NULL) < 0)
      {
          LOGE("%s","无法获取视频文件信息");
          return;
      }

      //获取视频流的索引位置
      //遍历所有类型的流(音频流、视频流、字幕流),找到视频流
      int v_stream_idx = -1;
      int i = 0;
      //number of streams
      for (; i < pFormatCtx->nb_streams; i++)
      {
          //流的类型
          if (pFormatCtx->streams[i]->codec->codec_type == AVMEDIA_TYPE_VIDEO)
          {
              v_stream_idx = i;
              break;
          }
      }

      if (v_stream_idx == -1)
      {
          LOGE("%s","找不到视频流\n");
          return;
      }

      //只有知道视频的编码方式,才能够根据编码方式去找到解码器
      //获取视频流中的编解码上下文
      AVCodecContext *pCodecCtx = pFormatCtx->streams[v_stream_idx]->codec;
      //4.根据编解码上下文中的编码id查找对应的解码
      AVCodec *pCodec = avcodec_find_decoder(pCodecCtx->codec_id);
      //(迅雷看看,找不到解码器,临时下载一个解码器)
      if (pCodec == NULL)
      {
          LOGE("%s","找不到解码器\n");
          return;
      }

      //5.打开解码器
      if (avcodec_open2(pCodecCtx,pCodec,NULL)<0)
      {
          LOGE("%s","解码器无法打开\n");
          return;
      }

      //输出视频信息
      LOGI("视频的文件格式:%s",pFormatCtx->iformat->name);
      LOGI("视频时长:%ld", (pFormatCtx->duration)/1000000);
      LOGI("视频的宽高:%d,%d",pCodecCtx->width,pCodecCtx->height);
      LOGI("解码器的名称:%s",pCodec->name);

      //准备读取
      //AVPacket用于存储一帧一帧的压缩数据(H264)
      //缓冲区,开辟空间
      AVPacket *packet = (AVPacket*)av_malloc(sizeof(AVPacket));

      //AVFrame用于存储解码后的像素数据(YUV)
      //内存分配
      AVFrame *pFrame = av_frame_alloc();
      //YUV420
      AVFrame *pFrameYUV = av_frame_alloc();
      //只有指定了AVFrame的像素格式、画面大小才能真正分配内存
      //缓冲区分配内存
      uint8_t *out_buffer = (uint8_t *)av_malloc(avpicture_get_size(AV_PIX_FMT_YUV420P, pCodecCtx->width, pCodecCtx->height));
      //初始化缓冲区
      avpicture_fill((AVPicture *)pFrameYUV, out_buffer, AV_PIX_FMT_YUV420P, pCodecCtx->width, pCodecCtx->height);

      //用于转码(缩放)的参数,转之前的宽高,转之后的宽高,格式等
      struct SwsContext *sws_ctx = sws_getContext(pCodecCtx->width,pCodecCtx->height,pCodecCtx->pix_fmt,
          pCodecCtx->width, pCodecCtx->height, AV_PIX_FMT_YUV420P,
          SWS_BICUBIC, NULL, NULL, NULL);


      int got_picture, ret;

      FILE *fp_yuv = fopen(output_cstr, "wb+");

      int frame_count = 0;

      //6.一帧一帧的读取压缩数据
      while (av_read_frame(pFormatCtx, packet) >= 0)
      {
          //只要视频压缩数据(根据流的索引位置判断)
          if (packet->stream_index == v_stream_idx)
          {
              //7.解码一帧视频压缩数据,得到视频像素数据
              ret = avcodec_decode_video2(pCodecCtx, pFrame, &got_picture, packet);
              if (ret < 0)
              {
                  LOGE("%s","解码错误");
                  return;
              }

              //为0说明解码完成,非0正在解码
              if (got_picture)
              {
                  //AVFrame转为像素格式YUV420,宽高
                  //2 6输入、输出数据
                  //3 7输入、输出画面一行的数据的大小 AVFrame 转换是一行一行转换的
                  //4 输入数据第一列要转码的位置 从0开始
                  //5 输入画面的高度
                  sws_scale(sws_ctx, pFrame->data, pFrame->linesize, 0, pCodecCtx->height,
                      pFrameYUV->data, pFrameYUV->linesize);

                  //输出到YUV文件
                  //AVFrame像素帧写入文件
                  //data解码后的图像像素数据(音频采样数据)
                  //Y 亮度 UV 色度(压缩了) 人对亮度更加敏感
                  //U V 个数是Y的1/4
                  int y_size = pCodecCtx->width * pCodecCtx->height;
                  fwrite(pFrameYUV->data[0], 1, y_size, fp_yuv);
                  fwrite(pFrameYUV->data[1], 1, y_size / 4, fp_yuv);
                  fwrite(pFrameYUV->data[2], 1, y_size / 4, fp_yuv);

                  frame_count++;
                  LOGI("解码第%d帧",frame_count);
              }
          }

          //释放资源
          av_free_packet(packet);
      }

      fclose(fp_yuv);

      (*env)->ReleaseStringUTFChars(env,input_jstr,input_cstr);
      (*env)->ReleaseStringUTFChars(env,output_jstr,output_cstr);

      av_frame_free(&pFrame);

      avcodec_close(pCodecCtx);

      avformat_free_context(pFormatCtx);

  }
  1. Activity 实现
public class MainActivity extends AppCompatActivity {


    private boolean permission;
    private VideoThread videoThread;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        requestPermission();
        videoThread = new VideoThread();
    }

    /**
     * 获取权限
     */
    private void requestPermission() {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
            String[] perms = {"android.permission.WRITE_EXTERNAL_STORAGE"};
            if (checkSelfPermission(perms[0]) == PackageManager.PERMISSION_DENIED) {
                permission = false;
                requestPermissions(perms, 200);
            } else {
                permission = true;
            }
        }
    }

    @Override
    public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
        super.onRequestPermissionsResult(requestCode, permissions, grantResults);
        if (requestCode == 200) {
            if (grantResults[0] == PackageManager.PERMISSION_GRANTED) {
                permission = true;
            }
        }
    }

    public void decode(View view) {
        if (!permission) {
            Toast.makeText(this, "请允许存储权限", Toast.LENGTH_SHORT).show();
            requestPermission();
            return;
        }
        if (videoThread == null) {
            videoThread = new VideoThread();
        }
        try {
            if(!videoThread.isAlive()) {
                videoThread.start();
            }
        }catch (Exception e){
            e.printStackTrace();
        }

    }


    @Override
    protected void onDestroy() {
        videoThread = null;
        super.onDestroy();
    }

    public static class VideoThread extends Thread {
        @Override
        public void run() {
            super.run();
            String input = Environment.getExternalStorageDirectory().getAbsolutePath() + "/input.mp4";
            String output = Environment.getExternalStorageDirectory().getAbsolutePath() + "/out.yuv";
            VideoUtils.decode(input, output);
            Log.i("Activity","编码完成");
        }
    }
}

  1. 运行程序,安装到手机,进行解码成 yuv