FFMPEG利用H264+AAC合成TS文件

news2025/3/6 8:16:53

        本次的DEMO是利用FFMPEG框架把H264文件和AAC文件合并成一个TS文件。这个DEMO很重要,因为在后面的推流项目中用到了这方面的技术。所以,大家最好把这个项目好好了解。

        下面这个是流程图

        从这个图我们能看出来,在main函数中我们主要做了这几步: 

        1.创建read_video_file_thread(读取视频文件线程)。

        2. 创建read_audio_file_thread(读取音频文件线程)。

        3.初始化音视频队列,初始化ffmpeg的 H264,aac合成模块的容器。

        4.创建muxer_ts_thread线程进行H264文件和aac文件的合成ts文件。

 每一个代码模块的讲解:

        初始化视频队列、音频队列、FFMPEG合成容器。

        在代码第一步就需要把所有东西给初始化,包括队列、FFMPEG输出容器。

int main
{
    // 为音频和视频队列对象分配内存
    video_queue = new VIDEO_QUEUE();
    audio_queue = new AUDIO_QUEUE();

    // 为ffmpeg_group结构体分配内存
    FFMPEG_CONFIG *ffmpeg_config = (FFMPEG_CONFIG *)malloc(sizeof(FFMPEG_CONFIG));
    // 判断是否分配成功,如果不成功就退出
    if (ffmpeg_config == NULL)
    {
        printf("malloc ffmpeg_group failed\n");
        return -1;
    }

    // 初始化ffmpeg_group结构体相关成员
    ffmpeg_config->config_id = 0; // 流媒体id
    ffmpeg_config->network_type = TS_FILE;
    ffmpeg_config->video_codec = AV_CODEC_ID_H264;                     // 视频编码格式为h264
    ffmpeg_config->audio_codec = AV_CODEC_ID_AAC;                      // 音频编码格式为AAC
    memcpy(ffmpeg_config->network_addr, "test.ts", strlen("test.ts")); // 进行流媒体地址字符赋值,这是写到本地文件
    // 初始化流媒体格式上下文
    init_ffmpeg_config_params(ffmpeg_config);
    
    
}

         FFMPEG合成容器,这部分非常重要,只有做好这个一步,接下来的工作才有意义,因为里面配置了生产什么的复合流是ts还是flv等,要往哪里推流......。

 init_ffmpeg_config_params

        初始化推流器

