查看原文
其他

NDK FFmpeg 音视频解码

haohao 字节流动 2022-05-24



阅读本文大概需要 3.28 分钟。



诸葛亮躬耕隆中的时候,与好友石广元、徐庶、孟公威在一起求学,其他三人读书都是一定要滚瓜烂熟,只有诸葛亮只观其大略。



本篇介绍一下 NDK FFmpeg 音视频解码。

前期推送的 NDK 开发系列文章:


1. 封装格式


我们经常所说的视频格式,如 mp4 、 mkv 、 rmvb 、flv 等,表示的是音视频的封装格式,封装格式实质上是把音频数据、视频数据和字幕数据打包成一个文件的规范。从技术的角度来讲,优秀的音视频封装格式应该支持大多数音视频编码标准。


主要的封装格式


名称机构支持的视频编码支持的音频编码使用领域
avi微软几乎所有格式几乎所有格式影视
mp4MPEG H.264 , H.263 等AAC , MPEG-1 等视频网站
flvAdobeVP6 , H.264MP3 , AAC 等视频网站
mkvCoreCodec几乎所有格式几乎所有格式视频网站
rmvbReal NetRealVideo AAC  等影视


2. 编码格式


编码的目的在于通过压缩算法降低数据量,提高数据的存储和传输效率。视频编码是将视频像素数据( RGB , YUV 等)压缩成为视频码流。音频编码是将音频采样数据( PCM 等)压缩成为音频码流。


主要视频编码格式


名称机构推出时间使用领域
H.265MPEG/ITU-T2013研发中
H.264MPEG/ITU-T2003各个领域
MPEG4MPEG2001小众
MPEG2MPEG1994数字电视
VP9Google2013研发中
VP8Google2008小众


主要音频编码格式


名称机构推出时间使用领域
AACMPEG1997各个领域
AC-3Dolby1992电影
MP3MPEG1993早期普及
WMV微软1999Windows


3. 音视频解码流程


音视频解码流程图


如上图所示,音视频解码流程可分为三步:

1. 解封装格式。将输入的按照一定格式封装的音视频数据,分离成为音频流压缩编码数据和视频流压缩编码数据。

2. 解码。将视频和音频的压缩编码数据,解码成为非压缩的视频和音频原始数据。视频压缩数据通过解码输出为像素数据,如 YUV420P 、 RGB 等;音频压缩数据通过解码输出为非压缩的音频抽样数据,如 PCM 数据。

3. 音视频同步。同步解码出来的视频和音频数据,并将音视频数据送至系统的声卡和显卡,播放和显示出来。


4. FFmpeg 函数库


FFmpeg 一般有 8 个函数库,各个函数库的功能如下:


函数库功能
avcodec音视频编解码
avdevice多媒体设备输入输出
avfilter滤镜特效
avformat封装格式处理
postproc后加工
avutil工具库
swresample音频采样数据格式转换
swscale视频像素数据格式转换


5. FFmpeg 音视频解码


FFmpeg 音视频解码主要流程代码描述:


1. av_register_all() //注册组件
2. avformat_alloc_context //获取封装格式上下文
3. avformat_find_stream_info //获取输入文件信息
4. avcodec_find_decoder //获取解码器
5. avcodec_open2 //打开解码器
6. avcodec_decode_video2 或 avcodec_decode_audio4 //解码音视频帧


在 AS 工程中引入 FFmpeg 8 个动态库和 libyuv (负责视频像素数据格式转换)动态库。


工程的头文件目录:



工程的动态库目录:



PS: 

1. AS 工程声明文件中需要添加文件读写权限;

2. 公众号后台回复 ffmpeglib 即可获取相应的函数库及头文件。


Java 层 API :


package com.haohao.ffmpeg;

import android.media.AudioFormat;
import android.media.AudioManager;
import android.media.AudioTrack;
import android.util.Log;
import android.view.Surface;

/**
* author: haohao
* time: 2017/12/19
* mail: haohaochang86@gmail.com
* desc: AVUtils
*/

public class AVUtils {
   private static final String TAG = "AVUtils";
   private static AVCallback AVCallback;
   private static AVCallback sAVCallback;
   public static void registerCallback(AVCallback callback) {
       sAVCallback = callback;
   }

