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.pswp.cn/diannao/92217.shtml
繁体地址,请注明出处:http://hk.pswp.cn/diannao/92217.shtml

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

相关文章

使用 go-redis-entraid 实现 Entra ID 无密钥认证

1、依赖与安装 步骤命令说明安装&#xff08;或升级&#xff09; go-redis v9.9go get github.com/redis/go-redis/v9latestentraid 必须 ≥ 9.9.0安装 go-redis-entraidgo get github.com/redis/go-redis-entraid自动拉取 transit 依赖 2、认证方式一览 方式说明创建 Stream…

window上docker安装RabbitMQ

1、要进http://localhost:15672管理页面需要安装management版本2、搜索镜像并pull3、启动镜像时将端口映射出来4、启动成功&#xff0c;点击可查看日志详情&#xff0c;浏览器访问5、直接使用guest/guest登录会报错User can only log in via localhost解决办法有两个&#xff1…

异世界历险之数据结构世界(排序(插入,希尔,堆排))

前言 介绍 插入排序 基本知识&#xff1a; 直接插入排序是一种简单的插入排序法&#xff0c;其基本思想是&#xff1a; 把待排序的记录按其关键码值的大小逐个插入到一个已经排好序的有序序列中&#xff0c;直到所有的记录插入完为止&#xff0c;得到一个新的有序序列 直接插入…

oracle 数据库中,将几张表的数据按指定日期范围实时同步至同一个数据库的备份表中。

以下是一个Oracle数据库中实现表数据按指定日期范围实时同步至备份表的解决方案。这个方案使用存储过程和触发器组合实现&#xff1a; 1. 创建备份表结构 首先需要为每张需要备份的表创建对应的备份表&#xff0c;结构与原表相同&#xff1a; -- 为原表创建备份表&#xff08;示…

电脑网络连接正常,微信、QQ能正常使用,但无法访问网页?DNS问题的解决方案和背后原理。

文章目录1. 问题背景2. 解决方案2.1 手动刷新DNS2.1.1 Windows版本2.1.2 Mac版本2.2 手动设置DNS服务器2.2.1 Windows版2.2.2 Mac版2.3 其他解决方案3. DNS是什么&#xff1f;3.1 详细解释DNS3.1.1 A distributed, hierarchical database&#xff08;一个分布式和分层数据库结构…

【HTML】图片比例和外部div比例不一致,最大程度占满

图片比例和外部div比例不一致&#xff0c;最大程度占满&#xff0c;并且图片比例不变 其中1.jpg,2.jpg,1.html在同一目录 |-----|- 1.jpg|- 2.jpg|- 1.html1.jpg2.jpg<!DOCTYPE html> <html lang"en"><head><meta charset"UTF-8" /&g…

如何使用python网络爬虫批量获取公共资源数据技术

如何快速批量地获取海量公共资源数据决定了科研的效率。Python网络爬虫是快速批量获取网络数据的重要手段&#xff0c;它按照发送请求、获得页面、解析页面、下载内容、储存内容等流程&#xff1f; 一&#xff1a;Python软件的安装及入门1 Python软件安装及入门1)Anaconda软件安…

Kiro vs Cursor: AI IDE 终极对比指南

概述 随着生成式 AI 革命性地改变了我们编写代码的方式&#xff0c;新一代 AI 驱动的集成开发环境 (IDE) 正在崛起。Kiro 和 Cursor 代表了这一运动的前沿&#xff0c;但它们采用了截然不同的方法。 核心理念对比 特性AWS KiroCursor核心理念结构化开发流程 (Spec-driven)对…

Python获取网页乱码问题终极解决方案 | Python爬虫编码处理指南

在Python网络爬虫开发中&#xff0c;乱码是最常见的问题之一。本文将深入探讨乱码产生的原因&#xff0c;并提供多种有效的解决方案&#xff0c;帮助您彻底解决Python获取网页内容时的乱码问题。常见网页编码格式编码类型使用场景Python解码方式UTF-8现代网站标准编码.decode(u…

Android MTK平台预置多张静态壁纸

执行 adb shell pm list package -f wallpaper 命令&#xff0c;查看壁纸应用路径&#xff1a; /product/app/MtkWallpaperPicker/MtkWallpaperPicker.apkcom.android.wallpaperpicker 结果中带 Mtk 就可确定MTK有对应用进行重构。其源码路径在 vendor/mediatek/proprietary/…