int init_ffmpeg_config_params(FFMPEG_CONFIG *ffmpeg_config)
{
    AVOutputFormat *fmt = NULL; // 音视频输出格式
    AVCodec *audio_codec = NULL; // 音频编码器
    AVCodec *video_codec = NULL; // 视频编码器
    int ret = 0;

    //第二步:创建一个用于输出的容器对象
    /**
     * 为什么?
     * 推流需要将音视频数据封装成某种格式(比如 FLV 是 RTMP 的常用容器格式),
     * 这个上下文对象管理流的元数据、格式信息和输出目标。
     * 如果没有这个对象,FFmpeg 无法知道数据要以什么形式输出到哪里。
     * 
     * 原理: 
     * 输出格式上下文是 FFmpeg 的核心数据结构,负责协调编码、封装和写入操作。它相当于推流的“总指挥”。
     */
    // 流媒体类型判断并分配输出上下文
    if (ffmpeg_config->network_type == FLV_FILE)
    {
        // 分配FLV格式的AVFormatContext
        ret = avformat_alloc_output_context2(&ffmpeg_config->oc, NULL, "flv", ffmpeg_config->network_addr);
        if (ret < 0)
        {
            return -1;
        }
    }
    else if (ffmpeg_config->network_type == TS_FILE)
    {
        // 分配TS格式的AVFormatContext
        ret = avformat_alloc_output_context2(&ffmpeg_config->oc, NULL, "mpegts", ffmpeg_config->network_addr);
        if (ret < 0)
        {
            return -1;
        }
    }

    // 音视频输出格式上下文句柄
    fmt = ffmpeg_config->oc->oformat;

    /* 指定编码器 */
    fmt->video_codec = ffmpeg_config->video_codec;
    fmt->audio_codec = ffmpeg_config->audio_codec;

    //第三步:创建输出流
    /**
     * 作用:
     * 在输出格式上下文中添加一个流(如视频流或音频流),并为其分配编码参数。
     * 为什么需要?
     * 推流通常包含视频和音频两种数据,每种数据需要独立的流来承载。
     * 创建流是为了定义这些数据的结构(如分辨率、帧率、采样率等),否则 FFmpeg 无法组织数据。
     * 原理:
     * 流(AVStream)是容器中的独立轨道,推流时服务器和客户端会根据流信息解码数据。
     */
    // 初始化视频流
    if (fmt->video_codec != AV_CODEC_ID_NONE)
    {
        // 创建视频输出流
        ret = add_stream(&ffmpeg_config->video_stream, ffmpeg_config->oc, &video_codec, fmt->video_codec);
        if (ret < 0)
        {
            avcodec_free_context(&ffmpeg_config->video_stream.enc);
            close_stream(ffmpeg_config->oc, &ffmpeg_config->video_stream);
            avformat_free_context(ffmpeg_config->oc);
            return -1;
        }

        // 打开视频编码器
        ret = open_video(ffmpeg_config->oc, video_codec, &ffmpeg_config->video_stream, NULL);
        if (ret < 0)
        {
            avformat_free_context(ffmpeg_config->oc);
            return -1;
        }
    }

    // 初始化音频流
    if (fmt->audio_codec != AV_CODEC_ID_NONE)
    {
        // 创建音频输出流
        ret = add_stream(&ffmpeg_config->audio_stream, ffmpeg_config->oc, &audio_codec, fmt->audio_codec);
        if (ret < 0)
        {
            avcodec_free_context(&ffmpeg_config->audio_stream.enc);
            close_stream(ffmpeg_config->oc, &ffmpeg_config->audio_stream);
            avformat_free_context(ffmpeg_config->oc);
            return -1;
        }

        // 打开音频编码器
        ret = open_audio(ffmpeg_config->oc, audio_codec, &ffmpeg_config->audio_stream, NULL);
        if (ret < 0)
        {
            avformat_free_context(ffmpeg_config->oc);
            return -1;
        }
    }

    // 打印输出格式信息
    av_dump_format(ffmpeg_config->oc, 0, ffmpeg_config->network_addr, 1);

    // 打开输出文件
    if (!(fmt->flags & AVFMT_NOFILE))
    {
        // 打开输出文件用于建立与rtmp服务器的连接
        ret = avio_open(&ffmpeg_config->oc->pb, ffmpeg_config->network_addr, AVIO_FLAG_WRITE);
        if (ret < 0)
        {
            close_stream(ffmpeg_config->oc, &ffmpeg_config->video_stream);
            close_stream(ffmpeg_config->oc, &ffmpeg_config->audio_stream);
            avformat_free_context(ffmpeg_config->oc);
            return -1;
        }
    }

    // 写入文件头信息
    //这行代码会根据ffmpeg_config->oc中指定的输出格式(如FLV或TS),
    //生成并写入相应的头部信息到输出文件或流中。
    avformat_write_header(ffmpeg_config->oc, NULL);
    return 0;
}

        ts流合成的流程,是一样ffmpeg的初始化很重要,在合成工作都是根据这个ffmpeg的配置来做的,是和成ts流还是flv,是推动远端还是保存到本地, FFmpeg 的核心数据结构,负责协调编码、封装和写入操作。它相当于推流的“总指挥”。

 


        创建线程 

       初始化所有容器和队列之后,紧接着就需要创建三个线程:分别是read_video_file_thread线程、read_audio_file_thread线程、muxer_ts_thread线程。

int main
{
    
    //读取视频文件线程
    pthread_t pid;
    int ret = pthread_create(&pid, NULL, read_video_file_thread, NULL);
    if (ret != 0)
    {
        printf("Create read_video_file_thread failed...\n");
    }

    //读取音频文件线程
    ret = pthread_create(&pid, NULL, read_audio_file_thread, NULL);
    if (ret != 0)
    {
        printf("Create read_video_file_thread failed...\n");
    }


    //TS复用线程函数
    ret = pthread_create(&pid, NULL, muxer_ts_thread, (void *)ffmpeg_config);
    if (ret != 0)
    {
        printf("Create muxer_ts_thread failed...\n");
    }
}

