音视频开发—FFMpeg编码解码

news2024/11/27 0:42:38

FFMpeg 作为音视频领域的开源工具,它几乎可以实现所有针对音视频的处理,本文主要利用 FFMpeg 官方提供的 SDK 实现音视频最简单的几个实例:编码、解码、封装、解封装、转码、缩放以及添加水印。

接下来会由发现问题->分析问题->解决问题->实现方案,循序渐进的完成。

FFMpeg 编码实现

本例子实现的是将视频域 YUV 数据编码为压缩域的帧数据,编码格式包含了 H.264/H.265/MPEG1/MPEG2 四种 CODEC 类型。

实现的过程,可以大致用如下图表示:

从图中可以大致看出视频编码的流程:

  • 首先要有未压缩的 YUV 原始数据。
  • 其次要根据想要编码的格式选择特定的编码器。
  • 最后编码器的输出即为编码后的视频帧。

根据流程可以推倒出大致的代码实现:

  • 存放待压缩的 YUV 原始数据。此时可以利用 FFMpeg 提供的 AVFrame 结构体,并根据 YUV 数据来填充 AVFrame 结构的视频宽高、像素格式;根据视频宽高、像素格式可以分配存放数据的内存大小,以及字节对齐情况。
  • 获取编码器。利用想要压缩的格式,比如 H.264/H.265/MPEG1/MPEG2 等,来获取注册的编解码器,编解码器在 FFMpeg 中用 AVCodec 结构体表示,对于编解码器,肯定要对其进行配置,包括待压缩视频的宽高、像素格式、比特率等等信息,这些信息,FFMpeg 提供了一个专门的结构体 AVCodecContext 结构体。
  • 存放编码后压缩域的视频帧。FFMpeg 中用来存放压缩编码数据相关信息的结构体为 AVPacket。最后将 AVPacket 存储的压缩数据写入文件即可。

AVFrame 结构体的分配使用av_frame_alloc()函数,该函数会对 AVFrame 结构体的某些字段设置默认值,它会返回一个指向 AVFrame 的指针或 NULL指针(失败)。

AVFrame 结构体的释放只能通过av_frame_free()来完成。

注意,该函数只能分配 AVFrame 结构体本身,不能分配它的 data buffers 字段指向的内容,该字段的指向要根据视频的宽高、像素格式信息手动分配,本例使用的是av_image_alloc()函数。

代码实现大致如下:

​
//allocate AVFrame struct
AVFrame *frame = NULL;
frame = av_frame_alloc();
if(!frame){
 printf("Alloc Frame Fail\n");
 return -1;
}
​
//fill AVFrame struct fields
frame->width = width;
frame->height = height;
frame->pix_fmt = AV_PIX_FMT_YUV420P;
​
//allocate AVFrame data buffers field point
ret = av_image_alloc(frame->data, frame->linesize, frame->width, frame->height, frame->pix_fmt, 32);
if(ret < 0){
 printf("Alloc Fail\n");
 return -1;
}
​
//write input file data to frame->data buffer
fread(frame->data[0], 1, frame->width*frame->height, pInput_File);
...
av_frame_free(frame);

编解码器相关的 AVCodec 结构体的分配使用avcodec_find_encoder(enum AVCodecID id)完成,该函数的作用是找到一个与 AVCodecID 匹配的已注册过得编码器;成功则返回一个指向 AVCodec ID 的指针,失败返回 NULL 指针。

该函数的作用是确定系统中是否有该编码器,只是能够使用编码器进行特定格式编码的最基本的条件,要想使用它,至少要完成两个步骤:

  • 根据特定的视频数据,对该编码器进行特定的配置;
  • 打开该编码器。

针对第一步中关于编解码器的特定参数,FFMpeg 提供了一个专门用来存放 AVCodec 所需要的配置参数的结构体 AVCodecContext 结构。

它的分配使用avcodec_alloc_context3(const AVCodec *codec)完成,该函数根据特定的 CODEC 分配一个 AVCodecContext 结构体,并设置一些字段为默认参数,成功则返回指向 AVCodecContext 结构体的指针,失败则返回 NULL 指针。

