1、环境搭建
首先需要导入所需要的包include、armeabi-v7a。

?然后跟项目建立连接,在CMakeList.txt,并做了相关的解释:
cmake_minimum_required(VERSION 3.4.1)
file(GLOB source_file src/main/cpp/*.cpp) //cpp文件下所有的包
# Declares and names the project.
add_library( # Sets the name of the library.
native-lib
# Sets the library as a shared library.
SHARED
# Provides a relative path to your source file(s).
${source_file})
include_directories(src/main/cpp/include)
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -L${CMAKE_SOURCE_DIR}/src/main/cpp/libs/${ANDROID_ABI}") 导入libs下的所有的包
target_link_libraries( # Specifies the target library.
native-lib
avfilter avformat avcodec avutil swresample swscale
# Links the target library to the log library
# included in the NDK.
log z android) //armeabi下的包
然后在build.gradle里面进行配置:
ndk {
abiFilters 'armeabi-v7a'
}
然后在native-lib下导入看看能否成功。
extern "C" {
#include <libavformat/avformat.h>
}
下面正式进入视频解码与播放的阶段:
准备阶段:
首先在创建一个类,在里面先写好准备、开始、画布等功能。
package com.example.player08;
import android.media.MediaPlayer;
import android.view.Surface;
import android.view.SurfaceHolder;
import android.view.SurfaceView;
import androidx.annotation.NonNull;
/*
提供java 进行播放 停止等操作
*/
public class DNPlayer implements SurfaceHolder.Callback {
static {
System.loadLibrary("native-lib");
}
private String dataSource;
private SurfaceHolder holder;
private OnPrepareListener listener;
/**
* 让使用 设置播放的文件 或者 直播地址
*/
public void setDataSource(String dataSource) {
this.dataSource = dataSource;
}
/**
* 设置播放显示的画布
*
* @param surfaceView
*/
public void setSurfaceView(SurfaceView surfaceView) {
holder = surfaceView.getHolder();
holder.addCallback(this);
}
public void onError(int errorCode){
System.out.println("Java接到回调:"+errorCode);
}
public void onPrepare(){
if (null != listener){
listener.onPrepare();
}
}
public void setOnPrepareListener(OnPrepareListener listener){
this.listener = listener;
}
public interface OnPrepareListener{
void onPrepare();
}
/**
* 准备好 要播放的视频
*/
public void prepare() {
native_prepare(dataSource);
}
/**
* 开始播放
*/
public void start() {
native_start();
}
/**
* 停止播放
*/
public void stop() {
}
public void release() {
holder.removeCallback(this);
}
/**
* 画布创建好了
*
* @param holder
*/
@Override
public void surfaceCreated(SurfaceHolder holder) {
}
/**
* 画布发生了变化(横竖屏切换、按了home都会回调这个函数)
*
* @param holder
* @param format
* @param width
* @param height
*/
@Override
public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
native_setSurface(holder.getSurface());
}
/**
* 销毁画布 (按了home/退出应用/)
*
* @param holder
*/
@Override
public void surfaceDestroyed(SurfaceHolder holder) {
}
native void native_prepare(String dataSource);
}
在MainActivity里面进行地址获取等信息:
package com.example.player08;
import androidx.appcompat.app.AppCompatActivity;
import android.os.Bundle;
import android.view.SurfaceView;
import android.view.View;
import android.widget.TextView;
import android.widget.Toast;
import com.example.player08.databinding.ActivityMainBinding;
public class MainActivity extends AppCompatActivity {
// Used to load the 'player08' library on application startup.
private DNPlayer dnPlayer;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate( savedInstanceState );
setContentView( R.layout.activity_main );
SurfaceView surfaceView=findViewById( R.id.surfaceView );
dnPlayer=new DNPlayer();
dnPlayer.setSurfaceView(surfaceView);
dnPlayer.setDataSource("rtmp://47.94.57.236/myapp/");
// dnPlayer.setDataSource("rtmp://live.hkstv.hk.lxdns.com/live/hks");
dnPlayer.setOnPrepareListener(new DNPlayer.OnPrepareListener() {
@Override
public void onPrepare() {
runOnUiThread(new Runnable() {
@Override
public void run() {
Toast.makeText(MainActivity.this, "可以开始播放了", Toast.LENGTH_LONG).show();
}
});
dnPlayer.start();
}
});
}
public void start(View view) {
dnPlayer.prepare();
}
}
接下来开始进行c++的编写。
首先,native-lib只是一个桥梁,只是负责传输信息,然后和c++进行连接。
首先在native里面创建播放器:
extern "C"
JNIEXPORT void JNICALL
Java_com_example_player08_DNPlayer_native_1prepare(JNIEnv *env, jobject instance, jstring dataSource_) {
const char *dataSource=env->GetStringUTFChars(dataSource_,0);
//创建播放器
ffmpeg = new DNFFMPEG( dataSource);
env->ReleaseStringUTFChars(dataSource_, dataSource);
}
?接下来创建DNFFMPEH.h和.cpp,视频解码与播放和音频的解码与播放主要在里面进行。
首先native-lib传送的数据datasource需要拷贝到DNFFMPEG中,以防止信息被处理,传出一个空数据。
DNFFMPEG::DNFFMPEG(JavaCallHelper *callHelper,const char *dataSource) { //构造方法
//this->dataSource=const_cast<char *>(dataSource);//不能这么实用,因为native-lib里面会释放dataSource,会造成指针悬空
//防止 dataSourec参数 指向的内存被释放
//strlen 获得字符串的长度 不包括\0
this->dataSource=new char [strlen(dataSource)+1];//进行内存的拷贝
strcpy(this->dataSource,dataSource); //拷贝
}
DNFFMPEG::~DNFFMPEG() { //析构方法
//释放
DELETE(dataSource);
}
创建线程准备视频的解码:
void DNFFMPEG::prepare() {
//创建一个线程
pthread_create(&pid,0, task_prepare, this);
}
void* task_prepare(void *args){
DNFFMPEG *ffmpeg=static_cast<DNFFMPEG *>(args);
ffmpeg->_prepare(); //为了方便起见,防止每次调用都需要ffmpeg-> 创建有个新的线程
return 0;
}
同时在DNFFMPEG.里面进行相应的注册:
public:
DNFFMPEG(const char* dataSource); //接收播放的地址
~DNFFMPEG();
void prepare(); //解析datasource 地址
void _prepare();
private:
char *dataSource;
pthread_t pid;
pthread_t pid_play;
};
在解码过程中,C++会出现报错现象,需要传递给java代码,所以需要进行java回调、签名来讲c++中的错误传递给java代码。
在java代码中加入onError()方法:
public void onError(int errorCode){
System.out.println("Java接到回调:"+errorCode);
}
public interface OnPrepareListener{
void onPrepare();
}
然后在cpp文件中创建JavaCallHelper.cpp/.h来实现java的反射。
在编写该代码时,需要注意两点。一个是传递什么参数,为什么传递该参数的问题,已经在代码中详细注释了。另一个问题是需要判断在子线程还是在主线程,在主线程可以直接使用env进行java回调,在子线程,需要借助vm进行java方法的回调,具体看代码:
JavaCallHelper.h代码:
//
// Created by 14452 on 2022/9/16.
//
#ifndef PLAYER08_JAVACALLHELPER_H
#define PLAYER08_JAVACALLHELPER_H
#include <jni.h>
class JavaCallHelper { //用来将c++里面程序报错传给java
public:
//instance:表示反射的对象 dnplayer env:简单调用接口函数 vm是为了跨线程
JavaCallHelper(JavaVM *vm,JNIEnv* env,jobject instance);
~JavaCallHelper();
//回调java
void onError(int thread,int errorCode); //第一个参数判断是否在主线程还是子线程,第二个参数是错误信息
void onPrepare(int thread);
private:
JavaVM *vm;
JNIEnv *env;
jobject instance;
jclass clazz;
jmethodID onErrorID;
jmethodID onPrepareID;
};
#endif //PLAYER08_JAVACALLHELPER_H
JavaCallHelper.cpp:
//
// Created by 14452 on 2022/9/16.
//
#include "JavaCallHelper.h"
#include "macro.h"
JavaCallHelper::JavaCallHelper(JavaVM *vm, JNIEnv *env, jobject instance) {
this->vm=vm;
//如果在主线程 直接进行env回调,不需要使用java vm
this->env=env;
//一旦涉及到jobject跨方法 跨线程 就需要创建全局引用
this->instance=env->NewGlobalRef(instance);
clazz=env->GetObjectClass(instance);
onErrorID=env->GetMethodID(clazz,"onError","(I)V"); //获取java里面onerror方法
onPrepareID=env->GetMethodID(clazz,"onPrepare","()V");
}
JavaCallHelper::~JavaCallHelper() {
env->DeleteGlobalRef(instance);
}
void JavaCallHelper::onError(int thread, int errorCode) {
//主线程
if(thread==THREAD_MAIN){
env->CallVoidMethod(instance,onErrorID,errorCode);
} else{
//子线程
JNIEnv *env;
//获得属于我这一个线程的jnienv
vm->AttachCurrentThread(&env,0);
env->CallVoidMethod(instance,onErrorID,errorCode);
vm->DetachCurrentThread();
}
}
void JavaCallHelper::onPrepare(int thread) {
//主线程 直接使用env
if(thread==THREAD_MAIN){
env->CallVoidMethod(instance,onPrepareID);
} else{
//子线程 需要使用 vm
JNIEnv *env;
//获得属于我这一个线程的jnienv
vm->AttachCurrentThread(&env,0);
env->CallVoidMethod(instance,onPrepareID);
vm->DetachCurrentThread();
}
}
在native-lib创建javaCallHelper将javaCallHelper传递给DNFFMEPG.cpp
JavaVM *javaVm=0;
int JNI_OnLoad(JavaVM *vm,void *r){
javaVm=vm;
return JNI_VERSION_1_6;
}
extern "C"
JNIEXPORT void JNICALL
Java_com_example_player08_DNPlayer_native_1prepare(JNIEnv *env, jobject instance, jstring dataSource_) {
const char *dataSource=env->GetStringUTFChars(dataSource_,0);
//创建播放器
JavaCallHelper *helper = new JavaCallHelper(javaVm, env, instance);
ffmpeg = new DNFFMPEG(helper, dataSource);
ffmpeg->prepare();
env->ReleaseStringUTFChars(dataSource_, dataSource);
}
以上基本上实现java方法的回调。
接下来在音频解码个视频解码公用的一部分,如打开流媒体、打开编码器等操作。
void DNFFMPEG::_prepare() {
// 初始化网络 让ffmpeg能够使用网络
avformat_network_init();
//1、打开媒体地址(文件地址、直播地址)
// AVFormatContext 包含了 视频的 信息(宽、高等)
formatContext = 0;
//文件路径不对 手机没网
int ret = avformat_open_input(&formatContext, dataSource, 0, 0);
//ret不为0表示 打开媒体失败
if (ret != 0) {
LOGE("打开媒体失败:%s", av_err2str(ret));
callHelper->onError(THREAD_CHILD, FFMPEG_CAN_NOT_OPEN_URL);
return;
}
//2、查找媒体中的 音视频流 (给 contxt里的 streams等成员赋)
ret = avformat_find_stream_info(formatContext, 0);
// 小于0 则失败
if (ret < 0) {
LOGE("查找流失败:%s", av_err2str(ret));
callHelper->onError(THREAD_CHILD, FFMPEG_CAN_NOT_FIND_STREAMS);
return;
}
//nb_streams :几个流(几段视频/音频)
for (int i = 0; i < formatContext->nb_streams; ++i) {
//可能代表是一个视频 也可能代表是一个音频
AVStream *stream = formatContext->streams[i];
//包含了 解码 这段流 的各种参数信息(宽、高、码率、帧率)
AVCodecParameters *codecpar = stream->codecpar;
//无论视频还是音频都需要干的一些事情(获得解码器)
// 1、通过 当前流 使用的 编码方式,查找解码器
AVCodec *dec = avcodec_find_decoder(codecpar->codec_id);
if (dec == NULL) {
LOGE("查找解码器失败:%s", av_err2str(ret));
callHelper->onError(THREAD_CHILD, FFMPEG_FIND_DECODER_FAIL);
return;
}
//2、获得解码器上下文
AVCodecContext *context = avcodec_alloc_context3(dec);
if (context == NULL) {
LOGE("创建解码上下文失败:%s", av_err2str(ret));
callHelper->onError(THREAD_CHILD, FFMPEG_ALLOC_CODEC_CONTEXT_FAIL);
return;
}
//3、设置上下文内的一些参数 (context->width)
// context->width = codecpar->width;
// context->height = codecpar->height;
ret = avcodec_parameters_to_context(context, codecpar);
//失败
if (ret < 0) {
LOGE("设置解码上下文参数失败:%s", av_err2str(ret));
callHelper->onError(THREAD_CHILD, FFMPEG_CODEC_CONTEXT_PARAMETERS_FAIL);
return;
}
// 4、打开解码器
ret = avcodec_open2(context, dec, 0);
if (ret != 0) {
LOGE("打开解码器失败:%s", av_err2str(ret));
callHelper->onError(THREAD_CHILD, FFMPEG_OPEN_DECODER_FAIL);
return;
}
//单位
AVRational time_base=stream->time_base;
//音频
if (codecpar->codec_type == AVMEDIA_TYPE_AUDIO) {
//0
audioChannel = new AudioChannel(i,context,time_base);
} else if (codecpar->codec_type == AVMEDIA_TYPE_VIDEO) {
//1
//帧率:单位时间内 需要显示多少个图像
AVRational frame_rate=stream->avg_frame_rate;
int fps= av_q2d(frame_rate);
videoChannel = new VideoChannel(i,context,time_base,fps);
videoChannel->setRenderFrameCallback(callback);
}
}
//没有音视频 (很少见)
if (!audioChannel && !videoChannel) {
LOGE("没有音视频");
callHelper->onError(THREAD_CHILD, FFMPEG_NOMEDIA);
return;
}
// 准备完了 通知java 你随时可以开始播放
callHelper->onPrepare(THREAD_CHILD);
};
以上信息完成后,需要将已准备好的信息传递给java层,所以需要创建prepare方法和上面error报错的方法差不多。
为了简化代码,将videoChannel和AudioChannel共有的参数放进一个新建的类BaseChannel。第一个参数id,0代表音频,1代表视频。
然后开始进行播放:
void DNFFMPEG::start() {
// 正在播放
isPlaying = 1;
// //启动声音的解码与播放
if (audioChannel){
audioChannel->play();
}
if (videoChannel){
if (audioChannel){
videoChannel->play();
}
}
pthread_create(&pid_play, 0, play, this);
}
void *play(void *args) {
DNFFMPEG *ffmpeg = static_cast<DNFFMPEG *>(args);
ffmpeg->_start();
return 0;
}
/**
* 专门读取数据包
*/
void DNFFMPEG::_start() {
//1、读取媒体数据包(音视频数据包)
int ret;
while (isPlaying) {
AVPacket *packet = av_packet_alloc();
ret = av_read_frame(formatContext, packet);
//=0成功 其他:失败
if (ret == 0) {
//stream_index 这一个流的一个序号
if (audioChannel && packet->stream_index == audioChannel->id) {
audioChannel->packets.push(packet);
}
if (videoChannel && packet->stream_index == videoChannel->id) {
videoChannel->packets.push(packet);
}
} else if (ret == AVERROR_EOF) {
//读取完成 但是可能还没播放完
} else {
//
}
}
};
packet申请的内存在堆中,需要释放内存,且packet参数公用在音频和视频的解码中,所以在baseChannel里面进行内存释放。
/**
* 释放 AVPacket
* @param packet
*/
static void releaseAvPacket(AVPacket** packet) {
if (packet) {
av_packet_free(packet);
//为什么用指针的指针?
// 指针的指针能够修改传递进来的指针的指向
*packet = 0;
}
}
解码:取出数据包->将包丢给解码器->从解码器中读取 解码后的数据包
播放(目标是先将数据包转换成RGBA,通过sws_scale进行转换,然后在ANativeWindow里面进行画画。(注意:要是用同步锁,防止在画画过程中被释放)
解码:
void VideoChannel::play() {
isPlaying = 1;
frames.setWork(1);
packets.setWork(1);
//1、解码
pthread_create(&pid_decode, 0, decode_task, this);
//2、播放
pthread_create(&pid_render, 0, render_task, this);
}
void *decode_task(void *args) {
VideoChannel *channel = static_cast<VideoChannel *>(args);
channel->decode();
return 0;
}
//解码
void VideoChannel::decode() {
AVPacket *packet = 0;
while (isPlaying) {
//取出一个数据包
int ret = packets.pop(packet);
if (!isPlaying) {
break;
}
//取出失败
if (!ret) {
continue;
}
//把包丢给解码器
ret = avcodec_send_packet(avCodecContext, packet);
releaseAvPacket(&packet);
//重试
if (ret != 0) {
break;
}
//代表了一个图像 (将这个图像先输出来)
AVFrame *frame = av_frame_alloc();
//从解码器中读取 解码后的数据包 AVFrame
ret = avcodec_receive_frame(avCodecContext, frame);
//需要更多的数据才能够进行解码
if (ret == AVERROR(EAGAIN)) {
continue;
} else if(ret != 0){
break;
}
//再开一个线程 来播放 (流畅度)
frames.push(frame);
}
releaseAvPacket(&packet);
}
播放:
void VideoChannel::play() {
isPlaying = 1;
frames.setWork(1);
packets.setWork(1);
//1、解码
pthread_create(&pid_decode, 0, decode_task, this);
//2、播放
pthread_create(&pid_render, 0, render_task, this);
}
void *render_task(void *args) {
VideoChannel *channel = static_cast<VideoChannel *>(args);
channel->render();
return 0;
}
//播放
void VideoChannel::render() {
//目标: RGBA
swsContext = sws_getContext(
avCodecContext->width, avCodecContext->height,avCodecContext->pix_fmt,
avCodecContext->width, avCodecContext->height,AV_PIX_FMT_RGBA,
SWS_BILINEAR,0,0,0);
AVFrame* frame = 0;
//指针数组
uint8_t *dst_data[4];
int dst_linesize[4];
av_image_alloc(dst_data, dst_linesize,
avCodecContext->width, avCodecContext->height,AV_PIX_FMT_RGBA, 1);
while (isPlaying){
int ret = frames.pop(frame);
if (!isPlaying){
break;
}
//src_linesize: 表示每一行存放的 字节长度
sws_scale(swsContext, reinterpret_cast<const uint8_t *const *>(frame->data),
frame->linesize, 0,
avCodecContext->height,
dst_data,
dst_linesize);
//回调出去进行播放
callback(dst_data[0],dst_linesize[0],avCodecContext->width, avCodecContext->height);
releaseAvFrame(&frame);
}
av_freep(&dst_data[0]);
releaseAvFrame(&frame);
}
在native-lib中画画:
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER ;
//画画
void render(uint8_t *data, int lineszie, int w, int h) {
pthread_mutex_lock(&mutex);
if (!window) {
pthread_mutex_unlock(&mutex);
return;
}
//设置窗口属性
ANativeWindow_setBuffersGeometry(window, w,
h,
WINDOW_FORMAT_RGBA_8888);
ANativeWindow_Buffer window_buffer;
if (ANativeWindow_lock(window, &window_buffer, 0)) {
ANativeWindow_release(window);
window = 0;
pthread_mutex_unlock(&mutex);
return;
}
//填充rgb数据给dst_data
uint8_t *dst_data = static_cast<uint8_t *>(window_buffer.bits);
// stride:一行多少个数据(RGBA) *4
int dst_linesize = window_buffer.stride * 4;
//一行一行的拷贝
for (int i = 0; i < window_buffer.height; ++i) {
//memcpy(dst_data , data, dst_linesize);
memcpy(dst_data + i * dst_linesize, data + i * lineszie, dst_linesize);
}
ANativeWindow_unlockAndPost(window);
pthread_mutex_unlock(&mutex);
}
extern "C"
JNIEXPORT void JNICALL
Java_com_example_player08_DNPlayer_native_1setSurface(JNIEnv *env, jobject instance, jobject surface) {
pthread_mutex_lock(&mutex);
if (window){ //判断之前是否有surface
//把老的释放
ANativeWindow_release(window);
window=0;
}
window=ANativeWindow_fromSurface(env,surface);
pthread_mutex_unlock(&mutex);
}
然后采用EV录屏进行在线播放:

?
?链接:https://pan.baidu.com/s/1au6zAAa7-Fdh6uNggPTRig? 提取码:j7qn? ?
|