1、环境搭建

首先需要导入所需要的包include、armeabi-v7a。

ffmpeg实现web在线转码播放(ffmpeg播放器一)(1)

然后跟项目建立连接,在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 的编写。

相关学习资料推荐,点击下方链接免费报名,先码住不迷路~】

音视频免费学习地址:FFmpeg/WebRTC/RTMP/NDK/Android音视频流媒体高级开发

【免费分享】音视频学习资料包、大厂面试题、技术视频和学习路线图,资料包括(C/C ,Linux,FFmpeg webRTC rtmp hls rtsp ffplay srs 等等)有需要的可以点击788280672加群免费领取~

ffmpeg实现web在线转码播放(ffmpeg播放器一)(2)

首先,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); }

接下来创建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录屏进行在线播放:

ffmpeg实现web在线转码播放(ffmpeg播放器一)(3)

链接:https:///s/1au6zAAa7-Fdh6uNggPTRig

提取码:j7qn

原文 ffmpeg播放器(一) 视频解码与播放

,