   static {
       System.loadLibrary("avfilter-5");
       System.loadLibrary("avdevice-56");
       System.loadLibrary("yuv");
       System.loadLibrary("avutil-54");
       System.loadLibrary("swresample-1");
       System.loadLibrary("avcodec-56");
       System.loadLibrary("avformat-56");
       System.loadLibrary("swscale-3");
       System.loadLibrary("postproc-53");
       System.loadLibrary("native-lib");
   }
   /**
    * 解码视频中的视频压缩数据
    * @param input_file_path 输入的视频文件路径
    * @param output_file_path 视频压缩数据解码后输出的 YUV 文件路径
    */

   public static native void videoDecode(String input_file_path, String output_file_path);

   /**
    * 显示视频视频解码后像素数据
    * @param input 输入的视频文件路径
    * @param surface 用于显示视频视频解码后的 RGBA 像素数据
    */

   public static native void videoRender(String input, Surface surface);

   /**
    * 解码视频中的音频压缩数据
    * @param input 输入的视频文件路径
    * @param output 音频压缩数据解码后输出的 PCM 文件路径
    */

   public static native void audioDecode(String input, String output);

   /**
    * 播放视频中的音频数据
    * @param input 输入的视频文件路径
    */

   public static native void audioPlay(String input);

   /**
    * 创建一个 AudioTrack 对象,用于播放音频,在 Native 层中调用。
    */

   public static AudioTrack createAudioTrack(int sampleRate, int num_channel) {
       int audioFormat = AudioFormat.ENCODING_PCM_16BIT;
       Log.i(TAG, "声道数:" + num_channel);
       int channelConfig;
       if (num_channel == 1) {
           channelConfig = android.media.AudioFormat.CHANNEL_OUT_MONO;
       } else if (num_channel == 2) {
           channelConfig = android.media.AudioFormat.CHANNEL_OUT_STEREO;
       } else {
           channelConfig = android.media.AudioFormat.CHANNEL_OUT_STEREO;
       }

       int bufferSize = AudioTrack.getMinBufferSize(sampleRate, channelConfig, audioFormat);

       AudioTrack audioTrack = new AudioTrack(
               AudioManager.STREAM_MUSIC,
               sampleRate, channelConfig,
               audioFormat,
               bufferSize, AudioTrack.MODE_STREAM);
       return audioTrack;
   }

   public interface AVCallback {
       void onFinish();
   }
}


MySurfaceView.java 文件代码:


/**
* author: haohao
* time: 2017/12/20
* mail: haohaochang86@gmail.com
* desc: MySurfaceView
*/

public class MySurfaceView extends SurfaceView {
   public MySurfaceView(Context context) {
       super(context);
   }

   public MySurfaceView(Context context, AttributeSet attrs) {
       super(context, attrs);
   }

   public MySurfaceView(Context context, AttributeSet attrs, int defStyleAttr) {
       super(context, attrs, defStyleAttr);
   }

   private void init(){
       // 设置像素绘制格式为 RGBA_8888
       SurfaceHolder holder = getHolder();
       holder.setFormat(PixelFormat.RGBA_8888);
   }
}


布局文件 activity_main.xml 代码:


<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
   xmlns:app="http://schemas.android.com/apk/res-auto"
   xmlns:tools="http://schemas.android.com/tools"
   android:layout_width="match_parent"
   android:layout_height="match_parent">


   <com.haohao.ffmpeg.MySurfaceView
       android:id="@+id/my_surface_view"
       android:layout_width="match_parent"
       android:layout_height="match_parent" />

   <LinearLayout
       android:layout_width="match_parent"
       android:layout_height="wrap_content"
       android:orientation="vertical">

       <LinearLayout
           android:layout_width="match_parent"
           android:layout_height="wrap_content"
           android:alpha="0.7"
           android:orientation="horizontal">


           <Button
               android:id="@+id/video_decode_btn"
               android:layout_width="0dp"
               android:layout_weight="1"
               android:layout_height="wrap_content"
               android:text="视频解码" />


           <Button
               android:id="@+id/video_render_btn"
               android:layout_width="0dp"
               android:layout_weight="1"
               android:layout_height="wrap_content"
               android:text="视频渲染" />


       </LinearLayout>
       <LinearLayout
           android:layout_width="match_parent"
           android:layout_height="wrap_content"
           android:alpha="0.7"
           android:orientation="horizontal">


           <Button
               android:id="@+id/audio_decode_btn"
               android:layout_width="0dp"
               android:layout_weight="1"
               android:layout_height="wrap_content"
               android:text="音频解码" />


           <Button
               android:id="@+id/audio_play_btn"
               android:layout_width="0dp"
               android:layout_weight="1"
               android:layout_height="wrap_content"
               android:text="音频播放" />


       </LinearLayout>
   </LinearLayout>
