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

NDK FFmpeg音视频播放器二

NDK前期基础知识终于学完了,现在开始进入项目实战学习,通过FFmpeg实现一个简单的音视频播放器。

本文主要内容如下:

  1. 阻塞式队列SafeQueue。

  1. 音视频BaseChannel基础通道。

  1. 音视频压缩包加入队列。

  1. 视频解码与播放。

  1. ANativeWindow渲染

用到的ffmpeg、rtmp等库资源:

https://wwgl.lanzout.com/iN21C0qiiija

音视频播放流程:

1.准备工作完成,音视频解封装后,

通过音视频媒体上下文AVFormatContext获取到具体的音视频压缩包AVPacket

2.将音视频压缩包AVPacket解压,得到音视频原始包AVFrame(可播放的文件包)

3.拿到音视频原始包AVFrame,进行播放。

代码逻辑:

1.获取压缩包AVPacket、获取原始包AVFrame、播放;是个生产消费,重复并发进行的过程,可以通过队列queue来完成。

2.创建两个队列queue,压缩包AVPacket队列和原始包AVFrame队列;

3.创建循环获取压缩包AVPacket,并push压缩包到AVPacket队列;

4.创建循环去AVPacket队列中获取压缩包AVPacket,解压得到原始包AVFrame,并push原始包到AVFrame队列;

5.创建循环去AVFrame队列中获取原始包AVFrame,进行播放;

6.音频和视频都有相同的解压、原始包、播放动作,故创建分别创建音频和视频队列,并封装到音频AudioChannel通道和视频VideoChannel通道中去处理;音频AudioChannel通道和视频VideoChannel通道,重复部分封装BaseChannel通道去。

一、阻塞式队列SafeQueue

封装线程安全队列SafeQueue,通过pthread_mutex_t互斥锁和pthread_cond_t条件变量来实现数据入队,出队,等待和唤醒工作。