read_video_file_threa

// 读取视频线程
void *read_video_file_thread(void *args)
{
    const char *input_video_file_name = "test_output.h264";

    AVFormatContext *ifmt_ctx_v = NULL;
    int in_stream_index_v = -1;
    int ret;

    // 打开输入test_output.h264文件
    if ((ret = avformat_open_input(&ifmt_ctx_v, input_video_file_name, NULL, NULL)) < 0)
    {
        av_log(NULL, AV_LOG_ERROR, "Cannot open input file\n");
    }

    // 这个函数的作用不仅会检索视频的一些信息(宽、高、帧率等),而且会持续的读取和解码一些视频帧和音频帧,读取到的帧会放到缓存中;如果你知道这个媒体流的信息,可以不用这个函数去查找
    if ((ret = avformat_find_stream_info(ifmt_ctx_v, NULL)) < 0)
    {
        av_log(NULL, AV_LOG_ERROR, "Cannot find stream information\n");
    }
    ret = av_find_best_stream(ifmt_ctx_v, AVMEDIA_TYPE_VIDEO, -1, -1, NULL, 0);
    if (ret < 0)
    {
        av_log(NULL, AV_LOG_ERROR, "Cannot find video stream\n");
    }
    in_stream_index_v = ret;
    av_dump_format(ifmt_ctx_v, 0, input_video_file_name, 0);

    AVPacket *packet = av_packet_alloc();

    while (av_read_frame(ifmt_ctx_v, packet) >= 0)
    {
        if (packet->stream_index == in_stream_index_v)
        {
            printf("detect video index.....\n");
            // 自己封装的视频数据包结构体video_data_packet_t分配内存
            video_data_packet_t *video_data_packet = (video_data_packet_t *)malloc(sizeof(video_data_packet_t));
            // 把读取到的视频数据拷贝到结构体成员里面的buffer缓冲区里面区
            memcpy(video_data_packet->buffer, packet->data, packet->size);
            // 数据帧的大小赋值
            video_data_packet->video_frame_size = packet->size;
            // 往视频队列里面写入视频流数据
            video_queue->putVideoPacketQueue(video_data_packet);
        }
    }

    return NULL;
}

        read_video_file_thread线程主要的作用是,利用ffmpeg的api avformat_open_input打开我们需要的H264文件,然后用av_read_frame读取每一帧的视频数据。当成功读取一帧数据的时候,把这个数据存放到VIDEO_QUEUE里面(这里封装的函数是putVideoPacketQueue)。流程如下

read_audio_file_thread线程 

//读取音频文件
void *read_audio_file_thread(void *args)
{
    AVFormatContext *pFormatCtx = NULL;
    // 给pFormatCtx进行内存分配
    pFormatCtx = avformat_alloc_context();

    char *aac_ptr = "test_mic.aac";
    // 打开输入test_mic.aac文件
    if (avformat_open_input(&pFormatCtx, aac_ptr, NULL, NULL) != 0)
    {
        printf("无法打开信息流");
    }

    // 这个函数的作用不仅会检索视频的一些信息(宽、高、帧率等),而且会持续的读取和解码一些视频帧和音频帧,读取到的帧会放到缓存中;如果你知道这个媒体流的信息,可以不用这个函数去查找
    if (avformat_find_stream_info(pFormatCtx, NULL) < 0)
    {
        printf("无法查找到流信息");
    }

    int audioindex = -1;
    audioindex = -1;
    //遍历找到音频流,有可能多语言音轨,立体声/环绕声备用音轨
    for (int i = 0; i < pFormatCtx->nb_streams; i++)
    {
        //找到第一个音频流,就可以了
        if (pFormatCtx->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_AUDIO)
        {
            //保存音频流索引
            audioindex = i;
            break;
        }
    }
    // 没有找到音频流
    if (audioindex < 0)
    {
        printf("No Audio Stream...\n");
    }

    // 给AVPacket结构体分配堆空间,释放堆空间使用av_packet_free
    AVPacket *packet = av_packet_alloc();
    while (av_read_frame(pFormatCtx, packet) >= 0)
    {
        if (packet->stream_index == audioindex)
        {
            printf("detect audio index.....\n");
            // 自己封装的视频数据包结构体audio_data_packet_t分配内存
            audio_data_packet_t *audio_data_packet = (audio_data_packet_t *)malloc(sizeof(audio_data_packet_t));
            // 把读取到的视频数据拷贝到结构体成员里面的buffer缓冲区里面区
            memcpy(audio_data_packet->buffer, packet->data, packet->size);
            // 数据帧的大小赋值
            audio_data_packet->audio_frame_size = packet->size;
             // 往音频队列里面写入视频流数据
            audio_queue->putAudioPacketQueue(audio_data_packet);
        }
    }

    return NULL;
}

        read_audio_file_thread线程主要的作用是利用ffmpeg的api avformat_open_input打开我们需要的音频aac文件,然后用av_read_frame读取每一帧的音频数据。当成功读取一帧数据的时候,把这个数据存放到AUDIO_QUEUE里面(这里封装的函数是putAudioPacketQueue)。处理流程大概和视频一样。

