此系列文章将记录我学习FFmpeg的过程。

首先我们要新建一个项目,然后按照《Android平台下的FFmpeg的学习之路------(二)环境搭建》,这篇文章的知识搭建好环境。

大概流程是:获取视频文件路径 -> 把视频文件路径传递到NDK层 -> NDK层通过FFmpeg打开视频文件 -> FFmpeg获取视频文件的信息 -> FFmpeg通过视频文件信息获得视频流 -> FFmpeg通过视频流获取所需要的解码器的信息 -> FFmpeg通过解码器的信息在FFmpeg中获取解码器 -> 打开解码器 -> 解码视频获得原生数据 -> 原生数据转码为RGBA数据 -> 锁定Surface获得缓存 -> 往缓存写入RGBA数据 -> 解锁Surface


这样就完成了解码和绘制的流程,下面开始写代码。


因为我们要从SD卡获取视频,然后解码绘制,所以,我们要在项目的AndroidManifest.xml文件中添加权限:

<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />

然后编写XML代码,需要的控件是一个SurfaceView:用于绘制,和一个Button:用于点击开始,代码如下:

<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout 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"
    tools:context="com.jamingx.ffmpegtest.MainActivity">

    <SurfaceView
        android:id="@+id/surfaceview"
        android:layout_width="match_parent"
        android:layout_height="match_parent"/>

    <Button
        android:id="@+id/btn_play"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="开始" />

</android.support.constraint.ConstraintLayout>

接着编辑Java代码,首先编写native方法:

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

其中input是用于传入的视频路径,surface用于绘制,代码如下:

package com.jamingx.ffmpegtest;

import android.view.Surface;

/**
 * Created by Administrator on jamingx 2018/1/18 16:13
 */

public class FFmpegTest {
//    public native static String getFFmpegCodecInfo();
    public native static void deCodeVideo(String input,Surface surface);
    static {
        System.loadLibrary("ffmpeg");
        System.loadLibrary("ffmpeg_test");
    }
}

然后生成头文件

/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class com_jamingx_ffmpegtest_FFmpegTest */

#ifndef _Included_com_jamingx_ffmpegtest_FFmpegTest
#define _Included_com_jamingx_ffmpegtest_FFmpegTest
#ifdef __cplusplus
extern "C" {
#endif
/*
 * Class:     com_jamingx_ffmpegtest_FFmpegTest
 * Method:    getFFmpegCodecInfo
 * Signature: ()Ljava/lang/String;
 */
JNIEXPORT void JNICALL Java_com_jamingx_ffmpegtest_FFmpegTest_deCodeVideo
  (JNIEnv *, jclass,jstring,jobject);

#ifdef __cplusplus
}
#endif
#endif

接下来编写MainActivity代码:

package com.jamingx.ffmpegtest;

import android.graphics.PixelFormat;
import android.os.Bundle;
import android.os.Environment;
import android.support.v7.app.AppCompatActivity;
import android.util.Log;
import android.view.Surface;
import android.view.SurfaceHolder;
import android.view.SurfaceView;
import android.view.View;

public class MainActivity extends AppCompatActivity {
    private SurfaceView surfaceview;
    private Thread playThread; 

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
//        ((TextView)findViewById(R.id.tv)).setText(FFmpegTest.getFFmpegCodecInfo());
        surfaceview = (SurfaceView) findViewById(R.id.surfaceview);
        SurfaceHolder holder = surfaceview.getHolder();
        holder.setFormat(PixelFormat.RGBA_8888);//注意:设置SurfaceView显示的格式为RGBA_8888
        findViewById(R.id.btn_play).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                if (playThread != null){
                    playThread.interrupt();
                    playThread = null;
                }
                playThread = new Thread(){
                    @Override
                    public void run() {
                        String input = Environment.getExternalStorageDirectory().getAbsolutePath() + "/input1.mp4";
                        Log.e("TAG",input);
                        Surface surface = surfaceview.getHolder().getSurface();
                        FFmpegTest.deCodeVideo(input,surface);
                    }
                };
                playThread.start();
            }
        });
    }
}

注意:要设置SurfaceView 的显示格式为RGBA_8888,缓存大小为 宽度x高度x4,而通过FFmpeg解码出来的数据要转换为RGBA数据,缓存大小也是 宽度x高度x4,而宽度,高度都是固定视频的大小,二者的缓存空间大小一致才不会出错,且排列顺序都为RGBA。