分配完成后,根据视频特性,手动指定与编码器相关的一些参数,比如视频宽高、像素格式、比特率、GOP 大小等。最后根据参数信息,打开找到的编码器,此处使用avcodec_open2()函数完成。

代码实现大致如下:

AVCodec *codec = NULL;
AVCodecContext *codecCtx = NULL;
​
//register all encoder and decoder
avcodec_register_all();
​
//find the encoder
codec = avcodec_find_encoder(codec_id);
if(!codec){
 printf("Could Not Find the Encoder\n");
 return -1;
}
​
//allocate the AVCodecContext and fill it's fields
codecCtx = avcodec_alloc_context3(codec);
if(!codecCtx){
 printf("Alloc AVCodecCtx Fail\n");
 return -1;
}
codecCtx->bit_rate = 4000000;
codecCtx->width    = frameWidth;
codecCtx->height   = frameHeight;
codecCtx->time_base= (AVRational){1, 25};
//open the encoder
if(avcodec_open2(codecCtx, codec, NULL) < 0){
 printf("Open Encoder Fail\n");
}

存放编码数据的结构体为 AVPacket,使用之前要对该结构体进行初始化,初始化函数为av_init_packet(AVPacket *pkt),该函数会初始化 AVPacket 结构体中一些字段为默认值,但它不会设置其中的 data 和 size 字段,需要单独初始化,如果此处将 data 设为 NULL、size 设为 0,编码器会自动填充这两个字段。

有了存放编码数据的结构体后,我们就可以利用编码器进行编码了。

FFMpeg 提供的用于视频编码的函数为avcodec_encode_video2,它作用是编码一帧视频数据,该函数比较复杂,单独列出如下:

int avcodec_encode_video2(AVCodecContext *avctx, AVPacket *avpkt,
const AVFrame *frame, int *got_packet_ptr);

它会接收来自 AVFrame->data 的视频数据,并将编码数据放到 AVPacket->data 指向的位置,编码数据大小为 AVPacket->size。

其参数和返回值的意义:

  • avctx: AVCodecContext 结构,指定了编码的一些参数;
  • avPkt: AVPacket对象的指针,用于保存输出的码流;
  • frame:AVFrame结构,用于传入原始的像素数据;
  • got_packet_ptr:输出参数,用于标识是否已经有了完整的一帧;
  • 返回值:编码成功返回 0, 失败返回负的错误码;

编码完成后就可将AVPacket->data内的编码数据写到输出文件中;代码实现大致如下:

AVPacket pkt;
​
//init AVPacket
av_init_packet(&pkt);
pkt.data = NULL;
pkt.size = 0;
​
//encode the image
ret = avcodec_encode_video2(codecCtx, &pkt, frame, &got_output);
if(ret < 0){
 printf("Encode Fail\n");
 return -1;
}
​
if(got_output){
 fwrite(pkt.data, 1, pkt.size, pOutput_File);
}

编码的大致流程已经完成了,剩余的是一些收尾工作,比如释放分配的内存、结构体等等。

FFMpeg 解码实现

解码实现的是将压缩域的视频数据解码为像素域的 YUV 数据。实现的过程,可以大致用如下图所示。

从图中可以看出,大致可以分为下面三个步骤:

  • 首先要有待解码的压缩域的视频。
  • 其次根据压缩域的压缩格式获得解码器。
  • 最后解码器的输出即为像素域的 YUV 数据。

根据流程可以推倒出大致的代码实现:

  • 关于输入数据。首先,要分配一块内存,用于存放压缩域的视频数据;之后,对内存中的数据进行预处理,使其分为一个一个的 AVPacket 结构(AVPacket 结构的简单介绍如上面的编码实现)。最后,将 AVPacket 结构中的 data 数据给到解码器。
  • 关于解码器。首先,利用 CODEC_ID 来获取注册的解码器;之后,将预处理过得视频数据给到解码器进行解码。
  • 关于输出。FFMpeg 中,解码后的数据存放在 AVFrame 中;之后就将 AVFrame 中的 data 字段的数据存放到输出文件中。

