Android使用FFmpeg播放视频

3,045 阅读4分钟

1.编译动态链接库(so包)

1.1平台和版本

操作系统:macos
NDK版本:r21
FFmpeg版本: 4.x.x

1.2编译准备

下载NDK
NDK developer.android.google.cn/ndk/downloa…
FFmpeg ffmpeg.org/download.ht…

1.3配置修改

解压缩下载好的FFmpeg源码,进入解压缩后的目录,编辑configure文件

SLIBNAME_WITH_MAJOR='$(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)/$(LIBNAME)"'

SLIB_INSTALL_NAME='$(SLIBNAME_WITH_MAJOR)'

SLIB_INSTALL_LINKS='$(SLIBNAME)'

1.4编辑编译脚本

在FFmpeg源码根目录新建shell脚本build_android.sh
输入以下内容

export NDK=/Users/mac/Library/Android/ndk #替换成你的NDK路径
export API=21 #API 版本
export ARCH=aarch64 #目标CPU架构
export PLATFORM=aarch64 
export TARGET=$PLATFORM-linux-android
export TOOLCHAIN=$NDK/toolchains/llvm/prebuilt/darwin-x86_64/bin
#正确的sysroot
export SYSROOT=$NDK/toolchains/llvm/prebuilt/darwin-x86_64/sysroot
export CPU=aarch64
export PREFIX=/Users/mac/Desktop/Temp/$ARCH #编译生成目录
export CFLAG="-D__ANDROID_API__=$API -Os -fPIC -DANDROID "

./configure \
--prefix=$PREFIX \
--cc=$TOOLCHAIN/$TARGET$API-clang \
--cxx=$TOOLCHAIN/$TARGET$API-clang++ \
--ld=$TOOLCHAIN/$TARGET$API-clang \
--target-os=android  \
--arch=$ARCH \
--cross-prefix=$TOOLCHAIN/$ARCH-linux-android- \
--disable-doc \
--enable-shared \
--disable-static \
--disable-yasm \
--disable-symver \
--enable-gpl \
--disable-ffmpeg \
--disable-ffplay \
--disable-ffprobe \
--enable-cross-compile \
--enable-runtime-cpudetect \
--sysroot=$SYSROOT \
--extra-cflags="$CFLAG" \
--extra-ldflags=""

make clean
make
make install

Note:
很多教程编译失败是因为旧版本的NDK(r17及之前)的Sysroot和C/C++编译器(gcc/g++、clang/clang++)的目录与新版本的位置不同
Linux版本编译可以参考上面的脚本,但是darwin是macos下的目录名称,linux下请根据ndk的目录名称做相应修改
不同平台的so包需要根据自己的需求修改上面的ARCH和PLATFORM,可以封装成函数传参调用,本文只是简单的实现编译

执行build_android.sh,如遇权限不足,可以赋予文件执行权限
chmod +x build_android.sh

编译完成后会在PREFIX目录下生成如下目录和文件

2.使用FFmpeg播放视频

1.创建Android项目

如果Android Studio支持新建支持C++的项目,请直接勾选,否则新建普通项目,然后按照下面的内容配置C++

2.复制动态库和头文件

在app/src/main/目录下新建jniLibs目录
将FFmpeg生成的lib和include复制到jniLibs目录下 将lib目录名称改成对应的arch名称 如:areabi-v7a

2.创建CMakeLists.txt

在app目录下新建CMakeLists.txt 内容如下

cmake_minimum_required(VERSION 3.4.1)

add_library( native-lib
             SHARED
             src/main/cpp/native-lib.cpp )

find_library( log-lib
              log )

set(JNI_LIBS_DIR ${CMAKE_SOURCE_DIR}/src/main/jniLibs)

add_library(avutil
        SHARED
        IMPORTED )

set_target_properties(avutil
        PROPERTIES IMPORTED_LOCATION
        ${JNI_LIBS_DIR}/${ANDROID_ABI}/libavutil.so )

add_library(swresample
        SHARED
        IMPORTED )
set_target_properties(swresample
        PROPERTIES IMPORTED_LOCATION
        ${JNI_LIBS_DIR}/${ANDROID_ABI}/libswresample.so )


add_library(swscale
        SHARED
        IMPORTED )
set_target_properties(swscale
        PROPERTIES IMPORTED_LOCATION
        ${JNI_LIBS_DIR}/${ANDROID_ABI}/libswscale.so )

add_library(avcodec
        SHARED
        IMPORTED )
set_target_properties(avcodec
        PROPERTIES IMPORTED_LOCATION
        ${JNI_LIBS_DIR}/${ANDROID_ABI}/libavcodec.so )