接下来开始写C++代码:

#include "com_jamingx_ffmpegtest_FFmpegTest.h"
#include <android/native_window_jni.h>
#include <android/native_window.h>
#include <android/log.h>
#define LOGE(FORMAT,...) __android_log_print(ANDROID_LOG_ERROR,"TAG",FORMAT,##__VA_ARGS__);

extern "C" {
#include "libavcodec/avcodec.h"
#include "libavformat/avformat.h"
#include "libavutil/imgutils.h"
#include "libswscale/swscale.h"
}

JNIEXPORT void JNICALL Java_com_jamingx_ffmpegtest_FFmpegTest_deCodeVideo
        (JNIEnv *env, jclass jcls,jstring input_jstr,jobject surface){
    const char* input_path = env->GetStringUTFChars(input_jstr,NULL);// java String -> C char*
    //一.注册所有组件
    //void av_register_all(void);
    av_register_all();

    //二.打开输入文件
    //int avformat_open_input(AVFormatContext **ps, const char *url, AVInputFormat *fmt, AVDictionary **options);
    //1. 初始化 AVFormatContext *pFormatCtx
    AVFormatContext *pFormatCtx = avformat_alloc_context();
    //2. 打开输入文件
    if (avformat_open_input(&pFormatCtx,input_path,NULL,NULL) != 0){
        LOGE("打开输入文件失败");
        return;
    }


    //三.获取视频文件信息
    //int avformat_find_stream_info(AVFormatContext *ic, AVDictionary **options);
    if (avformat_find_stream_info(pFormatCtx,NULL) < 0){
        LOGE("获取视频文件信息失败");
        return;
    }

    //四.查找编解码器
    //AVCodec *avcodec_find_decoder(enum AVCodecID id);
    //1.获取视频流的索引(下标)位置
    int video_stream_index = -1;//存放视频流的索引(下标)位置
    for (int i = 0; i < pFormatCtx->nb_streams; ++i) {
        if (pFormatCtx->streams[i]->codec->codec_type == AVMEDIA_TYPE_VIDEO){
            video_stream_index = i;
            break;
        }
    }
    if (video_stream_index == -1){
        LOGE("没有找到视频流");
        return;
    }
    //2.获取视频流的编解码器上下文(保存了视频或音频编解码器的信息)
    AVCodecContext * pCodecCtx = pFormatCtx->streams[video_stream_index]->codec;
    //3.通过编解码器上下文(存放的编解码器信息)存放的编解码器ID获取编解码器
    AVCodec * pCodec = avcodec_find_decoder(pCodecCtx->codec_id);

    //五.打开编码器
    //int avcodec_open2(AVCodecContext *avctx, const AVCodec *codec, AVDictionary **options);
    if(avcodec_open2(pCodecCtx,pCodec,NULL) < 0){
        LOGE("打开编码器失败");
        return;
    }

    ANativeWindow* nativeWindow = ANativeWindow_fromSurface(env,surface);
    ANativeWindow_Buffer outBuffer;

    //六.从输入文件读取数据(循环读取),av_read_frame只能读取1帧
    //int av_read_frame(AVFormatContext *s, AVPacket *pkt);
    //1.初始化 AVPacket *pPacket -> 存放解码前数据
    AVPacket *pPacket = av_packet_alloc();
    //2.初始化 AVFrame *pFrame -> 存放解码后的数据
    AVFrame *pFrame = av_frame_alloc();
    //3.初始化 AVFrame *pFrameRGBA -> 存放转换为RGBA后的数据
    AVFrame *pFrameRGBA = av_frame_alloc();
    //4.初始化用于格式转换的SwsContext,由于解码出来的帧格式不是RGBA的,在渲染之前需要进行格式转换
    //struct SwsContext *sws_getContext(int srcW, int srcH, enum AVPixelFormat srcFormat,
    //                                  int dstW, int dstH, enum AVPixelFormat dstFormat,
    //                                  int flags, SwsFilter *srcFilter,
    //                                  SwsFilter *dstFilter, const double *param);
    SwsContext *sws_ctx = sws_getContext(pCodecCtx->width, pCodecCtx->height, pCodecCtx->pix_fmt,
                                         pCodecCtx->width, pCodecCtx->height, AV_PIX_FMT_RGBA,
                                         SWS_BILINEAR, NULL,
                                         NULL, NULL);
    int got_picture_ptr;//如果没有帧可以解压缩,则为零,否则为非零。
    int countFrame = 0;
    while (av_read_frame(pFormatCtx,pPacket) == 0){
        //解码视频
        if (pPacket->stream_index == video_stream_index){
            //七.解码一帧数据
            //int avcodec_decode_video2(AVCodecContext *avctx, AVFrame *picture,int *got_picture_ptr,const AVPacket *avpkt);
            if (avcodec_decode_video2(pCodecCtx,pFrame,&got_picture_ptr,pPacket) < 0){
                LOGE("解码错误");
            }
            if (got_picture_ptr >= 0){
                LOGE("解码第%d帧",++countFrame);

                ANativeWindow_setBuffersGeometry(nativeWindow, pCodecCtx->width, pCodecCtx->height,WINDOW_FORMAT_RGBA_8888);
                ANativeWindow_lock(nativeWindow,&outBuffer,NULL);
                av_image_fill_arrays(pFrameRGBA->data, pFrameRGBA->linesize, (uint8_t*)outBuffer.bits, AV_PIX_FMT_RGBA,
                                     pCodecCtx->width, pCodecCtx->height, 1);
                //格式转换
                //int sws_scale(struct SwsContext *c, const uint8_t *const srcSlice[],
                //              const int srcStride[], int srcSliceY, int srcSliceH,
                //              uint8_t *const dst[], const int dstStride[]);
                sws_scale(sws_ctx,(const uint8_t *const*)pFrame->data,
                          pFrame->linesize,0,pCodecCtx->height,
                          pFrameRGBA->data,pFrameRGBA->linesize);

                //unlock
                ANativeWindow_unlockAndPost(nativeWindow);
            }
        }


        av_packet_unref(pPacket);
    }
    ANativeWindow_release(nativeWindow);
    av_free(pPacket);
    av_free(pFrame);
    av_free(pFrameRGBA);
    sws_freeContext(sws_ctx);
    //八.关闭解编码器
    avcodec_close(pCodecCtx);

    //九.关闭输入文件
    avformat_close_input(&pFormatCtx);

    env->ReleaseStringUTFChars(input_jstr,input_path);//释放

}



