NDK前期基础知识终于学完了,现在开始进入项目实战学习,通过FFmpeg实现一个简单的音视频播放器。
音视频播放器系列:
NDK FFmpeg音视频播放器一
NDK FFmpeg音视频播放器二
NDK FFmpeg音视频播放器三
NDK FFmpeg音视频播放器四
NDK FFmpeg音视频播放器五
NDK FFmpeg音视频播放器六
本文主要内容如下:
CMake配置项目环境。
项目流程图与FFmpeg的函数图解分析。
Java层Player搭建。
Native层Player搭建与线程(完成音视频--解封装)。
Native层与Java层交互。
用到的ffmpeg、rtmp等库资源:
ffmpeg_rtmp库.zip - 蓝奏云
源码:
NdkPlayer: 通过FFmpeg实现一个简单的音视频播放器。
一、CMake配置项目环境。
1)导入ffmpeg、rtmp等库
cmake_minimum_required(VERSION 3.10.2)
set(FFMPEG ${CMAKE_SOURCE_DIR}/ffmpeg) # ffmpeg的路径
set(RTMP ${CMAKE_SOURCE_DIR}/rtmp) # rtmp的路径
include_directories(${FFMPEG}/include) # 导入ffmpeg的头文件
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -L${FFMPEG}/libs/${CMAKE_ANDROID_ARCH_ABI}") # ffmpeg库指定
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -L${RTMP}/libs/${CMAKE_ANDROID_ARCH_ABI}") # rtmp库指定
file(GLOB src_files *.cpp) # 批量导入 源文件
add_library(native-lib # 总库libnative-lib.so
SHARED
${src_files})
target_link_libraries(native-lib # 总库libnative-lib.so
-Wl,--start-group # 忽略顺序的方式,导入
avcodec avfilter avformat avutil swresample swscale
-Wl,--end-group
log # 日志库,打印日志用的
z # libz.so库,是FFmpeg需要用ndk的z库,FFMpeg需要额外支持 libz.so
rtmp # rtmp
android # android ANativeWindow 用来渲染画面的
OpenSLES # OpenSLES 用来播放声音的
)
2)配置build.gradle
defaultConfig {
externalNativeBuild {
cmake {
// cppFlags ""
// 指定CPU架构,Cmake的本地库, 例如:native-lib ---> armeabi-v7a
abiFilters "armeabi-v7a"
}
}
// 指定CPU架构,打入APK lib/CPU平台
ndk {
abiFilters "armeabi-v7a"
}
}
二、项目流程图与FFmpeg的函数图解分析
1)视音频播放器流程概况:
2)ffmpeg解封装解码流程API概况:
三、Java层Player搭建
1)简单布局文件
2)MainActivity
音视频的准备播放工作主要放在NdkPlayer.class中实现,MainActivity主要作用在于各生命周期触发时,调用NdkPlayer.class去实现功能。
package com.ndk.player;
import androidx.appcompat.app.AppCompatActivity;
import android.os.Bundle;
import android.os.Environment;
import android.util.Log;
import android.widget.Toast;
import java.io.File;
public class MainActivity extends AppCompatActivity {
private NdkPlayer mNdkPlayer;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
initData();
}
private void initData() {
String dataSource = "file:///android_asset/video.mp4";
File file = Environment.getExternalStoragePublicDirectory(
Environment.DIRECTORY_DOCUMENTS + "/NDK/video/test.mp4");
if (file.exists()) {
dataSource = file.getPath();
}
Log.i("MainActivity", "initData dataSource = " + dataSource);
mNdkPlayer = new NdkPlayer(dataSource);
// 准备成功的回调处 <---- native层 在子线程调用的
mNdkPlayer.setOnPreparedListener((int code, String msg) -> {
runOnUiThread(() ->
Toast.makeText(MainActivity.this,
msg, Toast.LENGTH_SHORT).show());
if (code == 200) {
mNdkPlayer.start(); // 调用native层 开始播放
}
});
}
@Override
protected void onResume() {
super.onResume();
// 准备工作:触发
mNdkPlayer.prepare();
}
@Override
protected void onStop() {
super.onStop();
mNdkPlayer.stop();
}
@Override
protected void onDestroy() {
super.onDestroy();
mNdkPlayer.release();
}
}
3)NdkPlayer.class
定义音视频的准备、开始、停止播放的等功能;
定义native层实现方法,接口回调等。
package com.ndk.player;
public class NdkPlayer {
static {
System.loadLibrary("native-lib");
}
/**
* 播放源(文件路径, 直播地址rtmp)
*/
private String dataSource;
/**
* 准备情况的接口
*/
private OnPreparedListener onPreparedListener;
public NdkPlayer(String dataSource) {
this.dataSource = dataSource;
}
/**
* 播放前的 准备工作
*/
public void prepare() {
prepareNative(dataSource);
}
/**
* 开始播放
*/
public void start() {
startNative();
}
/**
* 停止播放
*/
public void stop() {
stopNative();
}
/**
* 释放资源
*/
public void release() {
releaseNative();
}
/**
* 给native层jni反射调用的
*/
public void onPrepared(int code, String msg) {
if (onPreparedListener != null) {
onPreparedListener.onPrepared(code, msg);
}
}
/**
* 设置准备的监听方法
*/
public void setOnPreparedListener(OnPreparedListener onPreparedListener) {
this.onPreparedListener = onPreparedListener;
}
/**
* 准备的监听接口
*/
public interface OnPreparedListener {
void onPrepared(int code, String msg);
}
/**
* native函数区域
*/
private native void prepareNative(String dataSource);
private native void startNative();
private native void stopNative();
private native void releaseNative();
}
四、Native层Player搭建与线程(完成音视频--解封装)
1)native-lib.cpp
Java层调用的Native层方法在native-lib.cpp编写。
音视频准备播放等实现在NdkPlayer.cpp中完成,
通过JINCallbackHelper.cpp回调给Java层。
#include
#include
#include "NdkPlayer.h"
NdkPlayer *ndk_player = 0;
JavaVM *java_vm = 0;
/**
* 在Java层执行 System.loadLibrary时,调用该函数
* @param vm
* @param args
* @return
*/
jint JNI_OnLoad(JavaVM *vm, void *args) {
::java_vm = vm;
return JNI_VERSION_1_6;
}
/**
* 准备工作
*/
extern "C"
JNIEXPORT void JNICALL
Java_com_ndk_player_NdkPlayer_prepareNative(JNIEnv *env, jobject thiz, jstring data_source) {
const char *data_source_ = env->GetStringUTFChars(data_source, 0);
JINCallbackHelper *helper = new JINCallbackHelper(java_vm, env, thiz);
ndk_player = new NdkPlayer(data_source_, helper);
ndk_player->prepare();
}
/**
* 开始播放
*/
extern "C"
JNIEXPORT void JNICALL
Java_com_ndk_player_NdkPlayer_startNative(JNIEnv *env, jobject thiz) {
}
/**
* 停止播放
*/
extern "C"
JNIEXPORT void JNICALL
Java_com_ndk_player_NdkPlayer_stopNative(JNIEnv *env, jobject thiz) {
}
/**
* 释放资源
*/
extern "C"
JNIEXPORT void JNICALL
Java_com_ndk_player_NdkPlayer_releaseNative(JNIEnv *env, jobject thiz) {
}
2)NdkPlayer.cpp
调用FFmpeg API 实现音视频播放器功能
#include "NdkPlayer.h"
NdkPlayer::NdkPlayer(const char *data_source, JINCallbackHelper *helper) {
// 报错,如果被释放,会造成悬空指针
// this->data_source = data_source;
// 深拷贝
// this->data_source = new char[strlen(data_source)];
// Java: xxx.mp4
// C层:xxx.mp4\0 C层会自动 + \0, strlen不计算\0的长度,所以我们需要手动加 \0
this->data_source = new char[strlen(data_source) + 1];
// 把源 Copy给成员
strcpy(this->data_source, data_source);
this->helper = helper;
}
NdkPlayer::~NdkPlayer() {
if (data_source) {
delete data_source;
}
}
/**
* 函数指针
* 此函数和NdkPlayer这个对象没有关系,你没法拿NdkPlayer的私有成员(data_source)
* @return
*/
void *task_prepare(void *ndk_player) {
NdkPlayer *ndk_player_ = static_cast(ndk_player);
// 无法获取私有成员data_source
// ndk_player_->data_source
// 在NdkPlayer内部再创建一个prepare_方法,在prepare_()里面可以获取data_source
ndk_player_->prepare_();
return 0; // 必须返回,否则报错
}
/**
* 解封装
* 通过FFmpeg来解析data_source(文件io流或直播网络rtmp)
* 是耗时操作,故在子线程执行
*/
void NdkPlayer::prepare() {
LOGI("NdkPlayer::prepare()");
// 创建子线程
pthread_create(&pid_prepare, 0, task_prepare, this);
}
/**
* 真正开始 解封装
*/
void NdkPlayer::prepare_() {
LOGI("NdkPlayer::prepare_() %s\n", data_source);
/**
* TODO 第一步:打开媒体地址(文件路径, 直播地址rtmp)
* FFmpeg源码,大量使用上下文Context,
* 因为FFmpeg源码是纯C的,他不像C++、Java ,
* 上下文的出现是为了贯彻环境,就相当于Java的this能够操作成员
*/
format_context = avformat_alloc_context();
AVDictionary *dictionary = 0;
// 设置解封装超时时间
av_dict_set(&dictionary, "timeout", "5000000", 0); // 单位微妙
/**
* 打开媒体格式
* 参数1,AVFormatContext *
* 参数2,路径
* 参数3,AVInputFormat *fmt Mac、Windows 摄像头、麦克风, 安卓不支持
* 参数4,各种设置:例如:Http 连接超时, 打开rtmp的超时 AVDictionary **options
* @return 0 on success
*/
int result = avformat_open_input(&format_context, data_source, 0, &dictionary);
LOGI("NdkPlayer::avformat_open_input = %d\n", result);
// 用完释放
av_dict_free(&dictionary);
if (result) {
// 打开媒体格式失败,把错误信息反馈给Java层,Toast【打开媒体格式失败,请检查代码】
this->helper->prepare(0, "打开媒体格式失败,请检查代码");
return;
}
/**
* TODO 第二步:查找媒体中的音视频流的信息
* @return >=0 if OK
*/
result = avformat_find_stream_info(format_context, 0);
LOGI("NdkPlayer::avformat_find_stream_info = %d\n", result);
if (result < 0) {
// 失败,通过JNI反射回调到Java层方法,并提示
this->helper->prepare(0, "查找音视频流信息失败");
return;
}
/**
* TODO 第三步:根据流信息,流的个数,用循环来找 音频流和视频流
*/
for (int i = 0; i < format_context->nb_streams; ++i) {
LOGI("NdkPlayer::开始遍历流信息 i = %d\n", i);
/**
* TODO 第四步:获取媒体流(视频,音频)
*/
AVStream *stream = format_context->streams[i];
/**
* TODO 第五步:从上面的流中 获取 编码解码的【参数】
* 由于:后面的编码器 解码器 都需要参数(宽高 等等)
*/
AVCodecParameters *parameters = stream->codecpar;
/**
* TODO 第六步:(根据上面的【参数】)获取编解码器
*/
AVCodec *codec = avcodec_find_decoder(parameters->codec_id);
/**
* TODO 第七步:编解码器 上下文
*/
AVCodecContext *codec_context = avcodec_alloc_context3(codec);
if (!codec_context) {
// 失败,通过JNI反射回调到Java层方法,并提示
this->helper->prepare(0, "获取编解码器失败");
return;
}
/**
* TODO 第八步:把参数复制到编解码器上下文(parameters copy codecContext)
* @return >= 0 on success
*/
result = avcodec_parameters_to_context(codec_context, parameters);
LOGI("NdkPlayer::avcodec_parameters_to_context = %d\n", result);
if (result < 0) {
// 失败,通过JNI反射回调到Java层方法,并提示
this->helper->prepare(0, "把参数复制到编解码器上下文失败");
return;
}
/**
* TODO 第九步:打开解码器
* zero on success
*/
result = avcodec_open2(codec_context, codec, 0);
LOGI("NdkPlayer::avcodec_open2 = %d\n", result);
// 非0就是true,非0就是失败,true就是失败
if (result) {
// 失败,通过JNI反射回调到Java层方法,并提示
this->helper->prepare(0, "打开解码器失败");
return;
}
/**
* TODO 第十步:从编解码器参数中,获取流的类型 codec_type === 音频 视频
*/
if (parameters->codec_type == AVMediaType::AVMEDIA_TYPE_AUDIO) {
// 音频
audio_channel = new AudioChannel();
} else if (parameters->codec_type == AVMediaType::AVMEDIA_TYPE_VIDEO) {
// 视频
video_channel = new VideoChannel();
}
} // for end
/**
* TODO 第十一步: 如果流中没有音频 也没有视频,则失败【健壮性校验】
*/
if (!audio_channel && !video_channel) {
// 失败,通过JNI反射回调到Java层方法,并提示
this->helper->prepare(0, "没有音频 也没有视频");
return;
}
/**
* TODO 第十二步:准备成功,我媒体文件 OK了,通知给java层
*/
int code = 200;
// 定义c++层字符串
const char *msg = "准备成功,即将开始播放";
LOGI("NdkPlayer::helper->prepare = %s\n", msg);
this->helper->prepare(code, msg);
}
3)NdkPlayer.h
NdkPlayer.cpp的头文件,主要作用:导包,声明函数和成员属性。
#ifndef NDKPLAYER_NDKPLAYER_H
#define NDKPLAYER_NDKPLAYER_H
#include
#include
#include
#include "AudioChannel.h"
#include "VideoChannel.h"
#include "JINCallbackHelper.h"
// ffmpeg是纯c写的,必须采用c的编译方式,否则奔溃
extern "C" {
#include
}
// log宏
#define TAG "NDK"
#define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG, TAG, __VA_ARGS__)
#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, TAG, __VA_ARGS__)
#define LOGI(...) __android_log_print(ANDROID_LOG_INFO, TAG, __VA_ARGS__)
class NdkPlayer {
private:
JINCallbackHelper *helper = 0;
char *data_source = 0;
pthread_t pid_prepare = 0;
AVFormatContext *format_context = 0;
AudioChannel *audio_channel = 0;
VideoChannel *video_channel = 0;
public:
NdkPlayer(const char *data_source, JINCallbackHelper *helper);
virtual ~NdkPlayer();
void prepare();
void prepare_();
};
#endif //NDKPLAYER_NDKPLAYER_H
五、Native层与Java层交互
1)JINCallbackHelper.cpp
实现Native层与Java层的通讯,通过jni反射调用Java层的方法。
#include "JINCallbackHelper.h"
JINCallbackHelper::JINCallbackHelper(JavaVM *vm, JNIEnv *env, jobject job) {
// JavaVM:能够跨越线程,能够跨越函数;
this->vm = vm;
// JNIEnv:不能跨越线程,否则奔溃,可以跨越函数;使用时判断是否跨越线程。
this->env = env;
/**
* jobject:不能跨越线程,否则奔溃,不能跨越函数,否则奔溃。
* 解决方案:提升全局引用
* 注:此时使用的env跟调用new JINCallbackHelper(java_vm, env, thiz)时,
* 传递的参数env是在同一个线程,故无需做任何处理,直接使用env。
*/
this->job = env->NewGlobalRef(job);
// 获取Java层的方法Id,即java层的NdkPlayer#onPrepared(int code, String msg)
jclass clazz = env->GetObjectClass(job);
this->jmd_prepared = env->GetMethodID(clazz, "onPrepared", "(ILjava/lang/String;)V");
}
JINCallbackHelper::~JINCallbackHelper() {
// 释放
vm = 0;
env->DeleteGlobalRef(job);
job = 0;
env = 0;
}
void JINCallbackHelper::prepare(int code, const char *msg) {
/**
* prepare()方法是在子线程中调用的,跟new JINCallbackHelper()
* 参数env不在同一个线程,需要做处理,否则崩溃。
* JNIEnv:不能跨越线程,否则奔溃,可以跨越函数;
* 解决方案:使用全局的JavaVM附加当前异步线程 得到权限env操作
*/
JNIEnv *env_prepare;
vm->AttachCurrentThread(&env_prepare, 0);
// 回调java层 NdkPlayer#onPrepared(int code, String msg)
// int -> jint无需转换,char * 需转换为 jstring
jstring jstr_msg = env_prepare->NewStringUTF(msg);
env_prepare->CallVoidMethod(job, jmd_prepared, code, jstr_msg);
vm->DetachCurrentThread();
}
2)JINCallbackHelper.h
JINCallbackHelper.cpp的头文件,主要作用:导包,声明函数和成员属性。
#ifndef NDKPLAYER_JINCALLBACKHELPER_H
#define NDKPLAYER_JINCALLBACKHELPER_H
#include
class JINCallbackHelper {
private:
JavaVM *vm = 0;
JNIEnv *env = 0;
jobject job;
jmethodID jmd_prepared;
public:
JINCallbackHelper(JavaVM *vm, JNIEnv *env, jobject job);
virtual ~JINCallbackHelper();
void prepare(int code, const char *msg);
};
#endif //NDKPLAYER_JINCALLBACKHELPER_H
音视频--解封装功能完成,接下来开始播放工作。。。