add_library(avformat
        SHARED
        IMPORTED )
set_target_properties(avformat
        PROPERTIES IMPORTED_LOCATION
        ${JNI_LIBS_DIR}/${ANDROID_ABI}/libavformat.so )

add_library( avfilter
        SHARED
        IMPORTED )
set_target_properties(avfilter
        PROPERTIES IMPORTED_LOCATION
        ${JNI_LIBS_DIR}/${ANDROID_ABI}/libavfilter.so )

add_library(avdevice
        SHARED
        IMPORTED )
set_target_properties(avdevice
        PROPERTIES IMPORTED_LOCATION
        ${JNI_LIBS_DIR}/${ANDROID_ABI}/libavdevice.so )

include_directories(${JNI_LIBS_DIR}/include)


target_link_libraries(
                        native-lib
                        avdevice
                        avutil
                        android
                        swresample
                        swscale
                        avcodec
                        avformat
                        avfilter
                        ${log-lib} )

关于JNI开发相关知识,此处不做讲解,如果不了解或者想要研究请自行参考其他

3.修改build.gradle

对app目录下的build.gradlew做如下修改

android {
    ...
    defaultConfig {
        ...
        //新增如下内容
        externalNativeBuild {
            cmake{
                cppFlags ""
                abiFilters "arm64-v8a" //此处换成你需要支持的cpu架构
            }
        }
    }
    //新增如下内容
    externalNativeBuild {
        cmake{
            path "CMakeLists.txt"
        }
    }
}

4.新建native源文件

在app/src/main 目录下新建cpp目录
在cpp目录下创建native-lib.cpp (与CMakeLists.txt中对应即可,不一定非得是这个名字) native-lib.cpp 内容如下

此处参考了 blog.csdn.net/johanman/ar… 感谢大神

your_package_name 要换成你自己的包名, 包名中的点 . 用 下划线 _ 替换

#include <jni.h>
#include <android/native_window.h>
#include <android/native_window_jni.h>
#include <android/log.h>

#pragma clang diagnostic push
#pragma ide diagnostic ignored "CannotResolve"

extern "C" {

#include "libavformat/avformat.h"
#include "libavcodec/avcodec.h"
#include "libswscale/swscale.h"
#include "libavutil/imgutils.h"

}

#define LOGE(FORMAT, ...) __android_log_print(ANDROID_LOG_ERROR, "FFPlayer", FORMAT, ##__VA_ARGS__);


