当前位置: 首页 > news >正文

Android NDK ffmpeg 音视频开发实战

文章目录

  • 接入FFmpeg
    • 1.下载FFmpeg 源码
    • 2.编译FFmpeg.so库
      • 异常处理
    • 3.自定义FFmpeg交互so库创建
    • 4.配置CMakeLists.txt
    • 5.CMakeLists.txt 环境配置
    • 6.Native与Java层调用
  • 解码器准备

接入FFmpeg

1.下载FFmpeg 源码

FFmpeg官网地址

2.编译FFmpeg.so库

  • 移动 FFmpeg 源码文件夹至 Android Studio 的 cpp 包下(也可以不移)
  • 在 FFmpeg 文件夹内创建用来编译 .so 库 的 sh脚本

编译脚本是基于以下 Android 各较新版本:

版本
Android SDK35
NDK26
CMake3.6
JDK11
Gradle8.11.0
Android Gradle Plugin8.6.0
#!/usr/bin/env bash
# 声明脚本解释器为 bash# -------------------- 配置路径 --------------------
# ‼️修改成自己的NDK版本和路径
NDK=/Users/xxx/Library/Android/sdk/ndk/26.1.10909125
# Android NDK 路径HOST_TAG=darwin-x86_64
# 主机系统标识,macOS 64位TOOLCHAIN=$NDK/toolchains/llvm/prebuilt/$HOST_TAG
# LLVM 交叉编译工具链路径SYSROOT=$TOOLCHAIN/sysroot
# sysroot 路径,NDK 中包含标准库等文件的根目录API=21
# Android API 级别FFMPEG_SRC=$(pwd)
# 当前工作目录,即 ffmpeg 源码根目录OUTPUT_ROOT=$FFMPEG_SRC/android-build
# 编译输出目录ARCHS=("armeabi-v7a" "arm64-v8a" "x86_64")
# 需要编译的架构列表# -------------------- 函数:编译单个架构 --------------------
build_one() {ARCH=$1# 接收参数,架构名称echo "==================== 编译架构: $ARCH ===================="# 打印当前编译的架构,方便查看进度case $ARCH inarmeabi-v7a)TARGET=armv7a-linux-androideabi# armeabi-v7a 架构对应的目标三元组;;arm64-v8a)TARGET=aarch64-linux-android# arm64-v8a 架构对应的目标三元组;;x86_64)TARGET=x86_64-linux-android# x86_64 架构对应的目标三元组;;*)echo "❌ 未知架构: $ARCH"exit 1# 如果传入架构不在已知列表,退出脚本;;esacCC="$TOOLCHAIN/bin/clang --target=${TARGET}${API} --sysroot=$SYSROOT"# 定义 C 编译器,带上目标三元组和 API 版本,指定 sysrootCXX="$TOOLCHAIN/bin/clang++ --target=${TARGET}${API} --sysroot=$SYSROOT"# 定义 C++ 编译器,参数同上PREFIX=$OUTPUT_ROOT/$ARCH# 该架构编译后文件的安装目录mkdir -p $PREFIX# 创建安装目录,若不存在则新建cd $FFMPEG_SRC# 进入 ffmpeg 源码目录,准备开始编译make clean# 清理之前的编译结果,避免干扰./configure \--prefix=$PREFIX \--target-os=android \--arch=$ARCH \--enable-cross-compile \--cc="$CC" \--cxx="$CXX" \--sysroot=$SYSROOT \--enable-shared \--disable-static \--disable-doc \--disable-programs \--disable-symver \--disable-debug \--disable-asm \--extra-cflags="-Os -fPIC" \--extra-ldflags=""# 调用 ffmpeg 的 configure 脚本,配置编译选项:# --prefix:安装路径# --target-os=android:目标操作系统为 Android# --arch:目标架构# --enable-cross-compile:启用交叉编译# --cc 和 --cxx:指定 C 和 C++ 编译器# --sysroot:指定 sysroot 路径# --enable-shared:生成动态库(so)# --disable-static:不生成静态库# --disable-doc:不生成文档# --disable-programs:不编译 ffmpeg 命令行工具# --disable-symver:关闭符号版本控制# --disable-debug:关闭调试# --disable-asm:禁用汇编优化(可根据需求开启)# --extra-cflags:额外的编译参数,此处优化大小且使用 PIC# --extra-ldflags:额外的链接参数,当前为空if [ $? -ne 0 ]; thenecho "❌ 配置失败: $ARCH"exit 1fi# 如果 configure 出错,则打印失败信息并退出make -j$(sysctl -n hw.ncpu)# 多线程编译,线程数为当前 CPU 核数make install# 安装编译结果到指定目录
}# -------------------- 循环编译所有架构 --------------------
for ARCH in "${ARCHS[@]}"; dobuild_one $ARCH# 按顺序调用函数,编译每个架构
doneecho "==================== 全部架构编译完成 ===================="
# 全部架构编译结束,打印提示
  • 编译成功后在路径下可以看到生成的 .so 库和 .h 头文件