对于输入数据,首先,通过 fread 函数实现将固定长度的输入文件的数据存放到一块 buffer 内。

H.264中一个包的长度是不定的,读取固定长度的码流通常不可能刚好读出一个包的长度;

对此,FFMpeg 提供了一个 AVCoderParserContext 结构用于解析读到 buffer 内的码流信息,直到能够取出一个完整的 H.264 包。

为此,FFMpeg 提供的函数为av_parser_parse2,该函数比较复杂,定义如下:

​
int av_parser_parse2(AVCodecParserContext *s,
                     AVCodecContext *avctx,
                     uint8_t **poutbuf, int *poutbuf_size,
                     const uint8_t *buf, int buf_size,
                     int64_t pts, int64_t dts,
                     int64_t pos);

函数的参数和返回值含义如下:

  • AVCodecParserContext *s:初始化过的 AVCodecParserContext 对象,决定了码流该以怎样的标准进行解析;
  • AVCodecContext *avctx:预先定义好的 AVCodecContext 对象;
  • uint8_t **poutbuf:AVPacket::data 的地址,保存解析完成的包数据。
  • int *poutbuf_size:AVPacket 的实际数据长度,如果没有解析出完整的一个包,该值为 0;
  • const uint8_t *but:待解码的码流的地址;
  • int buf_size:待解码的码流的长度;
  • int64_t pts, int64_t dts:显示和解码的时间戳;
  • int64_t pos:码流中的位置;
  • 返回值为解析所使用的比特位的长度;

FFMpeg 中为我们提供的该函数常用的使用方式为:

while(in_len){
 len = av_parser_parse2(myparser. AVCodecContext, &data, &size, in_data, in len, pts, dts, pos);
​
 in_data += len;
 in_len  -= len;
​
 if(size)
  decode_frame(data, size);
}

如果参数poutbuf_size的值为0,那么应继续解析缓存中剩余的码流;如果缓存中的数据全部解析后依然未能找到一个完整的包,那么继续从输入文件中读取数据到缓存,继续解析操作,直到pkt.size不为0为止。

因此,关于输入数据的处理,代码大致如下:

//open input file
FILE *pInput_File = fopen(Input_FileName, "rb+");
if(!pInput_File){
 printf("Open Input File Fail\n");
 return -1;
}
​
//read compressed bitstream form file to buffer
uDataSize = fread(inbuf, 1, INBUF_SIZE, pInput_File);
if(uDataSize == 0){ //decode finish
 return -1;
}
​
//decode the data in the buffer to AVPacket.data
while(uDataSize > 0){
 len = av_parser_parse2(pCodecParserCtx, codecCtx,
       &(pkt.data), &(pkt.size),
       pDataPtr, uDataSize,
       AV_NOPTS_VALUE, AV_NOPTS_VALUE,
       AV_NOPTS_VALUE);
 uDataSize -= len;
 uDataPtr  += len;
​
 if(pkt.size == 0) continue;
 decode_frame(pkt.data, pkt.size);
}

注意,上面提到的av_parser_parse2函数用的几个参数,其实是与具体的编码格式有关的,它们应该在之前已经分配好了,我们只是放到后面来讲一下,因为它们是与具体的解码器强相关的。

对于解码器。

与上面提到的编码实现类似,首先,根据 CODEC_ID 找到注册的解码器 AVCodec,FFMpeg 为此提供的函数为avcodec_find_decoder();

其次,根据找到的解码器获取与之相关的解码器上下文结构体 AVCodecC,使用的函数为编码中提到的avcodec_alloc_context3;

再者,如上面提到的要获取完整的一个 NALU,解码器需要分配一个 AVCodecParserContext 结构,使用函数av_parser_init;

最后,前面的准备工作完成后,打开解码器,即可调用 FFMpeg 提供的解码函数avcodec_decode_video2对输入的压缩域的码流进行解码,并将解码数据存放到 AVFrame->data 中。

