FFmpeg学习记录



  • FFmpeg编译

    1. 编译环境与版本

    ubuntu 18.04

    ffmpeg-3.4.7

    android-ndk-r14b-linux-x86_64

    2. 修改生成的动态链接库名称

    默认编译生成的动态链接库名称格式为 xx.so.版本号

    由于Android工程只支持导入 xx.so 格式的动态链接库

    故修改生成的动态链接库格式为 xx.so

    # 将 ffmpeg 源码中的 configure 文件中的:
    SLIBNAME_WITH_MAJOR='(SLIBNAME).(SLIBNAME).(LIBMAJOR)' 
    LIB_INSTALL_EXTRA_CMD='$$(RANLIB) "$(LIBDIR)/$(LIBNAME)"' 
    SLIB_INSTALL_NAME='$(SLIBNAME_WITH_VERSION)' 
    SLIB_INSTALL_LINKS='$(SLIBNAME_WITH_MAJOR) $(SLIBNAME)'
    
    #替换为:
    SLIBNAME_WITH_MAJOR='$(SLIBPREF)$(FULLNAME)-$(LIBMAJOR)$(SLIBSUF)'
    LIB_INSTALL_EXTRA_CMD='$$(RANLIB) "(LIBDIR)/(LIBDIR)/(LIBNAME)"'
    SLIB_INSTALL_NAME='(SLIBNAMEWITHMAJOR)SLIBINSTALLLINKS=(SLIBNAME_WITH_MAJOR)'
    SLIB_INSTALL_LINKS='(SLIBNAME)'
    

    3. 使用configure对ffmpeg进行剪裁配置

    通过 ./configure 命令后面加上配置参数对 ffmpeg 进行修剪

    由于参数列表过长

    我们可以编写一个bash脚本来方便运行我们的命令

    ps:网上有许多剪裁配置脚本,此处只是列出其中一个,待我参透剪裁命令之后再做解释

    #!/bin/bash
    
    # 清除上次编译的文件
    make clean
    
    # 配置编译的ndk路径
    export NDK= /home/xhw/Documents/ffmpegWork/android-ndk-r14b-linux-x86_64/android-ndk-r14b
    export PREBUILT= $NDK/toolchains/arm-linux-androideabi-4.9/prebuilt
    
    # 支持的最低 android 版本
    export PLATFORM= $NDK/platforms/android-16/arch-arm
    
    # 配置编译生成的文件路径
    # 此处设置为上一级目录的simplefflib文件夹下
    export PREFIX= ../simplefflib
    
    # 配置命令函数
    build_one(){
    ./configure --target-os=linux --prefix=$PREFIX \
    --enable-cross-compile \
    --enable-runtime-cpudetect \
    --disable-asm \
    --arch=arm \
    --cc=$PREBUILT/linux-x86_64/bin/arm-linux-androideabi-gcc \
    --cross-prefix=$PREBUILT/linux-x86_64/bin/arm-linux-androideabi- \
    --disable-stripping \
    --nm=$PREBUILT/linux-x86_64/bin/arm-linux-androideabi-nm \
    --sysroot=$PLATFORM \
    --enable-gpl --enable-shared --disable-static --enable-small \
    --disable-ffprobe --disable-ffplay --disable-ffmpeg --disable-ffserver --disable-debug \
    --extra-cflags="-fPIC -DANDROID -D__thumb__ -mthumb -Wfatal-errors -Wno-deprecated -mfloat-abi=softfp -marm -march=armv7-a"
    }
    
    # 运行配置命令
    build_one
    

    4. 编译安装ffmpeg

    由于编译并不一定是一帆风顺的

    所以我们没有把这两个命令直接写入脚本中

    而是推荐单独运行以便于出现错误时及时解决

    # 编译
    make
    
    # 安装
    make install
    

    5. 错误记录

    • C compiler test failed.

      经过多次试验发现,此问题大概率是因为ffmpeg和ndk的版本不合适所导致的

      推荐选择较低版本的ffmpeg和ndk



  • 移植FFmpeg到Android平台

    由于在ubuntu编译出来的FFmpeg动态链接库并不能直接在Android平台使用

    所以我们需要使用 ndk 进行进一步的交叉编译

    使得编译出来的动态链接库和我们的Android平台CPU架构相对应

    1. 编译方式

    本文采用 ndk + JNI + Android.mk + Application.mk 的方式进行编译

    2. 编写调用C语言函数的java类

    阅读本文前可以先了解JNI的相关内容来帮助理解

    由于java并不能直接调用C语言的函数

    所以我们需要编写一个java类和函数

    来与我们希望调用的C语言函数一一对应

    package com.jniutil;
    
    public class FFmpegJni {
        public static native int run(String[] commands) ;
    }
    

    3. 生成java类对应的C语言函数头文件

    我们上面的 java 类包名为 com.jniutil

    故切换到 com 包的上一级目录下运行命令

    javah -classpath . com.jniutil.FFmpegJni
    

    运行结束后我们可以在 com 包的同级目录下找到生成的C语言头文件

    com_jniutil_FFmpegJni.h

    /* DO NOT EDIT THIS FILE - it is machine generated */
    #include <jni.h>
    /* Header for class com_jniutil_FFmpegJni */
    
    #ifndef _Included_com_jniutil_FFmpegJni
    #define _Included_com_jniutil_FFmpegJni
    #ifdef __cplusplus
    extern "C" {
    #endif
    /*
     * Class:     com_jniutil_FFmpegJni
     * Method:    run
     * Signature: ([Ljava/lang/String;)I
     */
    JNIEXPORT jint JNICALL Java_com_jniutil_FFmpegJni_run
      (JNIEnv *, jclass, jobjectArray);
    
    #ifdef __cplusplus
    }
    #endif
    #endif
    

    我们可以发现生成的文件名和函数名都特别有规律

    都是在java的不同包名类名函数名之间加上下划线拼接而成的

    4. 编写C语言实现函数

    为了方便进行编译工作

    我们可以在 main 文件夹下新建一个 jni_cmd 文件夹

    有关 ffmpeg 编译的文件都会放在该文件夹下

    故我们将刚刚生成的 com_jniutil_FFmpegJni.h 文件剪切到 jni_cmd 文件夹下

    并创建一个 com_jniutil_FFmpegJni.c 文件

    文件内容如下

    // 导入一个 android_log.h 用于将FFmpeg的输出内容打印到我们的控制台中
    // 这个文件我们等一下会实现
    #include "android_log.h"
    
    // 导入该C语言文件的头文件
    #include "com_jniutil_FFmpegJni.h"
    
    // 导入头文件 ffmpeg.h
    // 这个文件在 ffmpeg 源码中的 fftools 文件夹下
    // 用于我们调用 ffmpeg 中的函数
    #include "ffmpeg.h"
    
    JNIEXPORT jint JNICALL Java_com_jniutil_FFmpegJni_run
      (JNIEnv *env, jclass obj, jobjectArray commands){
      	  // 获取传入的String数组长度
          int argc = (*env)->GetArrayLength(env, commands);
          // 根据String数组长度创建Char数组
          char *argv[argc];
          int i;
          // 复制String数组的内容到char数组
          for (i = 0; i < argc; i++) {
              jstring js = (jstring) (*env)->GetObjectArrayElement(env, commands, i);
              argv[i] = (char*) (*env)->GetStringUTFChars(env, js, 0);
          }
          // LOGD是我们在android_log.h中定义的一个宏
          // 对应一个具体的打印函数输出到控制台
          LOGD("----------begin----------");
          
          // 调用ffmpeg中的main函数并传入我们的命令行参数
          // 此处实现了用命令行调用ffmpeg
          // 可以不用手动调用ffmpeg复杂的api方法
          return main(argc, argv);
    }
    

    5. 添加编译依赖文件

    • 实现 android_log.h 内容如下

      #ifdef ANDROID
      #include <android/log.h>
      #ifndef LOG_TAG
      #define  MY_TAG   "MYTAG@@@"
      #define  AV_TAG   "AVLOG@@@"
      #endif
      #define LOGE(format, ...)  __android_log_print(ANDROID_LOG_ERROR, MY_TAG, format, ##__VA_ARGS__)
      #define LOGD(format, ...)  __android_log_print(ANDROID_LOG_DEBUG,  MY_TAG, format, ##__VA_ARGS__)
      #define  XLOGD(...)  __android_log_print(ANDROID_LOG_INFO,AV_TAG,__VA_ARGS__)
      #define  XLOGE(...)  __android_log_print(ANDROID_LOG_ERROR,AV_TAG,__VA_ARGS__)
      #else
      #define LOGE(format, ...)  printf(MY_TAG format "\n", ##__VA_ARGS__)
      #define LOGD(format, ...)  printf(MY_TAG format "\n", ##__VA_ARGS__)
      #define XLOGE(format, ...)  fprintf(stdout, AV_TAG ": " format "\n", ##__VA_ARGS__)
      #define XLOGI(format, ...)  fprintf(stderr, AV_TAG ": " format "\n", ##__VA_ARGS__)
      #endif
      
    • 从FFmpeg源码中复制以下文件

      ffmpeg.c ffmpeg.h

      cmdutils.c cmdutils.h

      ffmpeg_filter.c

      ffmpeg_opt.c

      ffmpeg_hw.c (该文件可能视版本而定不一定要添加

    • 复制FFmpeg源码

      拷贝整个FFmpeg源码文件夹到 jni_cmd 目录下

    • 复制ubuntu编译生成动态链接库文件夹

      拷贝动态链接库文件夹到 jni_cmd 目录下

    6. 修改ffmpeg.c文件

    • 添加日志打印头文件

      #include "android_log.h"
      
    • 实现log_callback_null方法

      这个方法在ffmpeg.c中原本就有

      只是没有实现,是一个空方法

      我们实现之后可以将ffmpeg内部的log输出到控制台上方便进行调试

      static void log_callback_null(void *ptr, int level, const char *fmt, va_list vl)
      {
          static int print_prefix = 1;
          static int count;
          static char prev[1024];
          char line[1024];
          static int is_atty;
          av_log_format_line(ptr, level, fmt, vl, line, sizeof(line), &print_prefix);
          strcpy(prev, line);
          if (level <= AV_LOG_WARNING){
              XLOGE("%s", line);
          }else{
              XLOGD("%s", line);
          }
      }
      
    • 设置回调方法为log_callback_null

      在main函数开头添加回调设置

      int main(int argc, char **argv)
      {
          av_log_set_callback(log_callback_null);
          int i, ret;
          ......
      
    • 修改ffmpeg_cleanup方法

      由于在Android中执行一条ffmpeg命令后并不需要退出程序

      所以需要我们在每执行一条命令后都对变量进行初始化以便于重复使用

      故在ffmpeg_cleanup方法结尾添加以下代码

      filtergraphs = NULL;
      nb_filtergraphs = 0;
      output_files = NULL;
      nb_output_files = 0;
      output_streams = NULL;
      nb_output_streams = 0;
      input_files = NULL;
      nb_input_files = 0;
      input_streams = NULL;
      nb_input_streams = 0;
      
    • 调用ffmpeg_cleanup方法

      在main函数的最后手动调用一次ffmpeg_cleanup来进行初始化

          ......
          ffmpeg_cleanup(0);
          return main_return_code;
      }
      

    7. 修改cmdutils.c文件

    // exit_program原方法
    void exit_program(int ret)
    {
        if (program_exit)
            program_exit(ret);
    
        exit(ret);
    }
    
    // 修改exit_program方法如下
    // 使其在执行完一条命令后不会主动退出程序
    int exit_program(int ret)
    {
       return ret;
    }
    

    同时我们也需要更新一下头文件cmdutils.h的函数定义

    // 原定义
    void exit_program(int ret) av_noreturn;
    
    // 修改为
    int exit_program(int ret);
    

    8. 编写Application.mk文件

    # 希望编译得到的运行平台cpu架构
    # 我使用的是小米6骁龙835
    APP_ABI := armeabi-v7a
    
    # 最低的android版本
    APP_PLATFORM := android-20
    

    9. 编写Android.mk文件

    LOCAL_PATH:= $(call my-dir)
    
    # 定义两个变量记录了动态链接库 include 和 lib 文件位置
    INCLUDE_PATH := ./simplefflib/include
    FFMPEG_LIB_PATH := ./simplefflib/lib
    
    # 编译libavcodec-57.so
    include $(CLEAR_VARS)
    LOCAL_MODULE:= libavcodec
    LOCAL_SRC_FILES:= $(FFMPEG_LIB_PATH)/libavcodec-57.so
    LOCAL_EXPORT_C_INCLUDES := $(INCLUDE_PATH)
    include $(PREBUILT_SHARED_LIBRARY)
    
    # 编译libavformat-57.so
    include $(CLEAR_VARS)
    LOCAL_MODULE:= libavformat
    LOCAL_SRC_FILES:= $(FFMPEG_LIB_PATH)/libavformat-57.so
    LOCAL_EXPORT_C_INCLUDES := $(INCLUDE_PATH)
    include $(PREBUILT_SHARED_LIBRARY)
    
    # 编译libswscale-4.so
    include $(CLEAR_VARS)
    LOCAL_MODULE:= libswscale
    LOCAL_SRC_FILES:= $(FFMPEG_LIB_PATH)/libswscale-4.so
    LOCAL_EXPORT_C_INCLUDES := $(INCLUDE_PATH)
    include $(PREBUILT_SHARED_LIBRARY)
    
    # 编译libavutil-55.so
    include $(CLEAR_VARS)
    LOCAL_MODULE:= libavutil
    LOCAL_SRC_FILES:= $(FFMPEG_LIB_PATH)/libavutil-55.so
    LOCAL_EXPORT_C_INCLUDES := $(INCLUDE_PATH)
    include $(PREBUILT_SHARED_LIBRARY)
    
    # 编译libavfilter-6.so
    include $(CLEAR_VARS)
    LOCAL_MODULE:= libavfilter
    LOCAL_SRC_FILES:= $(FFMPEG_LIB_PATH)/libavfilter-6.so
    LOCAL_EXPORT_C_INCLUDES := $(INCLUDE_PATH)
    include $(PREBUILT_SHARED_LIBRARY)
    
    # 编译libswresample-2.so
    include $(CLEAR_VARS)
    LOCAL_MODULE:= libswresample
    LOCAL_SRC_FILES:= $(FFMPEG_LIB_PATH)/libswresample-2.so
    LOCAL_EXPORT_C_INCLUDES := $(INCLUDE_PATH)
    include $(PREBUILT_SHARED_LIBRARY)
    
    # 编译libpostproc-54.so
    include $(CLEAR_VARS)
    LOCAL_MODULE:= libpostproc
    LOCAL_SRC_FILES:= $(FFMPEG_LIB_PATH)/libpostproc-54.so
    LOCAL_EXPORT_C_INCLUDES := $(INCLUDE_PATH)
    include $(PREBUILT_SHARED_LIBRARY)
    
    # 编译libavdevice-57.so
    include $(CLEAR_VARS)
    LOCAL_MODULE:= libavdevice
    LOCAL_SRC_FILES:= $(FFMPEG_LIB_PATH)/libavdevice-57.so
    LOCAL_EXPORT_C_INCLUDES := $(INCLUDE_PATH)
    include $(PREBUILT_SHARED_LIBRARY)
    
    # 编译我们自己编写的C语言文件com_jniutil_FFmpegJni.c
    include $(CLEAR_VARS)
    # 希望编译的得到的模块名称
    # 此处我们最后会得到一个 libffmpeg-cmd.so
    LOCAL_MODULE := ffmpeg-cmd
    # 编译时所依赖的文件需要一起编译
    LOCAL_SRC_FILES := com_jniutil_FFmpegJni.c \
                      cmdutils.c \
                      ffmpeg.c \
                      ffmpeg_opt.c \
                      ffmpeg_filter.c \
                      ffmpeg_hw.c
    # 依赖的源码
    LOCAL_C_INCLUDES := ./ffmpeg-3.4.7
    LOCAL_LDLIBS := -lm -llog
    LOCAL_SHARED_LIBRARIES := libavcodec libavfilter libavformat libavutil libswresample libswscale libavdevice
    include $(BUILD_SHARED_LIBRARY)
    
    

    10. 最后生成的文件夹结构如下

    0_1599383310524_Snipaste_2020-04-25_18-20-59.png

    其中 libs 和 obj 文件夹是最后编译得到的文件,未编译暂时不存在

    simplefflib 是ubuntu编译得到的动态链接库,包括 include、lib、share 三个文件夹

    11. ndk编译

    在 jni_cmd 目录下运行命令

    需要配置ndk环境才可以直接使用ndk-build命令

    得到的libs文件夹就是我们编译完成可以使用的动态链接库

    ndk-build NDK_PROJECT_PATH=. APP_BUILD_SCRIPT=./Android.mk NDK_APPLICATION_MK=./Application.mk
    

    12. 使用动态链接库

    在build.gradle中设置我们依赖的动态链接库目录路径

    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
        }
    
        sourceSets.main {
            jni.srcDirs = []
            jniLibs.srcDirs = ['src/main/jni_cmd/libs']
        }
    }
    

    有必要的话有也可以设置一下app限定运行的cpu架构

    defaultConfig {
        applicationId "com.test"
        minSdkVersion 16
        targetSdkVersion 28
        versionCode 1
        versionName "1.0"
    
        testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
    
        ndk {
            abiFilters 'armeabi-v7a'
        }
    }
    
    

    在MainActivity中

    // 引入依赖的动态链接库
    static {
        System.loadLibrary("avutil-55");
        System.loadLibrary("avcodec-57");
        System.loadLibrary("avformat-57");
        System.loadLibrary("avdevice-57");
        System.loadLibrary("swresample-2");
        System.loadLibrary("swscale-4");
        System.loadLibrary("postproc-54");
        System.loadLibrary("avfilter-6");
        System.loadLibrary("ffmpeg-cmd");
    }
    
    // 调用java中的native函数接口
    String[] cmd = new String[]{"ffmpeg", "-i", source, "-acodec", "copy", "-vn", target} ;
    FFmpegJni.run(cmd) ;
    
    

    13. 错误记录

    • error: undefined reference to 'postproc_version'

      删除cmdutils.c中的

      PRINT_LIB_INFO(postproc, POSTPROC, flags, level)

    • error: invalid suffix on literal; C++11 requires a space between literal and identifier [-Wreserved-user-defined-literal] snprintf(name, sizeof(name), "0x%"PRIx64, ch_layout)

      修改cmdutils.h中的"0x%"PRIx64

      为"0x%" PRIx64

      即中间加一个空格

    • 出现一些变量名称冲突报错比如B0

      修改B0为b0



  • 使用FFmpeg播放音乐

    本文使用 ffmpeg-3.4.7 + AudioTrack 播放音乐

    1. 准备工作

    由于需要借助 AudioTrack 来播放音乐,所以我们先编写一个java类做好AudioTrack的准备工作,

    设计好接口以供 ffmpeg 来反射调用,java类举例如下

    public class MusicPlay {
        //导入所需要的依赖库
        static{
            System.loadLibrary("avutil-55");
            System.loadLibrary("avcodec-57");
            System.loadLibrary("avformat-57");
            System.loadLibrary("avdevice-57");
            System.loadLibrary("swresample-2");
            System.loadLibrary("swscale-4");
            System.loadLibrary("postproc-54");
            System.loadLibrary("avfilter-6");
            System.loadLibrary("ffaudio");
        }
        
        //真正播放音乐的方法,在C语言代码中实现
        public native void playSound(String input);
        
        //供外部调用的播放方法,开启播放线程
        public void play(final String input){
            new Thread(new Runnable() {
                @Override
                public void run() {
                    playSound(input) ;
                }
            }).start() ;
        }
        
        //AudioTrack类对象
        private AudioTrack audioTrack;
    
        //由C语言调用,传入采样率和通道数
        //根据参数得到最小缓冲区大小,构造AudioTrack对象
        public void createTrack(int sampleRateInHz, int nb_channals) {
    
            int channaleConfig ;//通道数
            if (nb_channals == 1) {
                channaleConfig = AudioFormat.CHANNEL_OUT_MONO;
            } else if (nb_channals == 2) {
                channaleConfig = AudioFormat.CHANNEL_OUT_STEREO;
            }else {
                channaleConfig = AudioFormat.CHANNEL_OUT_MONO;
            }
            int buffersize = AudioTrack.getMinBufferSize(sampleRateInHz,
                    channaleConfig, AudioFormat.ENCODING_PCM_16BIT);
            audioTrack = new AudioTrack(
                AudioManager.STREAM_MUSIC, sampleRateInHz, channaleConfig,
                AudioFormat.ENCODING_PCM_16BIT, buffersize,AudioTrack.MODE_STREAM) ;
            audioTrack.play();
        }
        
        //由C语言调用,传入音频数据来播放音频
        public void playTrack(byte[] buffer, int lenth) {
            if (audioTrack != null && audioTrack.getPlayState() == AudioTrack.PLAYSTATE_PLAYING) {
                audioTrack.write(buffer, 0, lenth);
            }
        }
    }
    

    该java类实现了两个重要方法

    • createTrack 传入参数构造AudioTrack对象
    • playTrack 传入音频数据播放音频

    2. 实现C语言playSound函数代码

    • 初始化

      /*
      *参数列表
      *(JNIEnv *env, jobject instance, jstring input_)
      */
      
      //转换jstring为char*
      const char *input = (*env)->GetStringUTFChars(env, input_, 0) ;
      
      //注册ffmpeg组件
      av_register_all();
      
    • 打开文件

      //申请AVFormatContext对象,该对象用于保存音视频信息
      AVFormatContext *pFormatCtx = avformat_alloc_context() ;
      
      //打开输入音视频文件
      if (avformat_open_input(&pFormatCtx, input, NULL, NULL) != 0) {
          LOGE("%s", "打开输入音视频文件失败")
          return ;
      }
      
    • 获取音视频信息

      if(avformat_find_stream_info(pFormatCtx,NULL) < 0){
          LOGE("%s", "获取音视频信息失败")
          return ;
      }
      
    • 寻找音频流

      由于一个视频文件包含有视频流、音频流、字幕流等多个流信息

      所以我们要遍历找到音频流信息

      //音频流索引
      int audio_stream_idx = -1 ;
      int i = 0 ;
      //number of streams 流的数量
      for (i = 0; i < pFormatCtx->nb_streams ; i++) {
          //codecpar => codec parameter 是保存流参数的对象
          if (pFormatCtx->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_AUDIO) {
              audio_stream_idx = i ;
              break ;
          }
      }
      
    • 获取解码器上下文

      //申请解码器上下文对象
      AVCodecContext *pCodecCtx = avcodec_alloc_context3(NULL) ;
      if(pCodecCtx == NULL){
          LOGE("%s", "获取解码器上下文失败")
          return ;
      }
      //将音频流的参数信息拷贝到解码器上下文当中
      avcodec_parameters_to_context(pCodecCtx, 
                                    pFormatCtx->streams[audio_stream_idx]->codecpar) ;
      
    • 获取解码器

      //通过解码器上下文得到的解码器id获取对应的解码器
      AVCodec *pCodex = avcodec_find_decoder(pCodecCtx->codec_id) ;
      //打开解码器
      if (avcodec_open2(pCodecCtx, pCodex, NULL) < 0) {
          LOGE("%s", "打开解码器失败")
          return ;
      }
      
    • 设置默认播放音频参数

      //输出声道数为双声道
      uint64_t out_ch_layout = AV_CH_LAYOUT_STEREO ;
      //采样位数为16bit
      enum AVSampleFormat out_formart = AV_SAMPLE_FMT_S16;
      //采样率同输入采样率相同
      int out_sample_rate = pCodecCtx->sample_rate ;
      
    • 申请AVPacket和AVFrame以及缓冲区

      AVPacket 用于保存解码前的数据

      AVFrame 用于保存解码后的数据

      //申请AVPacket和AVFrame
      AVPacket *packet = (AVPacket *)av_malloc(sizeof(AVPacket)) ;
      AVFrame *frame = av_frame_alloc() ;
      
      //申请缓冲区
      uint8_t *out_buffer = (uint8_t *) av_malloc(44100 * 2) ;
      
    • 重采样对象初始化

      重采样就是将输入的音频数据转换为我们所期望的参数输出

      比如此处我们希望音频统一输出为双声道、16bit、44100采样率

      //申请重采样对象
      SwrContext *swrContext = swr_alloc() ;
      //设置重采样的参数
      /*
      *	1.重采样对象 2. 输出通道数 3.输出采样位数 4.输出采样率
      *	5.输入通道数 6.输入采样位数 7.输入采样率 8.9.和log相关可忽略
      */
      swr_alloc_set_opts(swrContext, out_ch_layout, out_formart, out_sample_rate,
                             pCodecCtx->channel_layout, pCodecCtx->sample_fmt, pCodecCtx->sample_rate, 0,
                             NULL);
      //初始化重采样对象
      swr_init(swrContext) ;
      
    • 利用反射对AudioTrack进行初始化

      GetMethodID的最后一个参数指的是方法签名

      签名格式为(参数类型)返回值类型

      比如函数 void createTrack(int , int) 其方法签名为 (II)V

      两个 int 缩写为大写的 I

      返回值 void 缩写为大写的 V

      关于其他参数的方法签名缩写可自行查阅

      //通过双声道参数得到声道数,其实就是2
      int out_channel_nb = av_get_channel_layout_nb_channels(AV_CH_LAYOUT_STEREO);
      
      //得到Class对象
      jclass david_player = (*env)->GetObjectClass(env, instance);
      
      //得到Method对象,createTrack方法
      jmethodID createAudio = (*env)->GetMethodID(env, david_player, "createTrack", "(II)V");
      
      //调用createTrack方法
      (*env)->CallVoidMethod(env, instance, createAudio, 44100, out_channer_nb) ;
      
      //得到Method对象,playTrack方法
      jmethodID audio_write = (*env)->GetMethodID(env, david_player, "playTrack", "([BI)V");
      
    • 音频解码播放

      以下就是主要的音频解码播放部分

      主要流程为通过while循环不断读取音频流数据 => 音频解码 => 重采样 => 反射调用playTarck播放

      //读取未解码数据保存到packet中
      while (av_read_frame(pFormatCtx, packet) >= 0) {
          //判断是否是音频流的数据
          if (packet->stream_index == audio_stream_idx) {
              //调用avcodec_send_packet函数进行解码
              if(avcodec_send_packet(pCodecCtx, packet) == 0){
                  //通过while循环接收解码数据保存到frame中
                  while(avcodec_receive_frame(pCodecCtx, frame) == 0){
                      //进行重采样,并把重采样结果输出到out_buffer缓冲区中
                      swr_convert(swrContext, &out_buffer, 44100 * 2, 
                                  (const uint8_t **) frame->data, frame->nb_samples);
                      //获取缓冲区数据长度
                      int size = av_samples_get_buffer_size(NULL, out_channel_nb, frame->nb_samples,
                                                            AV_SAMPLE_FMT_S16, 1);
                      //构造byte[]数组用于反射调用playTrack方法的参数
                      jbyteArray audio_sample_array = (*env)->NewByteArray(env, size);
                      //拷贝缓冲区数据到byte[]数组中
                      (*env)->SetByteArrayRegion(env, audio_sample_array, 0, size, (const jbyte *) out_buffer);
                      //反射调用playTrack方法
                      (*env)->CallVoidMethod(env, instance, audio_write, audio_sample_array, size);
                      //释放byte[]数组占用
                      (*env)->DeleteLocalRef(env, audio_sample_array);
                  }
              }else{
                  LOGE("%s", "音视频解码失败")
              }
          }
      }
      
    • 释放变量内存

      av_frame_free(&frame) ;
      swr_free(&swrContext) ;
      avcodec_close(pCodecCtx) ;
      avformat_close_input(&pFormatCtx) ;
      (*env)->ReleaseStringUTFChars(env, input_, input) ;
      


  • FFmpeg常用命令

    • ffmpeg -i [input_name] -vcodec copy -acodec copy -f flv [output_name]

      -i 指定输入文件路径

      -vcodec copy 复制视频编码方式

      -acodec copy 复制音频编码方式

      -f flv 指定输出文件格式为flv

    • ffmpeg -i [input_name] -vcodec copy -acodec copy

      -ss 00:00:20 -t 00:00:10 [output_name]

      -ss 00:00:20 从视频20s开始剪辑

      -t 00:00:10 剪辑时长10s

    • ffmpeg -i [input_name] -vn -acodec aac [output_name]

      -vn 取消视频输出

      -an 取消音频输出

      -acodec aac 指定音频编码方式为AAC

    • ffmpeg -i [input_name] -b:v 2000k -bufsize 2000k -maxrate 2500k [output_name]

      -b:v 2000k 指定转换码率为2000kbps

      -bufsize 2000k 设置码率控制缓冲器大小有助于减少码率波动

      -maxrate 2500k 设置码率波动不超过的最大阈值



  • 补充一个可以把movv box放到视频开头的指令:
    -movflags +faststart (较高版本中不需要+)
    movv box 中包括 mvhd 等媒体信息,放在开头可以使得网络请求中优先传输视频的metadata


 

Copyright © 2018 bbs.dian.org.cn All rights reserved.

与 Dian 的连接断开,我们正在尝试重连,请耐心等待