cpp/ffmpeg-7.1.1/android-build/arm64-v8a/lib .so库位置
cpp/ffmpeg-7.1.1/android-build/arm64-v8a/include .h 头文件

一共输出7个 .so库:

so库功能
libavcodec.so编解码器核心库
libavformat.so负责封装/解封装(容器格式,如 mp4、mkv)
libavutil.so工具库(数学、字节序、日志等)
libswscale.so视频像素格式转换(YUV ↔ RGB 等)
libswresample.so音频重采样、通道布局转换
libavfilter.so滤镜处理(裁剪、特效等)
libavdevice.so设备输入输出(通常可以不用)

编译.so库完成。

异常处理

  • 报错:nasm/yasm not found or too old. Use --disable-x86asm for a crippled build.
    • 安装 nasm

      		brew install nasm   
      
  • 报错:C compiler test failed.
    • 在系统NDK路径确认 clang 编译路径的有效性
      $NDK/toolchains/llvm/prebuilt/darwin-arm64/bin/clang
      

3.自定义FFmpeg交互so库创建

  • 创建一个Java类,自定义调用 native 层的方法

    package com.example.video;public class FFmpegLoader {//自定义方法,返回FFmpeg的版本,判断接入成功public native String getFFmpegVersion();static {//自定义so库名System.loadLibrary("native-lib-myFFmpeg");}
    }
    
  • 通过命令行在当前路径中生成 .h 文件,再放入cpp文件夹中

    javac -h ./jni FFmpegLoader.java
    

    .h文件内容:

    /* DO NOT EDIT THIS FILE - it is machine generated */
    #include <jni.h>
    /* Header for class com_example_video_FFmpegLoader */#ifndef _Included_com_example_video_FFmpegLoader
    #define _Included_com_example_video_FFmpegLoader
    #ifdef __cplusplus
    extern "C" {
    #endif
    /** Class:     com_example_video_FFmpegLoader* Method:    getFFmpegVersion* Signature: ()V*/
    JNIEXPORT void JNICALL 	Java_com_example_video_FFmpegLoader_getFFmpegVersion(JNIEnv *, jobject);#ifdef __cplusplus
    }
    #endif
    #endif
    

    移入路径:cpp/com_example_video_FFmpegLoader.h

  • 在同一cpp路径下,创建 cpp 文件,会有 #include <libavcodec/avcodec.h> 等文件报红,是因为没有配置CMakeLists.txt 链接,可以先不管,或者先注释掉 FFmpge 库的引用,编译完之后再调用。

    /* DO NOT EDIT THIS FILE - it is machine generated */
    #include <jni.h>
    /* Header for class com_example_video_FFmpegLoader */
    #include <string>
    extern "C" {
    #include <libavcodec/avcodec.h>
    #include <libavformat/avformat.h>
    #include <libavutil/avutil.h>
    }#ifndef _Included_com_example_video_FFmpegLoader
    #define _Included_com_example_video_FFmpegLoader
    #ifdef __cplusplus
    extern "C" {
    #endif
    /** Class:     com_example_video_FFmpegLoader* Method:    getFFmpegVersion* Signature: ()V*/
    JNIEXPORT jstring JNICALL 	Java_com_example_video_FFmpegLoader_getFFmpegVersion(JNIEnv *env, jobject){const char *version = av_version_info();return env->NewStringUTF(version);
    }#ifdef __cplusplus
    }
    #endif
    #endif
    

