【FFmpeg实战】ffmpeg播放器-音视频解码流程

音视频介绍

音视频解码流程

在这里插入图片描述

FFmpeg解码的数据结构说明

在这里插入图片描述

  • AVFormatContext:封装格式上下文结构体,全局结构体,保存了视频文件封装格式相关信息
  • AVInputFormat:每种封装格式,对应一个该结构体
  • AVStream[0]:视频文件中每个视频(音频)流对应一个该结构体
  • AVCodecContext:编码器上下文结构体,保存了视频(音频)编解码相关信息
  • AVCodec:每种视频(音频)编解码器(例如H.264解码器)对应一个该结构体

AVFormatContext数据结构说明

在这里插入图片描述

  • iformat:输入视频的AVInputFormat
  • nb_streams:输入视频的AVStream 个数
  • streams:输入视频的AVStream []数组
  • duration:输入视频的时长(以微秒为单位)
  • bit_rate:输入视频的码率

AVInputFormat数据结构说明

在这里插入图片描述

  • name:封装格式名称
  • long_name:封装格式的长名称
  • extensions:封装格式的扩展名
  • id:封装格式ID
  • 一些封装格式处理的接口函数

AVStream数据结构说明

在这里插入图片描述

  • id:序号
  • codec:该流对应的AVCodecContext
  • time_base:该流的时基
  • avg_frame_rate:该流的帧率

AVCodecContext数据结构说明

在这里插入图片描述

  • codec:编解码器的AVCodec
  • width, height:图像的宽高
  • pix_fmt:像素格式
  • sample_rate:音频采样率
  • channels:声道数
  • sample_fmt:音频采样格式

AVCodec数据结构说明

在这里插入图片描述

  • name:编解码器名称
  • long_name:编解码器长名称
  • type:编解码器类型
  • id:编解码器ID
  • 一些编解码的接口函数

AVPacket数据结构说明

在这里插入图片描述

  • pts:显示时间戳
  • dts:解码时间戳
  • data:压缩编码数据
  • size:压缩编码数据大小
  • stream_index:所属的AVStream

AVFrame数据结构说明

在这里插入图片描述

  • data:解码后的图像像素数据(音频采样数据)
  • linesize:对视频来说是图像中一行像素的大小;对音频来说是整个音
  • width, height:图像的宽高
  • key_frame:是否为关键帧
  • pict_type:帧类型(只针对视频) 。例如I,P,B

音视频实战

将编译好的FFmpeg库考入到工程

在这里插入图片描述

编写CMakeLists.txt