extern "C"
JNIEXPORT void JNICALL
Java_your_package_name_MainActivity_play(JNIEnv *env, jobject type, jstring source, jobject surface) {
    int result = 0;
    const char *path = env->GetStringUTFChars(source, 0);
    av_register_all();
    AVFormatContext *format_context = avformat_alloc_context();
    // 打开视频文件
    result = avformat_open_input(&format_context, path, NULL, NULL);
    if (result < 0) {
        LOGE("Player Error : Can not open video file");
        return;
    }
    result = avformat_find_stream_info(format_context, NULL);
    if (result < 0) {
        LOGE("Player Error : Can not find video file stream info");
        return;
    }

    // 查找视频编码器
    int video_stream_index = -1;
    for (int i = 0; i < format_context->nb_streams; i++) {
        // 匹配视频流
        if (format_context->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_VIDEO) {
            video_stream_index = i;
        }
    }
    // 没找到视频流
    if (video_stream_index == -1) {
        LOGE("Player Error : Can not find video stream");
        return;
    }

    // 初始化视频编码器上下文
    AVCodecContext *video_codec_context = avcodec_alloc_context3(NULL);
    avcodec_parameters_to_context(video_codec_context,
                                  format_context->streams[video_stream_index]->codecpar);
    // 初始化视频编码器
    AVCodec *video_codec = avcodec_find_decoder(video_codec_context->codec_id);
    if (video_codec == NULL) {
        LOGE("Player Error : Can not find video codec");
        return;
    }

    result = avcodec_open2(video_codec_context, video_codec, NULL);
    if (result < 0) {
        LOGE("Player Error : Can not find video stream");
        return;
    }

    // 获取视频的宽高
    int videoWidth = video_codec_context->width;
    int videoHeight = video_codec_context->height;
    // R4 初始化 Native Window 用于播放视频
    ANativeWindow *native_window = ANativeWindow_fromSurface(env, surface);
    if (native_window == NULL) {
        LOGE("Player Error : Can not create native window");
        return;
    }
    // 通过设置宽高限制缓冲区中的像素数量,而非屏幕的物理显示尺寸。
    // 如果缓冲区与物理屏幕的显示尺寸不相符,则实际显示可能会是拉伸,或者被压缩的图像
    result = ANativeWindow_setBuffersGeometry(native_window, videoWidth, videoHeight,
                                              WINDOW_FORMAT_RGBA_8888);
    if (result < 0) {
        LOGE("Player Error : Can not set native window buffer");
        ANativeWindow_release(native_window);
        return;
    }

    // 定义绘图缓冲区
    ANativeWindow_Buffer window_buffer;
    // 声明数据容器 有3个
    // R5 解码前数据容器 Packet 编码数据
    AVPacket *packet = av_packet_alloc();
    // R6 解码后数据容器 Frame 像素数据 不能直接播放像素数据 还要转换
    AVFrame *frame = av_frame_alloc();
    // R7 转换后数据容器 这里面的数据可以用于播放
    AVFrame *rgba_frame = av_frame_alloc();
    // 数据格式转换准备
    // 输出 Buffer
    int buffer_size = av_image_get_buffer_size(AV_PIX_FMT_RGBA, videoWidth, videoHeight, 1);
    // R8 申请 Buffer 内存
    uint8_t *out_buffer = (uint8_t *) av_malloc(buffer_size * sizeof(uint8_t));
    av_image_fill_arrays(rgba_frame->data, rgba_frame->linesize, out_buffer, AV_PIX_FMT_RGBA,
                         videoWidth, videoHeight, 1);
    // R9 数据格式转换上下文
    struct SwsContext *data_convert_context = sws_getContext(
            videoWidth, videoHeight, video_codec_context->pix_fmt,
            videoWidth, videoHeight, AV_PIX_FMT_RGBA,
            SWS_BICUBIC, NULL, NULL, NULL);

    // 开始读取帧
    //读取帧
    while (av_read_frame(format_context, packet) >= 0) {
        if (packet->stream_index == video_stream_index) {
        
            /***
            * 很多教程写的是这个函数,这是旧版FFmpeg里面的写法,新版中已经移除了这个函数
            * avcodec_decode_video2(context, frame, &count, packet)
            * 用下面两个函数替代
            * avcodec_send_packet(context, packet)
            * avcodec_receive_frame(context, frame)
            ***/
            
            //视频解码
            int ret = avcodec_send_packet(video_codec_context, packet);
            if (ret < 0 && ret != AVERROR(EAGAIN) && ret != AVERROR_EOF) {
                continue;
            }

            ret = avcodec_receive_frame(video_codec_context, frame);
            if (ret < 0 && ret != AVERROR_EOF) {
                continue;
            }

            sws_scale(data_convert_context, (const uint8_t *const *) frame->data, frame->linesize,
                      0, video_codec_context->height,
                      rgba_frame->data, rgba_frame->linesize);

            if (ANativeWindow_lock(native_window, &window_buffer, NULL) < 0) {
                continue;
            } else {
                //将图像绘制到界面上,注意这里pFrameRGBA一行的像素和windowBuffer一行的像素长度可能不一致
                //需要转换好,否则可能花屏
                uint8_t *dst = (uint8_t *) window_buffer.bits;
                for (int h = 0; h < videoHeight; h++) {
                    memcpy(dst + h * window_buffer.stride * 4,
                           out_buffer + h * rgba_frame->linesize[0],
                           rgba_frame->linesize[0]);
                }
                ANativeWindow_unlockAndPost(native_window);
            }
        }
        av_packet_unref(packet);
    }
    //释放内存
    sws_freeContext(data_convert_context);
    av_free(packet);
    av_free(rgba_frame);
    avcodec_close(video_codec_context);
    avformat_close_input(&format_context);
}

#pragma clang diagnostic pop

5.修改MainActivity.java

import ...

public class MainActivity extends Activity {
    //新增加载动态库代码
    static {
        System.loadLibrary("native-lib"); //请根据CMakeLists.txt中配置的名字做相应修改
    }
    
    //新增组件
    private  SurfaceView surfaceView;
    private SurfaceHolder holder;
    
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        ...
        surfaceView = findViewById(R.id.surface_view);
        holder = surfaceView.getHolder();
    }
    
    //新增native对应的方法
    public native void play(String url, Surface surface);
    
    public void play(View view){
        //url支持本地文件,网络文件,直播流(HLS,RTMP,RTSP)
        //别忘了添加对应的权限即可(网络访问,读写存储)
        //网络访问http协议新版Android需要在manifest application节点添加
        //android:usesCleartextTraffic="true"
        //最好在子线程中执行,此处只实现了功能
        String url = "path/to/file"; 
        Surface surface = holder.getSurface();
        play(url, surface);
    }
}

6.修改布局文件

第一次写博客,如有不周,望各位不吝赐教。