4.配置CMakeLists.txt

  • 外部so库调用 :将编译成功的 so 库(路径:cpp/ffmpeg-7.1.1/android-build/arm64-v8a/lib),集中存放到 jni 文件夹下(/src/main/jni)。并且根据 不同架构分别存放,用于之后在CMakeList.txt 中可以动态链接到这些so库。

    根据 jni 文件夹相对 CMakeLists.txt 文件的位置,找到各 so 库,通过外部引入。

    # 引入 FFmpeg 的 so 库
    # 1.编解码器核心库
    add_library(avcodec SHARED IMPORTED)
    set_target_properties(avcodec PROPERTIES IMPORTED_LOCATION${CMAKE_SOURCE_DIR}/../jni/${ANDROID_ABI}/libavcodec.so)
    
  • 添加头文件目录:让自定义 cpp 文件,可以通过头文件链接到其他 so 库

    	# 添加头文件目录include_directories(${CMAKE_SOURCE_DIR}/include)
    
  • 将自定义 cpp 文件(native-lib-myFFmpeg 库)与FFmpeg 库 链接到一起,使得我的 native 方法可以调用到 FFmpage 的内容。

完整代码:

cmake_minimum_required(VERSION 3.6)
project(ffmpeg_test)# 添加头文件目录
include_directories(${CMAKE_SOURCE_DIR}/include)# 引入 FFmpeg 的 so 库
# 1.编解码器核心库
add_library(avcodec SHARED IMPORTED)
set_target_properties(avcodec PROPERTIES IMPORTED_LOCATION${CMAKE_SOURCE_DIR}/../jni/${ANDROID_ABI}/libavcodec.so)#2.负责封装/解封装(容器格式,如 mp4、mkv)
add_library(avformat SHARED IMPORTED)
set_target_properties(avformat PROPERTIES IMPORTED_LOCATION${CMAKE_SOURCE_DIR}/../jni/${ANDROID_ABI}/libavformat.so)#3.工具库(数学、字节序、日志等)
add_library(avutil SHARED IMPORTED)
set_target_properties(avutil PROPERTIES IMPORTED_LOCATION${CMAKE_SOURCE_DIR}/../jni/${ANDROID_ABI}/libavutil.so)#4.视频像素格式转换(YUV ↔ RGB 等)
add_library(swscale SHARED IMPORTED)
set_target_properties(swscale PROPERTIES IMPORTED_LOCATION${CMAKE_SOURCE_DIR}/../jni/${ANDROID_ABI}/libswscale.so)#5.音频重采样、通道布局转换
add_library(swresample SHARED IMPORTED)
set_target_properties(swresample PROPERTIES IMPORTED_LOCATION${CMAKE_SOURCE_DIR}/../jni/${ANDROID_ABI}/libswresample.so)#6.滤镜处理(裁剪、特效等)
add_library(avfilter SHARED IMPORTED)
set_target_properties(avfilter PROPERTIES IMPORTED_LOCATION${CMAKE_SOURCE_DIR}/../jni/${ANDROID_ABI}/libavfilter.so)# 添加你的本地库
add_library(native-lib-myFFmpeg SHARED native-lib-myFFmpeg.cpp)# 链接 FFmpeg 库
target_link_libraries(native-lib-myFFmpegavcodecavformatavutilswscaleswresampleavfilterandroid${log-lib}
)

5.CMakeLists.txt 环境配置

配置如下三点:

  • CMakeLists.txt 路径
  • ndk 编译架构
  • C++编译语言
android {defaultConfig {🧠 externalNativeBuild{cmake{cppFlags("")}}🧠 ndk{abiFilters += listOf("armeabi-v7a","x86_64","arm64-v8a")}}🧠  externalNativeBuild{cmake {path = file("src/main/cpp/CMakeLists.txt")}}
}

build 项目,生成自定义的 native-lib-myFFmpeg.so 文件,路径:

build/intermediates/cxx/Debug/48u475k3/obj/armeabi-v7a/libnative-lib-myFFmpeg.so

6.Native与Java层调用

        TextView tv = findViewById(R.id.video_text);FFmpegLoader fFmpegLoader = new FFmpegLoader();tv.setText(fFmpegLoader.getFFmpegVersion());

显示FFmpeg的版本,完成FFmpeg 接入。


解码器准备

native-lib-myFFmpeg.h

/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class com_example_video_FFmpegLoader */#ifndef _Included_com_example_video_FFmpegLoader
#define _Included_com_example_video_FFmpegLoader
#ifdef __cplusplus
extern "C" {
#endif
/** Class:     com_example_video_FFmpegLoader* Method:    getFFmpegVersion* Signature: ()V*/
JNIEXPORT void JNICALL Java_com_example_video_SimplePlayer_getFFmpegVersion(JNIEnv *, jobject);#ifdef __cplusplus
}
#endif
#endif