代码实现大致如下:

AVFrame *frame = NULL;
AVCodec *codec = NULL;
AVCodecContext *codecCtx = NULL;
AVCodecParserContext *pCodecParserCtx = NULL;
​
//register all encoder and decoder
avcodec_register_all();
​
//Allocate AVFrame to Store the Decode Data
frame = av_frame_alloc();
if(!frame){
 printf("Alloc Frame Fail\n");
 return -1;
}
​
//Find the  AVCodec Depending on the CODEC_ID
codec = avcodec_find_decoder(AV_CODEC_ID_H264);
if(!codec){
 printf("Find the Decoder Fail\n");
 return -1;
}
​
//Allocate the AVCodecContext 
codecCtx = avcodec_alloc_context3(codec);
if(!codecCtx){
 printf("Alloc AVCodecCtx Fail\n");
 return -1;
}
​
//Allocate the AVCodecParserContext 
pCodecParserCtx = av_parser_init(AV_CODEC_ID_H264);
if(!pCodecParserCtx){
 printf("Alloc AVCodecParserContext Fail\n");
 return -1;
}
​
//Open the Decoder
if(avcodec_open2(codecCtx, codec, NULL) < 0){
 printf("Could not Open the Decoder\n");
 return -1;
}
​
//read compressed bitstream form file to buffer
uDataSize = fread(inbuf, 1, INBUF_SIZE, pInput_File);
if(uDataSize == 0){ //decode finish
 return -1;
}
​
//decode the data in the buffer to AVPacket.data
while(uDataSize > 0){
 len = av_parser_parse2(pCodecParserCtx, codecCtx,
       &(pkt.data), &(pkt.size),
       pDataPtr, uDataSize,
       AV_NOPTS_VALUE, AV_NOPTS_VALUE,
       AV_NOPTS_VALUE);
 uDataSize -= len;
 uDataPtr  += len;
​
 if(pkt.size == 0) continue;
 //decode start
 avcodec_decode_video2(codecCtx, frame, &got_frame, pkt);
}

注意,上面解码的过程中,针对具体的实现,可能要做一些具体参数上的调整,此处只是理清解码的流程。

对于输出数据。

解码完成后,解码出来的像素域的数据存放在 AVFrame 的 data 字段内,只需要将该字段内存放的数据之间写文件到输出文件即可。

解码函数avcodec_decode_video2函数完成整个解码过程,对于它简单介绍如下:

​
pOutput_File = fopen(Output_FileName, "wb");
if(!pOutput_File){
 printf("Open Output File Fail\n");
 return -1;
}
 
if(*got_picture_ptr){
 fwrite(frame->data[0],1, Len, pOutput_File)
}

该函数各个参数的意义:

  • AVCodecContext *avctx:编解码器上下文对象,在打开编解码器时生成;
  • AVFrame *picture: 保存解码完成后的像素数据;我们只需要分配对象的空间,像素的空间codec会为我们分配好;
  • int *got_picture_ptr: 标识位,如果为1,那么说明已经有一帧完整的像素帧可以输出了;
  • const AVPacket *avpkt: 前面解析好的码流包;

由此可见,当标识位为1时,代表解码一帧结束,可以写数据到文件中。代码如下:

pOutput_File = fopen(Output_FileName, "wb");
if(!pOutput_File){
 printf("Open Output File Fail\n");
 return -1;
}
​
if(*got_picture_ptr){
 fwrite(frame->data[0],1, Len, pOutput_File)
}

解码的大致流程已经完成了,剩余的是一些收尾工作,比如释放分配的内存、结构体等等

FFmpeg解码相关变量

1、AVFormatContext

AVFormatContext描述了一个媒体文件或媒体流的构成和基本信息,位于avformat.h文件中;

2、AVInputFormat

AVInputFormat是类似COM接口的数据结构,表示输入文件容器格式,着重于功能函数,一种文件容器格式对应一个AVInputFormat结构,在程序运行时有多个实例,位于avoformat.h文件中;