muxer_ts_thread线程 

/**
 * TS复用线程函数
 * 该函数负责根据ffmpeg配置,将音视频数据复用到一个TS流中
 * @param args 传递给线程的参数,这里是ffmpeg配置的指针
 * @return 线程的返回值,本函数中始终返回NULL
 */
void *muxer_ts_thread(void *args)
{
    // 解析传入的ffmpeg配置,并释放相应的内存
    FFMPEG_CONFIG ffmpeg_config = *(FFMPEG_CONFIG *)args;
    free(args);

    // 初始化输出格式指针
    AVOutputFormat *fmt = NULL;
    // 初始化返回值变量
    int ret;

    // 无限循环,直到遇到错误或完成处理
    while (1)
    {
        /*
       我们以转换到同一时基下的时间戳为例,假设上一时刻音、视频帧的保存时间戳都是0。
       当前任意保存一种视频帧,例如保存视频的时间戳为video_t1。接着比较时间戳,发现音频时间戳为0 < video_t1,保存一帧音频,时间戳为audio_t1。
       继续比较时间戳,发现audio_t1 < video_t1,选择保存一帧音频,时间戳为audio_t2。
       再一次比较时间戳video_t1 < audio_t2,选择保存一帧视频,时间戳为video_t2。
        //同过比较时间基处理时间戳,如果视频时间戳小于音频时间戳,则保存视频,否则保存音频。写进复合流里面
        */
       
        // 比较视频和音频的时间戳,决定下一步处理哪个流
        ret = av_compare_ts(ffmpeg_config.video_stream.next_pts,
                            ffmpeg_config.video_stream.enc->time_base,
                            ffmpeg_config.audio_stream.next_pts,
                            ffmpeg_config.audio_stream.enc->time_base);
        if (ret <= 0)  //== 0 一样快,<0视频快,处理视频
        {
            // 处理视频包
            ret = deal_video_packet(ffmpeg_config.oc, &ffmpeg_config.video_stream);
            // 如果处理失败,打印错误信息并退出循环
            if (ret == -1)
            {
                printf("deal video packet error break");
                break;
            }
        }
        else //>1音频快
        {
            // 处理音频包
            ret = deal_audio_packet(ffmpeg_config.oc, &ffmpeg_config.audio_stream);
            // 如果处理失败,打印错误信息并退出循环
            if (ret == -1)
            {
                printf("deal audio packet error break");
                break;
            }
        }
    }

    // 线程结束,返回NULL
    return NULL;
}

         muxer_ts_thread线程的作用是通过av_compare_ts(这个函数是重中之重)的api进行音视频PTS时间戳的对比。若判断此PTS是视频的pts,则调用deal_video_packet从视频队列取出每一个视频包然后发送到ts文件;若判断此PTS是视频的pts,则调用deal_audio_packet从音频队列取出每一个音频包然后发送到ts文件。

        为什么需要进行时间基比较?我们要搞明白一个问题就是一个是时间基,一个是时间戳,时间基是代表单位,时间戳是代表,一个时间里面的格子,比如钱,1,2,3,4,5,这样是没有任何意义的,但是配合起时间基就有意义了,时间基就是美元或者rmb。所以这两个东西是决定了我们音频和视频在什么时候写进去复合流里面。