</FrameLayout>


MainActivity.java 文件代码:


public class MainActivity extends AppCompatActivity implements View.OnClickListener, AVUtils.AVCallback {
   private static final String TAG = "MainActivity";
   private static final String BASE_PATH = Environment.getExternalStorageDirectory().getAbsolutePath() + File.separatorChar;

   private String input_video_file_path = BASE_PATH
           + "input.mp4";
   private String output_video_file_path = BASE_PATH
           + "output.yuv";
   private String input_audio_file_path = BASE_PATH
           + "hello.mp3";
   private String output_audio_file_path = BASE_PATH
           + "hello.pcm";
   private String video_src = BASE_PATH
           + "ffmpeg.mp4";
   private Button mDecodeVideoBtn;
   private Button mVideoRenderBtn;
   private Button mAudioPlayBtn, mAudioDecodeBtn;
   private ProgressDialog mProgressDialog;
   private ExecutorService mExecutorService;
   private MySurfaceView mySurfaceView;

   @Override
   protected void onCreate(Bundle savedInstanceState) {
       super.onCreate(savedInstanceState);
       setContentView(R.layout.activity_main);

       if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
           requestPermissions(new String[]{Manifest.permission.READ_EXTERNAL_STORAGE, Manifest.permission.WRITE_EXTERNAL_STORAGE, Manifest.permission.MOUNT_UNMOUNT_FILESYSTEMS}, 0);
       }

       mDecodeVideoBtn = (Button)findViewById(R.id.video_decode_btn);
       mVideoRenderBtn = (Button)findViewById(R.id.video_render_btn);
       mAudioDecodeBtn = (Button) findViewById(R.id.audio_decode_btn);
       mAudioPlayBtn = (Button)findViewById(R.id.audio_play_btn);

       mySurfaceView = (MySurfaceView) findViewById(R.id.my_surface_view);
       mDecodeVideoBtn.setOnClickListener(this);
       mVideoRenderBtn.setOnClickListener(this);
       mAudioDecodeBtn.setOnClickListener(this);
       mAudioPlayBtn.setOnClickListener(this);

       AVUtils.registerCallback(this);
       mProgressDialog = new ProgressDialog(this);
       mProgressDialog.setCanceledOnTouchOutside(false);
       mExecutorService = Executors.newFixedThreadPool(2);
   }

   @Override
   public void onClick(View view) {
       int id = view.getId();
       switch (id) {
           case R.id.video_decode_btn:
               mProgressDialog.setMessage("正在解码...");
               mProgressDialog.show();
               mExecutorService.submit(new Runnable() {
                   @Override
                   public void run() {
                       AVUtils.videoDecode(input_video_file_path, output_video_file_path);
                   }
               });

               break;
           case R.id.video_render_btn:
               mExecutorService.submit(new Runnable() {
                   @Override
                   public void run() {
                       AVUtils.videoRender(input_video_file_path, mySurfaceView.getHolder().getSurface());
                   }
               });
               break;
           case R.id.audio_decode_btn:
               mProgressDialog.setMessage("正在解码...");
               mProgressDialog.show();
               mExecutorService.submit(new Runnable() {
                   @Override
                   public void run() {
                       AVUtils.audioDecode(input_audio_file_path, output_audio_file_path);
                   }
               });
               break;
           case R.id.audio_play_btn:
               mExecutorService.submit(new Runnable() {
                   @Override
                   public void run() {
                       AVUtils.audioPlay(input_video_file_path);
                   }
               });
               break;
       }

   }

   @Override
   public void onFinish() {
       runOnUiThread(new Runnable() {
           @Override
           public void run() {
               if (mProgressDialog.isShowing()) {
                   mProgressDialog.dismiss();
               }
               Toast.makeText(MainActivity.this, "解码完成", Toast.LENGTH_SHORT).show();
           }
       });
   }

   @Override
   protected void onDestroy() {
       super.onDestroy();
       mExecutorService.shutdown();
   }
}