3、AVDictionary

AVDictionary是一个字典集合,键值对,用于配置相关信息;

4、AVCodecContext

AVCodecContext是一个描述编码器上下文的数据结构,包含了众多编码器需要的参数信息,位于avcodec.h文件中;

5、AVPacket

AVPacket是FFmpeg中很重要的一个数据结构,它保存了解复用(demuxer)之后,解码(decode)之前的数据(仍然是压缩后的数据)和关于这些数据的一些附加的信息,如显示时间戳(pts),解码时间戳(dts),数据时长等; 使用前,使用av_packet_alloc()分配;

6、AVCodec

AVCodec是存储编码器信息的结构体,位于avcodec.h

7、AVFrame

AVFrame中存储的是经过解码后的原始数据。在解码中,AVFrame是解码器的输出;在编码中,AVFrame是编码器的输入; 使用前,使用av_frame_alloc()进行分配;

8、struct SwsContext

使用前,使用sws_getContext()进行获取,主要用于视频图像的转换;

FFmpeg解码流程相关函数原型

1、av_register_all

初始化libavformat并注册所有muxer、demuxer和协议;如果不调用此函数,则可以选择想要指定注册支持的哪种格式,通过av_register_input_format()、av_register_output_format();

void av_register_all(void)

2、avformat_open_input

打开输入流并读取标头;此时编解码器还未打开;流必须使用avformat_close_input()关闭,返回0成功,小于0失败错误码;

int avformat_open_input(AVFormatContext **ps,
                        const char *url,
                        AVInputFormat *fmt,
                        AVDictionary **options);
  • ps:指向用户提供的AVFormatContext(由avformat_alloc_context分配)的指针;
  • url:要打开的流的url;
  • fmt:fmt如果非空,则此参数强制使用特定的输入格式,否则将自动检测格式;
  • options:包含AVFormatContext和demuxer私有选项的字典。返回时,此参数将销毁并替换为包含找不到的选项;都有效则返回为空;

3、avformat_find_stream_info

读取检测媒体文件的数据包以获取具体的流信息,如媒体存入的编码格式;

int avformat_find_stream_info(AVFormatContext *ic,AVDictionary **options);

ic:媒体文件上下文; options:字典,一些配置选项;

4、avcodec_find_decoder

查找具有匹配编解码器ID的已注册解码器,解码时,已经获取到了,注册的解码器可以通过枚举查看;

AVCodec *avcodec_find_decoder(enum AVCodecID id);

5、avcodec_open2

初始化AVCodecContext以使用给定的AVCodec;

int avcodec_open2(AVCodecContext *avctx,
                  const AVCodec *codec,
                  AVDictionary **options);

6、sws_getContext

分配并返回一个SwsContext。需要它来执行sws_scale()进行缩放/旋转操作;

struct SwsContext *sws_getContext(int srcW,
                                  int srcH,
                                  enum AVPixelFormat srcFormat,
                                  int dstW,
                                  int dstH,
                                  enum AVPixelFormat dstFormat,
                                  int flags,
                                  SwsFilter *srcFilter,
                                  SwsFilter *dstFilter,
                                  const double *param);

7、avpictrue_get_size

返回存储具有给定参数的图像的缓存区域大小;

int avpicture_get_size(enum AVPixelFormat pix_fmt, int widget, int height);
  • pix_fmt:图像的像素格式;
  • width:图像的像素宽度;
  • height:图像的像素高度;

8、avpictrue_fill

根据指定的图像、提供的数组设置数据指针和线条大小参数;

int avpicture_fill(AVPicture *picture,
                   const uint8_t *ptr,
                   enum AVPixelFormat pix_fmt,
                   int width,
                   int height);
  • picture:输入AVFrame指针,强制转换为AVPicture即可;
  • ptr:映射到的缓存区,开发者自己申请的存放图像数据的缓存区;
  • pix_fmt:图像数据的编码格式;
  • width:图像像素宽度;
  • height:图像像素高度;

9、av_read_frame