解码流程如下(以下3张图片来源于 雷霄骅 ):

android ndk 读写文件 android ndk视频教程_Android

android ndk 读写文件 android ndk视频教程_android ndk 读写文件_02

android ndk 读写文件 android ndk视频教程_FFmpeg_03

音视频的基础知识请看雷霄骅的博客:[总结]FFMPEG视音频编解码零基础学习方法

解码流程详解:

一.注册所有组件
void av_register_all(void);

二.打开输入文件

int avformat_open_input(AVFormatContext **ps, const char *url, AVInputFormat *fmt, AVDictionary **options);

android ndk 读写文件 android ndk视频教程_Android_04

根据注释说的,可以通过AVFormatContext **ps可以通过avformat_alloc_context()分配,使用完以后通过avformat_close_input()释放,const char *url 是 文件路径,而剩余的2个参数我们给它NULL就可以了。然后0表示成功,非0表示打开失败。

三.获取视频文件信息
int avformat_find_stream_info(AVFormatContext *ic, AVDictionary **options);

android ndk 读写文件 android ndk视频教程_Android_05

这个函数需要一个AVFormatContext *ic,所以我们把刚刚分配的AVFormatContext *传进来,另外一个也给它一个NULL,返回值>=0表示没有错误,否则就有错误。
例如一个MP4文件就包含很多流,有视频流,音频流...

android ndk 读写文件 android ndk视频教程_视频解码_06

通过这一步就能把这些流以数组的形式储存到AVFormatContext *中。

四.查找编解码器
AVCodec *avcodec_find_decoder(enum AVCodecID id);

android ndk 读写文件 android ndk视频教程_NDK绘制_07

这个函数需要一个AVCodecID id,才能获得一个解码器。

我们先遍历AVFormatContext *存储的流数组,得到视频流(AVStream),每个AVStream都会存储一个编解码器上下文(AVCodecContext *),这个编解码器上下文(AVCodecContext *)保存了视频或音频编解码器的信息,其中就有AVCodecID。