nativelib.c 文件代码:


#include <jni.h>
#include <string.h>
#include <android/log.h>
#include <stdio.h>
#include <libavutil/time.h>

//编码
#include "include/libavcodec/avcodec.h"
//封装格式处理
#include "include/libavformat/avformat.h"
//像素处理
#include "include/libswscale/swscale.h"

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

//中文字符串转换
jstring charsToUTF8String(JNIEnv *env, char *s) {
   jclass string_cls = (*env)->FindClass(env, "java/lang/String");
   jmethodID mid = (*env)->GetMethodID(env, string_cls, "<init>", "([BLjava/lang/String;)V");

   jbyteArray jb_arr = (*env)->NewByteArray(env, strlen(s));
   (*env)->SetByteArrayRegion(env, jb_arr, 0, strlen(s), s);

   jstring charset = (*env)->NewStringUTF(env, "UTF-8");

   return (*env)->NewObject(env, string_cls, mid, jb_arr, charset);
}

JNIEXPORT void JNICALL
Java_com_haohao_ffmpeg_AVUtils_videoDecode(JNIEnv *env, jclass type, jstring input_,
                                          jstring output_)
{

   //访问静态方法
   jmethodID mid = (*env)->GetStaticMethodID(env, type, "onNativeCallback", "()V");
   //需要转码的视频文件(输入的视频文件)
   const char *input = (*env)->GetStringUTFChars(env, input_, 0);
   const char *output = (*env)->GetStringUTFChars(env, output_, 0);

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

   //封装格式上下文,统领全局的结构体,保存了视频文件封装格式的相关信息
   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 v_stream_idx = -1;
   int i = 0;

   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;

   //根据编解码上下文中的编码 id 查找对应的解码器
   AVCodec *pCodec = avcodec_find_decoder(pCodecCtx->codec_id);

   if (pCodec == NULL) {
       LOGE("%s", "找不到解码器,或者视频已加密\n");
       return;
   }

   //打开解码器,解码器有问题(比如说我们编译FFmpeg的时候没有编译对应类型的解码器)
   if (avcodec_open2(pCodecCtx, pCodec, NULL) < 0) {
       LOGE("%s", "解码器无法打开\n");
       return;
   }

   //输出视频信息
   LOGI("视频的文件格式:%s", pFormatCtx->iformat->name);
   LOGI("视频时长:%lld", (pFormatCtx->duration) / (1000 * 1000));
   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, "wb+");

   int frame_count = 0;

   //一帧一帧的读取压缩数据
   while (av_read_frame(pFormatCtx, packet) >= 0) {
       //只要视频压缩数据(根据流的索引位置判断)
       if (packet->stream_index == v_stream_idx) {
           //解码一帧视频压缩数据,得到视频像素数据
           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);

   av_frame_free(&pFrame);

   avcodec_close(pCodecCtx);

   avformat_free_context(pFormatCtx);

   (*env)->ReleaseStringUTFChars(env, input_, input);
   (*env)->ReleaseStringUTFChars(env, output_, output);
   //通知 Java 层解码完毕
   (*env)->CallStaticVoidMethod(env, type, mid);
}

//使用这两个 Window 相关的头文件需要在 CMake 脚本中引入 android 库
#include <android/native_window_jni.h>
#include <android/native_window.h>
#include "include/yuv/libyuv.h"