返回流的下一帧; 此函数返回存储在文件中的内容,不对有效的帧进行验证;获取存储在文件中的帧中,并未每个调用返回一个;不会省略有效帧之间的无效数据,以便给解码器最大可用于解码的信息; 返回0是成功,小于0则是错误,大于0则是文件末尾,所以大于等于0是返回成功;

10、avcodec_decode_video2

将大小为avpkt->size from avpkt->data的视频帧解码为图片。 一些解码器可以支持单个avpkg包中的多个帧,解码器将只解码第一帧;出错时返回负值,否则返回字节数,如果没有帧可以解压缩,则为0;

int avcodec_decode_video2(AVCodecContext *avctx,
                          AVFrame *picture,
                          int *got_picture_ptr,
                          const AVPacket *avpkt);
  • avctx:编解码器上下文;
  • picture:将解码视频帧存储在AVFrame中;
  • got_picture_ptr:输入缓冲区的AVPacket;
  • avpkt:如果没有帧可以解压,那么得到的图片是0;否则,它是非零的;

11、sws_scale

在srcSlice中缩放图像切片,并将结果缩放在dst中切片图像。切片是连续的序列图像中的行。

int sws_scale(struct SwsContext *c,
              const uint8_t *const srcSlice[],
              const int srcStride[],
              int srcSliceY,
              int srcSliceH,
              uint8_t *const dst[],
              const int dstStride[]);
  • c:以前用创建的缩放上下文sws+getContext();
  • srcSlice[]:包含指向源片段,就是AVFrame的data;
  • srcStride[]:包含每个平面的跨步的数组,其实就是AVFrame的linesize;
  • srcSliceY:切片在源图像中的位置,从开始计数0对应切片第一行的图像,所以直接填0即可;
  • srcSliceH:源切片的像素高度;
  • dst[]:目标数据地址映像,是目标AVFrame的data;
  • dstStride[]:目标每个平面的跨步的数组,就是linesize;

12、av_free_packet

释放一个包;

void av_free_packet(AVPacket *pkt);

13、avcodec_close

关闭给定的avcodeContext并释放与之关联的所有数据(但不是AVCodecContext本身);

int avcodec_close(AVCodecContext *avctx);

14、avformat_close_input

关闭打开的输入AVFormatContext,释放它和它的所有内容,并将*s设置为空;

void avformat_close_input(AVFormatContext **s);

音视频中的FFmpeg的解码与编码就是以上内容了,有关更多的FFmpeg的学习以及音视频的学习,大家可以参考《音视频入门到精通手册》点击获取里面内容。

文末

FFmpeg 的基本组成

  • FFmpeg 的基本组成包含Format、Codec、Filter、Devices、Utils等,结构如图:

  • AvFormats: 主要为媒体文件的封装格式,也可以称之为多媒体编码数据的容器,包含了音频数据,视频数据,字幕数据之类的容器;
  • AvCodecs:主要为媒体文件容器内的具体的数据对应的压缩方式,例如音频的aac压缩数据,音频的speex压缩数据,视频h264压缩数据等,或ASS字幕数据等;
  • AvFilters:主要为媒体的音频数据,视频数据进行渲染,例如多个视频文件视频数据前景与背景进行叠加、透明处理、音频重采样、音频合并等处理;
  • AvDevices:主要为媒体的播放输出设备,媒体的采集设备,例如音频采集设备,音频输出设备,视频的采集设备,视频的输出设备等;
  • Utils:主要为FFmpeg中的AVFormats,AVCodecs,AVFilters等所用到的公用的接口;
  • Swscale: 主要为FFmpeg中做缩放,rgb转yuv,混色计算等用的接口。

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

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

相关文章

Elasticsearch5.5.1 自定义评分插件开发

文本相似度插件开发&#xff0c;本文基于Elasticsearch5.5.1&#xff0c;Kibana5.5.1 下载地址为&#xff1a; Past Releases of Elastic Stack Software | Elastic 本地启动两个服务后&#xff0c;localhost:5601打开Kibana界面&#xff0c;点击devTools&#xff0c;效果图…

