• Android NDK 实现视音频播放器源码


    目录:

    • CMake配置环境项目,gradle代码块:

    • 项目流程图:

    • ffmpeg解封装解码流程API概况:

    • activity_main.xml:

    • 搭建C++上层:

    • Java层MainActivity(上层):

    • 完成Native函数实现(JNI函数):

    • C++实现文件:

    • C++头文件:

    CMake配置环境项目,gradle代码块:

    1. android {
    2. compileSdkVersion 30
    3. buildToolsVersion "30.0.3"
    4. defaultConfig {
    5. applicationId "cn.itcast.newproject"
    6. minSdkVersion 17
    7. targetSdkVersion 28
    8. externalNativeBuild{
    9. cmake{
    10. cppFlags ""
    11. abiFilters "armeabi-v7a" //给出CMakeLists.txt指定编译此平台
    12. }
    13. }
    14. ndk{
    15. abiFilters("armeabi-v7a") //apk/lib/libnative-lib.so指定编译的是此平台
    16. }
    17. versionCode 1
    18. versionName "1.0"
    19. testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
    20. }
    21. buildTypes {
    22. release {
    23. minifyEnabled false
    24. proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
    25. }
    26. }
    27. }

    手写FFmpeg && rtmp(导入别人的也行):

     

    CMakeLists.txt:

    1. cmake_minimum_required(VERSION 3.6.4111459)
    2. set(FFMPEG ${CMAKE_SOURCE_DIR}/ffmpeg) ##拿到ffmpeg的路径
    3. set(RTMP ${CMAKE_SOURCE_DIR}/rtmp) ##拿到rtmp的路径
    4. include_directories(${FFMPEG}/include) ##导入ffmpeg的头文件
    5. set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -L${FFMPEG}/libs/${CMAKE_ANDROID_ARCH_ABI}") ##导入ffmpeg库指定
    6. set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -L${RTMP}/libs/${CMAKE_ANDROID_ARCH_ABI}") # rtmp库指定
    7. ##批量导入 源文件
    8. file(GLOB src_files *.cpp)
    9. add_library(
    10. native-lib # 总库libnative-lib.so
    11. SHARED # 动态库
    12. ${src_files})
    13. target_link_libraries(
    14. native-lib # 总库libnative-lib.so
    15. ##忽略顺序的方式,导入
    16. -Wl,--start-group
    17. avcodec avfilter avformat avutil swresample swscale
    18. -Wl,--end-group
    19. log # 日志库,打印日志用的
    20. z # libz.so库,是FFmpeg需要用ndk的z库,FFMpeg需要额外支持 libz.so
    21. rtmp # rtmp 后面会专门介绍
    22. android # android 后面会专门介绍,目前先要明白的是 ANativeWindow 用来渲染画面的
    23. OpenSLES # OpenSLES 后面会专门介绍,目前先要明白的是 OpenSLES 用来播放声音的
    24. )

    熟悉一下之前的编码流程

    项目流程图:

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

    activity_main.xml: 

    1. <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    2. xmlns:tools="http://schemas.android.com/tools"
    3. android:layout_width="match_parent"
    4. android:layout_height="match_parent"
    5. android:orientation="vertical"
    6. tools:context=".MainActivity">
    7. <SurfaceView
    8. android:id="@+id/surfaceView"
    9. android:layout_width="match_parent"
    10. android:layout_height="200dp" />
    11. <LinearLayout
    12. android:layout_width="match_parent"
    13. android:layout_height="30dp"
    14. android:layout_margin="5dp">
    15. <TextView
    16. android:id="@+id/tv_time"
    17. android:layout_width="wrap_content"
    18. android:layout_height="match_parent"
    19. android:gravity="center"
    20. android:text="00:00/00:00"
    21. android:visibility="gone" />
    22. <SeekBar
    23. android:id="@+id/seekBar"
    24. android:layout_width="0dp"
    25. android:layout_height="match_parent"
    26. android:layout_weight="1"
    27. android:max="100"
    28. android:visibility="gone" />
    29. LinearLayout>
    30. LinearLayout>

    搭建C++上层:

    思路在注释上

    1. package cn.itcast.newproject;
    2. public class PlayerClass {
    3. static {
    4. System.loadLibrary("native-lib");
    5. }
    6. //第一步先声明接口
    7. //下层工作完上层要有接口,准备成功的接口,会去告诉上层
    8. //接口是给Java层的main用的
    9. private OnPreparedListener onPreparedListener;
    10. public PlayerClass() {
    11. }
    12. // 第二步
    13. // 设置媒体源(文件路径++++直播地址rtmp)
    14. // sdk卡本地有MP4文件
    15. //声明meidiePlay dataSource
    16. private String dataSource;
    17. public void setDataSource(String dataSource) {
    18. this.dataSource = dataSource;
    19. }
    20. //第三步
    21. // 播放器准备播放,因为解封装格式不一定成功,一定要打开测试一下
    22. //成功后再调用接口
    23. public void prepare() {
    24. //传参媒体源
    25. prepareNative(dataSource);
    26. }
    27. //第四步
    28. // 开始播放
    29. public void start() {
    30. startNative(); }
    31. //第五步
    32. // 停止播放
    33. public void stop() {
    34. stopNative(); }
    35. //第六步
    36. //程序关闭时,释放资源
    37. public void release() {
    38. releaseNative(); }
    39. //给JNI反射调用的
    40. //Native层为C++下层,提供函数给上层Java层调用
    41. public void onPrepared() {
    42. //判空,不为空就回调
    43. if (onPreparedListener != null) {
    44. onPreparedListener.onPrepared();
    45. }
    46. }
    47. //设置准备成功的监听
    48. public void setOnPreparedListener(OnPreparedListener onPreparedListener) {
    49. this.onPreparedListener = onPreparedListener; }
    50. //准备成功的监听
    51. public interface OnPreparedListener {
    52. void onPrepared();}
    53. //Native函数实现区域
    54. //使用软编解码,硬编解码的调参数太烦了就不用了
    55. private native void prepareNative(String dataSource);
    56. private native void startNative();
    57. private native void stopNative();
    58. private native void releaseNative();
    59. }

    Java层MainActivity(上层): 

    1. package cn.itcast.newproject;
    2. import androidx.appcompat.app.AppCompatActivity;
    3. import android.os.Bundle;
    4. import android.os.Environment;
    5. import android.view.WindowManager;
    6. import android.widget.Toast;
    7. import java.io.File;
    8. public class MainActivity extends AppCompatActivity {
    9. private PlayerClass player;
    10. @Override
    11. protected void onCreate(Bundle savedInstanceState) {
    12. super.onCreate(savedInstanceState);
    13. getWindow().setFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON, WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
    14. setContentView(R.layout.activity_main);
    15. //创建类
    16. player = new PlayerClass();
    17. player.setDataSource(
    18. new File(Environment.getExternalStorageDirectory() + File.separator + "demo.mp4")
    19. .getAbsolutePath());
    20. // 准备成功的回调处
    21. // 被C++调用 可能会是子线程调用的
    22. player.setOnPreparedListener(new PlayerClass.OnPreparedListener() {
    23. @Override
    24. public void onPrepared() {
    25. runOnUiThread(new Runnable() {
    26. @Override
    27. public void run() {
    28. Toast.makeText(MainActivity.this, "准备完成,即将开始播放", Toast.LENGTH_SHORT).show();
    29. }
    30. });
    31. //准备成功,调用 C++ 开始播放
    32. player.start();
    33. }
    34. });
    35. }
    36. @Override // ActivityThread.java Handler
    37. protected void onResume() { // 触发准备
    38. super.onResume();
    39. //保证一触发就传到C++层
    40. //C++如果是准备成功就返回成功信息,回到runOnUiThread函数打印
    41. //再调用Play.start(),最后再调回C++
    42. player.prepare();
    43. }
    44. @Override
    45. protected void onStop() {
    46. super.onStop();
    47. player.stop();
    48. }
    49. //Activity关闭的时候释放资源,爱放不放
    50. @Override
    51. protected void onDestroy() {
    52. super.onDestroy();
    53. player.release();
    54. }
    55. }

    完成Native函数实现(JNI函数): 

    1. #include
    2. #include
    3. #include "DerryPlayer.h"
    4. #include "JNICallbakcHelper.h"
    5. extern "C"{
    6. #include
    7. }
    8. extern "C" JNIEXPORT jstring JNICALL
    9. Java_com_derry_player_MainActivity_getFFmpegVersion(
    10. JNIEnv *env,
    11. jobject /* this */) {
    12. std::string info = "FFmpeg的版本号是:";
    13. info.append(av_version_info());
    14. return env->NewStringUTF(info.c_str());
    15. }
    16. DerryPlayer *player = 0;
    17. JavaVM *vm = 0;
    18. jint JNI_OnLoad(JavaVM * vm, void * args) {
    19. ::vm = vm;
    20. return JNI_VERSION_1_6;
    21. }
    22. //prepareNative
    23. extern "C"
    24. JNIEXPORT void JNICALL
    25. Java_com_derry_player_DerryPlayer_prepareNative(JNIEnv *env, jobject job, jstring data_source) {
    26. const char * data_source_ = env->GetStringUTFChars(data_source, 0);
    27. auto *helper = new JNICallbakcHelper(vm, env, job); // C++子线程回调 , C++主线程回调
    28. player = new DerryPlayer(data_source_, helper);
    29. player->prepare();
    30. env->ReleaseStringUTFChars(data_source, data_source_);
    31. }
    32. //startNative
    33. extern "C"
    34. JNIEXPORT void JNICALL
    35. Java_com_derry_player_DerryPlayer_startNative(JNIEnv *env, jobject thiz) {
    36. if (player) {
    37. // player.start();
    38. }
    39. }
    40. //stopNative
    41. extern "C"
    42. JNIEXPORT void JNICALL
    43. Java_com_derry_player_DerryPlayer_stopNative(JNIEnv *env, jobject thiz) {
    44. }
    45. //releaseNative
    46. extern "C"
    47. JNIEXPORT void JNICALL
    48. Java_com_derry_player_DerryPlayer_releaseNative(JNIEnv *env, jobject thiz) {
    49. }

    C++实现文件: 

    1. #include "DerryPlayer.h"
    2. DerryPlayer::PlayerClass(const char *data_source, JNICallbakcHelper *helper) {
    3. // this->data_source = data_source;
    4. // 如果一旦被释放,会一定造成悬空指针
    5. // 记得复习深拷贝
    6. // this->data_source = new char[strlen(data_source)];
    7. // Java: demo.mp4
    8. // C层:demo.mp4\0 C层会自动 + \0, strlen不计算\0的长度,需要手动加 \0
    9. this->data_source = new char[strlen(data_source) + 1];
    10. strcpy(this->data_source, data_source); // 把源 Copy给成员
    11. this->helper = helper;
    12. }
    13. PlayerClass::~PlayerClass() {
    14. if (data_source) {
    15. delete data_source;
    16. }
    17. if (helper) {
    18. delete helper;
    19. }
    20. }
    21. void *task_prepare(void *args) {
    22. // 此函数和PlayerClass这个对象没有关系,不能用PlayerClass的私有成员
    23. // avformat_open_input(0, this->data_source)
    24. auto *player = static_cast(args);
    25. player->prepare_();
    26. return 0; // 必须返回,坑,错误很难找
    27. }
    28. void PlayerClass::prepare_() { // 此函数 是 子线程
    29. /**
    30. * TODO 第一步:打开媒体地址(文件路径, 直播地址rtmp)
    31. */
    32. formatContext = avformat_alloc_context();
    33. AVDictionary *dictionary = 0;
    34. av_dict_set(&dictionary, "timeout", "5000000", 0); // 单位微妙
    35. //AVFormatContext *
    36. //路径
    37. //AVInputFormat *fmt Mac、Windows 摄像头、麦克风,用不到不写,也不想写
    38. //Http 连接超时, 打开rtmp的超时 AVDictionary **options
    39. int r = avformat_open_input(&formatContext, data_source, 0, &dictionary);
    40. // 释放字典
    41. av_dict_free(&dictionary);
    42. if (r) {
    43. // 把错误信息反馈给Java,回调给Java Toast——打开媒体格式失败,请检查代码
    44. //实现 JNI 反射回调到Java方法,并提示
    45. return;
    46. }
    47. //第二步:查找媒体中的音视频流的信息
    48. r = avformat_find_stream_info(formatContext, 0);
    49. if (r < 0) {
    50. // 这里实现 JNI 反射回调到Java方法
    51. return;
    52. }
    53. //根据流信息,流的个数,用循环来找
    54. for (int i = 0; i < formatContext->nb_streams; ++i) {
    55. //获取媒体流(视频,音频)
    56. AVStream *stream = formatContext->streams[i];
    57. // 第五步:从上面的流中 获取 编码解码的【参数】
    58. //由于:后面的编码器 解码器 都需要参数(宽高 等等)
    59. AVCodecParameters *parameters = stream->codecpar;
    60. //第六步:(根据上面的【参数】)获取编解码器
    61. AVCodec *codec = avcodec_find_decoder(parameters->codec_id);
    62. //第七步:编解码器 上下文
    63. AVCodecContext *codecContext = avcodec_alloc_context3(codec);
    64. if (!codecContext) {
    65. // 实现 JNI 反射回调到Java方法,并提示
    66. return;
    67. }
    68. //第八步:空白parameters copy codecContext)
    69. r = avcodec_parameters_to_context(codecContext, parameters);
    70. if (r < 0) {
    71. // 实现JNI 反射回调到Java方法,并提示
    72. return;
    73. }
    74. //第九步:打开解码器
    75. r = avcodec_open2(codecContext, codec, 0);
    76. if (r) { // 非0就是true
    77. // 实现JNI 反射回调到Java方法,并提示
    78. return;
    79. }
    80. //第十步:从编解码器参数中,获取流的类型 codec_type === 音频 视频
    81. if (parameters->codec_type == AVMediaType::AVMEDIA_TYPE_AUDIO) { // 音频
    82. audio_channel = new AudioChannel();
    83. } else if (parameters->codec_type == AVMediaType::AVMEDIA_TYPE_VIDEO) { // 视频
    84. video_channel = new VideoChannel();
    85. }
    86. } // for end
    87. /**
    88. //第十一步: 如果流中 没有音频 也没有 视频 健壮性校验
    89. */
    90. if (!audio_channel && !video_channel) {
    91. // 实现JNI 反射回调到Java方法,并提示
    92. return;
    93. }
    94. //第十二步:媒体文件可以了,通知给上层
    95. if (helper) {
    96. helper->onPrepared(THREAD_CHILD);
    97. }
    98. }
    99. void DerryPlayer::prepare() {
    100. // 最后创建子线程
    101. pthread_create(&pid_prepare, 0, task_prepare, this);
    102. }

    C++头文件: 

    1. #ifndef PLAYERCLASS_PLAYERCLASS_H
    2. #define PLAYERCLASS_PLAYERCLASS_H
    3. #include
    4. #include
    5. #include "AudioChannel.h"
    6. #include "VideoChannel.h"
    7. #include "JNICallbakcHelper.h"
    8. #include "util.h"
    9. extern "C" {//FFmpeg需要用C编译
    10. #include
    11. };
    12. class DerryPlayer {
    13. private:
    14. char *data_source = 0; // 指针
    15. pthread_t pid_prepare;
    16. AVFormatContext *formatContext = 0;
    17. AudioChannel *audio_channel = 0;
    18. VideoChannel *video_channel = 0;
    19. JNICallbakcHelper *helper = 0;
    20. public:
    21. PlayerClass(const char *data_source, JNICallbakcHelper *helper);
    22. ~PlayerClass();
    23. void prepare();
    24. void prepare_();
    25. };
    26. #endif //PLAYERCLASS_PLAYERCLASS_H

    原文链接:Android NDK 实现视音频播放器源码 - 资料 - 我爱音视频网 - 构建全国最权威的音视频技术交流分享论坛

    本文福利, C++音视频学习资料包、技术视频,内容包括(音视频开发,面试题,FFmpeg webRTC rtmp hls rtsp ffplay srs↓↓↓↓↓↓见下面↓↓私信或文章底部领取↓↓

  • 相关阅读:
    纯后端如何写前端?我用了低代码平台
    【Nacos】spring cloud (feign)+ Nacos 负载均衡实现
    react原理篇:setState()方法说明
    【Python机器学习】回归模型:推土机售价预测
    第三届机器学习、云计算与智能挖掘国际会议(MLCCIM 2024)
    CHATGPT----自然辩证法分析
    【Python】通过 Python 设置电脑代理端口
    揭秘分布式文件系统大规模元数据管理机制——以Alluxio文件系统为例
    stm32---用外部中断实现红外接收器
    安卓的ATV系统
  • 原文地址:https://blog.csdn.net/m0_60259116/article/details/126015218