CMake配置环境项目,gradle代码块:
项目流程图:
ffmpeg解封装解码流程API概况:
activity_main.xml:
搭建C++上层:
Java层MainActivity(上层):
完成Native函数实现(JNI函数):
C++实现文件:
C++头文件:
- android {
- compileSdkVersion 30
- buildToolsVersion "30.0.3"
- defaultConfig {
- applicationId "cn.itcast.newproject"
- minSdkVersion 17
- targetSdkVersion 28
- externalNativeBuild{
- cmake{
- cppFlags ""
- abiFilters "armeabi-v7a" //给出CMakeLists.txt指定编译此平台
- }
- }
- ndk{
- abiFilters("armeabi-v7a") //apk/lib/libnative-lib.so指定编译的是此平台
- }
- versionCode 1
- versionName "1.0"
- testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
- }
- buildTypes {
- release {
- minifyEnabled false
- proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
- }
- }
- }
手写FFmpeg && rtmp(导入别人的也行):
CMakeLists.txt:
- cmake_minimum_required(VERSION 3.6.4111459)
-
- 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 后面会专门介绍,目前先要明白的是 OpenSLES 用来播放声音的
- )
熟悉一下之前的编码流程
- <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">
-
- <SurfaceView
- android:id="@+id/surfaceView"
- android:layout_width="match_parent"
- android:layout_height="200dp" />
-
- <LinearLayout
- android:layout_width="match_parent"
- android:layout_height="30dp"
- android:layout_margin="5dp">
-
- <TextView
- android: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" />
-
- <SeekBar
- android:id="@+id/seekBar"
- android:layout_width="0dp"
- android:layout_height="match_parent"
- android:layout_weight="1"
- android:max="100"
- android:visibility="gone" />
- LinearLayout>
-
-
- LinearLayout>
思路在注释上
- package cn.itcast.newproject;
-
- public class PlayerClass {
-
- static {
- System.loadLibrary("native-lib");
- }
-
- //第一步先声明接口
- //下层工作完上层要有接口,准备成功的接口,会去告诉上层
- //接口是给Java层的main用的
- private OnPreparedListener onPreparedListener;
- public PlayerClass() {
- }
-
- // 第二步
- // 设置媒体源(文件路径++++直播地址rtmp)
- // sdk卡本地有MP4文件
- //声明meidiePlay dataSource
- private String dataSource;
- public void setDataSource(String dataSource) {
- this.dataSource = dataSource;
- }
-
- //第三步
- // 播放器准备播放,因为解封装格式不一定成功,一定要打开测试一下
- //成功后再调用接口
- public void prepare() {
- //传参媒体源
- prepareNative(dataSource);
- }
-
- //第四步
- // 开始播放
- public void start() {
- startNative(); }
-
- //第五步
- // 停止播放
- public void stop() {
- stopNative(); }
-
- //第六步
- //程序关闭时,释放资源
- public void release() {
- releaseNative(); }
-
-
- //给JNI反射调用的
- //Native层为C++下层,提供函数给上层Java层调用
- public void onPrepared() {
- //判空,不为空就回调
- if (onPreparedListener != null) {
- onPreparedListener.onPrepared();
- }
- }
-
- //设置准备成功的监听
- public void setOnPreparedListener(OnPreparedListener onPreparedListener) {
- this.onPreparedListener = onPreparedListener; }
-
- //准备成功的监听
- public interface OnPreparedListener {
- void onPrepared();}
-
- //Native函数实现区域
- //使用软编解码,硬编解码的调参数太烦了就不用了
- private native void prepareNative(String dataSource);
- private native void startNative();
- private native void stopNative();
- private native void releaseNative();
- }
-
- package cn.itcast.newproject;
-
- import androidx.appcompat.app.AppCompatActivity;
-
- import android.os.Bundle;
- import android.os.Environment;
- import android.view.WindowManager;
- import android.widget.Toast;
-
- import java.io.File;
-
- public class MainActivity extends AppCompatActivity {
-
- private PlayerClass player;
- @Override
- protected void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- getWindow().setFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON, WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
- setContentView(R.layout.activity_main);
-
- //创建类
- player = new PlayerClass();
- player.setDataSource(
- new File(Environment.getExternalStorageDirectory() + File.separator + "demo.mp4")
- .getAbsolutePath());
- // 准备成功的回调处
- // 被C++调用 可能会是子线程调用的
- player.setOnPreparedListener(new PlayerClass.OnPreparedListener() {
- @Override
- public void onPrepared() {
- runOnUiThread(new Runnable() {
- @Override
- public void run() {
- Toast.makeText(MainActivity.this, "准备完成,即将开始播放", Toast.LENGTH_SHORT).show();
- }
- });
- //准备成功,调用 C++ 开始播放
- player.start();
- }
- });
- }
- @Override // ActivityThread.java Handler
- protected void onResume() { // 触发准备
- super.onResume();
- //保证一触发就传到C++层
- //C++如果是准备成功就返回成功信息,回到runOnUiThread函数打印
- //再调用Play.start(),最后再调回C++
- player.prepare();
- }
-
- @Override
- protected void onStop() {
- super.onStop();
- player.stop();
- }
-
- //Activity关闭的时候释放资源,爱放不放
- @Override
- protected void onDestroy() {
- super.onDestroy();
- player.release();
- }
-
- }
-
- #include
- #include
- #include "DerryPlayer.h"
- #include "JNICallbakcHelper.h"
-
- extern "C"{
- #include
- }
-
- extern "C" JNIEXPORT jstring JNICALL
- Java_com_derry_player_MainActivity_getFFmpegVersion(
- JNIEnv *env,
- jobject /* this */) {
- std::string info = "FFmpeg的版本号是:";
- info.append(av_version_info());
- return env->NewStringUTF(info.c_str());
- }
-
- DerryPlayer *player = 0;
- JavaVM *vm = 0;
- jint JNI_OnLoad(JavaVM * vm, void * args) {
- ::vm = vm;
- return JNI_VERSION_1_6;
- }
-
- //prepareNative
- extern "C"
- JNIEXPORT void JNICALL
- Java_com_derry_player_DerryPlayer_prepareNative(JNIEnv *env, jobject job, jstring data_source) {
- const char * data_source_ = env->GetStringUTFChars(data_source, 0);
- auto *helper = new JNICallbakcHelper(vm, env, job); // C++子线程回调 , C++主线程回调
- player = new DerryPlayer(data_source_, helper);
- player->prepare();
- env->ReleaseStringUTFChars(data_source, data_source_);
- }
-
- //startNative
- extern "C"
- JNIEXPORT void JNICALL
- Java_com_derry_player_DerryPlayer_startNative(JNIEnv *env, jobject thiz) {
- if (player) {
- // player.start();
- }
- }
-
- //stopNative
- extern "C"
- JNIEXPORT void JNICALL
- Java_com_derry_player_DerryPlayer_stopNative(JNIEnv *env, jobject thiz) {
- }
-
- //releaseNative
- extern "C"
- JNIEXPORT void JNICALL
- Java_com_derry_player_DerryPlayer_releaseNative(JNIEnv *env, jobject thiz) {
- }
- #include "DerryPlayer.h"
-
- DerryPlayer::PlayerClass(const char *data_source, JNICallbakcHelper *helper) {
- // this->data_source = data_source;
- // 如果一旦被释放,会一定造成悬空指针
-
- // 记得复习深拷贝
- // this->data_source = new char[strlen(data_source)];
- // Java: demo.mp4
- // C层:demo.mp4\0 C层会自动 + \0, strlen不计算\0的长度,需要手动加 \0
-
- this->data_source = new char[strlen(data_source) + 1];
- strcpy(this->data_source, data_source); // 把源 Copy给成员
-
- this->helper = helper;
- }
-
- PlayerClass::~PlayerClass() {
- if (data_source) {
- delete data_source;
- }
-
- if (helper) {
- delete helper;
- }
- }
-
- void *task_prepare(void *args) {
- // 此函数和PlayerClass这个对象没有关系,不能用PlayerClass的私有成员
-
- // avformat_open_input(0, this->data_source)
-
- auto *player = static_cast
(args); - player->prepare_();
-
- return 0; // 必须返回,坑,错误很难找
- }
-
- void PlayerClass::prepare_() { // 此函数 是 子线程
- /**
- * TODO 第一步:打开媒体地址(文件路径, 直播地址rtmp)
- */
- formatContext = avformat_alloc_context();
-
- AVDictionary *dictionary = 0;
- av_dict_set(&dictionary, "timeout", "5000000", 0); // 单位微妙
-
-
- //AVFormatContext *
- //路径
- //AVInputFormat *fmt Mac、Windows 摄像头、麦克风,用不到不写,也不想写
- //Http 连接超时, 打开rtmp的超时 AVDictionary **options
-
- int r = avformat_open_input(&formatContext, data_source, 0, &dictionary);
-
- // 释放字典
- av_dict_free(&dictionary);
-
- if (r) {
- // 把错误信息反馈给Java,回调给Java Toast——打开媒体格式失败,请检查代码
- //实现 JNI 反射回调到Java方法,并提示
- return;
- }
-
- //第二步:查找媒体中的音视频流的信息
-
- r = avformat_find_stream_info(formatContext, 0);
- if (r < 0) {
- // 这里实现 JNI 反射回调到Java方法
- return;
- }
-
- //根据流信息,流的个数,用循环来找
-
- for (int i = 0; i < formatContext->nb_streams; ++i) {
-
- //获取媒体流(视频,音频)
- AVStream *stream = formatContext->streams[i];
-
-
- // 第五步:从上面的流中 获取 编码解码的【参数】
- //由于:后面的编码器 解码器 都需要参数(宽高 等等)
-
- AVCodecParameters *parameters = stream->codecpar;
-
-
- //第六步:(根据上面的【参数】)获取编解码器
- AVCodec *codec = avcodec_find_decoder(parameters->codec_id);
-
-
- //第七步:编解码器 上下文
-
- AVCodecContext *codecContext = avcodec_alloc_context3(codec);
- if (!codecContext) {
- // 实现 JNI 反射回调到Java方法,并提示
- return;
- }
-
- //第八步:空白parameters copy codecContext)
-
- r = avcodec_parameters_to_context(codecContext, parameters);
- if (r < 0) {
- // 实现JNI 反射回调到Java方法,并提示
- return;
- }
-
-
- //第九步:打开解码器
-
- r = avcodec_open2(codecContext, codec, 0);
- if (r) { // 非0就是true
- // 实现JNI 反射回调到Java方法,并提示
- return;
- }
-
-
- //第十步:从编解码器参数中,获取流的类型 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
-
- /**
- //第十一步: 如果流中 没有音频 也没有 视频 健壮性校验
- */
- if (!audio_channel && !video_channel) {
- // 实现JNI 反射回调到Java方法,并提示
- return;
- }
-
- //第十二步:媒体文件可以了,通知给上层
- if (helper) {
- helper->onPrepared(THREAD_CHILD);
- }
- }
-
- void DerryPlayer::prepare() {
- // 最后创建子线程
- pthread_create(&pid_prepare, 0, task_prepare, this);
- }
- #ifndef PLAYERCLASS_PLAYERCLASS_H
- #define PLAYERCLASS_PLAYERCLASS_H
-
- #include
- #include
- #include "AudioChannel.h"
- #include "VideoChannel.h"
- #include "JNICallbakcHelper.h"
- #include "util.h"
-
- extern "C" {//FFmpeg需要用C编译
- #include
- };
-
- class DerryPlayer {
-
- private:
- char *data_source = 0; // 指针
- pthread_t pid_prepare;
- AVFormatContext *formatContext = 0;
- AudioChannel *audio_channel = 0;
- VideoChannel *video_channel = 0;
- JNICallbakcHelper *helper = 0;
-
- public:
- PlayerClass(const char *data_source, JNICallbakcHelper *helper);
- ~PlayerClass();
-
- void prepare();
- void prepare_();
- };
-
-
- #endif //PLAYERCLASS_PLAYERCLASS_H
-
原文链接:Android NDK 实现视音频播放器源码 - 资料 - 我爱音视频网 - 构建全国最权威的音视频技术交流分享论坛
本文福利, C++音视频学习资料包、技术视频,内容包括(音视频开发,面试题,FFmpeg ,webRTC ,rtmp ,hls ,rtsp ,ffplay ,srs)↓↓↓↓↓↓见下面↓↓私信或文章底部领取↓↓