这样,我们就得到AVCodecID,通过avcodec_find_decoder(),就能从FFmpeg中得到所需要的解码器了AVCodec

五.打开解码器

int avcodec_open2(AVCodecContext *avctx, const AVCodec *codec, AVDictionary **options);

android ndk 读写文件 android ndk视频教程_FFmpeg_08

注释说了AVCodecContext *avctx 可以通过avcodec_alloc_context3()创建。

但是我们这里可以通过AVStream得到AVCodecContext *,所以我们就直接把得到的这个传进去,

第二个参数就把刚刚得到的AVCodec* 传进去,第三个参数直接传NULL就可以了。

六.从输入文件读取数据(循环读取)

因为av_read_frame()只能读取1帧,所以需要循环读取

android ndk 读写文件 android ndk视频教程_NDK绘制_09

读取前,我们先做一些初始化工作:

1.初始化 AVPacket *pPacket -> 存放解码前数据
AVPacket *pPacket = av_packet_alloc();

2.初始化 AVFrame *pFrame -> 存放解码后的数据
AVFrame *pFrame = av_frame_alloc();

3.初始化 AVFrame *pFrameRGBA -> 存放转换为RGBA后的数据
AVFrame *pFrameRGBA = av_frame_alloc();

4.初始化用于格式转换的SwsContext,由于解码出来的帧格式不是RGBA的,在渲染之前需要进行格式转换

struct SwsContext *sws_getContext(int srcW, int srcH, enum AVPixelFormat srcFormat,
                                  int dstW, int dstH, enum AVPixelFormat dstFormat,
                                  int flags, SwsFilter *srcFilter,
                                  SwsFilter *dstFilter, const double *param);

android ndk 读写文件 android ndk视频教程_视频解码_10

5.初始化NDK绘制

窗体:ANativeWindow* nativeWindow = ANativeWindow_fromSurface(env,surface);
绘制时的缓冲区:ANativeWindow_Buffer outBuffer;

通过av_read_frame()读取一帧数据后,会把解码前的数据存储到AVPacket *。

七.解码一帧数据

int avcodec_decode_video2(AVCodecContext *avctx, AVFrame *picture,int *got_picture_ptr,const AVPacket *avpkt);

android ndk 读写文件 android ndk视频教程_Android_11

把AVFrame *pFrame  和 AVPacket *解码前的数据解码出来

存到AVFrame *pFrame 

设置缓冲区的属性(宽、高、像素格式)

ANativeWindow_setBuffersGeometry(nativeWindow, pCodecCtx->width, pCodecCtx->height,WINDOW_FORMAT_RGBA_8888);

锁定,得到window的缓冲区

ANativeWindow_lock(nativeWindow,&outBuffer,NULL);

让AVFrame *pFrameRGBA缓冲区指针指向outBuffer.bits(window缓冲区)

av_image_fill_arrays(rgb_frame->data,rgb_frame->linesize,outBuffer.bits, AV_PIX_FMT_RGBA, pCodeCtx->width, pCodeCtx->height,1);

这样,我们操作AVFrame *pFrameRGBA缓冲区就相当于操作window缓冲区

解码出来的数据可能是YUV可能是RGB,所以我们要进行格式转换,转换为RGBA

格式转换:
int sws_scale(struct SwsContext *c, const uint8_t *const srcSlice[],
             const int srcStride[], int srcSliceY, int srcSliceH,
              uint8_t *const dst[], const int dstStride[]);

android ndk 读写文件 android ndk视频教程_视频解码_12

我们把AVFrame *pFrameRGBA 和AVFrame *pFrame 传到这个函数后

这个函数会把AVFrame *pFrame的数据转换为RGBA的数据

然后存放到AVFrame *pFrameRGBA的缓冲区

解锁

ANativeWindow_unlockAndPost(nativeWindow);

这样,解码绘制就完成了,剩下的是一些收尾工作。

内存回收free。

ANativeWindow_release(nativeWindow)

八.关闭解编码器

int avcodec_close(AVCodecContext *avctx);

九.关闭输入文件

void avformat_free_context(AVFormatContext *s);

以上函数翻译来自谷歌/滑稽

运行结果:

android ndk 读写文件 android ndk视频教程_FFmpeg_13