#ifndef NDKPLAYER_SAFEQUEUE_H
#define NDKPLAYER_SAFEQUEUE_H#include <queue>
#include <pthread.h>using namespace std;/*** 线程安全队列* @tparam T  泛型:存放任意类型*/
template<typename T>
class SafeQueue {
private:typedef void (*ReleaseCallback)(T *); // 函数指针定义 做回调 用来释放T里面的内容的
private:queue<T> queue;pthread_mutex_t mutex; // 互斥锁 安全pthread_cond_t cond; // 等待 和 唤醒int work; // 标记队列是否工作ReleaseCallback releaseCallback;
public:SafeQueue() {pthread_mutex_init(&mutex, 0); // 初始化互斥锁pthread_cond_init(&cond, 0); // 初始化条件变量}virtual ~SafeQueue() {pthread_mutex_destroy(&mutex); // 释放互斥锁pthread_cond_destroy(&cond); // 释放条件变量}/*** 入队 [ AVPacket *  压缩包]  [ AVFrame * 原始包]*/void insertToQueue(T value) {pthread_mutex_lock(&mutex); // 多线程的访问(先锁住)if (work) {// 工作状态,入队queue.push(value);// 当插入数据包 进队列后,发出通知唤醒pthread_cond_signal(&cond);} else {//非工作状态,释放valueif (releaseCallback) {releaseCallback(&value);}}pthread_mutex_unlock(&mutex); // 多线程的访问(要解锁)}/***  出队 [ AVPacket *  压缩包]  [ AVFrame * 原始包]*/int getQueueAndDel(T &value) {int result = 0;pthread_mutex_lock(&mutex); // 多线程的访问(先锁住)while (work && queue.empty()) {// 如果是工作 并且 队列里面没有数据,就阻塞在这里pthread_cond_wait(&cond, &mutex);}if (!queue.empty()) {// 取出队列的数据包 给外界,并删除队列数据包value = queue.front();// 删除队列中的数据queue.pop();// 成功 return trueresult = 1;}pthread_mutex_unlock(&mutex); // 多线程的访问(要解锁)return result;}/*** 设置工作状态,设置队列是否工作* @param work*/void setWork(int work) {pthread_mutex_lock(&mutex); // 多线程的访问(先锁住)this->work = work;// 每次设置状态后,就去唤醒pthread_cond_signal(&cond);pthread_mutex_unlock(&mutex); // 多线程的访问(要解锁)}int empty() {return queue.empty();}int size() {return queue.size();}/*** 清空队列中所有的数据,循环一个一个的删除*/void clear() {pthread_mutex_lock(&mutex); // 多线程的访问(先锁住)unsigned int size = queue.size();for (int i = 0; i < size; ++i) {//循环释放队列中的数据T value = queue.front();if (releaseCallback) {releaseCallback(&value); // 让外界去释放堆区空间}queue.pop(); // 删除队列中的数据,让队列为0}pthread_mutex_unlock(&mutex); // 多线程的访问(要解锁)}/*** 设置此函数指针的回调,让外界去释放* @param releaseCallback*/void setReleaseCallback(ReleaseCallback releaseCallback) {this->releaseCallback = releaseCallback;}
};#endif //NDKPLAYER_SAFEQUEUE_H

二、音视频BaseChannel基础通道

BaseChannel封装压缩包和原始包队列

#ifndef NDKPLAYER_BASECHANNEL_H
#define NDKPLAYER_BASECHANNEL_Hextern "C" {
#include "ffmpeg/include/libavcodec/avcodec.h"
};#include "SafeQueue.h"
#include <android/log.h>// log宏
#define TAG "NDK"
#define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG, TAG, __VA_ARGS__)
#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, TAG, __VA_ARGS__)
#define LOGI(...) __android_log_print(ANDROID_LOG_INFO, TAG, __VA_ARGS__)class BaseChannel {public:int stream_index; // 音频 或 视频 的下标SafeQueue<AVPacket *> packets; // 压缩的 数据包SafeQueue<AVFrame *> frames; // 原始的 数据包bool isPlaying; // 音频 和 视频 都会有的标记 是否播放AVCodecContext *codecContext = 0; // 音频 视频 都需要的 解码器上下文BaseChannel(int streamIndex, AVCodecContext *codecContext): stream_index(streamIndex), codecContext(codecContext) {packets.setReleaseCallback(releaseAVPacket);frames.setReleaseCallback(releaseAVFrame);}// 父类析构一定要加virtualvirtual ~BaseChannel() {// 清空队列packets.clear();frames.clear();}/*** 释放 队列中 所有的 AVPacket ** typedef void (*ReleaseCallback)(T *);*/static void releaseAVPacket(AVPacket **pPacket) {if (pPacket) {// 释放队列里面的 T == AVPacketav_packet_free(pPacket);*pPacket = 0;}}/*** 释放 队列中 所有的 AVFrame ** typedef void (*ReleaseCallback)(T *);*/static void releaseAVFrame(AVFrame **pFrame) {if (pFrame) {// 释放队列里面的 T == AVFrameav_frame_free(pFrame);*pFrame = 0;}}
};#endif //NDKPLAYER_BASECHANNEL_H

三、音视频压缩包加入队列

创建子线程,把音频和视频 压缩包 加入队列里面去