时间基 (time_base) :时间基也称之为时间基准,它代表的是每个刻度是多少秒。比方说:视频
帧率是 30FPS ,那它的时间刻度是 {1,30} 。相当于 1s 内划分出 30 个等分,也就是每隔 1/30 秒后显
示一帧视频数据。
时间戳 (PTS DTS) :首先时间戳它指的是在时间轴里面占了多少个格子,时间戳的单位不是具体
的秒数,而是时间刻度。只有当时间基和时间戳结合在一起的时候,才能够真正表达出来时间是
多少。
        正确的复合流,不会是连续几个视频流或者音频流

        错误的写入。

deal_video_packet 视频快,处理视频


int deal_video_packet(AVFormatContext *oc, OutputStream *ost)
{
    int ret;

    // 从队列中获取视频数据包
    AVPacket *video_pkt = get_video_ffmpeg_packet_from_queue(ost->packet);
    if (video_pkt != NULL)
    {
        // 更新数据包的呈现时间戳(PTS)和下一个预期的时间戳
        ost->packet->pts = ost->next_pts++;
    }

    // 将AV数据包写入到输出文件中
    ret = write_avffmpeg_packet(oc, &(ost->enc->time_base), ost->stream, ost->packet);
    if (ret != 0)
    {
        // 写入操作失败,返回错误代码
        return -1;
    }

    // 成功处理视频数据包,返回0
    return 0;
}

        我们来看看deal_video_packet做了什么功能,首先调用get_video_packet_from_queue取出每一个视频数据包,若video_queue不为空,则让视频的pts自增1(视频的pts每次都是自增1)。然后再利用write_avffmpeg_packet发送到ts文件。 

write_avffmpeg_packet

int write_avffmpeg_packet(AVFormatContext *fmt_ctx, const AVRational *time_base, AVStream *st, AVPacket *pkt)
{
    /*将输出数据包时间戳值从编解码器重新调整为流时基 */
    av_packet_rescale_ts(pkt, *time_base, st->time_base);
    // 设置数据包的流索引
    pkt->stream_index = st->index;

    // 将编码后的音视频数据(AVPacket)写入输出流,发送到服务器/或本地文件中。
    /**
    *为什么需要: 这是推流的核心步骤,所有的音视频内容都通过这一步传输到目标。
    *如果没有这一步,之前的准备工作就毫无意义,因为数据没有实际发送。
	*原理: FFmpeg 使用“交错写入”(interleaved)方式处理多路流(如视频和音频),
    *确保时间戳对齐,符合实时传输的要求。数据会被封装为 FLV 标签(Tag)并通过网络发送。
     */
    return av_interleaved_write_frame(fmt_ctx, pkt);
}

        第一步就是时间转换,视频时间基转换复合流时间基 ,假设视频时间基是{1,30},ts 封装格式的 time_base 为 {1,90000},这个api自动转换 av_packet_rescale_ts但是我们要知道

视频h264时间基转换成MPEGTS时间基:

DST_VIDEO_PTS = VIDEO_PTS * VIDEO_TIME_BASE / DST_TIME_BASE

音频AAC时间基转换成MPEGTS时间基:

**DST_AUDIO_PTS = AUDIO_PTS * AUDIO_TIME_BASE / DST_TIME_BASE

        最后后面调用这个 av_interleaved_write_frame(fmt_ctx, pkt);交错写入 

deal_audio_packet

int deal_audio_packet(AVFormatContext *oc, OutputStream *ost)
{
    int ret;

    AVCodecContext *c = ost->enc;
    AVPacket *pkt = get_audio_ffmpeg_packet_from_queue(ost->packet);
    if (pkt != NULL)
    {
        pkt->pts = av_rescale_q(ost->samples_count, (AVRational){1, c->sample_rate}, c->time_base);
        ost->samples_count += 1024;
        ost->next_pts = ost->samples_count;
    }

    ret = write_avffmpeg_packet(oc, &(ost->enc->time_base), ost->stream, pkt);
    if (ret != 0)
    {
        printf("[FFMPEG] write video frame error");
        return -1;
    }

    return 0;
}

        我们来看看deal_audio_packet做了什么功能,首先调用get_audio_packet_from_queue取出每一个视频数据包,若audio_queue不为空,则让音频pts自增1024(音频aacpts每一帧都是1024个采样点)。然后再利用write_avffmpeg_packet发送到ts文件。 处理过程是和音频一样的。

        其实把aac文件和h264文件,改成直接从摄像头,麦克风采集,就变成了一个录制ts流。

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

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

