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-libavfilter   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*/@Overridepublic void surfaceCreated(SurfaceHolder holder) {}/*** 画布发生了变化(横竖屏切换、按了home都会回调这个函数)** @param holder* @param format* @param width* @param height*/@Overridepublic void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {native_setSurface(holder.getSurface());}/*** 销毁画布 (按了home/退出应用/)** @param holder*/@Overridepublic 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;@Overrideprotected 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() {@Overridepublic void onPrepare() {runOnUiThread(new Runnable() {@Overridepublic 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 获得字符串的长度 不包括\0this->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();//回调javavoid 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 vmthis->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;//获得属于我这一个线程的jnienvvm->AttachCurrentThread(&env,0);env->CallVoidMethod(instance,onErrorID,errorCode);vm->DetachCurrentThread();}
}void JavaCallHelper::onPrepare(int thread) {//主线程 直接使用envif(thread==THREAD_MAIN){env->CallVoidMethod(instance,onPrepareID);} else{//子线程 需要使用 vmJNIEnv *env;//获得属于我这一个线程的jnienvvm->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) {//0audioChannel = 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();//从解码器中读取 解码后的数据包 AVFrameret = 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() {//目标: RGBAswsContext = 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_datauint8_t *dst_data = static_cast<uint8_t *>(window_buffer.bits);// stride:一行多少个数据(RGBA) *4int 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

ffmpeg播放器(一) 视频解码与播放相关推荐

  1. 搭建rtmp直播流服务之4:videojs和ckPlayer开源播放器二次开发(播放rtmp、hls直播流及普通视频)...

    前面几章讲解了使用 nginx-rtmp搭建直播流媒体服务器; ffmpeg推流到nginx-rtmp服务器; java通过命令行调用ffmpeg实现推流服务; 从数据源获取,到使用ffmpeg推流, ...

  2. iOS:制作简易的 AAC 播放器 —— 了解音频的播放流程

    ????????关注后回复 "进群" ,拉你进程序员交流群???????? 作者丨千帆直播 来源丨搜狐技术产品(ID:sohu-tech) 本文字数:1872字 预计阅读时间:8分 ...

  3. Ubuntu16.04下使用VLC media player播放器实现倍速播放

    Ubuntu16.04下使用VLC media player播放器实现倍速播放 打开软件 开启倍速功能 打开软件 视频文件右键"属性"-"打开方式"-" ...

  4. 网页播放器自定义倍速播放

    网页播放器自定义倍速播放,可设置播放器的播放速度为1,2,3,5,6,7等自定义播放速度.利用html5的特性进行处理. 一般网页播放器的速度限制在最高两倍速播放,通常这就符合一般的要求了.但是确实有 ...

  5. 如何给播放器增加倍速播放

    1.一般网页上播放的播放器没有倍速播放,有时候需要这样的功能 2.演示:http://www.gehweb.top/b.html 3.例如这样写就可以实现这个功能: <video id=&quo ...

  6. 借用 potplayer 播放器,在本地播放 b 站视频也能看弹幕了

    苏生不惑第164 篇原创文章,将本公众号设为星标,第一时间看最新文章. 关于b站之前已经写过了下列文章,有兴趣可以点击阅读: 那些我关注的 b 站 up 主 bilibili(b站)升级到BV号了,还 ...

  7. html 音乐 QQ播放器 外链 代码 播放器 外链 代码

    韩梦飞沙  韩亚飞  313134555@qq.com  yue31313  han_meng_fei_sha QQ播放器 外链 代码 播放器 外链 代码 ======== 歌曲链接 QQ播放器 外链 ...

  8. android+jiaozi播放器,android视频播放-饺子播放器

    饺子播放器是android主流播放器中之一,使用简单,功能全面. 不要按照github文档来,api有变动. 1.依赖 这个地方也要添加glide的依赖,因为视频默认图需要glide加载. imple ...

  9. 视频融合平台EasyCVR集成播放器,但是无法播放该如何解决?

    EasyCVR视频融合云平台基于云边端一体化架构,兼容性高.拓展性强,可支持多类型设备.多协议方式接入,包括国标GB/T28181.RTMP.RTSP/Onvif协议,以及厂家的私有协议,如:海康Eh ...

  10. android 音乐播放器论文,Android音乐播放器论文-Android文档类资源

    基于android系统的音乐播放器论文.里边有详细的介绍,没有代码.只是单独的一个论文. XXX科技大学本科生毕业设计(论文) 摘要 当今社会的生活节奏越来越快,随着硬件移动设备的越来越先进,人们 对 ...

最新文章

  1. Linux----函数中变量的作用域、local关键字。
  2. chrome插件下载
  3. [PHP]php发布和调用Webservice接口的案例
  4. PO增强,明细动抬头动
  5. 使用Oracle UTL_FILE包操作文件
  6. 关于python_关于python的基础知识
  7. Json.Net学习笔记(十二) 协议解析
  8. 天猫搜索前端架构演进之路
  9. 扫码枪WinForm程序的编写
  10. STL 堆 鱼塘钓鱼
  11. 给ImageView做圆角处理
  12. 理解Spring 容器设计理念
  13. Orge的一本书下载
  14. 混沌数学之Chua's circuit(蔡氏电路)
  15. mtk编译android,[Android6.0][MTK6737] MTK 编译环境搭建
  16. jdk8新特性-Lambda表达式,方法引用
  17. 怎么在线把pdf文件合并
  18. ASP.NET搜索引擎
  19. android网络传输唤醒系统,Android手机唤醒群晖NAS系统
  20. relative会脱离文档流吗_脱离文档流和恢复文档流的方法

热门文章

  1. 财务自由之路笔记 第一章 你真正想要什么
  2. CENTOS5.5下使用Roundcube搭建 Webmail
  3. python求15的因数_python学习第15期
  4. papers with code介绍(人工智能方向研究生的必备网站)
  5. 隐私全没了?30亿条个人信息被盗,BAT等96家公司全部中招
  6. 17种Vue适用于移动端的ui框架
  7. 魔兽UI跟随鼠标移动
  8. 习题4-9 打印菱形图案(15分)
  9. android滑动冲突的解决方案
  10. 【中亦安图】风险提醒之Oracle RAC高可用失效(2)