NDK FFmpeg音视频播放器一

文章详细介绍了如何使用NDK结合FFmpeg库,通过CMake配置项目环境,建立Java层和Native层的交互,实现一个简单的音视频播放器。内容涵盖了CMake的配置,项目流程图,FFmpeg解封装解码流程,以及Java层和Native层的Player搭建过程。
摘要由CSDN通过智能技术生成

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

音视频播放器系列:

NDK FFmpeg音视频播放器一

NDK FFmpeg音视频播放器二

NDK FFmpeg音视频播放器三

NDK FFmpeg音视频播放器四

NDK FFmpeg音视频播放器五

NDK FFmpeg音视频播放器六

本文主要内容如下:

  1. CMake配置项目环境。

  1. 项目流程图与FFmpeg的函数图解分析。

  1. Java层Player搭建。

  1. Native层Player搭建与线程(完成音视频--解封装)。

  1. 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)简单布局文件


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

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 <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的长度,所以我们需要手动加 \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<NdkPlayer *>(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 <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 * 需转换为 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 <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

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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

sziitjin

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值