相关文章

Linux搭建个人大模型RAG-(ollama+deepseek+anythingLLM)

本文是远程安装ollama deepseek&#xff0c;本地笔记本电脑安装anythingLLM&#xff0c;并上传本地文件作为知识库。 1.安装ollama 安装可以非常简单&#xff0c;一行命令完事。&#xff08;有没有GPU&#xff0c;都没有关系&#xff0c;自动下载合适的版本&#xff09; cd 到…

Docker 学习(二)——基于Registry、Harbor搭建私有仓库

Docker仓库是集中存储和管理Docker镜像的平台&#xff0c;支持镜像的上传、下载、版本管理等功能。 一、Docker仓库分类 1.公有仓库 Docker Hub&#xff1a;官方默认公共仓库&#xff0c;提供超过10万镜像&#xff0c;支持用户上传和管理镜像。 第三方平台&#xff1a;如阿里…

韩国互联网巨头 NAVER 如何借助 StarRocks 实现实时数据洞察

作者&#xff1a; Youngjin Kim Team Leader, NAVER Moweon Lee Data Engineer, NAVER 导读&#xff1a;开源无国界&#xff0c;在“StarRocks 全球用户精选案例”专栏中&#xff0c;我们将介绍韩国互联网巨头 NAVER 的 StarRocks 实践案例。 NAVER 成立于 1999 年&#xff0…

C语言学习笔记-初阶(30)深入理解指针2

1. 数组名的理解 在上一个章节我们在使用指针访问数组的内容时&#xff0c;有这样的代码&#xff1a; int arr[10] {1,2,3,4,5,6,7,8,9,10}; int *p &arr[0]; 这里我们使用 &arr[0] 的方式拿到了数组第⼀个元素的地址&#xff0c;但是其实数组名本来就是地址&…

【Wireshark 02】抓包过滤方法

一、官方教程 Wireshark 官网文档 &#xff1a; Wireshark User’s Guide 二、显示过滤器 2.1、 “数据包列表”窗格的弹出过滤菜单 例如&#xff0c;源ip地址作为过滤选项&#xff0c;右击源ip->prepare as filter-> 选中 点击选中完&#xff0c;显示过滤器&#…

MySQL基础四(JDBC)

JDBC(重点) 数据库驱动 程序会通过数据库驱动&#xff0c;和数据库打交道。 sun公司为了简化开发人员对数据库的统一操作&#xff0c;提供了一个Java操作数据库的规范。这个规范由具体的厂商去完成。对应开发人员来说&#xff0c;只需要掌握JDBC接口。 熟悉java.sql与javax.s…

基于CURL命令封装的JAVA通用HTTP工具

文章目录 一、简要概述二、封装过程1. 引入依赖2. 定义脚本执行类 三、单元测试四、其他资源 一、简要概述 在Linux中curl是一个利用URL规则在命令行下工作的文件传输工具&#xff0c;可以说是一款很强大的http命令行工具。它支持文件的上传和下载&#xff0c;是综合传输工具&…

cenos7网络安全检查

很多网络爱好者都知道&#xff0c;在Windows 2000和Windows 9x的命令提示符下可使用Windows系统自带的多种命令行网络故障检测工具&#xff0c;比如说我们最常用的ping。但大家在具体应用时&#xff0c;可能对这些命令行工具的具体含义&#xff0c;以及命令行后面可以使用的种…

蓝桥杯C组真题——巧克力

题目如下 思路 代码及解析如下 谢谢观看

The Rust Programming Language 学习 (三)

所有权 所有权&#xff08;系统&#xff09;是 Rust 最为与众不同的特性&#xff0c;它让 Rust 无需垃圾回收器&#xff08;garbage collector&#xff09;即可保证内存安全。因此&#xff0c;理解 Rust 中所有权的运作方式非常重要。 这里是非常重非常重的一个知识点,这里一…