koa ts kick off 搭建项目的基本架子

koa ts kick off 使用ts开发koa项目的基本架子&#xff0c;便于平时随手调研一些技术 项目结构 ├── src │ ├── controller //controller层 │ ├── service //service层 │ ├── routes.ts //路由 │ └── index.ts //项目入…

【图像配准】多图配准/不同特征提取算法/匹配器比较测试

前言 本文首先完成之前专栏前置博文未完成的多图配准拼接任务&#xff0c;其次对不同特征提取器/匹配器效率进行进一步实验探究。 各类算法原理简述 看到有博文[1]指出&#xff0c;在速度方面SIFT<SURF<BRISK<FREAK<ORB&#xff0c;在对有较大模糊的图像配准时&…

04 react css上下浮动动画效果

react css上下浮动动画效果html原生实现上下浮动react 实现上下浮动思路分析实现步骤1.引入useRef2.在所属组件内定义—个变量3.在按钮上添加事件4.定义点击事件对window.scrollTo()进行了解&#xff1a;在react中实现效果图&#xff1a;html原生实现上下浮动 我们有一个导向箭…

【分享】订阅金蝶KIS集简云连接器同步OA付款审批数据至金蝶KIS

方案简介 集简云基于钉钉连接平台完成与钉钉的深度融合&#xff0c;实现钉钉OA审批与数百款办公应用软件&#xff08;如金蝶KIS、用友等&#xff09;的数据互通&#xff0c;让钉钉的OA审批流程与企业内部应用软件的采购、付款、报销、收款、人事管理、售后工单、立项申请等环节…

【2023面试秘籍】 测试工程师的简历该怎么写?

作为软件测试的垂直领域深耕者&#xff0c;面试或者被面试都是常有的事&#xff0c;可是不管是啥&#xff0c;总和简历有着理不清的关系&#xff0c;面试官要通过简历了解面试者的基本信息、过往经历等&#xff0c;面试者希望通过简历把自己最好的一面体现给面试官&#xff0c;…

【Java 面试合集】重写以及重载有什么区别能简单说说嘛

重写以及重载有什么区别能简单说说嘛 前述 这是一道非常基础的面试题&#xff0c;我们在回答的过程中一定要逐一横向比较。 从方法的 修饰符&#xff0c;返回值&#xff0c;方法名&#xff0c;含义&#xff0c;参数等方面进行逐一分析来比较不同。 话不多话&#xff0c;看下…

什么样的台灯适合学生做作业的?开学季,适合孩子写作业的台灯

学生在做作业时&#xff0c;是离不开台灯的&#xff0c;在台灯下学习三四个小时&#xff0c;如果台灯质量不好&#xff0c;那对视力造成很大影响&#xff0c;研究表明&#xff0c;儿童在过亮或者过暗的环境中长时间学习&#xff0c;会导致视力下降等&#xff0c;那么什么样的台…

瀚博半导体载天VA1 加速卡安装过程

背景&#xff1a; 想用 瀚博半导体载天VA1 加速卡 代替 NVIDIA 显卡跑深度学习模型 感谢瀚博的周工帮助解答。 正文&#xff1a; 小心拔出 NVIDIA 显卡&#xff0c;在PCIe 接口插上瀚博半导体载天VA1加速卡&#xff0c;如图&#xff1a; 这时显示屏连接主板的集成显卡 卸载…

cookie和Session的作用和比较

目录 什么是cookie cookie的工作原理 什么是session Session的工作原理 为什么会有session和cookie cookie和session如何配合工作 cookie和Session作用 什么是会话 什么是cookie cookie是web服务器端向我们客户端发送的一块小文件&#xff0c;该文件是干嘛的呢&#xf…

Java基础知识疑难点

1. 基础 1.1. 正确使用 equals 方法1.2. 整型包装类值的比较1.3. BigDecimal 1.3.1. BigDecimal 的用处1.3.2. BigDecimal 的大小比较1.3.3. BigDecimal 保留几位小数1.3.4. BigDecimal 的使用注意事项1.3.5. 总结 1.4. 基本数据类型与包装数据类型的使用标准 2. 集合 2.1. Arr…