/*** 函数指针* 此函数和NdkPlayer这个对象没有关系,你没法拿NdkPlayer的私有成员* @return*/
void *task_start(void *ndk_player) {NdkPlayer *ndk_player_ = static_cast<NdkPlayer *>(ndk_player);ndk_player_->start_();return 0; // 必须返回,否则报错
}void NdkPlayer::start() {// 开始播放isPlaying = 1;// 音视频通道开始if (audio_channel) {audio_channel->start();}if (video_channel) {video_channel->start();}// 创建子线程,把音频和视频 压缩包 加入队列里面去pthread_create(&pid_start, 0, task_start, this);
}/*** 循环获取压缩包AVPacket,并push压缩包到队列*/
void NdkPlayer::start_() {LOGI("NdkPlayer::start_()");while (isPlaying) {// AVPacket 可能是音频 也可能是视频(压缩包)AVPacket *packet = av_packet_alloc();int result = av_read_frame(format_context, packet);// @return 0 if OKif (!result) {// 把压缩包AVPacket 分别加入音频 和 视频队列if (audio_channel && audio_channel->stream_index == packet->stream_index) {// 音频audio_channel->packets.insertToQueue(packet);} else if (video_channel && video_channel->stream_index == packet->stream_index) {// 视频video_channel->packets.insertToQueue(packet);}} else if (result == AVERROR_EOF) {// end of file == 读到文件末尾了 == AVERROR_EOF// 表示读完了,要考虑释放播放完成,并不代表播放完毕isPlaying = 0;LOGI("NdkPlayer::start_() end");} else {// av_read_frame 出现了错误,结束当前循环break;}} // end whileisPlaying = 0;audio_channel->stop();video_channel->stop();
}

四、视频解码与播放

第一个线程: 视频:取出队列的压缩包 进行编码 编码后的原始包 再push队列中去;

第二线线程:视频:从队列取出原始包,播放

#include "VideoChannel.h"VideoChannel::VideoChannel(int streamIndex, AVCodecContext *codecContext): BaseChannel(streamIndex, codecContext) {}VideoChannel::~VideoChannel() {}void VideoChannel::stop() {}/*** 函数指针 解码* @param video_channel* @return*/
void *task_video_decode(void *video_channel) {VideoChannel *video_channel_ = static_cast<VideoChannel *>(video_channel);video_channel_->video_decode();return 0;
}/*** 函数指针 播放* @param video_channel* @return*/
void *task_video_play(void *video_channel) {VideoChannel *video_channel_ = static_cast<VideoChannel *>(video_channel);video_channel_->video_play();return 0;
}void VideoChannel::start() {LOGI("VideoChannel::start()");isPlaying = 1;// 队列开始工作了packets.setWork(1);frames.setWork(1);// 第一个线程: 视频:取出队列的压缩包 进行编码 编码后的原始包 再push队列中去pthread_create(&pid_video_decode, 0, task_video_decode, this);// 第二线线程:视频:从队列取出原始包,播放pthread_create(&pid_video_play, 0, task_video_play, this);
}/*** 第一个线程: 视频:取出队列的压缩包 进行编码 编码后的原始包 再push队列中去*/
void VideoChannel::video_decode() {LOGI("VideoChannel::video_decode()");AVPacket *pkt = 0;while (isPlaying) {// 获取AVPacket *  压缩包int result = packets.getQueueAndDel(pkt);if (!isPlaying) {// 获取压缩包是耗时操作,获取完,如果关闭了播放,跳出循环break;}if (!result) {// 获取失败,可能是压缩包数据还没有加入队列,继续获取continue;}// 1.发送pkt(压缩包)给缓冲区,@return 0 on successresult = avcodec_send_packet(codecContext, pkt);// FFmpeg源码缓存一份pkt,释放即可releaseAVPacket(&pkt);if (result) {// avcodec_send_packet 出现了错误break;}AVFrame *frame = av_frame_alloc();// 2.从缓冲区拿出来(原始包),@return 0: successresult = avcodec_receive_frame(codecContext, frame);if (result == AVERROR(EAGAIN)) {// B帧  B帧参考前面成功  B帧参考后面失败   可能是P帧没有出来,再拿一次就行了continue;} else if (result != 0) {// avcodec_receive_frame 出现了错误break;}// 拿到了原始包,并将原始包push到队列frames.insertToQueue(frame);}// 解码获取原始包后,释放压缩包releaseAVPacket(&pkt);
}/*** 第二线线程:视频:从队列取出原始包,播放*/
void VideoChannel::video_play() {LOGI("VideoChannel::video_play()");AVFrame *frame = 0;uint8_t *dst_data[4]; // RGBA 播放文件int dst_linesize[4]; // RGBA//给 dst_data 申请内存   width * height * 4 xxxxav_image_alloc(dst_data, dst_linesize,codecContext->width, codecContext->height, AV_PIX_FMT_RGBA, 1);// SWS_BILINEAR 适中算法SwsContext *sws_ctx = sws_getContext(// 下面是输入环节codecContext->width,codecContext->height,codecContext->pix_fmt, // 自动获取 xxx.mp4 的像素格式  AV_PIX_FMT_YUV420P // 写死的// 下面是输出环节codecContext->width,codecContext->height,AV_PIX_FMT_RGBA,SWS_BILINEAR, NULL, NULL, NULL);while (isPlaying) {int result = frames.getQueueAndDel(frame);if (!isPlaying) {break; // 如果关闭了播放,跳出循环,releaseAVFrame(&frame);}if (!result) { // ret == 0continue; // 哪怕是没有成功,也要继续(假设:你生产太慢(原始包加入队列),我消费就等一下你)}// 格式转换 yuv ---> rgbasws_scale(sws_ctx,// 下面是输入环节 YUV的数据frame->data, frame->linesize,0, codecContext->height,// 下面是输出环节  成果:RGBA数据 dst_datadst_data,dst_linesize);/*** ANatvieWindows 渲染工作* SurfaceView ----- ANatvieWindows* 这里拿不到Surface,只能函数指针renderCallback()将RGBA数据 dst_data 回调给 native-lib.cpp,显示* 函数指针renderCallback()* 参数1:RGBA数据 dst_data 数组被传递会退化成指针,默认就是取第1元素* 参数2:视频宽* 参数3:视频高* 参数4:数据长度*/this->renderCallback(dst_data[0], codecContext->width, codecContext->height,dst_linesize[0]);// 释放原始包,因为已经被渲染完了,没用了releaseAVFrame(&frame);}releaseAVFrame(&frame);isPlaying = 0;av_free(&dst_data[0]);// free(sws_ctx); FFmpeg必须使用人家的函数释放,直接崩溃sws_freeContext(sws_ctx);
}void VideoChannel::setRenderCallback(RenderCallback renderCallback) {this->renderCallback = renderCallback;
}

五、ANativeWindow渲染

1)初始化surfaceView

private SurfaceView surfaceView;
surfaceView = findViewById(R.id.surfaceView);
mNdkPlayer = new NdkPlayer(dataSource);
mNdkPlayer.setSurfaceHolder(surfaceView);

2)绑定surfaceHolder