native-lib-myFFmpeg.cpp

/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class com_example_video_FFmpegLoader */
#include <string>
#include "NativeSimplePlayer.h"
#include "JNICallbackHelper.h"extern "C" {
#include <libavcodec/avcodec.h>
#include <libavformat/avformat.h>
#include <libavutil/avutil.h>
}#ifndef _Included_com_example_video_FFmpegLoader
#define _Included_com_example_video_FFmpegLoader
#ifdef __cplusplus
extern "C" {
#endif
/** Class:     com_example_video_FFmpegLoader* Method:    getFFmpegVersion* Signature: ()V*/
JNIEXPORT jstring JNICALL Java_com_example_video_SimplePlayer_getFFmpegVersion(JNIEnv *env, jobject){const char *version = av_version_info();return env->NewStringUTF(version);
}#ifdef __cplusplus
}
#endif
#endif//虚拟机可以跨线程
JavaVM  *vm = 0;
jint JNI_OnLoad(JavaVM *vm,void *args){::vm = vm;return JNI_VERSION_1_6;
}extern "C"
JNIEXPORT void JNICALL
Java_com_example_video_SimplePlayer_prepareNative(JNIEnv *env, jobject job, jstring data_source) {const char* data_source_ = env->GetStringUTFChars(data_source,0);//可能是主线程,也可能是子线程auto *helper = new JNICallbackHelper(vm,env,job);auto *player = new NativeSimplePlayer(data_source_,helper);player->prepare();env->ReleaseStringUTFChars(data_source,data_source_);
}extern "C"
JNIEXPORT void JNICALL
Java_com_example_video_SimplePlayer_startNative(JNIEnv *env, jobject thiz) {// TODO: implement startNative()
}extern "C"
JNIEXPORT void JNICALL
Java_com_example_video_SimplePlayer_stopNative(JNIEnv *env, jobject thiz) {// TODO: implement stopNative()
}extern "C"
JNIEXPORT void JNICALL
Java_com_example_video_SimplePlayer_releaseNative(JNIEnv *env, jobject thiz) {// TODO: implement releaseNative()
}

NativeSimplePlayer.cpp

//
// Created on 2025/7/22.
//#include <string.h>
#include "NativeSimplePlayer.h"NativeSimplePlayer::NativeSimplePlayer(const char *data_source_,JNICallbackHelper *helper) {//如果被释放,会造成悬空指针//this->data_source = data_source_;//深拷贝//C层:demo.mp4\0 C层会自动 + \0 ,strlen 不计算 \0 的长度this->data_source = new char[strlen(data_source_) + 1];strcpy(this->data_source,data_source_);this->helper = helper;
}NativeSimplePlayer::~NativeSimplePlayer() {if( data_source ){delete data_source;}if( helper){delete helper;}
}void* task_prepare(void * args){//读取文件auto *player = static_cast<NativeSimplePlayer *>(args);player->prepare_();return 0;//必须返回,错误很难找
}void NativeSimplePlayer::prepare_() {//为什么FFmpeg源码,大量使用上下文 Context//因为FFmpge源码是纯C,他没有对象,只能上下文贯穿环境,操作成员变量/*** 第一步,打开媒体文件地址* @parm AVFormatContext  -> formatContext 上下文* @parm filename         -> data_source 路径* @parm AVInputFormat    -> *fmt Mac、Windows 摄像头、麦克风* @param AVDictionary    -> 设置Http连接超时,打开rtmp超时*/formatContext = avformat_alloc_context();AVDictionary  *dictionary = 0;av_dict_set(&dictionary,"timeout","5000000",0);int result = avformat_open_input(&formatContext,data_source,0,&dictionary);//释放字典av_dict_free(&dictionary);if(result){//回调错误信息给Java,通过JNI反射return;}/*** 第二步,查找媒体中的音视频流信息*/result = avformat_find_stream_info(formatContext,0);if( result < 0 ){return;}/*** 第三步,根据流信息,流的个数,用循环来找*///    for(int i = 0;i < formatContext->nb_streams;++i){for(int i = 0;i < 2;++i){/*** 第四步,获取媒体流(视频,音频)*/AVStream *stream = formatContext->streams[i];/*** 第五步,从上面的流中 获取 编码解码的参数* 由于后面的解码器 编码器 都需要参数(记录的宽高)*/AVCodecParameters *parameters = stream->codecpar;/*** 第六步,获取编/解码器,根据上面的参数👆*/const AVCodec *codec = avcodec_find_decoder(parameters->codec_id);/*** 第七步,编解码器 上下文【真正干活】*/AVCodecContext *codecContext = avcodec_alloc_context3(codec);if( !codecContext ){return;}/*** 第八步,他目前是一张白纸* 把 parameter 拷贝给=> codecContext*/result = avcodec_parameters_to_context(codecContext,parameters);if( result < 0){return;}/*** 第九步,打开解码器*/result = avcodec_open2(codecContext,codec,0);if( result ){return;}/*** 第十步,从编解码器从,获取流的类型 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();}}/*** 第十一步,如果流中没有音频,也没有视频 【健壮性校验】*/if( !audio_channel && !video_channel){return;}/*** 第十二步,恭喜你,准备成功,媒体准备完成,通知上层*/if(helper){helper->onPrepared(THREAD_CHILD);}
}void NativeSimplePlayer::prepare() {//此时为Activity调用到的,所以为主线程//解封装 FFmpeg 来解析,要使用子线程pthread_create(&pid_prepare,0,task_prepare,this);}

NativeSimplePlayer.h

//
// Created on 2025/7/22.
//#ifndef COROUTINE_NATIVESIMPLEPLAYER_H
#define COROUTINE_NATIVESIMPLEPLAYER_H#include <cstring>
#include <pthread.h>
#include "AudioChannel.h"
#include "VideoChannel.h"
#include "JNICallbackHelper.h"
#include "AudioChannel.h"
#include "VideoChannel.h"
#include "util.h"extern "C"{ //ffmpeg 是纯C写的,必须采用C的编译方式,否则崩溃
#include "libavformat/avformat.h"
#include "libavcodec/avcodec.h"};class NativeSimplePlayer {
private:char  *data_source = 0;//指针需要初始值pthread_t pid_prepare;AVFormatContext  *formatContext = 0;AudioChannel *audio_channel = 0;VideoChannel *video_channel = 0;JNICallbackHelper *helper =0 ;
public:NativeSimplePlayer(const char *data_source,JNICallbackHelper *helper);~NativeSimplePlayer();void prepare();void prepare_();
};#endif //COROUTINE_NATIVESIMPLEPLAYER_H

