IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> 移动开发 -> Android NDK 实现视音频播放器源码 -> 正文阅读

[移动开发]Android NDK 实现视音频播放器源码


曾经的痛,之前搞Android的时候很多东西都没有做总结,很多也忘了,慢慢重新整理回来吧,下面是实现安卓视频开发的部分关键代码,主要是用于存档,思路总结于注释,可参考(如有错误,欢迎指正 !)

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

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 用来播放声音的
)

熟悉一下之前的编码流程

项目流程图:

在这里插入图片描述

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

在这里插入图片描述


activity_main.xml:

<?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">

<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>

搭建C++上层:

思路在注释上

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();
}


Java层MainActivity(上层):

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();
    }

}


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

#include <jni.h>
#include <string>
#include "DerryPlayer.h"
#include "JNICallbakcHelper.h"

extern "C"{
    #include <libavutil/avutil.h>
}

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) {
}

C++实现文件:

#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<PlayerClass *>(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);
}

C++头文件:

#ifndef PLAYERCLASS_PLAYERCLASS_H
#define PLAYERCLASS_PLAYERCLASS_H

#include <cstring>
#include <pthread.h>
#include "AudioChannel.h"
#include "VideoChannel.h"
#include "JNICallbakcHelper.h"
#include "util.h"

extern "C" {//FFmpeg需要用C编译
    #include <libavformat/avformat.h>
};

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

For Alizary
《后续再补充》
《原本漫长的一天却已日暮西山》
《夕阳无限好,却是近黄昏,明天会更好》
在这里插入图片描述

  移动开发 最新文章
Vue3装载axios和element-ui
android adb cmd
【xcode】Xcode常用快捷键与技巧
Android开发中的线程池使用
Java 和 Android 的 Base64
Android 测试文字编码格式
微信小程序支付
安卓权限记录
知乎之自动养号
【Android Jetpack】DataStore
上一篇文章      下一篇文章      查看所有文章
加:2021-07-11 16:45:21  更:2021-07-11 16:46:12 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年5日历 -2024/5/20 8:43:37-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码