JNIEXPORT void JNICALL
Java_com_haohao_ffmpeg_AVUtils_videoRender(JNIEnv *env, jclass type, jstring input_,
                                          jobject surface)
{
   //需要转码的视频文件(输入的视频文件)
   const char *input = (*env)->GetStringUTFChars(env, input_, 0);

   //注册所有组件
   av_register_all();
   //avcodec_register_all();

   //封装格式上下文,统领全局的结构体,保存了视频文件封装格式的相关信息
   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 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;

   //根据编解码上下文中的编码 id 查找对应的解码器
   AVCodec *pCodec = avcodec_find_decoder(pCodecCtx->codec_id);

   if (pCodec == NULL) {
       LOGE("%s", "找不到解码器,或者视频已加密\n");
       return;
   }

   //打开解码器,解码器有问题(比如说我们编译FFmpeg的时候没有编译对应类型的解码器)
   if (avcodec_open2(pCodecCtx, pCodec, NULL) < 0) {
       LOGE("%s", "解码器无法打开\n");
       return;
   }

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

   //AVFrame用于存储解码后的像素数据(YUV)
   //内存分配
   AVFrame *yuv_frame = av_frame_alloc();
   AVFrame *rgb_frame = av_frame_alloc();

   int got_picture, ret;
   int frame_count = 0;

   //窗体
   ANativeWindow *pWindow = ANativeWindow_fromSurface(env, surface);
   //绘制时的缓冲区
   ANativeWindow_Buffer out_buffer;

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

           //为0说明解码完成,非0正在解码
           if (got_picture) {

               //lock window
               //设置缓冲区的属性:宽高、像素格式(需要与Java层的格式一致)
               ANativeWindow_setBuffersGeometry(pWindow, pCodecCtx->width, pCodecCtx->height,
                                                WINDOW_FORMAT_RGBA_8888);
               ANativeWindow_lock(pWindow, &out_buffer, NULL);

               //初始化缓冲区
               //设置属性,像素格式、宽高
               //rgb_frame的缓冲区就是Window的缓冲区,同一个,解锁的时候就会进行绘制
               avpicture_fill((AVPicture *) rgb_frame, out_buffer.bits, AV_PIX_FMT_RGBA,
                              pCodecCtx->width,
                              pCodecCtx->height);

               //YUV格式的数据转换成RGBA 8888格式的数据, FFmpeg 也可以转换,但是存在问题,使用libyuv这个库实现
               I420ToARGB(yuv_frame->data[0], yuv_frame->linesize[0],
                          yuv_frame->data[2], yuv_frame->linesize[2],
                          yuv_frame->data[1], yuv_frame->linesize[1],
                          rgb_frame->data[0], rgb_frame->linesize[0],
                          pCodecCtx->width, pCodecCtx->height);

               //3、unlock window
               ANativeWindow_unlockAndPost(pWindow);

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

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

   av_frame_free(&yuv_frame);
   avcodec_close(pCodecCtx);
   avformat_free_context(pFormatCtx);
   (*env)->ReleaseStringUTFChars(env, input_, input);
}


#include "libswresample/swresample.h"

#define MAX_AUDIO_FRME_SIZE 48000 * 4

//音频解码(重采样)
JNIEXPORT void JNICALL
Java_com_haohao_ffmpeg_AVUtils_audioDecode(JNIEnv *env, jclass type, jstring input_,
                                          jstring output_)
{
   //访问静态方法
   jmethodID mid = (*env)->GetStaticMethodID(env, type, "onNativeCallback", "()V");
   const char *input = (*env)->GetStringUTFChars(env, input_, 0);
   const char *output = (*env)->GetStringUTFChars(env, output_, 0);

   //注册组件
   av_register_all();
   AVFormatContext *pFormatCtx = avformat_alloc_context();
   //打开音频文件
   if (avformat_open_input(&pFormatCtx, input, NULL, NULL) != 0) {
       LOGI("%s", "无法打开音频文件");
       return;
   }
   //获取输入文件信息
   if (avformat_find_stream_info(pFormatCtx, NULL) < 0) {
       LOGI("%s", "无法获取输入文件信息");
       return;
   }
   //获取音频流索引位置
   int i = 0, audio_stream_idx = -1;
   for (; i < pFormatCtx->nb_streams; i++) {
       if (pFormatCtx->streams[i]->codec->codec_type == AVMEDIA_TYPE_AUDIO) {
           audio_stream_idx = i;
           break;
       }
   }

   //获取解码器
   AVCodecContext *codecCtx = pFormatCtx->streams[audio_stream_idx]->codec;
   AVCodec *codec = avcodec_find_decoder(codecCtx->codec_id);
   if (codec == NULL) {
       LOGI("%s", "无法获取解码器");
       return;
   }
   //打开解码器
   if (avcodec_open2(codecCtx, codec, NULL) < 0) {
       LOGI("%s", "无法打开解码器");
       return;
   }
   //压缩数据
   AVPacket *packet = (AVPacket *) av_malloc(sizeof(AVPacket));
   //解压缩数据
   AVFrame *frame = av_frame_alloc();
   //frame->16bit 44100 PCM 统一音频采样格式与采样率
   SwrContext *swrCtx = swr_alloc();

   //重采样设置参数
   //输入的采样格式
   enum AVSampleFormat in_sample_fmt = codecCtx->sample_fmt;
   //输出采样格式16bit PCM
   enum AVSampleFormat out_sample_fmt = AV_SAMPLE_FMT_S16;
   //输入采样率
   int in_sample_rate = codecCtx->sample_rate;
   //输出采样率
   int out_sample_rate = 44100;
   //获取输入的声道布局
   //根据声道个数获取默认的声道布局(2个声道,默认立体声stereo)
   //av_get_default_channel_layout(codecCtx->channels);
   uint64_t in_ch_layout = codecCtx->channel_layout;
   //输出的声道布局(立体声)
   uint64_t out_ch_layout = AV_CH_LAYOUT_STEREO;

   swr_alloc_set_opts(swrCtx,
                      out_ch_layout, out_sample_fmt, out_sample_rate,
                      in_ch_layout, in_sample_fmt, in_sample_rate,
                      0, NULL);
   swr_init(swrCtx);

   //输出的声道个数
   int out_channel_nb = av_get_channel_layout_nb_channels(out_ch_layout);

   //重采样设置参数

   //位宽16bit 采样率 44100HZ 的 PCM 数据
   uint8_t *out_buffer = (uint8_t *) av_malloc(MAX_AUDIO_FRME_SIZE);

   FILE *fp_pcm = fopen(output, "wb");

   int got_frame = 0, index = 0, ret;
   //不断读取压缩数据
   while (av_read_frame(pFormatCtx, packet) >= 0) {
       //解码
       ret = avcodec_decode_audio4(codecCtx, frame, &got_frame, packet);

       if (ret < 0) {
           LOGI("%s", "解码完成");
       }
       //解码一帧成功
       if (got_frame > 0) {
           LOGI("解码:%d", index++);
           swr_convert(swrCtx, &out_buffer, MAX_AUDIO_FRME_SIZE, frame->data, frame->nb_samples);
           //获取sample的size
           int out_buffer_size = av_samples_get_buffer_size(NULL, out_channel_nb,
                                                            frame->nb_samples, out_sample_fmt, 1);
           fwrite(out_buffer, 1, out_buffer_size, fp_pcm);
       }

       av_free_packet(packet);
   }

   fclose(fp_pcm);
   av_frame_free(&frame);
   av_free(out_buffer);

   swr_free(&swrCtx);
   avcodec_close(codecCtx);
   avformat_close_input(&pFormatCtx);

   (*env)->ReleaseStringUTFChars(env, input_, input);
   (*env)->ReleaseStringUTFChars(env, output_, output);
   //通知 Java 层解码完成
   (*env)->CallStaticVoidMethod(env, type, mid);
}

JNIEXPORT void JNICALL
Java_com_haohao_ffmpeg_AVUtils_audioPlay(JNIEnv *env, jclass type, jstring input_)
{
   const char *input = (*env)->GetStringUTFChars(env, input_, 0);
   LOGI("%s", "sound");
   //注册组件
   av_register_all();
   AVFormatContext *pFormatCtx = avformat_alloc_context();
   //打开音频文件
   if (avformat_open_input(&pFormatCtx, input, NULL, NULL) != 0) {
       LOGI("%s", "无法打开音频文件");
       return;
   }
   //获取输入文件信息
   if (avformat_find_stream_info(pFormatCtx, NULL) < 0) {
       LOGI("%s", "无法获取输入文件信息");
       return;
   }
   //获取音频流索引位置
   int i = 0, audio_stream_idx = -1;
   for (; i < pFormatCtx->nb_streams; i++) {
       if (pFormatCtx->streams[i]->codec->codec_type == AVMEDIA_TYPE_AUDIO) {
           audio_stream_idx = i;
           break;
       }
   }

   //获取解码器
   AVCodecContext *codecCtx = pFormatCtx->streams[audio_stream_idx]->codec;
   AVCodec *codec = avcodec_find_decoder(codecCtx->codec_id);
   if (codec == NULL) {
       LOGI("%s", "无法获取解码器");
       return;
   }
   //打开解码器
   if (avcodec_open2(codecCtx, codec, NULL) < 0) {
       LOGI("%s", "无法打开解码器");
       return;
   }
   //压缩数据
   AVPacket *packet = (AVPacket *) av_malloc(sizeof(AVPacket));
   //解压缩数据
   AVFrame *frame = av_frame_alloc();
   //frame->16bit 44100 PCM 统一音频采样格式与采样率
   SwrContext *swrCtx = swr_alloc();

   //输入的采样格式
   enum AVSampleFormat in_sample_fmt = codecCtx->sample_fmt;
   //输出采样格式16bit PCM
   enum AVSampleFormat out_sample_fmt = AV_SAMPLE_FMT_S16;
   //输入采样率
   int in_sample_rate = codecCtx->sample_rate;
   //输出采样率
   int out_sample_rate = in_sample_rate;
   //获取输入的声道布局
   //根据声道个数获取默认的声道布局(2个声道,默认立体声stereo)
   //av_get_default_channel_layout(codecCtx->channels);
   uint64_t in_ch_layout = codecCtx->channel_layout;
   //输出的声道布局(立体声)
   uint64_t out_ch_layout = AV_CH_LAYOUT_STEREO;

   swr_alloc_set_opts(swrCtx,
                      out_ch_layout, out_sample_fmt, out_sample_rate,
                      in_ch_layout, in_sample_fmt, in_sample_rate,
                      0, NULL);
   swr_init(swrCtx);

   //输出的声道个数
   int out_channel_nb = av_get_channel_layout_nb_channels(out_ch_layout);
   //AudioTrack对象
   jmethodID create_audio_track_mid = (*env)->GetStaticMethodID(env, type, "createAudioTrack",
                                                                "(II)Landroid/media/AudioTrack;");
   jobject audio_track = (*env)->CallStaticObjectMethod(env, type, create_audio_track_mid,
                                                        out_sample_rate, out_channel_nb);

   //调用AudioTrack.play方法
   jclass audio_track_class = (*env)->GetObjectClass(env, audio_track);
   jmethodID audio_track_play_mid = (*env)->GetMethodID(env, audio_track_class, "play", "()V");
   jmethodID audio_track_stop_mid = (*env)->GetMethodID(env, audio_track_class, "stop", "()V");
   (*env)->CallVoidMethod(env, audio_track, audio_track_play_mid);

   //AudioTrack.write
   jmethodID audio_track_write_mid = (*env)->GetMethodID(env, audio_track_class, "write",
                                                         "([BII)I");
   //16bit 44100 PCM 数据
   uint8_t *out_buffer = (uint8_t *) av_malloc(MAX_AUDIO_FRME_SIZE);

   int got_frame = 0, index = 0, ret;
   //不断读取压缩数据
   while (av_read_frame(pFormatCtx, packet) >= 0) {
       //解码音频类型的Packet
       if (packet->stream_index == audio_stream_idx) {
           //解码
           ret = avcodec_decode_audio4(codecCtx, frame, &got_frame, packet);

           if (ret < 0) {
               LOGI("%s", "解码完成");
           }
           //解码一帧成功
           if (got_frame > 0) {
               LOGI("解码:%d", index++);
               swr_convert(swrCtx, &out_buffer, MAX_AUDIO_FRME_SIZE,
                           (const uint8_t **) frame->data, frame->nb_samples);
               //获取sample的size
               int out_buffer_size = av_samples_get_buffer_size(NULL, out_channel_nb,
                                                                frame->nb_samples, out_sample_fmt,
                                                                1);

               //out_buffer缓冲区数据,转成byte数组
               jbyteArray audio_sample_array = (*env)->NewByteArray(env, out_buffer_size);
               jbyte *sample_bytep = (*env)->GetByteArrayElements(env, audio_sample_array, NULL);
               //out_buffer的数据复制到sampe_bytep
               memcpy(sample_bytep, out_buffer, out_buffer_size);
               //同步
               (*env)->ReleaseByteArrayElements(env, audio_sample_array, sample_bytep, 0);

               //AudioTrack.write PCM数据
               (*env)->CallIntMethod(env, audio_track, audio_track_write_mid,
                                     audio_sample_array, 0, out_buffer_size);
               //释放局部引用
               (*env)->DeleteLocalRef(env, audio_sample_array);
           }
       }
       av_free_packet(packet);
   }

   (*env)->CallVoidMethod(env, audio_track, audio_track_stop_mid);

   av_frame_free(&frame);
   av_free(out_buffer);

   swr_free(&swrCtx);
   avcodec_close(codecCtx);
   avformat_close_input(&pFormatCtx);

   (*env)->ReleaseStringUTFChars(env, input_, input);
}


CMake 脚本:

cmake_minimum_required(VERSION 3.4.1)

include_directories(${CMAKE_SOURCE_DIR}/src/main/cpp/include)
set(jnilibs "${CMAKE_SOURCE_DIR}/src/main/jniLibs")
set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${jnilibs}/${ANDROID_ABI})