JNICallbackHelper.h Native层调用Java层

//
// Created on 2025/7/22.
//#ifndef COROUTINE_JNICALLBACKHELPER_H
#define COROUTINE_JNICALLBACKHELPER_H#include <jni.h>
#include "util.h"class JNICallbackHelper {
private:JavaVM *vm = 0;JNIEnv *env = 0;jobject job;jmethodID jmd_prepared;public:JNICallbackHelper(_JavaVM *vm, _JNIEnv *env, jobject jobject);virtual ~JNICallbackHelper();void onPrepared(int thread_mode);
};#endif //COROUTINE_JNICALLBACKHELPER_H

JNICallbackHelper.cpp

//
// Created on 2025/7/22.
//#include "JNICallbackHelper.h"JNICallbackHelper::JNICallbackHelper(_JavaVM *vm, _JNIEnv *env, jobject jobject) {this->vm = vm;this->env = env;//全局引用this->job = env->NewGlobalRef(jobject);jclass claz = env->GetObjectClass(jobject);//🌟这里没有赋值jmd_prepared = env->GetMethodID(claz,"onPrepared","()V");
}JNICallbackHelper::~JNICallbackHelper() {vm = 0;env->DeleteGlobalRef(job);job = 0;env = 0;
}void JNICallbackHelper::onPrepared(int thread_mode) {if( thread_mode == THREAD_MAIN){env->CallVoidMethod(job,jmd_prepared);} else if (thread_mode == THREAD_CHILD){//子线程不可以跨线程,要用全新的envJNIEnv  *env_child;vm->AttachCurrentThread(&env_child,0);env_child->CallVoidMethod(job,jmd_prepared);vm->DetachCurrentThread();}
}

util.h

//
// Created on 2025/7/22.
//#ifndef COROUTINE_UTIL_H
#define COROUTINE_UTIL_H#define THREAD_MAIN 1
#define THREAD_CHILD 2#endif //COROUTINE_UTIL_H

CMakeLists.txt 添加调用的 cpp

# 添加你的本地库
add_library(native-lib-myFFmpegSHAREDnative-lib-myFFmpeg.cppJNICallbackHelper.cppVideoChannel.cppAudioChannel.cppNativeSimplePlayer.cpp)

Jave层调用类

package com.example.video;public class SimplePlayer {public native String getFFmpegVersion();static {System.loadLibrary("native-lib-myFFmpeg");}public SimplePlayer() {}private String dataSource;private OnPreparedListener onPreparedListener;public void setDataSource(String dataSource) {this.dataSource = dataSource;}/*** 播放器的准备工作*/public void prepare(){prepareNative(dataSource);}/*** 开始播放*/public void start(){startNative();}/*** 停止播放*/public void stop(){stopNative();}/*** 释放资源*/public void release(){releaseNative();}public void onPrepared(){if( onPreparedListener != null){onPreparedListener.onPrepared();}}public void setOnPreparedListener(OnPreparedListener listener){onPreparedListener = listener;}public interface OnPreparedListener{void onPrepared();}/*** ======================= native 函数区 ==========================*/private native void prepareNative(String dataSource);private native void startNative();private native void stopNative() ;private native void releaseNative() ;}

Activity 准备视频编码器完成

package com.example.video;import android.os.Bundle;
import android.os.Environment;
import android.util.Log;
import android.widget.TextView;
import android.widget.Toast;import androidx.activity.EdgeToEdge;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.graphics.Insets;
import androidx.core.view.ViewCompat;
import androidx.core.view.WindowInsetsCompat;import java.io.File;public class VideoActivity extends AppCompatActivity {private SimplePlayer mPlayer;public static final String TAG = VideoActivity.class.getName();@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);EdgeToEdge.enable(this);setContentView(R.layout.activity_video);ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main), (v, insets) -> {Insets systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars());v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom);return insets;});TextView tv = findViewById(R.id.video_text);//tv.setText(mPlayer.getFFmpegVersion());initPlayer();}private void initPlayer(){mPlayer = new SimplePlayer();File videFile = new File(Environment.getExternalStorageDirectory()+ File.separator + "Download/Mcloud.mp4");mPlayer.setDataSource(videFile.getAbsolutePath());//准备成功的回调——C++子线程调用mPlayer.setOnPreparedListener(new SimplePlayer.OnPreparedListener() {@Overridepublic void onPrepared() {Log.d(TAG, "onPrepared: 准备完成");runOnUiThread(new Runnable() {@Overridepublic void run() {Toast.makeText(VideoActivity.this,"准备完成,即将播放",Toast.LENGTH_SHORT).show();}});mPlayer.start();}});}@Overrideprotected void onResume() {super.onResume();//触发mPlayer.prepare();}@Overrideprotected void onStop() {super.onStop();mPlayer.stop();}@Overrideprotected void onDestroy() {super.onDestroy();mPlayer.release();}
}
http://www.xdnf.cn/news/1164475.html

