文章目录
一 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层;