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

剖析 FFmpeg:从基本功能到过滤器,实现音视频处理的灵活性

目录

  • 1.解复用
  • 2 解码
    • 2.1 音频解码
    • 2.2 视频解码
  • 3 修饰
    • 3.1 avio
    • 3.2 重采样
  • 4 过滤器
    • 4.1 过滤器基本知识
    • 4.2 简单过滤器
    • 4.3 复杂滤镜图

1.解复用

解复用就是把容器中的媒体流分离出来,方便我们对媒体流处理。


step1对媒体文件上下文初始化

AVFormatContext *ifmt_ctx = NULL; int ret = avformat_open_input(&ifmt_ctx, in_filename, NULL, NULL);//头部信息ret = avformat_find_stream_info(ifmt_ctx, NULL);//av_dump_format(ifmt_ctx, 0, in_filename, 0);

解析
int avformat_open_input(AVFormatContext **ps, const char *url, ff_const59 AVInputFormat *fmt, AVDictionary **options);

  • AVFormatContext **ps:这是一个指向 AVFormatContext 指针的指针。AVFormatContext 属于 FFmpeg 里的核心结构体,它对输入或输出流的格式信息进行存储,像文件头、元数据、流信息等。调用该函数时,需传入一个指向 - - AVFormatContext 指针的指针,函数会为 AVFormatContext 分配内存,并且把其地址存储在这个指针里。
  • const char *url:这是一个字符串,代表输入流的 URL 或文件名。它可以是本地文件的路径,也能是网络流的 URL,比如 http://example.com/stream.mp4。
  • ff_const59 AVInputFormat *fmt:这是一个指向 AVInputFormat 结构体的指针,其作用是指定输入流的格式。若传入 NULL,FFmpeg 会自动对输入流的格式进行探测。
  • AVDictionary **options:这是一个指向 AVDictionary 指针的指针,用于传递额外的选项。AVDictionary 是一个键值对的集合,能够用来设置一些特定的参数,像编解码器选项、网络选项等。如果不需要传递额外选项,可以传入 NULL。

实现功能:读取输入流的文件头和元数据,将这些信息存储在AVFormatContext 中。
ret = avformat_find_stream_info(ifmt_ctx, NULL);
第二个参数与上面第四个一样
实现功能:

  • 填充流信息到 AVFormatContext:将探测到的每个流的详细信息填充到 AVFormatContext 中的 streams 数组里的对应 AVStream 结构体中。后续进行解码、处理等操作时,可以从这些结构体中获取所需的信息。
  • 在调用 avformat_open_input 后,AVFormatContext 中仅包含了部分基本的格式信息。该函数会读取一定数量的数据包,从中分析出每个流(如音频流、视频流、字幕流等)的详细信息,如编解码器类型、帧率、采样率、分辨率等。

step2 提取音频流-aac

audio_index = av_find_best_stream(ifmt_ctx, AVMEDIA_TYPE_AUDIO, -1, -1, NULL, 0);
AVPacket pkt;av_init_packet(&pkt);av_read_frame(ifmt_ctx, &pkt) >=0 fwrite( pkt.data, 1, pkt.size, aac_fd);

int av_find_best_stream(AVFormatContext *ic, enum AVMediaType type, int wanted_stream_nb, int related_stream, AVCodec **decoder_ret, int flags);

  • AVFormatContext *ic:即代码里的 ifmt_ctx,它是一个指向 AVFormatContext 结构体的指针。AVFormatContext 是 FFmpeg 中用于存储输入或输出流格式信息的核心结构体,其中包含了多个流的信息,像音频流、视频流、字幕流等。
  • enum AVMediaType type:也就是代码中的 AVMEDIA_TYPE_AUDIO,此参数用来指定要查找的流的类型。AVMediaType 是一个枚举类型,常见的值有 AVMEDIA_TYPE_AUDIO(音频流)、AVMEDIA_TYPE_VIDEO(视频流)、AVMEDIA_TYPE_SUBTITLE(字幕流)等。
  • int wanted_stream_nb:在代码里为 -1,该参数指定你期望查找的流的编号。若为 -1,函数会自动查找最优的流;若指定了一个具体的编号,函数会尝试查找该编号对应的流。
  • int related_stream:代码中同样是 -1,该参数用于指定一个相关的流编号。例如,在查找音频流时,可以指定一个相关的视频流编号,函数会优先查找与该视频流相关的音频流。若为 -1,则不考虑相关性。
  • AVCodec **decoder_ret:代码中为 NULL,这是一个指向 AVCodec 指针的指针。若不为 NULL,函数会将找到的流所对应的最优解码器的指针存储在该指针指向的位置。若为 NULL,则不返回解码器信息。
  • int flags:代码中是 0,该参数是一个标志位,用于指定查找时的一些额外选项。通常设为 0 即可

av_read_frame

AVFormatContext *s:指向 AVFormatContext 结构体的指针,此结构体包含了输入流的格式信息。
AVPacket *pkt:指向 AVPacket 结构体的指针,函数会把读取到的数据包存储在这个结构体里。

功能:av_read_frame 会从输入流里读取一个数据包,这个数据包可能是音频数据包,也可能是视频数据包。读取到的数据包包含了原始的编码数据


step 3 提取视频流 h264

videoindex = av_find_best_stream(ifmt_ctx, AVMEDIA_TYPE_VIDEO, -1, -1, NULL, 0);
AVPacket pkt;
pkt = av_packet_alloc();
av_init_packet(pkt);//涉及到两种 h264 两种模式 这里只讲最简单的ts流
ret = av_read_frame(ifmt_ctx, pkt);size_t size = fwrite(pkt->data, 1, pkt->size, outfp);

2 解码

硬件播放要求:播放器的硬件设备,如显示器、扬声器等,只能处理原始的音视频信号。显示器需要接收由像素点组成的图像数据,按照一定的时序和格式进行显示;扬声器需要接收模拟音频信号,通过振动发出声音。而编码后的音视频数据是经过特定算法处理后的二进制数据,无法直接被硬件设备识别和处理。

解码还原数据:因此,播放器需要通过解码操作,将编码后的音视频数据还原为原始的图像帧和音频样本,然后将这些数据转换为适合硬件设备处理的信号格式,最终实现音视频的播放**。例如,对于视频数据,解码后得到的是一帧一帧的图像,播放器将这些图像按照一定的帧率依次显示在屏幕上,形成动态的视频画面;对于音频数据,解码后得到的是音频样本,播放器将这些样本转换为模拟音频信号,通过扬声器播放出来。

2.1 音频解码

针对于aac协议
音频解码本质上 将aac数据还原为pcm数据


step1 初始化解码器


AVPacket *pkt = NULL;
AVFrame *decoded_frame = NULL;const AVCodec *codec;
AVCodecContext *codec_ctx= NULL;
AVCodecParserContext *parser = NULL;enum AVCodecID audio_codec_id = AV_CODEC_ID_AAC;
codec = avcodec_find_decoder(audio_codec_id);
parser = av_parser_init(codec->id);
codec_ctx = avcodec_alloc_context3(codec);
avcodec_open2(codec_ctx, codec, NULL);
ret = av_parser_parse2(parser, codec_ctx, &pkt->data, &pkt->size,data, data_size,AV_NOPTS_VALUE, AV_NOPTS_VALUE, 0);
decoded_frame = av_frame_alloc();

AVCodecParserContext 是 FFmpeg 库中的一个重要结构体,主要用于解析编码数据(如视频或音频的压缩数据包),将其拆分成一个个独立的编码单元(如视频中的帧、音频中的音频帧),以便后续进行解码处理.

  • AVCodecParserContext *s
    含义:指向 AVCodecParserContext 结构体的指针,该结构体是在调用 av_parser_init 函数时初始化得到的。它用于存储解析器的上下文信息,例如当前解析的状态、已经解析的数据等。
    作用:函数会根据这个解析器上下文的状态来继续解析输入的编码数据。
  • AVCodecContext *avctx
    含义:指向 AVCodecContext 结构体的指针,该结构体包含了编解码器的上下文信息,如编解码器的类型、帧率、分辨率等。
    作用:解析过程中可能需要这些编解码器的上下文信息来正确地解析数据。例如,在解析视频数据时,需要知道视频的分辨率和帧率等信息来判断一个视频帧的边界。
  • uint8_t **poutbuf
    含义:指向指针的指针,用于存储解析后输出的完整编码单元(如视频帧、音频帧)的数据地址。
    作用:函数在解析过程中,如果发现一个完整的编码单元,会将该编码单元的数据地址存储在 *poutbuf 中。
    int *poutbuf_size
    含义:指向整数的指针,用于存储解析后输出的完整编码单元的数据大小。
    作用:函数会将解析出的完整编码单元的数据大小存储在 *poutbuf_size 中。
    const uint8_t *buf
    含义:指向输入的编码数据缓冲区的指针,该缓冲区包含了需要解析的编码数据。
    作用:函数会从这个缓冲区中读取数据进行解析。
  • int buf_size
    含义:输入的编码数据缓冲区的大小,即 buf 所指向的缓冲区中数据的字节数。
    作用:函数会根据这个大小来确定需要解析的数据范围。
  • int64_t pts
    含义:表示输入数据的展示时间戳(Presentation Time Stamp),用于指示该数据应该在何时展示。通常在不知道具体的时间戳时,可以使用 AV_NOPTS_VALUE 来表示。
    作用:解析器可以根据这个时间戳来处理数据的展示顺序,同时也可以将其传递给后续的解码和播放环节。
  • int64_t dts
    含义:表示输入数据的解码时间戳(Decoding Time Stamp),用于指示该数据应该在何时进行解码。同样,在不知道具体的时间戳时,可以使用 AV_NOPTS_VALUE 来表示。
    作用:在一些复杂的编码格式中,解码顺序和展示顺序可能不同,解析器可以根据这个时间戳来安排数据的解码顺序。
  • int64_t pos
    含义:表示输入数据在原始流中的位置,通常用于标记数据的来源和位置信息。
    作用:在一些需要定位和调试的场景中,这个参数可以帮助开发者确定数据的具体来源。