public class NdkPlayer implements SurfaceHolder.Callback {private SurfaceHolder surfaceHolder;public void setSurfaceHolder(SurfaceView surfaceView) {if (surfaceHolder != null) {// 清除上一次数据surfaceHolder.removeCallback(this);}this.surfaceHolder = surfaceView.getHolder();// 添加监听surfaceHolder.addCallback(this);}@Overridepublic void surfaceCreated(@NonNull SurfaceHolder holder) {}@Overridepublic void surfaceChanged(@NonNull SurfaceHolder holder, int format, int width, int height) {setSurfaceNative(holder.getSurface());}@Overridepublic void surfaceDestroyed(@NonNull SurfaceHolder holder) {}/*** native函数区域*/private native void setSurfaceNative(Surface surface);
}

3)关联Native层ANativeWindow

ANativeWindow *window = 0;
/*** 实例化播放window 关联 surfaceView*/
extern "C"
JNIEXPORT void JNICALL
Java_com_ndk_player_NdkPlayer_setSurfaceNative(JNIEnv *env, jobject thiz, jobject surface) {// 线程安全,锁住pthread_mutex_lock(&mutex);// 先释放之前的显示窗口if (window) {ANativeWindow_release(window);window = 0;}// 创建新的窗口用于视频显示window = ANativeWindow_fromSurface(env, surface);pthread_mutex_unlock(&mutex);
}

4)VideoChannel将解析完的RGBA数据(可播放数据)回调给 native-lib.cpp,进行渲染显示。

/*** 定义函数指针 实现渲染工作,this->renderCallback()回调到这里来*/
void renderCallback(uint8_t *dst_data, int width, int height, int dst_linesize) {LOGI("native-lib::renderCallback playing");pthread_mutex_lock(&mutex);// 播放窗口为空,释放锁,小概率出现if (!window) {pthread_mutex_unlock(&mutex);return;}// 设置窗口的大小,各个属性ANativeWindow_setBuffersGeometry(window, width, height, WINDOW_FORMAT_RGBA_8888);// 定义缓冲区 bufferANativeWindow_Buffer window_buffer;// 如果在渲染的时候,是被锁住的,那就无法渲染,需要释放,防止出现死锁if (ANativeWindow_lock(window, &window_buffer, 0)) {ANativeWindow_release(window);window = 0;pthread_mutex_unlock(&mutex); // 解锁,怕出现死锁return;}// 开始渲染,把rgba数据 ---> 字节对齐 渲染,填充window_buffer画面就出来了uint8_t *dst_data_ = static_cast<uint8_t *>(window_buffer.bits);// ANativeWindow_Buffer 64字节对齐的数据长度int dst_linesize_ = window_buffer.stride * 4;for (int i = 0; i < window_buffer.height; ++i) {/*** 参数1:接收播放数据容器* 参数2:RGBA播放数据* 参数3:64字节对齐的数据长度*/memcpy(dst_data_ + i * dst_linesize_, dst_data + i * dst_linesize, dst_linesize_);}// 解锁并且刷新 window_buffer的数据显示画面ANativeWindow_unlockAndPost(window);pthread_mutex_unlock(&mutex);
}