Docker-用Jenkins发版Java项目-(1)Docke安装Jenkins

文章目录前言环境背景操作流程docker安装及jenkins软件安装jenkins配置登录配置安装插件及创建账号前言 学海无涯&#xff0c;旅“途”漫漫&#xff0c;“途”中小记&#xff0c;如有错误&#xff0c;敬请指出&#xff0c;在此拜谢&#xff01; 最近新购得了M2的MAC&#xff0c…

LeetCode刷题--- 138. 复制带随机指针的链表(哈希表+迭代)

文章目录一、编程题&#xff1a;430. 扁平化多级双向链表&#xff08;双指针&#xff09;1.题目描述2.示例1&#xff1a;3.示例2&#xff1a;4.示例3&#xff1a;5.提示&#xff1a;二、解题思路1. 题目分析2. 方法1&#xff08;哈希表&#xff09;思路&#xff1a;复杂度分析&…

备考 PMP 考试时需要着重注意什么?

PMP考试难度并不是很大。科学备考一定没有问题的&#xff5e;这里在和大家说说2023年PMP的考试时间&#xff1a;3月、5月、8月、11月&#xff08;其中3月不开启新报名&#xff09;需要注意的地方还是蛮多的。我就根据自己考试的经验和大家分享一下在考试整个过程中注意啥&#…

2023年广西最新建筑施工焊工(建筑特种作业)模拟试题及答案

百分百题库提供特种工&#xff08;焊工&#xff09;考试试题、特种工&#xff08;焊工&#xff09;考试预测题、特种工&#xff08;焊工&#xff09;考试真题、特种工&#xff08;焊工&#xff09;证考试题库等,提供在线做题刷题&#xff0c;在线模拟考试&#xff0c;助你考试轻…

【nodejs】nodejs入门核心知识(命令行使用、内置模块、node 模块化开发)

&#x1f4bb; nodejs入门核心知识(命令行使用、内置模块、node 模块化开发) &#x1f3e0;专栏&#xff1a;JavaScript &#x1f440;个人主页&#xff1a;繁星学编程&#x1f341; &#x1f9d1;个人简介&#xff1a;一个不断提高自我的平凡人&#x1f680; &#x1f50a;分享…

(1分钟突击面试) 高斯牛顿、LM、Dogleg后端优化算法

高斯牛顿法 LM法 DogLeg方法编辑切换为居中添加图片注释&#xff0c;不超过 140 字&#xff08;可选&#xff09;知识点&#xff1a;高斯牛顿是线搜索方法 LM方法是信赖域方法。编辑切换为居中添加图片注释&#xff0c;不超过 140 字&#xff08;可选&#xff09;这个就是JTJ是…

React设计原理—1框架原理

阅读前须知 本文是笔者学习卡颂的《React设计原理》的读书笔记&#xff0c;对书中有价值内容以Q&A方式进行呈现&#xff0c;同时结合了自己的理解&#x1f914;阅读时推荐先看问题&#xff0c;想想自己的答案&#xff0c;再和答案比对一下本文属于前端框架科普&#xff0c;…

68. Python的相对路径

68. Python的相对路径 文章目录68. Python的相对路径1. 知识回顾2. 什么是相对路径3. 相对路径的语法4. 查看相对路径的方法5. 写出所有txt文件的相对路径5.1 同目录5.2 上级目录6. 用相对路径读取txt文件6.1 读取旅游.txt6.2 读取旅游经费.txt6.3 读取笔记.txt和new.txt6.4 读…

微服务调用组件Feign

目录 JAVA 项目中如何实现接口调用&#xff1f; Httpclient Okhttp HttpURLConnection RestTemplate WebClient 什么是Feign 优势 Spring Cloud Alibaba快速整合OpenFeign 引入依赖 编写调用接口FeignClient注解 调用端在启动类上添加EnableFeignClients注解 发起调…