cmake_minimum_required(VERSION 3.4.1)file(GLOB source_file *.cpp)message("source_file = ${source_file}")add_library(z-playerSHARED${source_file})include_directories(${CMAKE_SOURCE_DIR}/include)set(CMAKE_CXX_FLAGS  "${CMAKE_CXX_FLAGS} -L${CMAKE_SOURCE_DIR}/libs/${CMAKE_ANDROID_ARCH_ABI}")find_library(log-liblog)target_link_libraries(z-player# 方法一:使用-Wl 忽略顺序
#        -Wl,--start-group #忽略顺序引发的错误
#        avcodec avfilter avformat avutil swresample swscale
#        -Wl,--end-group# 方法二:调整顺序avformat avcodec avfilter avutil swresample swscale #必须要把avformat放在avcodec的前面${log-lib}z)

这里target_link_libraries方法有两个问题:

1.FFmpeg是需要依赖了libz.so库的如下图:
在这里插入图片描述
所有要在target_link_libraries方法里添加z,否则会报错

在这里插入图片描述
2.FFmpeg添加顺序问题,
当我们添加

avcodec avfilter avformat avutil swresample swscale

这样一个顺序时会报错
在这里插入图片描述

解决方法有两个:

第一个:将avfilter放到avcodec前面就可以了

avformat avcodec avfilter avutil swresample swscale

第二个:使用-Wl忽略顺序

-Wl,--start-group #忽略顺序引发的错误
avcodec avfilter avformat avutil swresample swscale
-Wl,--end-group

编码

FFmpeg播放器结构类图

在这里插入图片描述

编写ZPlayer.java类
public class ZPlayer implements LifecycleObserver, SurfaceHolder.Callback {private static final String TAG = ZPlayer.class.getSimpleName();static {System.loadLibrary("z-player");}private final long nativeHandle;private OnPrepareListener listener;private OnErrorListener onErrorListener;private SurfaceHolder mHolder;private OnProgressListener onProgressListener;public ZPlayer() {nativeHandle = nativeInit();}/*** 设置播放显示的画布* @param surfaceView*/public void setSurfaceView(SurfaceView surfaceView) {if (this.mHolder != null) {mHolder.removeCallback(this); // 清除上一次的}mHolder = surfaceView.getHolder();mHolder.addCallback(this);}/*** 让使用者设置播放的文件,或者直播地址* @param dataSource*/public void setDataSource(String dataSource){setDataSource(nativeHandle, dataSource);}/*** 准备好要播放的视频*/@OnLifecycleEvent(Lifecycle.Event.ON_RESUME)public void prepare() {Log.e(TAG,"ZPlayer->prepare");nativePrepare(nativeHandle);}/*** 开始播放*/public void start(){Log.e(TAG,"ZPlayer->start");nativeStart(nativeHandle);}/*** 停止播放*/@OnLifecycleEvent(Lifecycle.Event.ON_STOP)public void stop(){Log.e(TAG,"ZPlayer->stop");nativeStop(nativeHandle);}@OnLifecycleEvent(Lifecycle.Event.ON_DESTROY)public void release(){Log.e(TAG,"ZPlayer->release");mHolder.removeCallback(this);nativeRelease(nativeHandle);}/*** JavaCallHelper 会反射调用此方法* @param errorCode*/public void onError(int errorCode, String ffmpegError){Log.e(TAG,"Java接收到回调了->onError:"+errorCode);String title = "\nFFmpeg给出的错误如下:\n";String msg = null;switch (errorCode){case Constant.FFMPEG_CAN_NOT_OPEN_URL:msg = "打不开视频"+title+ ffmpegError;break;case Constant.FFMPEG_CAN_NOT_FIND_STREAMS:msg = "找不到流媒体"+title+ ffmpegError;break;case Constant.FFMPEG_FIND_DECODER_FAIL:msg = "找不到解码器"+title+ ffmpegError;break;case Constant.FFMPEG_ALLOC_CODEC_CONTEXT_FAIL:msg = "无法根据解码器创建上下文"+title+ ffmpegError;break;case Constant.FFMPEG_CODEC_CONTEXT_PARAMETERS_FAIL:msg = "根据流信息 配置上下文参数失败"+title+ ffmpegError;break;case Constant.FFMPEG_OPEN_DECODER_FAIL:msg = "打开解码器失败"+title+ ffmpegError;break;case Constant.FFMPEG_NOMEDIA:msg = "没有音视频"+title+ ffmpegError;break;}if(onErrorListener != null ){onErrorListener.onError(msg);}}/*** JavaCallHelper 会反射调用此方法*/public void onPrepare(){Log.e(TAG,"Java接收到回调了->onPrepare");if(listener != null){listener.onPrepare();}}public void onProgress(int progress){if(onProgressListener != null){onProgressListener.onProgress(progress);}}@Overridepublic void surfaceCreated(SurfaceHolder surfaceHolder) {Log.e(TAG,"ZPlayer->surfaceCreated");}@Overridepublic void surfaceChanged(SurfaceHolder surfaceHolder, int i, int i1, int i2) {Log.e(TAG,"ZPlayer->surfaceChanged");nativeSetSurface(nativeHandle,surfaceHolder.getSurface());}@Overridepublic void surfaceDestroyed(SurfaceHolder surfaceHolder) {Log.e(TAG,"ZPlayer->surfaceDestroyed");}public int getDuration() {return getNativeDuration(nativeHandle);}public void seek(int playProgress) {nativeSeek(playProgress,nativeHandle);}public interface OnPrepareListener{void onPrepare();}public void setOnPrepareListener(OnPrepareListener listener){this.listener = listener;}public interface OnProgressListener{void onProgress(int progress);}public void setOnProgressListener(OnProgressListener listener){this.onProgressListener = listener;}public interface OnErrorListener{void onError(String errorCode);}public void setOnErrorListener(OnErrorListener listener){this.onErrorListener = listener;}private native long nativeInit();private native void setDataSource(long nativeHandle, String path);private  native void nativePrepare(long nativeHandle);private native void nativeStart(long nativeHandle);private native void nativeStop(long nativeHandle);private  native void nativeRelease(long nativeHandle);private native void nativeSetSurface(long nativeHandle, Surface surface);private native int getNativeDuration(long nativeHandle);private native void nativeSeek(int playValue,long nativeHandle);
}
编写MainActivity.java类
public class MainActivity extends AppCompatActivity implements SeekBar.OnSeekBarChangeListener {private ActivityMainBinding binding;private int PERMISSION_REQUEST = 0x1001;private ZPlayer mPlayer;// 用户是否拖拽里private boolean isTouch = false;// 获取native层的总时长private int duration ;@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);binding = ActivityMainBinding.inflate(getLayoutInflater());setContentView(binding.getRoot());checkPermission();binding.seekBar.setOnSeekBarChangeListener(this);mPlayer = new ZPlayer();getLifecycle().addObserver(mPlayer);mPlayer.setSurfaceView(binding.surfaceView);mPlayer.setDataSource("/sdcard/demo.mp4");
//        mPlayer.setDataSource("/sdcard/chengdu.mp4");mPlayer.setOnPrepareListener(new ZPlayer.OnPrepareListener() {@Overridepublic void onPrepare() {duration = mPlayer.getDuration();runOnUiThread(new Runnable() {@Overridepublic void run() {binding.seekBarTimeLayout.setVisibility(duration != 0 ? View.VISIBLE : View.GONE);if(duration != 0){binding.tvTime.setText("00:00/"+getMinutes(duration)+":"+getSeconds(duration));}binding.tvState.setTextColor(Color.GREEN);binding.tvState.setText("恭喜init初始化成功");}});mPlayer.start();}});mPlayer.setOnErrorListener(new ZPlayer.OnErrorListener() {@Overridepublic void onError(String errorCode) {runOnUiThread(new Runnable() {@Overridepublic void run() {binding.tvState.setTextColor(Color.RED);binding.tvState.setText(errorCode);}});}});mPlayer.setOnProgressListener(new ZPlayer.OnProgressListener() {@Overridepublic void onProgress(int progress) {if (!isTouch){runOnUiThread(new Runnable() {@Overridepublic void run() {// 非直播,是本地视频文件if(duration != 0) {binding.tvTime.setText(getMinutes(progress) + ":" + getSeconds(progress)+ "/" + getMinutes(duration) + ":" + getSeconds(duration));binding.seekBar.setProgress(progress * 100 / duration);}}});}}});}private void checkPermission() {if(ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) !=PackageManager.PERMISSION_GRANTED){Log.e("zuo","无权限,去申请权限");ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, PERMISSION_REQUEST);}else {Log.e("zuo","有权限");}}@Overridepublic void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {super.onRequestPermissionsResult(requestCode, permissions, grantResults);if(requestCode == PERMISSION_REQUEST){Log.e("zuo","申请到权限"+grantResults.length);if (grantResults[0] == PackageManager.PERMISSION_GRANTED){Toast.makeText(this,"已申请权限",Toast.LENGTH_SHORT).show();}else {Toast.makeText(this,"申请权限失败",Toast.LENGTH_SHORT).show();}}}private String getSeconds(int duration){int seconds = duration % 60;String str ;if(seconds <= 9){str = "0"+seconds;}else {str = "" + seconds;}return str;}private String getMinutes(int duration){int minutes = duration / 60;String str ;if(minutes <= 9){str = "0"+minutes;}else {str = "" + minutes;}return str;}/*** 当前拖动条进度发送了改变,毁掉此方法* @param seekBar 控件* @param progress 1~100* @param fromUser 是否用户拖拽导致到改变*/@Overridepublic void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {if(fromUser) {binding.tvTime.setText(getMinutes(progress * duration / 100) + ":" + getSeconds(progress * duration / 100)+ "/" + getMinutes(duration) + ":" + getSeconds(duration));}}//手按下去,毁掉此方法@Overridepublic void onStartTrackingTouch(SeekBar seekBar) {isTouch = true;}// 手松开(SeekBar当前值)回调此方法@Overridepublic void onStopTrackingTouch(SeekBar seekBar) {isTouch = false;int seekBarProgress = seekBar.getProgress();int playProgress = seekBarProgress * duration / 100;mPlayer.seek(playProgress);}
}
编写native-lib.cpp

java调用native方法的入口

#include <jni.h>
#include <string>
#include "ZPlayer.h"
#define LOG_TAG "native-lib"ZPlayer *zPlayer = nullptr;
JavaVM *javaVm = nullptr;
JavaCallHelper *helper = nullptr;
ANativeWindow *window = nullptr;
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;int JNI_OnLoad(JavaVM *vm,void *r){javaVm = vm;return JNI_VERSION_1_6;
}extern "C"
JNIEXPORT jlong JNICALL
Java_com_zxj_zplayer_ZPlayer_nativeInit(JNIEnv *env, jobject thiz) {//创建播放器helper = new JavaCallHelper(javaVm,env,thiz);zPlayer = new ZPlayer(helper);return (jlong)zPlayer;
}extern "C"
JNIEXPORT void JNICALL
Java_com_zxj_zplayer_ZPlayer_setDataSource(JNIEnv *env, jobject thiz, jlong native_handle,jstring path) {const char *dataSource = env->GetStringUTFChars(path, nullptr);ZPlayer *zPlayer = reinterpret_cast<ZPlayer *>(native_handle);zPlayer->setDataSource(dataSource);env->ReleaseStringUTFChars(path,dataSource);
}extern "C"
JNIEXPORT void JNICALL
Java_com_zxj_zplayer_ZPlayer_nativePrepare(JNIEnv *env, jobject thiz, jlong native_handle) {ZPlayer *zPlayer = reinterpret_cast<ZPlayer *>(native_handle);zPlayer->prepare();
}extern "C"
JNIEXPORT void JNICALL
Java_com_zxj_zplayer_ZPlayer_nativeStart(JNIEnv *env, jobject thiz, jlong native_handle) {ZPlayer *zPlayer = reinterpret_cast<ZPlayer *>(native_handle);zPlayer->start();
}extern "C"
JNIEXPORT void JNICALL
Java_com_zxj_zplayer_ZPlayer_nativeStop(JNIEnv *env, jobject thiz, jlong native_handle) {ZPlayer *zPlayer = reinterpret_cast<ZPlayer *>(native_handle);zPlayer->stop();if(helper){DELETE(helper);}
}extern "C"
JNIEXPORT void JNICALL
Java_com_zxj_zplayer_ZPlayer_nativeRelease(JNIEnv *env, jobject thiz, jlong native_handle) {pthread_mutex_lock(&mutex);if(window){ANativeWindow_release(window);window = nullptr;}pthread_mutex_unlock(&mutex);DELETE(helper);DELETE(zPlayer);DELETE(javaVm);DELETE(window);
}extern "C"
JNIEXPORT void JNICALL
Java_com_zxj_zplayer_ZPlayer_nativeSetSurface(JNIEnv *env, jobject thiz, jlong native_handle,jobject surface) {pthread_mutex_lock(&mutex);//先释放之前的显示窗口if(window){LOGE("nativeSetSurface->window=%p",window);ANativeWindow_release(window);window = nullptr;}window = ANativeWindow_fromSurface(env,surface);ZPlayer *zPlayer = reinterpret_cast<ZPlayer *>(native_handle);zPlayer->setWindow(window);pthread_mutex_unlock(&mutex);
}extern "C"
JNIEXPORT jint JNICALL
Java_com_zxj_zplayer_ZPlayer_getNativeDuration(JNIEnv *env, jobject thiz, jlong native_handle) {ZPlayer *zPlayer = reinterpret_cast<ZPlayer *>(native_handle);if(zPlayer){return zPlayer->getDuration();}return 0;
}extern "C"
JNIEXPORT void JNICALL
Java_com_zxj_zplayer_ZPlayer_nativeSeek(JNIEnv *env, jobject thiz, jint play_value,jlong native_handle) {ZPlayer *zPlayer = reinterpret_cast<ZPlayer *>(native_handle);if(zPlayer){zPlayer->seek(play_value);}
}
编写JavaCallHelper.cpp

这个类主要用作:处理音视频后各个状态回调java方法

#include "JavaCallHelper.h"
#define LOG_TAG "JavaCallHelper"JavaCallHelper::JavaCallHelper(JavaVM *vm, JNIEnv *env, jobject instace) {this->vm = vm;//如果在主线程回调this->env = env;//一旦涉及到jobject 跨方法/跨线程 就需要创建全局引用this->instace = env->NewGlobalRef(instace);jclass clazz = env->GetObjectClass(instace);onErrorId = env->GetMethodID(clazz, "onError", "(ILjava/lang/String;)V");onPrepareId = env->GetMethodID(clazz, "onPrepare", "()V");onProgressId = env->GetMethodID(clazz, "onProgress", "(I)V");
}JavaCallHelper::~JavaCallHelper() {env->DeleteGlobalRef(this->instace);
}void JavaCallHelper::onError(int thread, int errorCode,char * ffmpegError) {//主线程if(thread == THREAD_MAIN){jstring _ffmpegError = env->NewStringUTF(ffmpegError);env->CallVoidMethod(instace,onErrorId,errorCode,_ffmpegError);} else{//子线程JNIEnv *env;if (vm->AttachCurrentThread(&env, 0) != JNI_OK) {return;}jstring _ffmpegError = env->NewStringUTF(ffmpegError);env->CallVoidMethod(instace,onErrorId,errorCode,_ffmpegError);vm->DetachCurrentThread();//解除附加,必须要}
}void JavaCallHelper::onPrepare(int thread) {//主线程if(thread == THREAD_MAIN){env->CallVoidMethod(instace,onPrepareId);} else{//子线程JNIEnv *env;if (vm->AttachCurrentThread(&env, 0) != JNI_OK) {return;}env->CallVoidMethod(instace,onPrepareId);vm->DetachCurrentThread();}
}void JavaCallHelper::onProgress(int thread, int progress) {if(thread == THREAD_MAIN){env->CallVoidMethod(instace,onProgressId,progress);} else{//子线程JNIEnv *env;if (vm->AttachCurrentThread(&env, 0) != JNI_OK) {return;}env->CallVoidMethod(instace,onProgressId,progress);vm->DetachCurrentThread();}
}
编写ZPlayer.cpp

主要处理音视频的

#include <cstring>
#include "ZPlayer.h"
#include "macro.h"void *task_prepare(void *args) {ZPlayer *zFmpeg = static_cast<ZPlayer *>(args);zFmpeg->_prepare();return 0;
}ZPlayer::ZPlayer(JavaCallHelper *callHelper, const char *dataSource) {this->callHelper = callHelper;//这样写会报错,dataSource会在native-lib.cpp里的方法里会被释放掉,那么这里拿到的dataSource是悬空指针
//    this->dataSource = const_cast<char *>(dataSource);this->dataSource = new char[strlen(dataSource)];strcpy(this->dataSource, dataSource);
}ZPlayer::~ZPlayer() {//释放
//    delete this->dataSource;
//    this->dataSource = nullptr;DELETE(dataSource);DELETE(callHelper);
}void ZPlayer::prepare() {pthread_create(&pid, 0, task_prepare, this);
}void ZPlayer::_prepare() {//初始化网络,不调用这个,FFmpage是无法联网的avformat_network_init();//AVFormatContext 包含了视频的信息(宽、高等)formatContext = 0;//1、打开媒体地址(文件地址、直播地址)int ret = avformat_open_input(&formatContext, dataSource, 0, 0);//ret不为0表示打开媒体失败if (ret) {LOGE("打开媒体失败:%s", av_err2str(ret));callHelper->onError(THREAD_CHILD, FFMPEG_CAN_NOT_OPEN_URL);return;}//2、查找媒体中的音视频流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;}//经过avformat_find_stream_info方法后,formatContext->nb_streams就有值了unsigned int streams = formatContext->nb_streams;//nb_streams :几个流(几段视频/音频)for (int i = 0; i < streams; ++i) {//可能代表是一个视频,也可能代表是一个音频AVStream *stream = formatContext->streams[i];//包含了解码 这段流的各种参数信息(宽、高、码率、帧率)AVCodecParameters *codecpar = stream->codecpar;//无论视频还是音频都需要干的一些事情(获得解码器)// 1、通过当前流使用的编码方式,查找解码器AVCodec *avCodec = avcodec_find_decoder(codecpar->codec_id);if (avCodec == nullptr) {LOGE("查找解码器失败:%s", av_err2str(ret));callHelper->onError(THREAD_CHILD, FFMPEG_FIND_DECODER_FAIL);return;}//2、获得解码器上下文AVCodecContext *context3 = avcodec_alloc_context3(avCodec);if (context3 == nullptr) {LOGE("创建解码上下文失败:%s", av_err2str(ret));callHelper->onError(THREAD_CHILD, FFMPEG_ALLOC_CODEC_CONTEXT_FAIL);return;}//3、设置上下文内的一些参数 (context->width)ret = avcodec_parameters_to_context(context3, codecpar);if (ret < 0) {LOGE("设置解码上下文参数失败:%s", av_err2str(ret));callHelper->onError(THREAD_CHILD, FFMPEG_CODEC_CONTEXT_PARAMETERS_FAIL);return;}// 4、打开解码器ret = avcodec_open2(context3, avCodec, 0);if (ret != 0) {LOGE("打开解码器失败:%s", av_err2str(ret));callHelper->onError(THREAD_CHILD, FFMPEG_OPEN_DECODER_FAIL);return;}//音频if (codecpar->codec_type == AVMEDIA_TYPE_AUDIO) {audioChannel = new AudioChannel;} else if (codecpar->codec_type == AVMEDIA_TYPE_VIDEO) {videoChannel = new VideoChannel;}}if (!audioChannel && !videoChannel) {LOGE("没有音视频");callHelper->onError(THREAD_CHILD, FFMPEG_NOMEDIA);return;}// 准备完了 通知java 你随时可以开始播放callHelper->onPrepare(THREAD_CHILD);
}

核心代码差不多是这些,现在我们可以先测试一下,编译运行会发现报错

在这里插入图片描述
报这个错是因为,FFmpeg的版本问题,在上一篇《FFmpeg编译》中我们在编译FFmpeg的库的时候,指定了-D__ANDROID_API__=21,而我们项目中的minSdkVersion为14,所以需要修改minSdkVersion为21就可以了。

最后运行测试是没有问题的。

其他手机奔溃解决方法

上面编译源码使用到是"armeabi-v7a",但是有的手机是"arm64-v8a"架构到,所以直接运行就会报错,找不到so库

在这里插入图片描述
1.这时有两种解决方法

在build.gradle文件里加入ndk{abiFilters "armeabi-v7a"}就可以了
在这里插入图片描述
2.重新编译一个"arm64-v8a"的静态库,修改build.sh文件

#!/bin/bash#执行生成makefile的shell脚本
PREFIX=./android/armeabi-v7a2NDK_ROOT=/home/zuojie/android-ndk-r17cCPU=aarch64-linux-android
TOOLCHAIN=$NDK_ROOT/toolchains/$CPU-4.9/prebuilt/linux-x86_64FLAGS="-isystem $NDK_ROOT/sysroot/usr/include/$CPU -D__ANDROID_API__=21 -g -DANDROID -ffunction-sections -funwind-tables -fstack-protector-strong -no-canonical-prefixes -Wa,--noexecstack -Wformat -Werror=format-security  -O0 -fPIC"
#FLAGS="-isystem $NDK_ROOT/sysroot/usr/include/$CPU -D__ANDROID_API__=21 -g -DANDROID -ffunction-sections -funwind-tables -fstack-protector-strong -no-canonical-prefixes -march=armv7-a -mfloat-abi=softfp -mfpu=vfpv3-d16 -mthumb -Wa,--noexecstack -Wformat -Werror=format-security  -O0 -fPIC"INCLUDES=" -isystem $NDK_ROOT/sources/android/support/include"# \ 换行连接符
./configure --prefix=$PREFIX \--enable-small \--disable-programs \--disable-avdevice \--disable-postproc \--disable-encoders \--disable-muxers \--disable-filters \--enable-cross-compile \--cross-prefix=$TOOLCHAIN/bin/$CPU- \--disable-shared \--enable-static \--sysroot=$NDK_ROOT/platforms/android-21/arch-arm64 \--extra-cflags="$FLAGS $INCLUDES" \--extra-cflags="-isysroot $NDK_ROOT/sysroot/" \--arch=arm64 \--target-os=android # 清理一下 
make clean
#执行makefile
make install

将编译好生成的静态库考入到项目到指定目录下
在这里插入图片描述

原文地址: https://www.cnblogs.com/zuojie/p/16461050.html#autoid-1-2-0

主流的音视频全栈开发技术 跳转

整理了一些音视频开发学习书籍、视频资料(音视频流媒体高级开发FFmpeg6.0/WebRTC/RTMP/RTSP/编码解码),有需要的可以自行添加学习交流群:739729163 领取!
在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/151556.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

LongAdder功能和原理

AtomicLong能保证并发情况下计数的准确性&#xff0c;其内部通过CAS来解决并发安全性的问题。 AtomicLong的缺点&#xff1a; 可以看到在高并发情况下&#xff0c;当有大量线程同时去更新一个变量&#xff0c;任意一个时间点只有一个线程能够成功&#xff0c;绝大部分的线程在尝…

自动驾驶学习笔记(十)——Cyber通信

#Apollo开发者# 学习课程的传送门如下&#xff0c;当您也准备学习自动驾驶时&#xff0c;可以和我一同前往&#xff1a; 《自动驾驶新人之旅》免费课程—> 传送门 《Apollo Beta宣讲和线下沙龙》免费报名—>传送门 文章目录 前言 Cyber通信 编写代码 编译程序 运行…

FISCOBCOS入门(十)Truffle测试helloworld智能合约

本文带你从零开始搭建truffle以及编写迁移脚本和测试文件,并对测试文件的代码进行解释,让你更深入的理解truffle测试智能合约的原理,制作不易,望一键三连 在windos终端内安装truffle npm install -g truffle 安装truffle时可能出现网络报错,多试几次即可 truffle --vers…

人力资源小程序

人力资源管理对于企业的运营至关重要&#xff0c;而如今随着科技的发展&#xff0c;制作一个人力资源小程序已经变得非常简单和便捷。在本文中&#xff0c;我们将为您介绍如何通过乔拓云网制作一个人力资源小程序&#xff0c;只需五个简单的步骤。 第一步&#xff1a;注册登录乔…

基于单片机的自动循迹小车(论文+源码)

1.系统设计 此次基于单片机的自动循迹小车的设计系统&#xff0c;结合循迹模块来共同完成本次设计&#xff0c;实现小车的循迹功能&#xff0c;其其整体框架如图2.1所示。其中&#xff0c;采用STC89C52单片机来作为核心控制器&#xff0c;负责将各个传感器等模块链接起来&…

四旋翼无人机的飞行原理--【其利天下分享】

近年来&#xff0c;无人机在多领域的便捷应用促使其迅猛的发展&#xff0c;如近年来的多场战争&#xff0c;无人机的战场运用发挥得淋漓尽致。 下面我们针对生活中常见的四旋翼无人机的飞行原理做个基础的介绍&#xff0c;以飨各位对无人机有兴趣的朋友。 一&#xff1a;四旋翼…

基于变形卷积和注意机制的带钢表面缺陷快速检测网络DCAM-Net(论文阅读笔记)

原论文链接->DCAM-Net: A Rapid Detection Network for Strip Steel Surface Defects Based on Deformable Convolution and Attention Mechanism | IEEE Journals & Magazine | IEEE Xplore DCAM-Net: A Rapid Detection Network for Strip Steel Surface Defects Base…

1334. 阈值距离内邻居最少的城市/Floyd 【leetcode】

1334. 阈值距离内邻居最少的城市 有 n 个城市&#xff0c;按从 0 到 n-1 编号。给你一个边数组 edges&#xff0c;其中 edges[i] [fromi, toi, weighti] 代表 fromi 和 toi 两个城市之间的双向加权边&#xff0c;距离阈值是一个整数 distanceThreshold。 返回能通过某些路径…

Ubuntu18.04安装ROS系统+turtle测试

安装 1.设置安装源 sudo sh -c echo "deb http://packages.ros.org/ros/ubuntu $(lsb_release -sc) main" > /etc/apt/sources.list.d/ros-latest.list sudo sh -c . /etc/lsb-release && echo "deb http://mirrors.tuna.tsinghua.edu.cn/ros/ubun…

linux网络编程之TCP协议编程

Linux网络编程之TCP协议编程 tcp协议编程模型socket函数sockaddr_inbindlistenconnect 应用服务端代码客服端代码 TCP协议编程) tcp协议编程模型 Server 1.创建socket (socket函数) 2.确定服务器协议地址簇 (struct sockaddr) 3.绑定 (bind) 4.监听 ( listen) 5.接受客户端连接…

【并发编程】Synchronized的使用

&#x1f4eb;作者简介&#xff1a;小明java问道之路&#xff0c;2022年度博客之星全国TOP3&#xff0c;专注于后端、中间件、计算机底层、架构设计演进与稳定性建设优化&#xff0c;文章内容兼具广度、深度、大厂技术方案&#xff0c;对待技术喜欢推理加验证&#xff0c;就职于…

C进阶---文件操作

我们在日常使用电脑保存文件时&#xff0c;其目的就是为了便于以后查看、修改、更新等操作&#xff1b;保存在文件中可以使数据持久化&#xff0c;所以今天我们家里学习文件的相关操作。 一、文件 1.1什么是文件 磁盘上的文件是文件。 在程序设计中&#xff0c;文件一般分…

验证码案例 —— Kaptcha 插件介绍 后端生成验证码,前端展示并进行session验证(带完整前后端源码)

&#x1f9f8;欢迎来到dream_ready的博客&#xff0c;&#x1f4dc;相信你对这篇博客也感兴趣o (ˉ▽ˉ&#xff1b;) &#x1f4dc;表白墙/留言墙 —— 中级SpringBoot项目&#xff0c;MyBatis技术栈MySQL数据库开发&#xff0c;练手项目前后端开发(带完整源码) 全方位全步骤手…

了解一下知识付费系统的开发流程和关键技术点

知识付费系统的开发既涉及到前端用户体验&#xff0c;又需要强大的后端支持和复杂的付费逻辑。在这篇文章中&#xff0c;我们将深入探讨知识付费系统的开发流程和关键技术点&#xff0c;并提供一些相关的技术代码示例。 1. 需求分析和规划&#xff1a; 在着手开发知识付费系…

Spring Cloud Stream实践

概述 不同中间件&#xff0c;有各自的使用方法&#xff0c;代码也不一样。 可以使用Spring Cloud Stream解耦&#xff0c;切换中间件时&#xff0c;不需要修改代码。实现方式为使用绑定层&#xff0c;绑定层对生产者和消费者提供统一的编码方式&#xff0c;需要连接不同的中间…

8、创建第一个鸿蒙页面并实现页面跳转

一、创建页面 1、新建页面 在项目的"pages"目录上右键&#xff0c;选择”新建“——”page" 2、录入页面的名称 在“Page name”中输入页面的名称&#xff0c;并点击“Finish”完成创建 3、以下为创建的新页面 2、注册页面 新建的页面会自动在“resources”…

一起Talk Android吧(第五百五十五回:Retrofit中的注解)

文章目录 1. 概念介绍2. 注解的分类与功能2.1 方法类注解2.2 参数类注解3. 内容总结各位看官们大家好,上一回中分享了一个Retrofit使用错误的案例,本章回中将 介绍Retrofit请求中的注解。闲话休提,言归正转,让我们一起Talk Android吧! 1. 概念介绍 我们在前面章回中介绍R…

二十、虚拟机网络配置

1、Linux网络配置原理 我自己Linux虚拟机的IP地址是&#xff1a;192.168.159.131 vmnet8&#xff1a;192.168.159.1 无线网卡&#xff1a;192.168.159.1 2、查看网络IP和网关 查看虚拟网络编辑器和修改IP地址 如果把这个位置的子网IP换成&#xff1a;192.168.8.0的话重启虚拟机…

【2023云栖】陈守元:阿里云开源大数据产品年度发布

本文根据 2023 云栖大会演讲实录整理而成&#xff0c;演讲信息如下&#xff1a; 演讲人&#xff1a;陈守元 | 阿里云计算平台事业部开源大数据产品总监 演讲主题&#xff1a;阿里云开源大数据产品年度发布 随着云计算的不断发展&#xff0c;未来数据处理和应用的趋势将围绕C…

ES6中实现继承

本篇文章主要说明在ES6中如何实现继承&#xff0c;学过java的小伙伴&#xff0c;对class这个关键字应该不陌生&#xff0c;ES6中也提供了class这个关键字作为实现类的语法糖&#xff0c;咱们一起实现下ES6中的继承。 实现思路 首先直接通过class来声明一个Teacther类&#xff…