NDK FFmpeg音视频播放器一
NDK前期基础知识终于学完了,现在开始进入项目实战学习,通过FFmpeg实现一个简单的音视频播放器。

本文主要内容如下:
-
CMake配置项目环境。
-
项目流程图与FFmpeg的函数图解分析。
-
Java层Player搭建。
-
Native层Player搭建与线程(完成音视频--解封装)。
-
Native层与Java层交互。
用到的ffmpeg、rtmp等库资源:
https://wwgl.lanzout.com/iN21C0qiiija
一、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.soSHARED${src_files})target_link_libraries(native-lib # 总库libnative-lib.so-Wl,--start-group # 忽略顺序的方式,导入avcodec avfilter avformat avutil swresample swscale-Wl,--end-grouplog # 日志库,打印日志用的z # libz.so库,是FFmpeg需要用ndk的z库,FFMpeg需要额外支持 libz.sortmp # rtmp android # android ANativeWindow 用来渲染画面的OpenSLES # OpenSLES 用来播放声音的)
2)配置build.gradle
defaultConfig {externalNativeBuild {cmake {// cppFlags ""// 指定CPU架构,Cmake的本地库, 例如:native-lib ---> armeabi-v7aabiFilters "armeabi-v7a"}}// 指定CPU架构,打入APK lib/CPU平台ndk {abiFilters "armeabi-v7a"}
}
二、项目流程图与FFmpeg的函数图解分析
1)视音频播放器流程概况:

2)ffmpeg解封装解码流程API概况:

三、Java层Player搭建
1)简单布局文件
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"xmlns:tools="http://schemas.android.com/tools"android:layout_width="match_parent"android:layout_height="match_parent"android:orientation="vertical"tools:context=".MainActivity"><SurfaceViewandroid:id="@+id/surfaceView"android:layout_width="match_parent"android:layout_height="200dp" /><LinearLayoutandroid:layout_width="match_parent"android:layout_height="30dp"android:layout_margin="5dp"><TextViewandroid:id="@+id/tv_time"android:layout_width="wrap_content"android:layout_height="match_parent"android:gravity="center"android:text="00:00/00:00"android:visibility="gone" /><SeekBarandroid:id="@+id/seekBar"android:layout_width="0dp"android:layout_height="match_parent"android:layout_weight="1"android:max="100"android:visibility="gone" /></LinearLayout></LinearLayout>
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;@Overrideprotected 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层 开始播放}});}@Overrideprotected void onResume() {super.onResume();// 准备工作:触发mNdkPlayer.prepare();}@Overrideprotected void onStop() {super.onStop();mNdkPlayer.stop();}@Overrideprotected 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 <jni.h>
#include <string>
#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的长度,所以我们需要手动加 \\0this->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<NdkPlayer *>(ndk_player);// 无法获取私有成员data_source// ndk_player_->data_source// 在NdkPlayer内部再创建一个prepare_方法,在prepare_()里面可以获取data_sourcendk_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 <cstring>
#include <pthread.h>
#include <android/log.h>
#include "AudioChannel.h"
#include "VideoChannel.h"
#include "JINCallbackHelper.h"// ffmpeg是纯c写的,必须采用c的编译方式,否则奔溃
extern "C" {
#include <libavformat/avformat.h>
}// 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 * 需转换为 jstringjstring 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 <jni.h>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

音视频--解封装功能完成,接下来开始播放工作。。。