音视频--视频解码与播放渲染功能完成,接下来。。。

相关文章:

NDK FFmpeg音视频播放器二

NDK前期基础知识终于学完了&#xff0c;现在开始进入项目实战学习&#xff0c;通过FFmpeg实现一个简单的音视频播放器。本文主要内容如下&#xff1a;阻塞式队列SafeQueue。音视频BaseChannel基础通道。音视频压缩包加入队列。视频解码与播放。ANativeWindow渲染用到的ffmpeg、…...

Linux之进程信号

目录 一、生活中的信号 背景知识 生活中有没有信号的场景呢&#xff1f; 是不是只有这些场景真正的放在我面前的时候&#xff0c;我才知道怎么做呢&#xff1f; 进程在没有收到信号的时候&#xff0c;进程知道不知道应该如何识别哪一个是信号&#xff1f;以及如何处理它&a…...

AI绘画关键词网站推荐 :轻松获取百万个提示词!完全免费

一、lexica.art 该网站拥有数百万Stable Diffusion案例的文字描述和图片&#xff0c;可以为大家提供足够的创作灵感。 使用上也很简单&#xff0c;只要在搜索框输入简单的关键词或上传图片&#xff0c;就能为你提供大量风格不同的照片。点击照片就能看到完整的AI关键词&#…...

Java-Collections and Lambda

Java SE API know how 集合API 根据算法访选择合适集合 linkedlist不适合搜索 随机访问数据用hashmap 数据保持有序使用treemap 通过索引访问使用数组集合 同步和非同步 访问性能统计 与简单的非同步访问相比&#xff0c;使用任何数据保护技术都会有较小的损失 设置集合…...

KDGX-A光缆故障断点检测仪

一、产品概述 KDGX-A光纤寻障仪是武汉凯迪正大为光纤网络领域施工、测试、维护所设计的一款测试仪表。可实现对光纤链路状态和故障的快速分析&#xff0c;适用于室外维护作业&#xff0c;是现场光纤网络测试与维护中替代OTDR的经济型解决方案。 二、主要特点 1)一键式光纤链路…...

【刷题之路Ⅱ】牛客 NC107 寻找峰值

【刷题之路Ⅱ】牛客 NC107 寻找峰值一、题目描述二、解题1、方法1——直接遍历1.1、思路分析1.2、代码实现2、方法2——投机取巧的求最大值2.1、思路分析2.2、代码实现3、方法3——二分法3.1、思路分析3.2、代码实现一、题目描述 原题连接&#xff1a; NC107 寻找峰值 题目描…...

智能灯泡一Homekit智能家居系列

传统的灯泡是通过手动打开和关闭开关来工作。有时&#xff0c;它们可以通过声控、触控、红外等方式进行控制&#xff0c;或者带有调光开关&#xff0c;让用户调暗或调亮灯光。 智能灯泡内置有芯片和通信模块&#xff0c;可与手机、家庭智能助手、或其他智能硬件进行通信&#…...

外包离职,历时学习416天,成功上岸百度,分享成长过程~

前言&#xff1a; 没有绝对的天才&#xff0c;只有持续不断的付出。对于我们每一个平凡人来说&#xff0c;改变命运只能依靠努力幸运&#xff0c;但如果你不够幸运&#xff0c;那就只能拉高努力的占比。 2020年7月&#xff0c;我有幸成为了百度的一名Java后端开发&#xff0c…...

利用客户支持建立忠诚度和竞争优势

客户支持可以极大地改变您的业务;最细微、最微妙的差异都会使拥有一次性客户和拥有终身客户之间产生差异。在这篇博文中&#xff0c;我们将揭示客户对企业的忠诚度的三种核心类型&#xff0c;以及如何利用强大的客户支持工具和原则来提高理想的忠诚度并获得决定性的竞争优势。一…...

看他人代码小总结

针对几个功能类似的函数&#xff1a; 1.需要经常调试则定义一个参数比如is_debug来选择是否在调试&#xff0c;定义一些参数专门用于调试用&#xff0c;不用每次都修改这些参数&#xff0c;只需要修改is_debug这个参数&#xff1b; 2.把其中的变量(常量)单独拎出来放到一个文件…...

cudaMemGetInfo()函数cudaDeviceGetAttribute()函数来检查设备上的可用内存