add_library( # Sets the name of the library.
            native-lib

            # Sets the library as a shared library.
            SHARED

            # Provides a relative path to your source file(s).
            src/main/cpp/native-lib.c)

# 添加 FFmpeg 的 8 个函数库和 yuvlib 库
add_library(avutil-54 SHARED IMPORTED )
set_target_properties(avutil-54 PROPERTIES IMPORTED_LOCATION "${jnilibs}/${ANDROID_ABI}/libavutil-54.so")


add_library(swresample-1 SHARED IMPORTED )
set_target_properties(swresample-1 PROPERTIES IMPORTED_LOCATION "${jnilibs}/${ANDROID_ABI}/libswresample-1.so")


add_library(avcodec-56 SHARED IMPORTED )
set_target_properties(avcodec-56 PROPERTIES IMPORTED_LOCATION "${jnilibs}/${ANDROID_ABI}/libavcodec-56.so")


add_library(avformat-56 SHARED IMPORTED )
set_target_properties(avformat-56 PROPERTIES IMPORTED_LOCATION "${jnilibs}/${ANDROID_ABI}/libavformat-56.so")


add_library(swscale-3 SHARED IMPORTED )
set_target_properties(swscale-3 PROPERTIES IMPORTED_LOCATION "${jnilibs}/${ANDROID_ABI}/libswscale-3.so")