相关文章:

  • 数据结构 之 【排序】(直接插入排序、希尔排序)
  • 【C++】list的模拟实现
  • 音视频学习(四十二):H264帧间压缩技术
  • 周志华《机器学习导论》第13章 半监督学习
  • [深度学习] 大模型学习3上-模型训练与微调
  • 机器学习初学者理论初解
  • MySQL:表的增删查改
  • 基于VSCode的nRF52840开发环境搭建
  • C++高性能日志库spdlog介绍
  • 如何解决pip安装报错ModuleNotFoundError: No module named ‘pywifi’问题
  • boost::asio 中 io_service与线程的关系
  • Netty中CompositeByteBuf 的addComponents方法解析
  • React-useEffect的闭包陷阱(stale closure)
  • CentOS 系统上部署一个简单的 Web 应用程序
  • 关键成功因素法(CSF)深度解析:从战略目标到数据字典
  • AK视频下载工具:免费高效,多平台支持
  • 计算机网络:概述层---计算机网络的性能指标
  • 【c++】leetcode438 找到字符串中所有字母异位词
  • 易语言+懒人精灵/按键中控群控教程(手机、主板机、模拟器通用)
  • Three.js 从零入门:构建你的第一个 Web 3D 世界
  • 2025最新版PyCharm for Mac统一版安装使用指南
  • 树链剖分-苹果树
  • Java基础教程(010):面向对象中的this和就近原则
  • 图片转 PDF三个免费方法总结
  • 解决win10下Vmware虚拟机在笔记本睡眠唤醒后ssh连接不上的问题
  • 【STM32】485接口原理
  • C语言-字符串数组
  • xformers包介绍及代码示例
  • mcu中的调试接口是什么?
  • https正向代理 GoProxy