使用CUDA Runtime API中的cudaMemGetInfo()函数来检查设备上的可用内存。该函数将返回当前可用于分配的总设备内存大小和当前可用于分配的最大单个内存块大小。 示例代码&#xff0c;演示了如何在分配内存之前和之后调用cudaMemGetInfo()函数来检查可用内存 size_t free_byte…...

【基础阶段】01中华人民共和国网络安全法

文章目录1 网络安全行业介绍2 什么是黑客和白帽子3 网络安全课程整体介绍4 网络安全的分类5 常见的网站攻击方式6 安全常见术语介绍7 《网络安全法》制定背景和核心内容8 《全国人大常委会关于维护互联网安全的决定》9《中华人民共和国计算机信息系统安全保护条例》10 《中华人…...

隐私计算领域大咖推荐,这些国内外导师值得关注

开放隐私计算 经过近一个月的信息收集&#xff0c;研习社已经整理了多位国内外研究隐私计算的导师资料。邻近考研复试&#xff0c;研习社希望小伙伴们能够通过本文整理的信息&#xff0c;选择自己心仪的老师&#xff0c;在研究生的路途上一帆风顺&#xff01;1. 国内隐私计算导…...

009 uni-app之vue、vuex

vue.js 视频教程 vue3.js 中文官网 vue.js 视频教程 vue语法&#xff1a;https://uniapp.dcloud.net.cn/tutorial/vue-vuex.html vue2迁移到 vue3&#xff1a;https://uniapp.dcloud.net.cn/tutorial/migration-to-vue3.html Vuex Vuex 是一个专为 Vue.js 应用程序开发的…...

Linux防火墙——SNAT、DNAT

目录 NAT 一、SNAT策略及作用 1、概述 SNAT应用环境 SNAT原理 SNAT转换前提条件 1、临时打开 2、永久打开 3、SNAT转换1&#xff1a;固定的公网IP地址 4、SNAT转换2&#xff1a;非固定的公网IP地址&#xff08;共享动态IP地址&#xff09; 二、SNAT实验 配置web服务…...

递归理解三:深度、广度优先搜索,n叉树遍历,n并列递归理解与转非递归

参考资料&#xff1a; DFS 参考文章BFS 参考文章DFS 参考视频二叉树遍历规律递归原理源码N叉树规律总结&#xff1a; 由前面二叉树的遍历规律和递归的基本原理&#xff0c;我们可以看到&#xff0c;二叉树遍历口诀和二叉树递推公式有着紧密的联系 前序遍历&#xff1a;F(x…...

MATLAB 2023a安装包下载及安装教程

[软件名称]:MATLAB 2023a [软件大小]: 12.2 GB [安装环境]: Win11/Win 10/Win 7 [软件安装包下载]:https://pan.quark.cn/s/8e24d77ab005 MATLAB和Mathematica、Maple并称为三大数学软件。它在数学类科技应用软件中在数值计算方面首屈一指。行矩阵运算、绘制函数和数据、实现算…...

QT学习开发笔记(数据库之实用时钟)

数据库 数据库是什么&#xff1f;简易言之&#xff0c;就是保存数据的文件。可以存储大量数据&#xff0c;包括插入数据、更 新数据、截取数据等。用专业术语来说&#xff0c;数据库是“按照数据结构来组织、存储和管理数据的 仓库”。是一个长期存储在计算机内的、有组织的、…...

Docker常规安装简介

总体步骤 搜索镜像拉取镜像查看镜像启动镜像,服务端口映射停止容器移除容器 案例 安装tomcat docker hub上面查找tomcat镜像&#xff0c;docker search tomcat从docker hub上拉取tomcat镜像到本地 docker pull tomcatdocker images查看是否有拉取到的tomcat 使用tomcat镜像创…...

Python - PyQT5 - ui文件转为py文件

在QTdesigner图形化编辑工具中&#xff0c;有些控件我们是可以直接在编辑界面进行编辑的&#xff0c;有些是不可以编辑的&#xff0c;只能通过Python代码进行编辑&#xff0c;不过总体来说&#xff0c;所有能够通过图形化编辑界面可以编辑的&#xff0c;都可以通过Python语言实…...

Python爬虫实战:研究MechanicalSoup库相关技术

一、MechanicalSoup 库概述 1.1 库简介 MechanicalSoup 是一个 Python 库,专为自动化交互网站而设计。它结合了 requests 的 HTTP 请求能力和 BeautifulSoup 的 HTML 解析能力,提供了直观的 API,让我们可以像人类用户一样浏览网页、填写表单和提交请求。 1.2 主要功能特点…...

