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

本文主要内容如下:
阻塞式队列SafeQueue。
音视频BaseChannel基础通道。
音视频压缩包加入队列。
视频解码与播放。
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前期基础知识终于学完了,现在开始进入项目实战学习,通过FFmpeg实现一个简单的音视频播放器。本文主要内容如下:阻塞式队列SafeQueue。音视频BaseChannel基础通道。音视频压缩包加入队列。视频解码与播放。ANativeWindow渲染用到的ffmpeg、…...
Linux之进程信号
目录 一、生活中的信号 背景知识 生活中有没有信号的场景呢? 是不是只有这些场景真正的放在我面前的时候,我才知道怎么做呢? 进程在没有收到信号的时候,进程知道不知道应该如何识别哪一个是信号?以及如何处理它&a…...
AI绘画关键词网站推荐 :轻松获取百万个提示词!完全免费
一、lexica.art 该网站拥有数百万Stable Diffusion案例的文字描述和图片,可以为大家提供足够的创作灵感。 使用上也很简单,只要在搜索框输入简单的关键词或上传图片,就能为你提供大量风格不同的照片。点击照片就能看到完整的AI关键词&#…...
Java-Collections and Lambda
Java SE API know how 集合API 根据算法访选择合适集合 linkedlist不适合搜索 随机访问数据用hashmap 数据保持有序使用treemap 通过索引访问使用数组集合 同步和非同步 访问性能统计 与简单的非同步访问相比,使用任何数据保护技术都会有较小的损失 设置集合…...
KDGX-A光缆故障断点检测仪
一、产品概述 KDGX-A光纤寻障仪是武汉凯迪正大为光纤网络领域施工、测试、维护所设计的一款测试仪表。可实现对光纤链路状态和故障的快速分析,适用于室外维护作业,是现场光纤网络测试与维护中替代OTDR的经济型解决方案。 二、主要特点 1)一键式光纤链路…...
【刷题之路Ⅱ】牛客 NC107 寻找峰值
【刷题之路Ⅱ】牛客 NC107 寻找峰值一、题目描述二、解题1、方法1——直接遍历1.1、思路分析1.2、代码实现2、方法2——投机取巧的求最大值2.1、思路分析2.2、代码实现3、方法3——二分法3.1、思路分析3.2、代码实现一、题目描述 原题连接: NC107 寻找峰值 题目描…...
智能灯泡一Homekit智能家居系列
传统的灯泡是通过手动打开和关闭开关来工作。有时,它们可以通过声控、触控、红外等方式进行控制,或者带有调光开关,让用户调暗或调亮灯光。 智能灯泡内置有芯片和通信模块,可与手机、家庭智能助手、或其他智能硬件进行通信&#…...
外包离职,历时学习416天,成功上岸百度,分享成长过程~
前言: 没有绝对的天才,只有持续不断的付出。对于我们每一个平凡人来说,改变命运只能依靠努力幸运,但如果你不够幸运,那就只能拉高努力的占比。 2020年7月,我有幸成为了百度的一名Java后端开发,…...
利用客户支持建立忠诚度和竞争优势
客户支持可以极大地改变您的业务;最细微、最微妙的差异都会使拥有一次性客户和拥有终身客户之间产生差异。在这篇博文中,我们将揭示客户对企业的忠诚度的三种核心类型,以及如何利用强大的客户支持工具和原则来提高理想的忠诚度并获得决定性的竞争优势。一…...
看他人代码小总结
针对几个功能类似的函数: 1.需要经常调试则定义一个参数比如is_debug来选择是否在调试,定义一些参数专门用于调试用,不用每次都修改这些参数,只需要修改is_debug这个参数; 2.把其中的变量(常量)单独拎出来放到一个文件…...
cudaMemGetInfo()函数cudaDeviceGetAttribute()函数来检查设备上的可用内存
使用CUDA Runtime API中的cudaMemGetInfo()函数来检查设备上的可用内存。该函数将返回当前可用于分配的总设备内存大小和当前可用于分配的最大单个内存块大小。 示例代码,演示了如何在分配内存之前和之后调用cudaMemGetInfo()函数来检查可用内存 size_t free_byte…...
【基础阶段】01中华人民共和国网络安全法
文章目录1 网络安全行业介绍2 什么是黑客和白帽子3 网络安全课程整体介绍4 网络安全的分类5 常见的网站攻击方式6 安全常见术语介绍7 《网络安全法》制定背景和核心内容8 《全国人大常委会关于维护互联网安全的决定》9《中华人民共和国计算机信息系统安全保护条例》10 《中华人…...
隐私计算领域大咖推荐,这些国内外导师值得关注
开放隐私计算 经过近一个月的信息收集,研习社已经整理了多位国内外研究隐私计算的导师资料。邻近考研复试,研习社希望小伙伴们能够通过本文整理的信息,选择自己心仪的老师,在研究生的路途上一帆风顺!1. 国内隐私计算导…...
009 uni-app之vue、vuex
vue.js 视频教程 vue3.js 中文官网 vue.js 视频教程 vue语法:https://uniapp.dcloud.net.cn/tutorial/vue-vuex.html vue2迁移到 vue3: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:固定的公网IP地址 4、SNAT转换2:非固定的公网IP地址(共享动态IP地址) 二、SNAT实验 配置web服务…...
递归理解三:深度、广度优先搜索,n叉树遍历,n并列递归理解与转非递归
参考资料: DFS 参考文章BFS 参考文章DFS 参考视频二叉树遍历规律递归原理源码N叉树规律总结: 由前面二叉树的遍历规律和递归的基本原理,我们可以看到,二叉树遍历口诀和二叉树递推公式有着紧密的联系 前序遍历:F(x…...
MATLAB 2023a安装包下载及安装教程
[软件名称]:MATLAB 2023a [软件大小]: 12.2 GB [安装环境]: Win11/Win 10/Win 7 [软件安装包下载]:https://pan.quark.cn/s/8e24d77ab005 MATLAB和Mathematica、Maple并称为三大数学软件。它在数学类科技应用软件中在数值计算方面首屈一指。行矩阵运算、绘制函数和数据、实现算…...
QT学习开发笔记(数据库之实用时钟)
数据库 数据库是什么?简易言之,就是保存数据的文件。可以存储大量数据,包括插入数据、更 新数据、截取数据等。用专业术语来说,数据库是“按照数据结构来组织、存储和管理数据的 仓库”。是一个长期存储在计算机内的、有组织的、…...
Docker常规安装简介
总体步骤 搜索镜像拉取镜像查看镜像启动镜像,服务端口映射停止容器移除容器 案例 安装tomcat docker hub上面查找tomcat镜像,docker search tomcat从docker hub上拉取tomcat镜像到本地 docker pull tomcatdocker images查看是否有拉取到的tomcat 使用tomcat镜像创…...
Python - PyQT5 - ui文件转为py文件
在QTdesigner图形化编辑工具中,有些控件我们是可以直接在编辑界面进行编辑的,有些是不可以编辑的,只能通过Python代码进行编辑,不过总体来说,所有能够通过图形化编辑界面可以编辑的,都可以通过Python语言实…...
Java 语言特性(面试系列2)
一、SQL 基础 1. 复杂查询 (1)连接查询(JOIN) 内连接(INNER JOIN):返回两表匹配的记录。 SELECT e.name, d.dept_name FROM employees e INNER JOIN departments d ON e.dept_id d.dept_id; 左…...
Robots.txt 文件
什么是robots.txt? robots.txt 是一个位于网站根目录下的文本文件(如:https://example.com/robots.txt),它用于指导网络爬虫(如搜索引擎的蜘蛛程序)如何抓取该网站的内容。这个文件遵循 Robots…...
Selenium常用函数介绍
目录 一,元素定位 1.1 cssSeector 1.2 xpath 二,操作测试对象 三,窗口 3.1 案例 3.2 窗口切换 3.3 窗口大小 3.4 屏幕截图 3.5 关闭窗口 四,弹窗 五,等待 六,导航 七,文件上传 …...
python爬虫——气象数据爬取
一、导入库与全局配置 python 运行 import json import datetime import time import requests from sqlalchemy import create_engine import csv import pandas as pd作用: 引入数据解析、网络请求、时间处理、数据库操作等所需库。requests:发送 …...
协议转换利器,profinet转ethercat网关的两大派系,各有千秋
随着工业以太网的发展,其高效、便捷、协议开放、易于冗余等诸多优点,被越来越多的工业现场所采用。西门子SIMATIC S7-1200/1500系列PLC集成有Profinet接口,具有实时性、开放性,使用TCP/IP和IT标准,符合基于工业以太网的…...
保姆级【快数学会Android端“动画“】+ 实现补间动画和逐帧动画!!!
目录 补间动画 1.创建资源文件夹 2.设置文件夹类型 3.创建.xml文件 4.样式设计 5.动画设置 6.动画的实现 内容拓展 7.在原基础上继续添加.xml文件 8.xml代码编写 (1)rotate_anim (2)scale_anim (3)translate_anim 9.MainActivity.java代码汇总 10.效果展示 逐帧…...
【Post-process】【VBA】ETABS VBA FrameObj.GetNameList and write to EXCEL
ETABS API实战:导出框架元素数据到Excel 在结构工程师的日常工作中,经常需要从ETABS模型中提取框架元素信息进行后续分析。手动复制粘贴不仅耗时,还容易出错。今天我们来用简单的VBA代码实现自动化导出。 🎯 我们要实现什么? 一键点击,就能将ETABS中所有框架元素的基…...
云安全与网络安全:核心区别与协同作用解析
在数字化转型的浪潮中,云安全与网络安全作为信息安全的两大支柱,常被混淆但本质不同。本文将从概念、责任分工、技术手段、威胁类型等维度深入解析两者的差异,并探讨它们的协同作用。 一、核心区别 定义与范围 网络安全:聚焦于保…...
从零手写Java版本的LSM Tree (一):LSM Tree 概述
🔥 推荐一个高质量的Java LSM Tree开源项目! https://github.com/brianxiadong/java-lsm-tree java-lsm-tree 是一个从零实现的Log-Structured Merge Tree,专为高并发写入场景设计。 核心亮点: ⚡ 极致性能:写入速度超…...
EC2安装WebRTC sdk-c环境、构建、编译
1、登录新的ec2实例,证书可以跟之前的实例用一个: ssh -v -i ~/Documents/cert/qa.pem ec2-user70.xxx.165.xxx 2、按照sdk-c demo中readme的描述开始安装环境: https://github.com/awslabs/amazon-kinesis-video-streams-webrtc-sdk-c 2…...