功能:
av_parser_parse2 函数会尝试从 input_data 中解析出一个完整的帧,如果解析成功,pkt->data 和 pkt->size 会被设置为该视频帧的数据和大小,可以将其传递给解码器进行解码

ret = avcodec_send_packet(dec_ctx, pkt); 发送帧
ret = avcodec_receive_frame(dec_ctx, frame); 接受帧

0:表示成功从解码器接收到一个完整的解码帧。此时,frame 指针所指向的 AVFrame 结构体中就包含了解码后的有效数据,可以对这些数据进行后续处理,例如显示视频帧、播放音频帧等。
AVERROR(EAGAIN):意味着当前解码器还没有准备好输出一个完整的解码帧。这可能是因为之前发送的数据包还在解码过程中,或者解码器需要更多的输入数据包才能解码出一个完整的帧。在这种情况下,你可以继续调用 avcodec_send_packet 向解码器发送更多的编码数据包,然后再次尝试调用 avcodec_receive_frame。
AVERROR_EOF:表明解码器已经完成了所有输入数据的解码,不会再输出更多的帧。通常在调用 avcodec_send_packet 时发送了一个空的数据包(表示输入结束)后,解码器处理完剩余数据,就会返回这个错误码。


step 2 解码

static void decode(AVCodecContext *dec_ctx, AVPacket *pkt, AVFrame *frame,FILE *outfile)
{int i, ch;int ret, data_size;/* send the packet with the compressed data to the decoder */ret = avcodec_send_packet(dec_ctx, pkt);if(ret == AVERROR(EAGAIN)){fprintf(stderr, "Receive_frame and send_packet both returned EAGAIN, which is an API violation.\n");}else if (ret < 0){fprintf(stderr, "Error submitting the packet to the decoder, err:%s, pkt_size:%d\n",av_get_err(ret), pkt->size);
//        exit(1);return;}/* read all the output frames (infile general there may be any number of them */while (ret >= 0){// 对于frame, avcodec_receive_frame内部每次都先调用ret = avcodec_receive_frame(dec_ctx, frame);if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF)return;else if (ret < 0){fprintf(stderr, "Error during decoding\n");exit(1);}data_size = av_get_bytes_per_sample(dec_ctx->sample_fmt);if (data_size < 0){/* This should not occur, checking just for paranoia */fprintf(stderr, "Failed to calculate data size\n");exit(1);}static int s_print_format = 0;if(s_print_format == 0){s_print_format = 1;print_sample_format(frame);}for (i = 0; i < frame->nb_samples; i++){for (ch = 0; ch < dec_ctx->channels; ch++)  // 交错的方式写入, 大部分float的格式输出fwrite(frame->data[ch] + data_size*i, 1, data_size, outfile);}}
}

核心为

 ret = avcodec_send_packet(dec_ctx, pkt);ret = avcodec_receive_frame(dec_ctx, frame);for (i = 0; i < frame->nb_samples; i++){for (ch = 0; ch < dec_ctx->channels; ch++)  // 交错的方式写入, 大部分float的格式输出fwrite(frame->data[ch] + data_size*i, 1, data_size, outfile);}

int avcodec_receive_frame(AVCodecContext *avctx, AVFrame *frame);

0:表示成功从解码器接收到一个完整的解码帧。此时,frame 指针所指向的 AVFrame 结构体中就包含了解码后的有效数据,可以对这些数据进行后续处理,例如显示视频帧、播放音频帧等。
AVERROR(EAGAIN):意味着当前解码器还没有准备好输出一个完整的解码帧。这可能是因为之前发送的数据包还在解码过程中,或者解码器需要更多的输入数据包才能解码出一个完整的帧。在这种情况下,你可以继续调用 avcodec_send_packet 向解码器发送更多的编码数据包,然后再次尝试调用 avcodec_receive_frame。
AVERROR_EOF:表明解码器已经完成了所有输入数据的解码,不会再输出更多的帧。通常在调用 avcodec_send_packet 时发送了一个空的数据包(表示输入结束)后,解码器处理完剩余数据,就会返回这个错误码。

一个音频帧 包含许多样本点 ,一个样本点包含几个声道 这些是关于音频的知识点


2.2 视频解码


step1 初始化
前面和音频解码一模一样,就是解码器不一样

video_codec_id = AV_CODEC_ID_H264;

step2 解码
前面也是一模一样

 for(int j=0; j<frame->height; j++)fwrite(frame->data[0] + j * frame->linesize[0], 1, frame->width, outfile);for(int j=0; j<frame->height/2; j++)fwrite(frame->data[1] + j * frame->linesize[1], 1, frame->width/2, outfile);for(int j=0; j<frame->height/2; j++)fwrite(frame->data[2] + j * frame->linesize[2], 1, frame->width/2, outfile);

一般我们用的就是 yuv420p 也就说 平面写入 这个涉及到视频知识 建议了解


3 修饰

3.1 avio

AVIO(AV Input/Output)是 FFmpeg 里用于处理输入输出操作的模块,对于播放器而言,AVIO 非常必要,下面从几个方面为你详细阐述:
支持多种输入源

  • 本地文件播放:播放器需要能够读取本地存储的音视频文件,如 MP4、AVI、MKV 等。AVIO 提供了访问本地文件系统的能力,借助 avio_open 等函数可以打开本地文件,然后读取文件中的数据。例如,当用户在播放器中选择一个本地的 MP4 文件进行播放时,AVIO 会负责从文件系统中读取文件内容,为后续的解码和播放操作提供数据支持。
  • 网络流播放:如今在线播放音视频内容越来越普遍,像在线视频网站、直播平台等。AVIO 支持多种网络协议,如 HTTP、RTMP、RTSP 等,能够从网络上获取音视频流。通过 avio_open2 函数可以打开网络 URL,建立网络连接并接收数据。例如,在播放网络直播流时,AVIO 会持续从服务器接收音视频数据,保证播放的流畅性。

数据缓冲与管理

  • 缓冲机制:音视频数据的读取和处理需要一定的时间,为了避免播放过程中出现卡顿,播放器通常需要使用缓冲机制。AVIO 提供了数据缓冲功能,它会将读取到的数据暂时存储在缓冲区中,解码器可以从缓冲区中获取数据进行解码。这样可以平衡数据读取和处理的速度差异,提高播放的稳定性。
  • 数据管理:AVIO 还负责管理数据的读取位置和字节顺序等。在读取文件或网络流时,它会记录当前的读取位置,确保数据的正确读取。同时,对于不同字节顺序的数据,AVIO 会进行相应的转换,保证播放器能够正确处理数据。

统一的输入输出接口

  • 简化开发:AVIO 为播放器提供了统一的输入输出接口,无论输入源是本地文件还是网络流,播放器都可以使用相同的接口进行数据读取操作。这样可以简化播放器的开发过程,减少代码的复杂度。开发者只需要关注数据的解码和播放逻辑,而不需要关心具体的输入源和数据读取方式。
  • 可扩展性:由于 AVIO 提供了统一的接口,当需要支持新的输入源或协议时,只需要在 AVIO 模块中添加相应的实现即可,而不需要对播放器的其他部分进行大规模修改。这提高了播放器的可扩展性,使得播放器能够适应不断变化的需求。

其实本质上是将avformat_open_input 函数内部会调用 AVIO 的相关函数来打开本地文件,为后续的播放操作做好准备。

 uint8_t *io_buffer = av_malloc(BUF_SIZE);AVIOContext *avio_ctx = avio_alloc_context(io_buffer, BUF_SIZE, 0, (void *)in_file,    \read_packet, NULL, NULL);AVFormatContext *format_ctx = avformat_alloc_context();format_ctx->pb = avio_ctx;int ret = avformat_open_input(&format_ctx, NULL, NULL, NULL);

AVIOContext *avio_ctx = avio_alloc_context(io_buffer, BUF_SIZE, 0, (void *)in_file, read_packet, NULL, NULL);

io_buffer:之前分配的缓冲区。
BUF_SIZE:缓冲区的大小。
0:代表这是一个只读的 I/O 上下文。若为 1,则表示可写。
(void *)in_file:传递给回调函数的用户数据,一般是文件指针或者其他自定义的数据结构。
read_packet:自定义的读取数据包的回调函数,当需要从输入源读取数据时,会调用此函数。
NULL:这里是写数据包的回调函数,由于是只读上下文,所以设为 NULL。
NULL:这里是查找位置的回调函数,若不需要支持查找操作,可设为 NULL。

3.2 重采样

适配输出设备

  • 采样率差异:不同的音频输出设备支持的采样率有所不同。例如,CD 音频的标准采样率是 44.1kHz,而一些高清音频设备可能支持 96kHz 甚至 192kHz 的-- 采样率。当播放器播放的音频文件采样率与输出设备不匹配时,就需要进行重采样。比如,播放器播放一个 48kHz 采样率的音频文件,但输出设备只支持 44.1kHz,这时通过重采样将音频转换为 44.1kHz,才能让输出设备正常播放音频。
  • 声道布局不同:音频文件的声道布局和输出设备也可能存在差异。常见的声道布局有单声道、立体声、5.1 声道、7.1 声道等。若播放器播放的是 5.1 声道音频,而输出设备是立体声扬声器,就需要通过重采样将 5.1 声道音频转换为立体声,以适配输出设备。

兼容多种音频源

  • 不同格式音频:在实际应用中,播放器要支持多种音频格式,像 MP3、AAC、WAV 等,这些格式的音频可能采用不同的采样率和声道布局。播放器需要对这些不同格式的音频进行重采样,使其能够统一输出到音频设备。例如,一个 MP3 文件的采样率是 44.1kHz 立体声,另一个 AAC 文件的采样率是 48kHz 5.1 声道,播放器在播放时需要将它们重采样到相同的采样率和声道布局,方便后续处理和输出。
  • 多音频流混合:有些播放器具备同时播放多个音频流的功能,如音视频同步播放或者多语言音频切换。这些音频流的采样参数可能各不相同,为了实现音频的同步和混合,需要对它们进行重采样,让它们的采样率、声道数等参数保持一致。

优化音质

  • 提升采样率:在某些情况下,通过提高音频的采样率可以提升音质。例如,将低采样率的音频重采样到高采样率,能让音频包含更多的细节和更宽的频率范围。不过,这种提升效果也会受到原始音频质量的限制。
    抗锯齿处理:重采样过程中可以进行抗锯齿处理,减少音频信号在采样过程中产生的混叠噪声,从而提高音频的清晰度和纯净度。

确保音频同步

  • 音视频同步:在播放视频时,音频和视频的同步至关重要。由于视频和音频的编码、解码过程可能存在差异,导致它们的时间戳不一致。通过重采样可以对音频的播放速度进行微调,确保音频和视频能够同步播放,避免出现音画不同步的问题。
  • 多声道同步:对于多声道音频,各个声道之间的同步也很重要。重采样可以保证各个声道的音频数据在时间上保持一致,让听众能够感受到正确的音频空间感和立体感。

step1 初始化

struct SwrContext *swr_ctx;
swr_ctx = swr_alloc();// 设置重采样参数/* set options */// 输入参数av_opt_set_int(swr_ctx, "in_channel_layout",    src_ch_layout, 0);av_opt_set_int(swr_ctx, "in_sample_rate",       src_rate, 0);av_opt_set_sample_fmt(swr_ctx, "in_sample_fmt", src_sample_fmt, 0);// 输出参数av_opt_set_int(swr_ctx, "out_channel_layout",    dst_ch_layout, 0);av_opt_set_int(swr_ctx, "out_sample_rate",       dst_rate, 0);av_opt_set_sample_fmt(swr_ctx, "out_sample_fmt", dst_sample_fmt, 0);if ((ret = swr_init(swr_ctx)) < 0) {fprintf(stderr, "Failed to initialize the resampling context\n");goto end;}//给输入源分配内存空间ret = av_samples_alloc_array_and_samples(&src_data, &src_linesize, src_nb_channels,src_nb_samples, src_sample_fmt, 0);
//max_dst_nb_samples = dst_nb_samples =av_rescale_rnd(src_nb_samples, dst_rate, src_rate, AV_ROUND_UP);/* buffer is going to be directly written to a rawaudio file, no alignment */dst_nb_channels = av_get_channel_layout_nb_channels(dst_ch_layout);// 分配输出缓存内存ret = av_samples_alloc_array_and_samples(&dst_data, &dst_linesize, dst_nb_channels,dst_nb_samples, dst_sample_fmt, 0);
ret = swr_convert(swr_ctx, dst_data, dst_nb_samples, (const uint8_t **)src_data, src_nb_samples);

struct SwrContext *swr_ctx 中的 SwrContext 是 FFmpeg 库里用于音频重采样的上下文结构体,swr_ctx 则是指向该结构体的指针。借助这个结构体和相关函数,能够实现音频在采样率、声道布局、样本格式等方面的转换

int av_opt_set_int(void *obj, const char *name, int64_t val, int search_flags);

  • void *obj:指向要设置选项的对象的指针。在音频重采样场景中,通常是指向 SwrContext 结构体的指针,该结构体用于存储音频重采样的上下文信息;在其他场景下,也可能是指向编解码器上下文(AVCodecContext)等其他对象的指针。
  • const char *name:要设置的选项的名称,是一个字符串。例如,在设置音频重采样的参数时,可能会使用 “in_sample_rate” 表示输入音频的采样率,“out_channel_layout” 表示输出音频的声道布局等。
  • int64_t val:要设置的选项的值,是一个 64 位整数。具体的值根据选项的不同而不同,比如设置采样率时,该值就是具体的采样率数值(如 44100、48000 等);设置声道布局时,该值是对应的声道布局常量(如 AV_CH_LAYOUT_MONO、AV_CH_LAYOUT_STEREO 等)。
  • int search_flags:搜索标志,用于指定查找选项的方式。一般设置为 0 即可,表示使用默认的查找方式。

av_samples_alloc_array_and_samples 函数:这是 FFmpeg 中用于分配音频样本数据缓冲区的函数

&src_data:指向一个 uint8_t ** 类型的指针,用于存储分配后的音频数据缓冲区的指针数组。对于多声道音频,src_data 中的每个元素对应一个声道的数据缓冲区。
&src_linesize:指向一个 int 类型的指针,用于存储每行数据的大小。在音频处理中,每行数据通常对应一个声道的一个样本。
src_nb_channels:前面计算得到的输入音频的声道数量。
src_nb_samples:输入音频的样本数量。
src_sample_fmt:输入音频的样本格式,例如 AV_SAMPLE_FMT_S16 表示 16 位有符号整数格式。
0:表示是否进行内存对齐的标志,这里设置为 0 表示不进行特殊的内存对齐。

av_rescale_rnd 函数:这是 FFmpeg 提供的用于进行整数缩放和四舍五入的函数,其原型为 int64_t av_rescale_rnd(int64_t a, int64_t b, int64_t c, enum AVRounding rnd);。

参数解释:
src_nb_samples:输入音频的样本数量。
dst_rate:输出音频的采样率。
src_rate:输入音频的采样率。
AV_ROUND_UP:四舍五入模式,表示向上取整。

功能:该函数的作用是根据输入音频的样本数量、输入采样率和输出采样率,计算出重采样后输出音频的样本数量。在重采样过程中,由于采样率发生了变化,样本数量也会相应改变。通过 av_rescale_rnd 函数可以准确计算出输出样本的数量,保证音频时长在重采样前后基本一致。

结果存储:计算得到的输出样本数量同时赋值给 max_dst_nb_samples 和 dst_nb_samples,max_dst_nb_samples 通常用于后续分配足够大的输出缓冲区,以确保能容纳所有重采样后的样

int swr_convert(struct SwrContext *s, uint8_t **out, int out_count, const uint8_t **in, int in_count);

参数类型描述
sstruct SwrContext *指向 SwrContext 结构体的指针。SwrContext 是 FFmpeg 中用于音频重采样的上下文结构体,它包含了重采样所需的各种配置信息,如输入输出的声道布局、采样率、样本格式等。在调用 swr_convert 之前,需要对 SwrContext 进行正确的初始化和参数设置。
outuint8_t **指向输出音频数据缓冲区的指针数组。对于多声道音频,out 数组中的每个元素对应一个声道的数据缓冲区。重采样后的音频数据将被存储在这些缓冲区中。
out_countint输出缓冲区能够容纳的最大样本数量。这个参数决定了 swr_convert 函数最多可以输出多少个样本。需要注意的是,实际输出的样本数量可能会小于 out_count,具体取决于输入样本的数量和重采样的处理情况。
inconst uint8_t **指向输入音频数据缓冲区的指针数组。同样,对于多声道音频,in 数组中的每个元素对应一个声道的数据缓冲区。输入的音频数据将从这些缓冲区中读取。如果设置为 NULL,则表示没有新的输入数据,swr_convert 会继续处理之前缓存的输入数据。
in_countint输入缓冲区中的样本数量。如果 inNULL,则 in_count 应设置为 0。

4 过滤器

首先会对输入的音视频数据进行解码,将压缩的音视频数据转换为原始的音视频帧。然后,这些解码后的帧会被送入过滤器链中进行各种处理,如视频的缩放、裁剪、叠加,音频的音量调整、声道转换等操作。过滤器可以对帧的内容进行修改、变换,以满足用户的各种需求。处理完成后,再将处理后的帧进行编码,输出为新的音视频文件或流。

播放器使用过滤器主要有以下原因:一是提升播放质量,能改善音视频的画质和音质,如降噪、调整色彩等;二是实现格式转换与兼容,支持多种音视频格式,还能在不同格式间转换以适应设备;三是拓展功能,可添加字幕、特效,进行音频混音等;四是满足个性化需求,用户能根据自己的喜好对音视频进行定制化处理。

4.1 过滤器基本知识

滤镜图包含滤镜链 ->包含过滤器

AVFilterGraph
功能 : AVFilterGraph 是 FFmpeg 中用于处理音视频流的核心组件,它能将多个过滤器组合起来,对音视频数据进行复杂的处理
重要参数

struct AVFilterGraph
{
AVFilterContext **filters;//数组
unsigned nb_filters;
} 
  1. AVFilterContext **filters
    类型:指向 AVFilterContext 指针的指针,也就是一个 AVFilterContext 指针数组。
    功能:这个数组用于存储过滤器图中所有的过滤器上下文实例。每个 AVFilterContext 代表一个具体的过滤器,包含了该过滤器的状态信息和配置参数。通过这个数组,可以方便地对图中的所有过滤器进行遍历和管理。
    示例:假设我们创建了一个包含三个过滤器的过滤器图,分别是 scale(用于视频缩放)、crop(用于视频裁剪)和 overlay(用于视频叠加),那么 filters 数组可能会依次存储这三个过滤器对应的 AVFilterContext 指针。
  2. unsigned nb_filters
    类型:无符号整数。
    功能:表示 filters 数组中实际存储的 AVFilterContext 实例的数量,即过滤器图中当前包含的过滤器的个数。通过 nb_filters 可以准确知道过滤器图的规模,在遍历 filters 数组时也可以作为循环的终止条件。
    示例:如果 nb_filters 的值为 3,说明 filters 数组中存储了 3 个 AVFilterContext 指针,即当前过滤器图中有 3 个过滤器。

AVFilter
功能:AVFilter 是 FFmpeg 中用于表示一个抽象过滤器的结构体,它定义了过滤器的基本属性和功能
重要成员

  1. const char *name
    功能概述:该成员是一个指向常量字符数组的指针,用于存储过滤器的名称。过滤器名称是过滤器的唯一标识符,在 FFmpeg 的过滤器系统中具有重要作用,无论是用户在命令行中指定过滤器,还是代码中通过名称查找过滤器,都依赖于这个名称。
    示例说明:在你给出的注释中提到 overlay,这就是一个具体的过滤器名称。overlay 过滤器的功能是将一个视频叠加到另一个视频之上,常用于添加水印、画中画效果等场景。通过这个名称,用户可以在 FFmpeg 命令中使用该过滤器,例如 ffmpeg -i background.mp4 -i overlay.mp4 -filter_complex “overlay=10:10” output.mp4,其中 “overlay=10:10” 就指定了使用 overlay 过滤器,并设置叠加的位置为 (10, 10)。
  2. const AVFilterPad *inputs
    功能概述:该成员是一个指向常量 AVFilterPad 结构体数组的指针,用于描述过滤器的输入端口。输入端口定义了过滤器可以接收的输入数据的类型和数量,是过滤器与外部进行数据交互的入口。不同的过滤器可能有不同数量和类型的输入端口,通过这些端口,过滤器可以接收音视频数据进行处理。
    示例说明:以 overlay 过滤器为例,它有两个输入端口,一个用于接收背景视频,另一个用于接收要叠加的视频。inputs 数组会包含两个 AVFilterPad 结构体,分别描述这两个输入端口的属性,如支持的媒体类型(视频或音频)、端口名称等。这样,在构建过滤器图时,就可以将相应的视频流连接到这些输入端口,为过滤器提供输入数据。
  3. const AVFilterPad *outputs
    功能概述:该成员是一个指向常量 AVFilterPad 结构体数组的指针,用于描述过滤器的输出端口。输出端口定义了过滤器处理后输出数据的类型和数量,是过滤器将处理结果传递给其他过滤器或输出到外部的出口。与输入端口类似,不同的过滤器可能有不同数量和类型的输出端口。
    示例说明:还是以 overlay 过滤器为例,它只有一个输出端口,用于输出叠加后的视频。outputs 数组会包含一个 AVFilterPad 结构体,描述该输出端口的属性,如输出的媒体类型、端口名称等。在过滤器图中,这个输出端口可以连接到其他过滤器的输入端口,以便进一步处理叠加后的视频,或者直接作为最终的输出。

AVFilterContext
功能: FFmpeg 中表示一个具体过滤器实例的结构体,它将抽象的 AVFilter 实例化,包含了过滤器运行所需的状态和配置信息
重要成员:

struct AVFilterContext
{
const AVFilter *filter;//
char *name;
AVFilterPad *input_pads;
AVFilterLink **inputs;//输入连接
unsigned nb_inputs
AVFilterPad *output_pads;//输出连接
AVFilterLink **outputs;
unsigned nb_outputs;
struct AVFilterGraph *graph; // 从属于哪个AVFilterGraph
}
  1. const AVFilter *filter
    功能:指向 AVFilter 结构体的指针,用于指定该过滤器上下文所对应的抽象过滤器。AVFilter 定义了过滤器的通用属性和行为,而 AVFilterContext 则是其具体的运行实例。通过这个指针,AVFilterContext 能够调用 AVFilter 中定义的各种操作函数,实现具体的音视频处理功能。
    示例:若 filter 指向 scale 过滤器,那么该 AVFilterContext 就会按照 scale 过滤器的逻辑对输入的视频进行缩放处理。
  2. char *name
    功能:存储该过滤器上下文的名称,这是一个用户自定义的字符串,用于在调试、日志记录或过滤器图的管理中标识该过滤器实例。有了这个名称,在复杂的过滤器图中可以更方便地识别和定位特定的过滤器。
    示例:在一个包含多个 scale 过滤器的过滤器图中,可以将其中一个 scale 过滤器上下文命名为 “scale_main”,另一个命名为 “scale_preview”,这样就能清晰地区分它们。
  3. AVFilterPad *input_pads
    功能:指向 AVFilterPad 结构体数组的指针,描述了该过滤器的输入端口。AVFilterPad 定义了每个输入端口的属性,如支持的媒体类型(音频或视频)、端口名称等。通过这些输入端口,过滤器可以接收外部传入的音视频数据。
    示例:对于 overlay 过滤器,其 input_pads 数组可能包含两个 AVFilterPad,分别对应背景视频和要叠加的视频的输入端口。
  4. AVFilterLink **inputs
    功能:指向 AVFilterLink 指针数组的指针,每个 AVFilterLink 代表一个输入连接,用于连接该过滤器与其他过滤器的输出。通过这些连接,音视频数据可以从上游过滤器流入当前过滤器。
    示例:如果当前过滤器是 crop 过滤器,其 inputs 数组中的某个 AVFilterLink 可能连接到 scale 过滤器的输出,这样 scale 过滤器处理后的视频数据就可以流入 crop 过滤器进行裁剪处理。
  5. unsigned nb_inputs
    功能:表示 inputs 数组中实际存在的输入连接的数量,即该过滤器当前有多少个有效的输入。通过这个成员,可以准确知道过滤器接收数据的来源数量。
    示例:若 nb_inputs 的值为 2,说明该过滤器有两个输入连接,可能同时接收两个不同的音视频流进行处理。
  6. AVFilterPad *output_pads
    功能:指向 AVFilterPad 结构体数组的指针,描述了该过滤器的输出端口。与输入端口类似,AVFilterPad 定义了每个输出端口的属性,过滤器处理后的音视频数据将通过这些输出端口传递给其他过滤器或输出到外部。
    示例:scale 过滤器的 output_pads 数组通常只有一个 AVFilterPad,用于输出缩放后的视频数据。
  7. AVFilterLink **outputs
    功能:指向 AVFilterLink 指针数组的指针,每个 AVFilterLink 代表一个输出连接,用于连接该过滤器与其他过滤器的输入。通过这些连接,当前过滤器处理后的音视频数据可以流向其他过滤器进行进一步处理。
    示例:如果 scale 过滤器的 outputs 数组中的某个 AVFilterLink 连接到 overlay 过滤器的输入,那么 scale 过滤器缩放后的视频数据就会流入 overlay 过滤器进行叠加处理。
  8. unsigned nb_outputs
    功能:表示 outputs 数组中实际存在的输出连接的数量,即该过滤器当前有多少个有效的输出。通过这个成员,可以了解过滤器处理后的数据将流向多少个下游过滤器。
    示例:若 nb_outputs 的值为 1,说明该过滤器处理后的音视频数据只会流向一个下游过滤器。
  9. struct AVFilterGraph *graph
    功能:指向 AVFilterGraph 结构体的指针,用于指定该过滤器上下文所属的过滤器图。AVFilterGraph 是一个包含多个过滤器的集合,通过这个指针,AVFilterContext 可以与同一过滤器图中的其他过滤器进行交互,并且可以利用过滤器图提供的资源管理和调度功能。
    示例:在一个复杂的音视频处理流程中,多个过滤器可能会组成一个过滤器图,每个 AVFilterContext 都通过 graph 指针与这个过滤器图关联,共同完成音视频的处理任务。

AVFilterLink
功能:定义两个filters之间的联接
结构成员:

struct AVFilterLink
{
AVFilterContext *src;
AVFilterPad *srcpad;
AVFilterContext *dst;
AVFilterPad *dstpad;
struct AVFilterGraph *graph;
}

AVFilterPad
-定义filter的输⼊/输出接⼝
重要成员

struct AVFilterPad
{
const char *name;
AVFrame *(*get_video_buffer)(AVFilterLink *link, int w, int h);
AVFrame *(*get_audio_buffer)(AVFilterLink *link, int nb_samples);
int (*filter_frame)(AVFilterLink *link, AVFrame *frame);
AVFilterPad-定义filter的输⼊/输出接⼝22
int (*request_frame)(AVFilterLink *link);
}

AVFilterPad 结构体在 FFmpeg 里定义了过滤器的输入或输出接口,其成员规定了过滤器与外部交流数据的方式以及对数据进行处理的行为。下面详细解读该结构体的各个成员:

  1. const char *name
    功能:此成员是一个指向常量字符串的指针,代表输入或输出端口的名称。端口名称可用于在调试、日志记录以及过滤器图的构建过程中,对特定的输入或输出端口加以标识。
    示例:对于 overlay 过滤器,其输入端口可能分别命名为 “main” 和 “overlay”,以此表明哪个端口接收主视频流,哪个端口接收要叠加的视频流。
  2. AVFrame *(*get_video_buffer)(AVFilterLink *link, int w, int h);
    功能:这是一个函数指针,指向用于分配视频缓冲区的函数。当过滤器需要处理视频帧时,会调用此函数来获取一块合适的内存区域,用于存储视频数据。函数的参数包括一个 AVFilterLink 指针,它表示当前的连接,以及视频帧的宽度 w 和高度 h。
    示例:若一个 scale 过滤器要对输入的视频进行缩放处理,在处理过程中就会调用该函数来分配一个新的视频缓冲区,以存储缩放后的视频帧。
  3. AVFrame *(*get_audio_buffer)(AVFilterLink *link, int nb_samples);
    功能:同样是一个函数指针,不过指向的是用于分配音频缓冲区的函数。当过滤器需要处理音频帧时,会调用此函数来获取一块合适的内存区域,用于存储音频数据。函数的参数包含一个 AVFilterLink 指针以及音频帧中的样本数量 nb_samples。
    示例:在音频混音过滤器中,当需要处理多个音频流时,会调用该函数为混音后的音频数据分配缓冲区。
  4. int (*filter_frame)(AVFilterLink *link, AVFrame *frame);
    功能:这是一个函数指针,指向实际处理音视频帧的函数。当有新的音视频帧到达过滤器的输入端口时,会调用此函数对该帧进行处理。函数接收一个 AVFilterLink 指针和一个 AVFrame 指针作为参数,处理完成后返回一个整数值,表示处理结果(如成功或失败)。
    示例:在 crop 过滤器中,filter_frame 函数会对输入的视频帧进行裁剪操作,并将裁剪后的视频帧传递给下一个过滤器。
  5. int (*request_frame)(AVFilterLink *link);
    功能:这是一个函数指针,指向用于请求新帧的函数。当过滤器需要从上游过滤器获取新的音视频帧时,会调用此函数。函数接收一个 AVFilterLink 指针作为参数,返回一个整数值,表示请求结果(如成功、失败或需要更多数据)。
    示例:在一个视频播放流程中,当解码器将所有已解码的视频帧都传递给下游过滤器后,下游过滤器可能会调用 request_frame 函数,请求解码器继续解码并提供新的视频帧。

AVFilterInOut
过滤器链输⼊/输出的链接列表

常见的过滤器
以下是FFmpeg中常见的过滤器及其语法表格:

过滤器名称功能语法示例
scale对视频进行缩放处理scale=w:h
例如:scale=640:480 ,将视频缩放到宽640像素,高480像素;
scale=iw/2:ih/2 ,将视频宽高缩小为原来的一半,iwih 分别表示输入视频的宽和高。
crop裁剪视频画面crop=w:h:x:y
例如:crop=320:240:100:50 ,从视频中裁剪出宽320像素、高240像素的区域,起始坐标为 (x=100, y=50)
overlay将一个视频叠加到另一个视频上overlay=x:y
例如:overlay=10:20 ,将第二个输入视频叠加到第一个输入视频上,叠加位置为 (x=10, y=20)
fps改变视频的帧率fps=fps
例如:fps=25 ,将视频帧率设置为25帧每秒。
eq调整视频的亮度、对比度、饱和度等参数eq=contrast:brightness:saturation:gamma:gamma_r:gamma_g:gamma_b:gamma_weight
例如:eq=1.2:0.1:1.5 ,将对比度设置为1.2,亮度设置为0.1,饱和度设置为1.5。
hflip水平翻转视频画面hflip
无需额外参数,直接使用 hflip 即可将视频水平翻转。
vflip垂直翻转视频画面vflip
无需额外参数,直接使用 vflip 即可将视频垂直翻转。
volume调整音频的音量volume=volume
例如:volume=0.5 ,将音频音量降低为原来的一半;volume=2dB ,将音频音量提高2分贝。
pan调整音频声道布局pan=out_channel_layout:gain_matrix
例如:`pan=stereo
afade实现音频淡入淡出效果afade=type:start_time:duration
例如:afade=t=in:st=0:d=5 ,从音频开始处进行5秒的淡入效果;afade=t=out:st=10:d=3 ,从第10秒开始进行3秒的淡出效果。

这些只是FFmpeg众多过滤器中的一部分,每个过滤器还可能有更多的参数和用法,可以通过 ffmpeg -h filter=filter_name 命令查看具体过滤器的详细信息。

4.2 简单过滤器

这⾥需要重点提的是两个特别的filter,⼀个是buffer,⼀个是buffersink,
滤波器buffer代表filter graph中的源头,原始数据就往这个filter节点输⼊的;
⽽滤波器buffersink代表filter graph中的输出节点,处理完成的数据从这个filter节点输出

在这里插入图片描述


step1初始化

avfilter_register_all();AVFilterGraph* filter_graph = avfilter_graph_alloc();sprintf(args,"video_size=%dx%d:pix_fmt=%d:time_base=%d/%d:pixel_aspect=%d/%d",in_width, in_height, AV_PIX_FMT_YUV420P,1, 25, 1, 1);
AVFilter* bufferSrc = avfilter_get_by_name("buffer");   // AVFilterGraph的输入源 avfilter_get_by_name是 FFmpeg 提供的一个函数,其作用是根据滤镜的名称来查找并返回对应的AVFilter对象。
AVFilterContext* bufferSrc_ctx;//AVFilterContext结构体代表了一个滤镜的实例,每个滤镜在实际使用时都需要创建一个对应的
ret = avfilter_graph_create_filter(&bufferSrc_ctx, bufferSrc, "in", args, NULL, filter_graph);//初始化AVBufferSinkParams *bufferSink_params;AVFilterContext* bufferSink_ctx;AVFilter* bufferSink = avfilter_get_by_name("buffersink");//AV_PIX_FMT_YUV420P 作为支持的像素格式,AV_PIX_FMT_NONE 作为数组的结束标志。这意味着该 buffersink 滤镜只会接收 YUV420P 格式的视频帧。enum AVPixelFormat pix_fmts[] = { AV_PIX_FMT_YUV420P, AV_PIX_FMT_NONE };bufferSink_params = av_buffersink_params_alloc();bufferSink_params->pixel_fmts = pix_fmts;ret = avfilter_graph_create_filter(&bufferSink_ctx, bufferSink, "out", NULL,bufferSink_params, filter_graph);

int avfilter_graph_create_filter(AVFilterContext **filt_ctx, const AVFilter *filt, const char *name, const char *args, void *opaque, AVFilterGraph *graph_ctx);

AVFilterContext **filt_ctx
类型:指向 AVFilterContext 指针的指针。
功能:该参数用于存储创建好的过滤器上下文的指针。函数执行成功后,会将新创建的过滤器上下文的地址赋给 *filt_ctx。通过这个指针,后续就可以对该过滤器上下文进行操作,比如设置参数、连接其他过滤器等。
示例:调用函数前声明一个 AVFilterContext 指针变量 bufferSrc_ctx,然后将 &bufferSrc_ctx 作为参数传入函数,函数成功执行后,bufferSrc_ctx 就指向了新创建的过滤器上下文。
const AVFilter *filt
类型:指向 AVFilter 结构体的常量指针。
功能:指定要创建的过滤器的类型。AVFilter 结构体定义了过滤器的基本属性和操作函数,这个参数告诉函数需要创建哪种类型的过滤器实例。
示例:如果要创建一个 buffer 过滤器(常用于将数据输入到过滤器图中),可以先通过 avfilter_get_by_name(“buffer”) 获取 buffer 过滤器的 AVFilter 指针,然后将其作为该参数传入函数。
const char *name
类型:指向常量字符数组的指针。
功能:为新创建的过滤器上下文指定一个名称。这个名称主要用于调试、日志记录和在过滤器图中标识该过滤器实例。名称可以自定义,方便在复杂的过滤器图中区分不同的过滤器。
示例:可以将名称设置为 “in”,表示这个过滤器是输入过滤器,用于标识它在过滤器图中的作用。
const char *args
类型:指向常量字符数组的指针。
功能:用于传递初始化过滤器所需的参数。这些参数以字符串形式表示,不同的过滤器有不同的参数格式和含义。函数会根据这些参数对过滤器进行初始化设置。
示例:对于 buffer 过滤器,args 可能包含输入视频的宽度、高度、像素格式等信息,例如 “video_size=640x480:pix_fmt=rgb24:time_base=1/25”。
void *opaque
类型:通用指针。
功能:该参数通常用于传递一些额外的上下文信息,不过在大多数情况下可以设置为 NULL。某些过滤器可能会使用这个指针来访问自定义的数据或回调函数,但这取决于具体的过滤器实现。
示例:如果没有额外的上下文信息需要传递,就将其设置为 NULL。
AVFilterGraph *graph_ctx
类型:指向 AVFilterGraph 结构体的指针。
功能:指定新创建的过滤器上下文所属的过滤器图。过滤器图用于管理和组织多个过滤器上下文,将过滤器添加到过滤器图中后,它们可以相互连接,形成一个完整的音视频处理流程。
示例:先创建一个 AVFilterGraph 实例 filter_graph,然后将其作为该参数传入函数,这样新创建的过滤器上下文就会被添加到这个过滤器图中

在 FFmpeg 里,bufferSink_params 是为 buffersink 过滤器配置相关参数用的,它对应的类型是 AVBufferSinkParams 结构体

在处理视频时,像素格式是关键信息,不同的视频源或者处理步骤可能会产生不同像素格式的视频帧,像 AV_PIX_FMT_YUV420P、AV_PIX_FMT_RGB24 等。借助 bufferSink_params 能够明确 buffersink 过滤器所支持的像素格式。
要是处理的是音频数据,bufferSink_params 还能用于配置音频相关的参数,例如采样率、声道布局、样本格式等。通过指定这些参数,可以让 buffersink 过滤器接收特定格式的音频数据。


step2 滤镜图

 AVFilter *cropFilter = avfilter_get_by_name("crop");AVFilterContext *cropFilter_ctx;ret = avfilter_graph_create_filter(&cropFilter_ctx, cropFilter, "crop","out_w=iw:out_h=ih/2:x=0:y=0", NULL, filter_graph);if (ret < 0) {printf("Fail to create crop filter\n");return -1;}// vflip filter//flip 滤镜使用起来非常方便,只需指定滤镜名称就能完成垂直翻转任务。AVFilter *vflipFilter = avfilter_get_by_name("vflip");AVFilterContext *vflipFilter_ctx;ret = avfilter_graph_create_filter(&vflipFilter_ctx, vflipFilter, "vflip", NULL, NULL, filter_graph);if (ret < 0) {printf("Fail to create vflip filter\n");return -1;}// overlay filter//overlay 滤镜用于将一个视频叠加到另一个视频之上//main_w:表示主视频的宽度。
//    overlay_w:表示叠加视频的宽度。
//    main_h:表示主视频的高度。
//    overlay_h:表示叠加视频的高度。AVFilter *overlayFilter = avfilter_get_by_name("overlay");AVFilterContext *overlayFilter_ctx;ret = avfilter_graph_create_filter(&overlayFilter_ctx, overlayFilter, "overlay","y=0:H/2", NULL, filter_graph);if (ret < 0) {printf("Fail to create overlay filter\n");return -1;}

分别初始化过滤器


step3 配置滤镜图

 // src filter to split filter
//    // 将 sourceFilter1_ctx 的输出垫连接到 overlayFilter_ctx 的第一个输入垫
//    int ret1 = avfilter_link(sourceFilter1_ctx, 0, overlayFilter_ctx, 0);
//    // 将 sourceFilter2_ctx 的输出垫连接到 overlayFilter_ctx 的第二个输入垫
//    int ret2 = avfilter_link(sourceFilter2_ctx, 0, overlayFilter_ctx, 1);ret = avfilter_link(bufferSrc_ctx, 0, splitFilter_ctx, 0);if (ret != 0) {printf("Fail to link src filter and split filter\n");return -1;}// split filter's first pad to overlay filter's main padret = avfilter_link(splitFilter_ctx, 0, overlayFilter_ctx, 0);if (ret != 0) {printf("Fail to link split filter and overlay filter main pad\n");return -1;}// split filter's second pad to crop filterret = avfilter_link(splitFilter_ctx, 1, cropFilter_ctx, 0);if (ret != 0) {printf("Fail to link split filter's second pad and crop filter\n");return -1;}// crop filter to vflip filterret = avfilter_link(cropFilter_ctx, 0, vflipFilter_ctx, 0);if (ret != 0) {printf("Fail to link crop filter and vflip filter\n");return -1;}// vflip filter to overlay filter's second padret = avfilter_link(vflipFilter_ctx, 0, overlayFilter_ctx, 1);if (ret != 0) {printf("Fail to link vflip filter and overlay filter's second pad\n");return -1;}// overlay filter to sink filterret = avfilter_link(overlayFilter_ctx, 0, bufferSink_ctx, 0);if (ret != 0) {printf("Fail to link overlay filter and sink filter\n");return -1;}ret = avfilter_link(overlayFilter_ctx, 0, bufferSink_ctx, 0);
ret = avfilter_graph_config(filter_graph, NULL);

avfilter_graph_config 检测滤镜图是不是配置完整


step4 处理帧

   if (av_buffersrc_add_frame(bufferSrc_ctx, frame_in) < 0) {printf("Error while add frame.\n");break;}// filter内部自己处理/* pull filtered pictures from the filtergraph */ret = av_buffersink_get_frame(bufferSink_ctx, frame_out);if (ret < 0)break;

4.3 复杂滤镜图

int init_filters(const AVFrame* main_frame, const AVFrame* logo_frame, int x, int y)
{int ret = 0;//用于表示滤镜图的输入和输出AVFilterInOut *inputs = NULL;AVFilterInOut *outputs = NULL;char filter_args[1024] = { 0 };filter_graph = avfilter_graph_alloc();if (!filter_graph) {printf("Error: allocate filter graph failed\n");return -1;}snprintf(filter_args, sizeof(filter_args),"buffer=video_size=%dx%d:pix_fmt=%d:time_base=1/25:pixel_aspect=%d/%d[main];" // Parsed_buffer_0"buffer=video_size=%dx%d:pix_fmt=%d:time_base=1/25:pixel_aspect=%d/%d[logo];" // Parsed_bufer_1"[main][logo]overlay=%d:%d[result];" // Parsed_overlay_2"[result]buffersink", // Parsed_buffer_sink_3main_frame->width, main_frame->height, main_frame->format, main_frame->sample_aspect_ratio.num, main_frame->sample_aspect_ratio.den,logo_frame->width, logo_frame->height, logo_frame->format, logo_frame->sample_aspect_ratio.num, logo_frame->sample_aspect_ratio.den,x, y);ret = avfilter_graph_parse2(filter_graph, filter_args, &inputs, &outputs);//描述滤镜图if (ret < 0) {printf("Cannot parse graph\n");return ret;}ret = avfilter_graph_config(filter_graph, NULL);if (ret < 0) {printf("Cannot configure graph\n");return ret;}// Get AVFilterContext from AVFilterGraph parsing from stringmainsrc_ctx = avfilter_graph_get_filter(filter_graph, "Parsed_buffer_0");logosrc_ctx = avfilter_graph_get_filter(filter_graph, "Parsed_buffer_1");resultsink_ctx = avfilter_graph_get_filter(filter_graph, "Parsed_buffersink_3");return 0;
}

filter的语法
当滤波过程复杂到⼀定程度时,即需要多个滤波器进⾏复杂的连接来实现整个滤波过程,这时候对于
调⽤者来说,继续采⽤上述⽅法来构建滤波图就显得不够效率。对于复杂的滤波过程,ffmpeg提供了⼀个更为⽅便的滤波过程创建⽅式。
这种复杂的滤波器过程创建⽅式要求⽤户以字符串的⽅式描述各个滤波器之间的关系
关于语法解析
[variable]这个是一个输入或输出标签,一般语法就是 输入 filter=命令参数 输出
但是buffer没有输入,buffer_sink没有输出
int avfilter_graph_parse2(AVFilterGraph *graph, const char *filters, AVFilterInOut **inputs, AVFilterInOut **outputs);

AVFilterGraph *graph
类型:指向 AVFilterGraph 结构体的指针。
功能:表示要将解析后的过滤器添加到的目标过滤器图。AVFilterGraph 是 FFmpeg 中用于管理和组织多个过滤器上下文的结构体,所有解析得到的过滤器都会被添加到这个图中,以便后续进行音视频处理。
const char *filters
类型:指向常量字符数组的指针。
功能:该参数是一个包含过滤器图描述的字符串。字符串中描述了过滤器的名称、参数以及它们之间的连接关系。例如 “scale=640:480,crop=320:240💯50” 表示先使用 scale 过滤器将视频缩放到 640x480,再使用 crop 过滤器从缩放后的视频中裁剪出 320x240 的区域,起始坐标为 (100, 50)。
AVFilterInOut **inputs
类型:指向 AVFilterInOut 指针的指针。
功能:用于返回过滤器图的输入列表。AVFilterInOut 结构体表示过滤器图的输入或输出点,包含名称和对应的过滤器上下文等信息。函数执行成功后,*inputs 会指向一个链表,链表中的每个节点表示一个输入点。如果不需要输入信息,可以将其设置为 NULL。
AVFilterInOut **outputs
类型:指向 AVFilterInOut 指针的指针。
功能:用于返回过滤器图的输出列表。与 inputs 类似,函数执行成功后,*outputs 会指向一个链表,链表中的每个节点表示一个输出点。如果不需要输出信息,可以将其设置为 NULL。

相关文章:

剖析 FFmpeg:从基本功能到过滤器,实现音视频处理的灵活性

目录 1.解复用2 解码2.1 音频解码2.2 视频解码 3 修饰3.1 avio3.2 重采样 4 过滤器4.1 过滤器基本知识4.2 简单过滤器4.3 复杂滤镜图 1.解复用 解复用就是把容器中的媒体流分离出来&#xff0c;方便我们对媒体流处理。 step1&#xff1a;对媒体文件上下文初始化 AVFormatCont…...

maven如何搭建自己的私服(LINUX版)?

环境准备 安装 JDK &#xff1a;确保系统已安装 JDK 8 或更高版本。可以通过以下命令安装 JDK&#xff1a; 安装 OpenJDK &#xff1a;sudo apt update && sudo apt install openjdk-11-jdk 安装 Oracle JDK &#xff1a;需要添加第三方仓库&#xff0c;例如 WebUpd8 …...

机器视觉的手机FPC油墨丝印应用

在现代智能手机制造过程中&#xff0c;精密的组件装配和质量控制是确保产品性能和用户体验的关键。其中&#xff0c;柔性印刷电路板&#xff08;FPC&#xff09;的油墨丝印工艺尤为关键&#xff0c;它不仅影响到电路板的美观&#xff0c;更直接关系到电路的导电性能和可靠性。而…...

AI原生手机:三大技术阵营的终极对决与未来展望

引言&#xff1a;AI手机时代的真正到来 2024年&#xff0c;智能手机行业迎来了一个历史性转折点——AI原生手机从概念走向主流。根据IDC最新报告&#xff0c;中国AI手机出货量同比激增591%&#xff0c;渗透率从2023年的3%飙升至22%。这一数据背后&#xff0c;是手机厂商在硬件…...

CFCA受邀参加盛京银行手机银行7.0发布会

4月30日&#xff0c;盛京银行举办手机银行7.0发布会。 盛京银行手机银行7.0围绕“慧享生活&#xff0c;财富随行”主题&#xff0c;聚焦便捷体验、财富管理、惠民生活&#xff0c;构建12大类服务&#xff0c;升级142项功能&#xff0c;全新设置信用卡频道&#xff0c;推出“云…...

IT/OT 融合架构下的工业控制系统安全攻防实战研究

1. 引言 随着工业 4.0 和智能制造的浪潮席卷全球&#xff0c;信息技术 (IT) 与运营技术 (OT) 的融合已成为不可逆转的趋势。这种融合旨在通过实时数据交换和分析&#xff0c;打破传统的信息孤岛&#xff0c;显著提升生产效率、优化决策、降低运营成本并增强市场竞争力。IT 系统…...

AI优化高频PCB信号完整性:猎板PCB的技术突破与应用实践

随着5G通信、AI服务器及新能源汽车的快速发展&#xff0c;高频PCB的信号完整性已成为决定电子产品性能的关键。本文以猎板PCB的技术实践为例&#xff0c;解析如何通过AI算法与精密制造工艺的结合&#xff0c;实现高频信号传输的极致优化&#xff0c;为行业提供高可靠性的解决方…...

【Bluedroid】蓝牙 SDP(服务发现协议)模块代码解析与流程梳理

本文深入剖析Bluedroid蓝牙协议栈中 SDP&#xff08;服务发现协议&#xff09;服务记录的全生命周期管理流程&#xff0c;涵盖初始化、记录创建、服务搜索、记录删除等核心环节。通过解析代码逻辑与数据结构&#xff0c;揭示各模块间的协作机制&#xff0c;包括线程安全设计、回…...

obj = null; 赋值null之前没有其他引用指向obj对象,那么,当obj=null时,会被垃圾回收机制立即回收吗?

不会立即回收。 具体原因是&#xff1a; 赋值 obj null; 后&#xff0c;对象变成“不可达”&#xff0c;符合垃圾回收条件&#xff0c;但垃圾回收器并不会立刻回收它。垃圾回收是CLR自动控制的非确定性过程&#xff0c;什么时候执行回收取决于系统内存压力、GC策略、分代情况…...

Android 数据持久化之 文件存储

在 Android 开发中&#xff0c;存储文件是一个常见的需求。 本文中介绍 openFileOutput 和 File 两种不同的方式来操作文件。 一、File 方式 根据文件的存储位置和访问权限&#xff0c;可以将文件存储分为内部存储&#xff08;Internal Storage&#xff09;和外部存储&#x…...

差分OPA verilogaA 模型

做电路设计&#xff0c;需要提前用理想模型如VerilogA模型做验证。这里分享一个由ahdlib库里单端opamp改造而来的差分opamp。参考何乐年的《模拟集成电路设计与仿真》10.4节423页&#xff1b; 描述的小信号模型如上。 VerilogA 用到了SRI/C&#xff0c;GBWgm/C,gaingm*r1等概念…...

oracle goldengate非并行进程转换为并行进程

oracle goldengate非并行进程转换为并行进程 在上一期的文章中写道了直接创建并行进程的方式对大事务进行分解&#xff0c;这对于新建立同步进程的时候提前规划是很有帮助的&#xff0c;但是如果对已经进行了同步的进程重新建立需要耗时比较长&#xff0c;Oracle提供了非并行进…...

58.[前端开发-前端工程化]Day05-webpack-Git安装-配置-Git命令

Git版本控制工具详解 1 邂逅版本控制工具 认识版本控制&#xff08;版本控制&#xff09; 版本控制的功能 版本控制的历史 2 集中式和分布式区别 集中式版本控制 分布式版本控制 3 Git的环境安装搭建 Git的安装 Bash – CMD – GUI 区别 Git的配置分类 Git的配置选项 Git的…...

CF每日5题

每日刷题两小时颐养天年 1855A 800 思维 将不高兴的同学计数cnt 不高兴的同学之间两两交换&#xff0c;一定不会在 p i i p_ii pi​i的位置上&#xff0c;贡献是cnt/2 如果cnt%2>0&#xff0c;那就多交换一次 void solve() {int n;cin>>n;int cnt0;forr(i,1,n){in…...

Redis实现分布式获取全局唯一自增ID的案例。

【1】简易自增版本(从 1 开始 1,2,3&#xff0c;...) 项目结构 下面是一个基于 RedisTemplate 实现的分布式全局唯一自增 ID 生成器的案例。适用于 Java Spring Boot 环境&#xff0c;利用 Redis 的原子操作 INCR 指令。 ✅ 原理说明 Redis 提供的 INCR 命令是原子性的&…...

创建型模式:工厂方法(Factory Method)模式

一、简介 工厂方法(Factory Method)模式是一种创建型设计模式,它定义了一个创建对象的接口,但让子类决定实例化哪一个类。工厂方法使一个类的实例化延迟到其子类。在 C# 中,工厂方法模式提供了一种更灵活的对象创建方式,将对象的创建和使用分离,提高了代码的可维护性和…...

大型语言模型在网络安全领域的应用综述

大型语言模型在网络安全领域的应用综述 简介1. 引言1.1 背景与意义1.2 LLMs 的基本概念1.3 LLMs 在网络安全中的优势1.4 报告目标 2. 文献综述方法2.1 研究问题2.2 文献检索策略2.3 文献筛选标准 3. LLMs 在网络安全领域的应用3.1 软件和系统安全 (Software and System Securit…...

TDEngine 与 Grafana

目录 实践目录 Grafana 参考文档 实践目录 10.60.100.194&#xff1a;/home/dualven/tdengine Grafana systemctl status grafana-server http://10.60.100.194:3000/ 这个端口与mydoor的new server服务冲突 &#xff08;同时只开一个&#xff09; 参考文档 运行监…...

iPhone手机连接WiFi异常解决方法

iPhone手机连接WiFi异常解决方法 一、问题现象二、iPhone连不上可能的原因三、基础排查与快速修复第一步:重启大法第二步:忽略网络,重新认证第三步:关闭“私有无线局域网地址”第四步:修改DNS服务器第五步:还原网络设置四、路由器端排查及设置关闭MAC地址过滤或添加到白名…...

微服务不注册到nacos的方法

引言:在开发中,有时候多个开发一起开发,可能会同时注册到dev环境中,这样可能会影响dev环境,那么在idea添加2个参数即可解决 spring.cloud.nacos.discovery.register-enabled falsespring.cloud.nacos.discovery.enabled false...

Spring Boot + Vue 实现在线视频教育平台

一、项目技术选型 前端技术&#xff1a; HTML CSS JavaScript Vue.js 前端框架 后端技术&#xff1a; Spring Boot 轻量级后端框架 MyBatis 持久层框架 数据库&#xff1a; MySQL 5.x / 8.0 开发环境&#xff1a; IDE&#xff1a;Eclipse / IntelliJ IDEA JDK&…...

【嵌入式开发-SPI】

嵌入式开发-SPI ■ SPI简介■ SPI &#xff08;Standard SPI&#xff09;■ DSPI &#xff08;Dual SPI&#xff09;■ QSPI是 Queued SPI的简写 ■ SPI简介 SPI协议其实是包括&#xff1a;Standard SPI、Dual SPI和Queued SPI三种协议接口&#xff0c;分别对应3-wire, 4-wire…...

【链表扫盲】FROM GPT

链表是一种线性数据结构&#xff0c;由节点&#xff08;Node&#xff09;组成&#xff0c;每个节点包含两个部分&#xff1a; 数据域&#xff08;data&#xff09;&#xff1a; 存储节点值。指针域&#xff08;next&#xff09;&#xff1a; 存储指向下一个节点的引用。 链表…...

如何在macOS上通过SSHFS挂载远程文件系统

在macOS系统中&#xff0c;想要便捷地访问远程计算机上的目录&#xff1f;借助SSH文件系统&#xff08;SSHFS&#xff09;就能轻松实现。SSHFS是一款文件系统客户端&#xff0c;它基于SSH文件传输协议&#xff08;SFTP&#xff09;建立安全连接&#xff0c;进而实现对远程文件的…...

Android studio profiler使用

主要讲内存泄露排查 1、把怀疑内存泄露的页面都跑一边&#xff0c;然后回到初始页面 2、打开profile的home&#xff0c;找到Analysis Memory Usage&#xff0c;点击右下角start profiler task&#xff0c;开始分析内存&#xff0c;等待分析完成&#xff0c;分析过程中页面是卡…...

排序算法-选择排序

选择排序是一种简单直观的排序算法&#xff0c;其核心思想是每次从未排序的部分中选出最小&#xff08;或最大&#xff09;的元素&#xff0c;放到已排序部分的末尾。 选择排序步骤 初始化&#xff1a;将序列分为已排序部分&#xff08;初始为空&#xff09;和未排序部分&…...

云计算的基础概论

一、云计算基础概念 1. 云计算定义 • 英文&#xff1a;Cloud Computing • 定义&#xff1a;通过互联网&#xff08;Internet&#xff09;按需提供可扩展的计算资源&#xff08;如服务器、存储、数据库、网络、软件等&#xff09;&#xff0c;用户无需管理底层基础设施。 …...

仿LISP运算 - 华为OD机试真题(A卷、JavaScript题解)

华为OD机试题库《C》限时优惠 9.9 华为OD机试题库《Python》限时优惠 9.9 华为OD机试题库《JavaScript》限时优惠 9.9 针对刷题难&#xff0c;效率慢&#xff0c;我们提供一对一算法辅导&#xff0c; 针对个人情况定制化的提高计划&#xff08;全称1V1效率更高&#xff09;。 看…...

数据透视表控件DHTMLX Pivot v2.1发布,新增HTML 模板、增强样式等多个功能

DHTMLX Pivot数据透视表能快速地对数据进行计数、总计、平均和执行许多其他操作。近日&#xff0c;DHTMLX Pivot发布了2.1版本&#xff0c;该版本扩展了开发人员通过新增的 CSS 样式选项、HTML 模板以及数字和日期的自定义格式修改表格外观的能力。此外&#xff0c;该版本还增加…...

简易的考试系统设计(Web实验)

简易的考试系统设计&#xff08;Web实验&#xff09; 1.实验内容与设计思想&#xff08;一&#xff09;实验需求&#xff08;二&#xff09;设计思路 2.代码展示3.实验小结 1.实验内容与设计思想 &#xff08;一&#xff09;实验需求 1.编写两个页面程序&#xff0c;一个HTML…...

C++之set和map的运用

目录 序列式容器和关联式容器 熟识set 在STL中的底层结构&#xff1a; set的构造和迭代器 set的增删查 multiset和set的差异 练习题&#xff1a; 熟识map map类的介绍 pair类型介绍 map的构造 map的增删查 map的数据修改 测试样例&#xff1a; multimap和map的差…...

基于智能家居项目 RGB彩灯(P9813)

一、P9813 是什么&#xff1f; P9813 是一颗专门用来控制 RGB LED灯珠 的芯片&#xff0c;也就是说&#xff0c;它能控制红色、绿色、蓝色三种灯光的亮度&#xff0c;从而调出各种颜色。它最常见的用途就是在各种“会变色”的灯带中。 它的通信方式非常简单&#xff0c;只需要…...

EMQX 作为 MQTT Broker,支持 ​MQTT over TCP​ 和 ​MQTT over WebSocket​ 两种协议

1. EMQX 支持的协议与端口​ 协议类型默认端口用途说明​MQTT over TCP​1883标准的 MQTT 协议&#xff0c;基于 TCP 传输&#xff08;用于后端服务、物联网设备等&#xff09;。​MQTT over TLS​8883加密的 MQTT over TCP&#xff08;TLS/SSL 加密&#xff0c;安全性更高&am…...

软件测试学习笔记

第1章 绪论 软件测试 本质上说&#xff0c;就是寻找软件的缺陷、错误&#xff0c;对其质量度量的方法与过程。软件测试的一切活动都围绕着两个目标&#xff08;验证是否符合需求&#xff0c;识别差异&#xff09;而行进。它是测试思维、策略方针、设计实施的基本出发点。 学…...

Vue3 + Node.js 实现客服实时聊天系统(WebSocket + Socket.IO 详解)

Node.js 实现客服实时聊天系统&#xff08;WebSocket Socket.IO 详解&#xff09; 一、为什么选择 WebSocket&#xff1f; 想象一下淘宝客服的聊天窗口&#xff1a;你发消息&#xff0c;客服立刻就能看到并回复。这种即时通讯效果是如何实现的呢&#xff1f;我们使用 Vue3 作…...

python 上海新闻爬虫

1. 起因&#xff0c; 目的: 继续做新闻爬虫。我之前写过。此文先记录2个新闻来源。后面打算进行过滤&#xff0c;比如只选出某一个类型新闻。 2. 先看效果 过滤出某种类型的新闻&#xff0c;然后生成 html 页面&#xff0c;而且&#xff0c;自动打开这个页面。 比如科技犯罪…...

【Axure高保真原型】中继器表格批量上传数据

今天和大家分享中继器表格批量上传数据的原型模板&#xff0c;效果包括&#xff1a; 点击上传按钮&#xff0c;可以真实的打开本地文件夹选择文件&#xff1b; 选择的文件如果不是表格格式&#xff08;xls、xlsx、xlt、csv&#xff09;&#xff0c;就会显示提示弹窗&#xff1…...

复刻低成本机械臂 SO-ARM100 单关节控制(附代码)

视频讲解&#xff1a; 复刻低成本机械臂 SO-ARM100 单关节控制&#xff08;附代码&#xff09; 代码仓库&#xff1a;GitHub - LitchiCheng/SO-ARM100: Some Test code on SO-ARM100 昨天用bambot的web的方式调试了整个机械臂&#xff0c;对于后面的仿真的sim2real来说&#x…...

视频编解码学习7之视频编码简介

视频编码技术发展历程与主流编码标准详解 视频编码技术是现代数字媒体领域的核心技术之一&#xff0c;它通过高效的压缩算法大幅减少了视频数据的体积&#xff0c;使得视频的存储、传输和播放变得更加高效和经济。从早期的H.261标准到最新的AV1和H.266/VVC&#xff0c;视频编码…...

【NextPilot日志移植】整体功能概要

整体日志系统的实现功能 该日志系统主要实现了飞行日志的记录功能&#xff0c;支持多种日志记录模式&#xff0c;可将日志存储到文件或通过 MAVLink 协议传输&#xff0c;同时具备日志加密、空间管理、事件记录等功能。具体如下&#xff1a; 日志记录模式&#xff1a;支持按武…...

Windows系统下使用Kafka和Zookeeper,Python运行kafka(二)

1.配置 Zookeeper 进入解压后的 Zookeeper 目录&#xff08;例如 F:\zookeeper\conf&#xff09;&#xff0c;复制 zoo_sample.cfg 文件并命名为 zoo.cfg&#xff08;如果 zoo.cfg 已经存在&#xff0c;则直接编辑该文件&#xff09;。 打开 zoo.cfg 文件&#xff0c;配置相关…...

《构建社交应用用户激励引擎:React Native与Flutter实战解析》

React Native凭借其与JavaScript和React的紧密联系&#xff0c;为开发者提供了一个熟悉且灵活的开发环境。在构建用户等级体系时&#xff0c;它能够充分利用现有的前端开发知识和工具。通过将用户在社交应用中的各种行为进行量化&#xff0c;比如发布动态的数量、点赞评论的次数…...

Oracle OCP认证考试考点详解083系列13

题记&#xff1a; 本系列主要讲解Oracle OCP认证考试考点&#xff08;题目&#xff09;&#xff0c;适用于19C/21C,跟着学OCP考试必过。 61. 第61题&#xff1a; 题目 解析及答案&#xff1a; 关于基于RPM的Oracle数据库安装&#xff0c;以下哪两项是正确的&#xff1f; A) …...

【AI】DeepWiki 页面转换成 Markdown 保存 - Chrome 扩展

GitHub: https://github.com/zxmfke/deepwiki-md-chrome-extension 背景 个人比较喜欢整理项目架构&#xff0c;更多都是保存成 markdown 的格式保存&#xff0c;然后发博客。deepwiki 刚好把 github 仓库代码的架构输出出来了&#xff0c;不过没有办法下载成 markdown 格式&…...

HTTP 状态码是服务器对客户端请求的响应标识,用于表示请求的处理结果

以下是完整的 HTTP 状态码分类和常见状态码详解&#xff1a; 一、状态码分类&#xff08;5大类&#xff09; 分类范围描述常见场景1xx100-199信息性响应请求已被接收&#xff0c;继续处理2xx200-299成功响应请求成功处理3xx300-399重定向响应需要进一步操作4xx400-499客户端错…...

【AI论文】FlexiAct:在异构场景中实现灵活的动作控制

摘要&#xff1a;动作定制涉及生成视频&#xff0c;其中主体执行由输入控制信号指示的动作。 当前的方法使用姿势引导或全局运动定制&#xff0c;但受到空间结构&#xff08;如布局、骨架和视点一致性&#xff09;严格约束的限制&#xff0c;降低了在不同主题和场景下的适应性。…...

ubuntu24.04安装anaconda

1. ubuntu安装ananconda 进入官网&#xff1a;添加链接描述 直接点击Download下载&#xff0c;它会自动匹配合适的版本 打开保存下载文件&#xff0c;点击右键&#xff0c;选择在终端打开&#xff0c;输入 bash Anaconda3-2024.10-1-Linux-x86_64.sh不断点击Enter&#xff0c…...

六、Hadoop初始化与启动

成功部署一个Hadoop集群并不仅仅是安装好软件那么简单。在它真正能够为我们处理海量数据之前&#xff0c;还需要一系列精心的初始化和启动步骤。这些步骤确保了各个组件能够正确协同工作。完成启动后&#xff0c;Hadoop还提供了便捷的 Web 用户界面 (Web UI)&#xff0c;帮助我…...

边缘网关(边缘计算)

边缘网关是边缘计算架构中的关键组件&#xff0c;充当连接终端设备&#xff08;如传感器、IoT设备&#xff09;与云端或核心网络的桥梁。它在数据源头附近进行实时处理、分析和过滤&#xff0c;显著提升效率并降低延迟。 核心功能 协议转换 ○ 支持多种通信协议&#xff08;如…...

学成在线之课程管理

一&#xff1a;业务概述 我负责的课程管理这一块&#xff0c;可以发布课程&#xff0c;可以对课程列表进行一个管理&#xff0c;发布课程这分为三步&#xff1a;首先是需要进行填写课程相关的信息&#xff0c;再设计这个课程的大纲&#xff0c;最后是选择发布这门课程&#xff…...