【Axure高保真原型】引导弹窗

今天和大家中分享引导弹窗的原型模板&#xff0c;载入页面后&#xff0c;会显示引导弹窗&#xff0c;适用于引导用户使用页面&#xff0c;点击完成后&#xff0c;会显示下一个引导弹窗&#xff0c;直至最后一个引导弹窗完成后进入首页。具体效果可以点击下方视频观看或打开下方…...

Prompt Tuning、P-Tuning、Prefix Tuning的区别

一、Prompt Tuning、P-Tuning、Prefix Tuning的区别 1. Prompt Tuning(提示调优) 核心思想:固定预训练模型参数,仅学习额外的连续提示向量(通常是嵌入层的一部分)。实现方式:在输入文本前添加可训练的连续向量(软提示),模型只更新这些提示参数。优势:参数量少(仅提…...

.Net框架,除了EF还有很多很多......

文章目录 1. 引言2. Dapper2.1 概述与设计原理2.2 核心功能与代码示例基本查询多映射查询存储过程调用 2.3 性能优化原理2.4 适用场景 3. NHibernate3.1 概述与架构设计3.2 映射配置示例Fluent映射XML映射 3.3 查询示例HQL查询Criteria APILINQ提供程序 3.4 高级特性3.5 适用场…...

《从零掌握MIPI CSI-2: 协议精解与FPGA摄像头开发实战》-- CSI-2 协议详细解析 (一)

CSI-2 协议详细解析 (一&#xff09; 1. CSI-2层定义&#xff08;CSI-2 Layer Definitions&#xff09; 分层结构 &#xff1a;CSI-2协议分为6层&#xff1a; 物理层&#xff08;PHY Layer&#xff09; &#xff1a; 定义电气特性、时钟机制和传输介质&#xff08;导线&#…...

土地利用/土地覆盖遥感解译与基于CLUE模型未来变化情景预测;从基础到高级,涵盖ArcGIS数据处理、ENVI遥感解译与CLUE模型情景模拟等

&#x1f50d; 土地利用/土地覆盖数据是生态、环境和气象等诸多领域模型的关键输入参数。通过遥感影像解译技术&#xff0c;可以精准获取历史或当前任何一个区域的土地利用/土地覆盖情况。这些数据不仅能够用于评估区域生态环境的变化趋势&#xff0c;还能有效评价重大生态工程…...

[Java恶补day16] 238.除自身以外数组的乘积

给你一个整数数组 nums&#xff0c;返回 数组 answer &#xff0c;其中 answer[i] 等于 nums 中除 nums[i] 之外其余各元素的乘积 。 题目数据 保证 数组 nums之中任意元素的全部前缀元素和后缀的乘积都在 32 位 整数范围内。 请 不要使用除法&#xff0c;且在 O(n) 时间复杂度…...

优选算法第十二讲:队列 + 宽搜 优先级队列

优选算法第十二讲&#xff1a;队列 宽搜 && 优先级队列 1.N叉树的层序遍历2.二叉树的锯齿型层序遍历3.二叉树最大宽度4.在每个树行中找最大值5.优先级队列 -- 最后一块石头的重量6.数据流中的第K大元素7.前K个高频单词8.数据流的中位数 1.N叉树的层序遍历 2.二叉树的锯…...

AI书签管理工具开发全记录(十九):嵌入资源处理

1.前言 &#x1f4dd; 在上一篇文章中&#xff0c;我们完成了书签的导入导出功能。本篇文章我们研究如何处理嵌入资源&#xff0c;方便后续将资源打包到一个可执行文件中。 2.embed介绍 &#x1f3af; Go 1.16 引入了革命性的 embed 包&#xff0c;彻底改变了静态资源管理的…...

2025年渗透测试面试题总结-腾讯[实习]科恩实验室-安全工程师(题目+回答)

安全领域各种资源&#xff0c;学习文档&#xff0c;以及工具分享、前沿信息分享、POC、EXP分享。不定期分享各种好玩的项目及好用的工具&#xff0c;欢迎关注。 目录 腾讯[实习]科恩实验室-安全工程师 一、网络与协议 1. TCP三次握手 2. SYN扫描原理 3. HTTPS证书机制 二…...