基于Django的个人博客系统开发(开题报告)

毕业论文(设计)开题报告论文(设计)题目 基于Django的个人博客系统开发 1.选题目的和意义 随着云服务器的普及化以及编程培训机构大量涌现,学习网站开发技术以及编程技术,通过租用个人云服务器部署代码,构建个人博客网站,创建学习文档,记录学习过程,与他人交流技术学…

C++ 分配内存释放内存

C 分配内存释放内存一、new、delete、malloc和free最简单的分配内存自定义对象分配和释放内存二、new、delete与虚析构的问题三、一维、二维、多维数值创建和释放一维二维多维四、new的缺点以及连续内存的优点一、new、delete、malloc和free 最简单的分配内存 int* p_m (int*…

奥比中光深度相机开发

一、开发环境准备 1.1 硬件要求 奥比中光深度相机&#xff08;如Astra Pro、Gemini等&#xff09;USB 3.0接口&#xff08;确保数据传输稳定&#xff09;支持OpenGL的显卡&#xff08;可选&#xff0c;用于点云可视化&#xff09; 1.2 软件环境 SDK安装&#xff1a; 从奥比…

标题 “Python 网络爬虫 —— selenium库驱动浏览器

一、Selenium 库核心认知 Selenium 库是 Web 应用程序测试与自动化操作的利器 &#xff0c;能驱动浏览器&#xff08;如 Edge、Firefox 等&#xff09;执行点击、输入、打开、验证等操作 。与 Requests 库差异显著&#xff1a;Requests 库仅能获取网页原始代码&#xff0c;而 …

从实践出发--探究C/C++空类的大小,真的是1吗?

文章目录测试代码VS2022正常运行编译失败GCC总结Author: NemaleSu Data: 2025/07/21 测试环境&#xff1a; Win11&#xff1a;VS2022Ubuntu22.04&#xff1a;gcc (Ubuntu 11.4.0-1ubuntu1~22.04) 11.4.0 相信众多cpper听过太多书籍、视频、文档、博客等资料&#xff0c;说C/C…

数据结构自学Day11-- 排序算法

一、排序算法的概念排序&#xff08;Sorting&#xff09;是指&#xff1a;将一组“无序”的数据&#xff0c;按照某种“顺序规则”排列成“有序”的过程。1、按排序顺序分类&#xff1a;升序&#xff1a;从小到大排列&#xff0c;如 1, 3, 5, 7, 9降序&#xff1a;从大到小排列…

电子元器件—三极管(一篇文章搞懂电路中的三极管)(笔记)(面试考试必备知识点)

三极管的定义及工作原理1. 定义三极管&#xff08;Transistor&#xff09;是一种具有三层半导体材料&#xff08;P-N-P 或 N-P-N&#xff09;构成的半导体器件&#xff0c;用于信号放大、开关控制和信号调制等应用。三极管有三个引脚&#xff1a;发射极&#xff08;Emitter&…

数据结构之克鲁斯卡尔算法

前言&#xff1a;和Prim算法一样&#xff0c;Kruskal 算法也是用来生成最小生成树的&#xff0c;这篇文章来学习一下Kruskal算法的实现 一、实现流程 初始化的时候&#xff0c;将所有的边用一个数组存储&#xff0c;并且按权值从小到大进行排序&#xff0c;每次选一个权值最小的…

MongoDB 查询时区问题

MongoDB默认时区是UTC&#xff0c;比北京时区晚八小时&#xff0c;北京时间UTC8h。 // 北京时间的 2024-10-01 08:00:00 // (>) 大于 - $gt // (<) 小于 - $lt // (>) 大于等于 - $gte // (< ) 小于等于 - $lte// Z代表UTC时区1、{"gmtCreate":{"$…

Windows VS2019 编译 Apache Thrift 0.15.0

随着微服务架构的普及,高效的跨语言远程过程调用(RPC) 成为了构建分布式系统的重要基础。Apache Thrift 是 Facebook 开源的一个轻量级、高性能的 RPC 框架,它允许开发者通过一个通用的接口定义语言(IDL)来定义服务接口和数据结构,并自动生成多种语言的客户端和服务端代…