NDK24_FFmpeg集成及初始化

NDK开发汇总

一 FFmpeg集成

  • 将静态库和头文件拷贝到项目中
  • 静态库来源、CmakeLists.txt 和 build.gradle配置:NDK23_FFmpeg编译
  • 在编译FFmpeg时,日志中打印:其依赖libz库,所有要添加z库
  • 创建一个变量 source_file 它的值就是src/main/cpp/所有的.cpp文件
  • FFmpeg中会动态依赖ndk中的libz库,所以要引入 ;引入的库顺序要注意顺序:avformat 在avcodec之前,静态依赖
    完整的CmakeLists.txt
cmake_minimum_required(VERSION 3.4.1)

# 创建一个变量 source_file 它的值就是src/main/cpp/所有的.cpp文件
file(GLOB source_file src/main/cpp/*.cpp)

add_library( native-lib
             SHARED
             ${source_file} )

find_library( log-lib
              log )

include_directories(src/main/cpp/include)
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -L${CMAKE_SOURCE_DIR}/src/main/cpp/libs/${ANDROID_ABI}")

target_link_libraries(native-lib
                       avfilter avformat avcodec   avutil swresample swscale
                       ${log-lib} z )
#FFmpeg中会动态依赖ndk中的libz库,所以要引入 ;引入的库顺序要注意顺序:avformat 在avcodec之前,静态依赖


二 FFmpeg核心模块

libavformat

​ 用于各种音视频封装格式的生成和解析,包括获取解码所需信息以生成解码上下文结构和读取音视频帧等功能;音视频的格式解析协议,为 libavcodec 分析码流提供独立的音频或视频码流源。

libavcodec

​ 用于各种类型声音/图像编解码;该库是音视频编解码核心,实现了市面上可见的绝大部分解码器的功能,libavcodec 库被其他各大解码器 ffdshow,Mplayer 等所包含或应用。

libavfilter

​ filter(FileIO、FPS、DrawText)音视频滤波器的开发,如水印、倍速播放等。

libavutil

​ 包含一些公共的工具函数的使用库,包括算数运算 字符操作;

libswresample

​ 原始音频格式转码。

libswscale

​ (原始视频格式转换)用于视频场景比例缩放、色彩映射转换;图像颜色空间或格式转换,如 rgb565,rgb888 等与 yuv420 等之间转换。

libpostproc+libavcodec

解封装
在这里插入图片描述
解封装
在这里插入图片描述

三 FFmpeg相关API

常用结构体

AVCodec
name编解码器名称
long_name编解码器长名称
type编解码器类型
id编解码器ID
一些编解码的接口函数
AVCodecContext
codec编解码器的AVCodec
width, height图像的宽高
pix_fmt像素格式
sample_rate音频采样率
channels声道数
sample_fmt音频采样格式
AVStream
id序号
codec该流对应的AVCodecContext
time_base该流的时基
avg_frame_rate该流的帧率
AVFrame
data解码后的图像像素数据(音频采样数据)
linesize对视频来说是图像中一行像素的大小;对音频来说是整个音
width, height图像的宽高
key_frame是否为关键帧
pict_type帧类型(只针对视频) 。例如I,P,B
AVPacket
pts显示时间戳
dts解码时间戳
data压缩编码数据
size压缩编码数据大小
stream_index所属的AVStream
AVInputFormat
name封装格式名称
long_name封装格式的长名称
extensions封装格式的扩展名
id封装格式ID
一些封装格式处理的接口函数
AVFormatContext
iformat输入视频的AVInputFormat
nb_streams输入视频的AVStream 个数
streams输入视频的AVStream []数组
duration输入视频的时长(以微秒为单位)
bit_rate输入视频的码率

FFmpeg 解码的数据结构

在这里插入图片描述

四 FFmpeg初始化(解码)

解码流程
在这里插入图片描述
程序结构
在这里插入图片描述

1 Java层调用

MainActivity

public class MainActivity extends AppCompatActivity {

    private DNPlayer dnPlayer;

    private SurfaceView surfaceView;

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        surfaceView = findViewById(R.id.surface_view);
        dnPlayer = new DNPlayer();
        dnPlayer.setSurfaceView(surfaceView);
        dnPlayer.setDataSource("http://ivi.bupt.edu.cn/hls/cctv6hd.m3u8");
        dnPlayer.setOnPrepareListener(new DNPlayer.OnPrepareListener() {
            @Override
            public void onPrepare() {
                runOnUiThread(new Runnable() {
                    @Override
                    public void run() {
                        Toast.makeText(MainActivity.this,"可以播放",Toast.LENGTH_SHORT).show();
                    }
                });
            }
        });
    }

    public void start(View view) {
        dnPlayer.prepare();
    }
}

DNPlayer

**
 * 提供java 进行播放 停止 等函数
 */
public class DNPlayer implements SurfaceHolder.Callback {
    static {
        System.loadLibrary("native-lib");
    }

    private String dataSource;
    private SurfaceHolder holder;
    private OnPrepareListener listener;

    /**
     * 让使用 设置播放的文件 或者 直播地址
     */
    public void setDataSource(String dataSource){
        this.dataSource = dataSource;
    }

    /**
     * 准备好要播放的视频
     */
    public void prepare() {
        native_prepare(dataSource);
    }

    /**
     * 开始播放
     */
    public void start(){

    }

    /**
     * 停止播放
     */
    public void stop(){

    }

    public void release(){
        holder.removeCallback(this);
    }

    public void setSurfaceView(SurfaceView surfaceView) {
        holder = surfaceView.getHolder();
        holder.addCallback(this);
    }

    public void onError(int errorCode){
        Log.e("FFmpeg","onError"+errorCode);
    }

    public void onPrepare(){
        if(null != listener){
            listener.onPrepare();
        }
    }

    public void setOnPrepareListener(OnPrepareListener listener){
        this.listener = listener;
    }

    public interface OnPrepareListener{
        void onPrepare();
    }

    /**
     * 画布创建好了
     * @param surfaceHolder
     */
    @Override
    public void surfaceCreated(SurfaceHolder surfaceHolder) {

    }

    /**
     * 画布发生了变化(横竖屏切换、按Home键都会回调这个函数)
     * @param surfaceHolder
     * @param i
     * @param i1
     * @param i2
     */
    @Override
    public void surfaceChanged(SurfaceHolder surfaceHolder, int i, int i1, int i2) {

    }

    /**
     * 销毁画布 (按了home/退出应用/..)
     * @param surfaceHolder
     */
    @Override
    public void surfaceDestroyed(SurfaceHolder surfaceHolder) {

    }

    native void native_prepare(String dataSource);
}

2 C++层解析,并在解析完成后回调java

native_lib.cpp

DNFFmpeg *ffmpeg = 0;
JavaVM *javaVm = 0;

int JNI_OnLoad(JavaVM *vm, void *r) {
    javaVm = vm;
    return JNI_VERSION_1_6;
}

extern "C"
JNIEXPORT void JNICALL
Java_com_cn_ray_player_DNPlayer_native_1prepare(JNIEnv *env, jobject instance,
                                                jstring dataSource_) {
    const char *dataSource = env->GetStringUTFChars(dataSource_, 0);
    //创建播放器
    JavaCallHelper *helper = new JavaCallHelper(javaVm, env, instance);
    ffmpeg = new DNFFmpeg(helper, dataSource);
    ffmpeg->setRenderFrameCallback(render);
    ffmpeg->prepare();
    env->ReleaseStringUTFChars(dataSource_, dataSource);
}

DNFFmpeg

DNFFmpeg.h

//
// Created by PF0ZYBAJ on 2020-9-8.
//

#ifndef PLAYER_DNFFMPEG_H
#define PLAYER_DNFFMPEG_H


#include "JavaCallHelper.h"
#include "AudioChannel.h"
#include "VideoChannel.h"

extern "C" {
#include <libavformat/avformat.h>
}

class DNFFmpeg {

public:
    DNFFmpeg(JavaCallHelper *callHelper,const char* dataSource);
    ~DNFFmpeg();

    void prepare();
    void _prepare();
private:
    char *dataSource;
    pthread_t pic;
    AVFormatContext *formatContext;
    JavaCallHelper *callHelper;
    AudioChannel *audioChannel;
    VideoChannel *videoChannel;
};


#endif //PLAYER_DNFFMPEG_H

DNFFmpeg.cpp

//
// Created by PF0ZYBAJ on 2020-9-8.
//

#include <cstring>
#include <pthread.h>

#include "DNFFmpeg.h"
#include "macro.h"


void *task_prepare(void *args) {
    DNFFmpeg *ffmpeg = static_cast<DNFFmpeg *>(args);
    ffmpeg->_prepare();
    return 0;
}

DNFFmpeg::DNFFmpeg(JavaCallHelper *callHelper, const char *dataSource) {
    this->callHelper = callHelper;
    //防止dataSource参数指向的内存被释放
    this->dataSource = new char[strlen(dataSource)];
    strcpy(this->dataSource, dataSource);
}

DNFFmpeg::~DNFFmpeg() {
    //释放
    DELETE(dataSource);
    DELETE(callHelper);
}

void DNFFmpeg::prepare() {
    //创建线程
    pthread_create(&pic, 0, task_prepare, this);
}

void DNFFmpeg::_prepare() {
    //初始化网络 让ffmpeg能够使用网络
    avformat_network_init();
    //1、打开媒体(文件地址、直播地址)
    //AVFormatContext 包含了 视频的 信息(宽、高)
    formatContext = 0;
    //文件路径不对 、手机没网
    int ret = avformat_open_input(&formatContext, dataSource, 0, 0);
    //ret不为0表示 打开媒体失败
    if (ret != 0) {
        LOGE("打开媒体失败:%s",av_err2str(ret));
        callHelper->onError(THREAD_CHILD, FFMPEG_CAN_NOT_OPEN_URL);
        return;
    }

    //2、查找媒体中的音视频流
    ret = avformat_find_stream_info(formatContext, 0);
    //小于0 则失败
    if (ret < 0) {
        LOGE("打开流失败:%s",av_err2str(ret));
        callHelper->onError(THREAD_CHILD, FFMPEG_CAN_NOT_FIND_STREAMS);
        return;
    }

    //nb_streams:几个流(几段视频/音频)
    for (int i = 0; i < formatContext->nb_streams; ++i) {
        //可能代表是一个视频 也可能代表是一个音频
        AVStream *stream = formatContext->streams[i];
        //包含了 解码 这段流 的各种参数信息
        AVCodecParameters *codecpar = stream->codecpar;

        //无论视频还是音频都需要干的一些事情(获得解码器)
        //1) 通过当前流使用的 编码方式,查找 解码器
        AVCodec *dec = avcodec_find_decoder(codecpar->codec_id);
        if (dec == NULL) {
            LOGE("查找解码器失败:%s",av_err2str(ret));
            callHelper->onError(THREAD_CHILD, FFMPEG_FIND_DECODER_FAIL);
            return;
        }
        //2) 获得解码器上下文
        AVCodecContext *context = avcodec_alloc_context3(dec);
        if (context == NULL) {
            LOGE("创建上下文失败:%s",av_err2str(ret));
            callHelper->onError(THREAD_CHILD, FFMPEG_ALLOC_CODEC_CONTEXT_FAIL);
            return;
        }
        //3)设置上下文内的一些参数
        ret = avcodec_parameters_to_context(context, codecpar);
        if (ret < 0) {
            LOGE("设置上下文失败:%s",av_err2str(ret));
            callHelper->onError(THREAD_CHILD, FFMPEG_CODEC_CONTEXT_PARAMETERS_FAIL);
            return;
        }
        //4)打开解码器
        ret = avcodec_open2(context, dec, 0);
        if (ret != 0) {
            LOGE("打开解码器失败:%s",av_err2str(ret));
            callHelper->onError(THREAD_CHILD, FFMPEG_OPEN_DECODER_FAIL);
            return;
        }
        //音频
        if (codecpar->codec_type == AVMEDIA_TYPE_AUDIO) {
            audioChannel = new AudioChannel;
        } else if (codecpar->codec_type == AVMEDIA_TYPE_VIDEO) {
            videoChannel = new VideoChannel;
        }
        //没有音视频(很少见)
        if (!audioChannel && !videoChannel) {
            LOGE("没有音视频:%s",av_err2str(ret));
            callHelper->onError(THREAD_CHILD, FFMPEG_NOMEDIA);
            return;
        }

        //准备完了 通知java 随时可以播放
        callHelper->onPrepare(THREAD_CHILD);
    }
}

JavaCallHelper

JavaCallHelper.h

//
// Created by PF0ZYBAJ on 2020-9-8.
//

#ifndef PLAYER_JAVACALLHELPER_H
#define PLAYER_JAVACALLHELPER_H


#include <jni.h>

class JavaCallHelper {
public:
    JavaCallHelper(JavaVM *vm,JNIEnv *env,jobject instance);
    ~JavaCallHelper();
    //回调Java
    void onError(int thread,int errorCode);
    void onPrepare(int thread);
private:
    //涉及到跨线程问题
    JavaVM *vm;
    JNIEnv *env;
    jobject instance;
    jmethodID onErrorId;
    jmethodID onPrepareId;;
};


#endif //PLAYER_JAVACALLHELPER_H

JavaCallHelper.cpp

//
// Created by PF0ZYBAJ on 2020-9-8.
//

#include "JavaCallHelper.h"
#include "macro.h"

JavaCallHelper::JavaCallHelper(JavaVM *vm, JNIEnv *env, jobject instance) {
    this->vm = vm;
    //如果在主线程 回调
    this->env = env;
    //一旦涉及到jobject 跨方法 跨线程 就需要创建全局引用
    this->instance = env->NewGlobalRef(instance);

    jclass clazz = env->GetObjectClass(instance);
    onErrorId = env->GetMethodID(clazz, "onError", "(I)V");
    onPrepareId = env->GetMethodID(clazz,"onPrepare","()V");
}

JavaCallHelper::~JavaCallHelper() {
    env->DeleteGlobalRef(instance);
}

void JavaCallHelper::onError(int thread, int errorCode) {
    //主线程
    if (thread == THREAD_MAIN) {
        env->CallVoidMethod(instance, onErrorId, errorCode);
    } else {
        //子线程
        JNIEnv *env;
        //获得属于我这一个线程的jinenv
        vm->AttachCurrentThread(&env, 0);
        env->CallVoidMethod(instance, onErrorId, errorCode);
        vm->DetachCurrentThread();
    }
}

void JavaCallHelper::onPrepare(int thread) {
    //主线程
    if (thread == THREAD_MAIN) {
        env->CallVoidMethod(instance, onPrepareId);
    } else {
        //子线程
        JNIEnv *env;
        //获得属于我这一个线程的jinenv
        vm->AttachCurrentThread(&env, 0);
        env->CallVoidMethod(instance, onPrepareId);
        vm->DetachCurrentThread();
    }
}

3 常量和一些宏定义

macro.h



#ifndef DNPLAYER_MACRO_H
#define DNPLAYER_MACRO_H

#include <android/log.h>


#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR,"FFMPEG",__VA_ARGS__)

//宏函数
#define DELETE(obj) if(obj){ delete obj; obj = 0; }

//标记线程 因为子线程需要attach
#define THREAD_MAIN 1
#define THREAD_CHILD 2

//错误代码
//打不开视频
#define FFMPEG_CAN_NOT_OPEN_URL 1
//找不到流媒体
#define FFMPEG_CAN_NOT_FIND_STREAMS 2
//找不到解码器
#define FFMPEG_FIND_DECODER_FAIL 3
//无法根据解码器创建上下文
#define FFMPEG_ALLOC_CODEC_CONTEXT_FAIL 4
//根据流信息 配置上下文参数失败
#define FFMPEG_CODEC_CONTEXT_PARAMETERS_FAIL 6
//打开解码器失败
#define FFMPEG_OPEN_DECODER_FAIL 7
//没有音视频
#define FFMPEG_NOMEDIA 8



#endif //DNPLAYER_MACRO_H

4 代码执行顺序

  • Java层调用了DNFFmpeg的prepare方法;
  • prepare方法开启了一个线程,执行_prepare方法;
  • _prepare方法中执行的内容最为关键,初始化网络、打开媒体、查找音频流、获取解码器、打开解码器、判断音频还是视频;最后回调Java层;
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值