add_library(postproc-53 SHARED IMPORTED )
set_target_properties(postproc-53 PROPERTIES IMPORTED_LOCATION "${jnilibs}/${ANDROID_ABI}/libpostproc-53.so")


add_library(avfilter-5 SHARED IMPORTED )
set_target_properties(avfilter-5 PROPERTIES IMPORTED_LOCATION "${jnilibs}/${ANDROID_ABI}/libavfilter-5.so")


add_library(avdevice-56 SHARED IMPORTED )
set_target_properties(avdevice-56 PROPERTIES IMPORTED_LOCATION "${jnilibs}/${ANDROID_ABI}/libavdevice-56.so")


add_library(yuv SHARED IMPORTED )
set_target_properties(yuv PROPERTIES IMPORTED_LOCATION "${jnilibs}/${ANDROID_ABI}/libyuv.so")


find_library( # Sets the name of the path variable.
             log-lib

             # Specifies the name of the NDK library that
             # you want CMake to locate.
             log )


#找到 Android 系统 Window 绘制相关的库
find_library(
           android-lib
           android
           )


target_link_libraries( native-lib
                      ${log-lib}
                      ${android-lib}
                      avutil-54
                      swresample-1
                      avcodec-56
                      avformat-56
                      swscale-3
                      postproc-53
                      avfilter-5
                      avdevice-56
                      yuv)


6. 参考文章


雷霄骅博客  

http://blog.csdn.net/leixiaohua1020/article/details/15811977

Jason 的 NDK 开发高级教程









-- END --

迷茫的时候,选择最难走的那条路

Copyright © 2018 AndroidGeeks

本文由“135编辑器”提供技术支持




AndroidGeeks



您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存