【一个月备战蓝桥算法】递归与递推

字典序 在刷题和计算机科学领域&#xff0c;字典序&#xff08;Lexicographical order&#xff09;也称为词典序、字典顺序、字母序&#xff0c;是一种对序列元素进行排序的方式&#xff0c;它模仿了字典中单词的排序规则。下面从不同的数据类型来详细解释字典序&#xff1a; …

ArcGIS操作:07 绘制矢量shp面

1、点击目录 2、右侧显示目录 3、选择要存储的文件夹&#xff0c;新建shp 4、定义名称、要素类型、坐标系 5、点击开始编辑 6、点击创建要素 7、右侧选择图层、创建面 8、开始绘制&#xff0c;双击任意位置结束绘制

Axure原型模板与元件库APP交互设计素材(附资料)

为了高效地进行APP和小程序的设计与开发&#xff0c;原型设计工具Axure凭借其强大的功能和灵活性&#xff0c;成为了众多产品经理和设计师的首选。本文将详细介绍Axure原型模板APP常用界面组件元件库、交互设计素材&#xff0c;以及多套涵盖电商、社区服务、娱乐休闲、农业农村…

<网络> TCP协议

目录 TCP协议 与系统相关联 文件与套接字的关系 C语言的多态 谈谈可靠性 TCP协议格式 目的端口号 4位首部长度 16位窗口大小 序号与确认序号 32位序号 32位确认序号 标志位 TCP连接 三次握手 四次挥手 三次握手状态变化 四次挥手状态变化 流量控制 滑动窗口 拥塞控制 延迟应…

JAVA编程【jvm垃圾回收的差异】

jvm垃圾回收的差异 JVM&#xff08;Java Virtual Machine&#xff09;的垃圾回收&#xff08;GC&#xff09;机制是自动管理内存的一种方式&#xff0c;能够帮助开发者释放不再使用的内存&#xff0c;避免内存泄漏和溢出等问题。不同的垃圾回收器&#xff08;GC&#xff09;有…

VBA信息获取与处理第五节:如何在单个工作表中查找某个给定值

《VBA信息获取与处理》教程(版权10178984)是我推出第六套教程&#xff0c;目前已经是第一版修订了。这套教程定位于最高级&#xff0c;是学完初级&#xff0c;中级后的教程。这部教程给大家讲解的内容有&#xff1a;跨应用程序信息获得、随机信息的利用、电子邮件的发送、VBA互…

ubuntu20.04 安装离线版docker-20.10.0

1. 安装步骤 步骤一&#xff1a;官网下载 docker 安装包 wget https://download.docker.com/linux/static/stable/x86_64/docker-20.10.0.tgz步骤二&#xff1a;解压安装包; tar -zxvf docker-20.10.0.tgz 步骤三&#xff1a;将解压之后的docker文件移到 /usr/bin目录下; c…

【推荐算法】python游戏数据分析可视化推荐系统(完整系统源码+数据库+开发笔记+详细部署教程)✅

目录 一、项目背景 二、项目拟解决问题 &#xff08;1&#xff09;数据价值断层 &#xff08;2&#xff09;用户画像模糊 &#xff08;3&#xff09;推荐策略单一 &#xff08;4&#xff09;决策可视化缺失 三、研究目的 &#xff08;1&#xff09;轻量化服务架构验证 …

Vue 3 整合 WangEditor 富文本编辑器:从基础到高级实践

本文将详细介绍如何在 Vue 3 项目中集成 WangEditor 富文本编辑器&#xff0c;实现图文混排、自定义扩展等高阶功能。 一、为什么选择 WangEditor&#xff1f; 作为国内流行的开源富文本编辑器&#xff0c;WangEditor 具有以下优势&#xff1a; 轻量高效&#xff1a;压缩后仅…

基于Asp.net的农产品销售管理系统

作者&#xff1a;计算机学姐 开发技术&#xff1a;SpringBoot、SSM、Vue、MySQL、JSP、ElementUI、Python、小程序等&#xff0c;“文末源码”。 专栏推荐&#xff1a;前后端分离项目源码、SpringBoot项目源码、Vue项目源码、SSM项目源码、微信小程序源码 精品专栏&#xff1a;…