项目1:FFMPEG推流器讲解(二):FFMPEG输出模块初始化

一.本章节内容:

FFMPEG输出模块主要用于初始化音视频推流功能,确保RV1126的码流能够通过FFMPEG正常推送。该模块的初始化流程包含以下步骤:

  1. 使用avformat_alloc_output_context2分配AVFormatContext
  2. 通过avformat_new_stream初始化AVStream结构体
  3. 调用avcodec_find_encoder获取对应的编码器
  4. 使用avcodec_alloc_context3分配AVCodecContext
  5. 配置AVCodecContext结构体参数
  6. 通过avcodec_parameters_from_context将编码器参数传递到AVStream
  7. 使用avio_open初始化FFMPEG的IO结构体
  8. 调用avformat_write_header完成AVFormatContext初始化

在RV1126+FFMPEG多路码流推流项目中,输出模块的初始化实现位于rkmedia_ffmpeg_config.cpp文件的init_rkmedia_ffmpeg_context函数中。

二.FFMPEG 输出配置的框图

上图是整体的框图,我们具体来看看每个框图的代码实现。

2.1. 分配F FMPEG AVFormatContext 输出的上下文结构体指针

cpp 复制代码
 //FLV_PROTOCOL is RTMP TCP
    if (ffmpeg_config->protocol_type == FLV_PROTOCOL)
    {
        //初始化一个FLV的AVFormatContext
        ret = avformat_alloc_output_context2(&ffmpeg_config->oc, NULL, "flv", ffmpeg_config->network_addr); 
        if (ret < 0)
        {
            return -1;
        }
    }
    //TS_PROTOCOL is SRT UDP RTSP
    else if (ffmpeg_config->protocol_type == TS_PROTOCOL)
    {
        //初始化一个TS的AVFormatContext
        ret = avformat_alloc_output_context2(&ffmpeg_config->oc, NULL, "mpegts", ffmpeg_config->network_addr);
        if (ret < 0)
        {
            return -1;
        }
    }
cpp 复制代码
int avformat_alloc_output_context2(AVFormatContext **ctx, AVOutputFormat *oformat, const char *format_name, const char *filename)

第一个传输参数: AVFormatContext结构体指针的指针,是存储音视频封装格式中包含的信息的结构体,所有对文件的封装、编码都是从这个结构体开始。

第二个传输参数: AVOutputFormat的结构体指针,它主要存储复合流信息的常规配置,默认为设置NULL。

第三个传输参数: format_name指的是复合流的格式,比方说:flv、ts、mp4等等

第四个传输参数: filename是输出地址,输出地址可以是本地文件(如:xxx.mp4、xxx.ts等等)。也可以是网络流地址(如:rtmp://xxx.xxx.xxx.xxx:1935/live/01)

上面这个API是根据我们流媒体类型去分配AVFormatContext 结构体。我们传进来的类型会分为FLV_PROTOCOLTS_PROTOCOL ,具体如何配置如下面:

若TS_PROTOCOL类型 :avformat_alloc_output_context2(&group->oc, NULL, "mpegts ", group->url_addr);

若FLV_PROTOCOL类型 :avformat_alloc_output_context2(&group->oc, NULL, "flv" , group->url_addr);

注意:TS格式分别可以适配以下流媒体复合流,包括:SRT、UDP、TS本地文件等。flv格式包括:RTMP、FLV本地文件等等。

2 .2. 配置推流器编码参数和A VStream 结构体

A VS tream 主要是存储流信息结构体,这个流信息包含音频流和视频流。创建的API是avformat_new_stream

cpp 复制代码
    fmt = ffmpeg_config->oc->oformat;//fmt = ffmpeg_config->oc->oformat; 是FFmpeg中获取输出格式描述符的关键操作,其作用是从已创建的输出上下文(AVFormatContext)中提取对应的输出格式元数据,AVFormatContext的成员变量,指向AVOutputFormat结构体。该结构体是FFmpeg对容器格式的抽象定义在avformat_alloc_output_context2(&ffmpeg_config->oc, NULL, "flv", ...)调用后,FFmpeg内部会为oc->oformat自动关联到libavformat/flvenc.c中的ff_flv_muxer(FLV格式)或libavformat/mpegtsenc.c中的ff_ts_muxer(TS格式)
    /*指定编码器*/
    fmt->video_codec = ffmpeg_config->video_codec;
    fmt->audio_codec = ffmpeg_config->audio_codec;

    if (fmt->video_codec != AV_CODEC_ID_NONE)
    {
        ret = add_stream(&ffmpeg_config->video_stream, ffmpeg_config->oc, &video_codec, fmt->video_codec,ffmpeg_config->width,ffmpeg_config->height);
        if (ret < 0)
        {
            avcodec_free_context(&ffmpeg_config->video_stream.enc);
            free_stream(ffmpeg_config->oc, &ffmpeg_config->video_stream);
            avformat_free_context(ffmpeg_config->oc);
            return -1;
        }
     }
cpp 复制代码
int add_stream(OutputStream *ost, AVFormatContext *oc, AVCodec **codec, enum AVCodecID codec_id, int width, int height)
{
    AVCodecContext *c = NULL;//声明编码器上下文指针,用于后续操作编码器参数

    //创建输出码流的AVStream, AVStream是存储每一个视频/音频流信息的结构体
    ost->stream = avformat_new_stream(oc, NULL);
    if (!ost->stream)
    {
        printf("Can't not avformat_new_stream\n");
        return 0;
    }
    else
    {
        printf("Success avformat_new_stream\n");
    }
}

AVS tream * avformat _new_stream(AVFormatContext *s, AVDictionary **options);

第一个传输参数: AVFormatContext的结构体指针

第二个传输参数: AVDictionary结构体指针的指针

返回值: AVStream结构体指针

2.3. 设置对应的推流器编码器参数

cpp 复制代码
int add_stream(OutputStream *ost, AVFormatContext *oc, AVCodec **codec, enum AVCodecID codec_id, int width, int height)
{
    AVCodecContext *c = NULL;//声明编码器上下文指针,用于后续操作编码器参数

    //创建输出码流的AVStream, AVStream是存储每一个视频/音频流信息的结构体
    ost->stream = avformat_new_stream(oc, NULL);
    if (!ost->stream)
    {
        printf("Can't not avformat_new_stream\n");
        return 0;
    }
    else
    {
        printf("Success avformat_new_stream\n");
    }

    //通过codecid找到对应的编码器
    *codec = avcodec_find_encoder(codec_id);
    if (!(*codec))
    {
        printf("Can't not find any encoder");
        return 0;
    }
    else
    {
        printf("Success find encoder");
    }
}

AVCodec *avcodec_find_encoder(enum AVCodecID id); / /

第一个传输参数: 传递参数AVCodecID

2.4. 根据编码器ID分配 AVCodecContext 结构体

cpp 复制代码
int add_stream(OutputStream *ost, AVFormatContext *oc, AVCodec **codec, enum AVCodecID codec_id, int width, int height)
{
    AVCodecContext *c = NULL;//声明编码器上下文指针,用于后续操作编码器参数

    //创建输出码流的AVStream, AVStream是存储每一个视频/音频流信息的结构体
    ost->stream = avformat_new_stream(oc, NULL);
    if (!ost->stream)
    {
        printf("Can't not avformat_new_stream\n");
        return 0;
    }
    else
    {
        printf("Success avformat_new_stream\n");
    }

    //通过codecid找到对应的编码器
    *codec = avcodec_find_encoder(codec_id);
    if (!(*codec))
    {
        printf("Can't not find any encoder");
        return 0;
    }
    else
    {
        printf("Success find encoder");
    }

    //nb_streams 输入视频的AVStream 个数 就是当前有几种Stream,比如视频流、音频流、字幕,这样就算三种了,
    // oc->nb_streams - 1其实对应的应是AVStream 中的 index
    ost->stream->id = oc->nb_streams - 1;
    //通过CODEC分配编码器上下文,关联到ost->enc供后续操作使用
    c = avcodec_alloc_context3(*codec);
    if (!c)
    {
        printf("Can't not allocate context3\n");
        return 0;
    }
    else
    {
        printf("Success allocate context3");
    }

    ost->enc = c;
}
markdown 复制代码
`AVCodecContext *avcodec_alloc_context3(const AVCodec *codec);`

参数说明:
- 传入参数:指向AVCodec结构体的指针

功能说明:
`avcodec_find_encoder`函数通过codec_id(编码器ID)查找对应的AVCodec结构体。在RV1126推流项目中,我们使用两种编码器ID:`AV_CODEC_ID_H264`和`AV_CODEC_ID_H265`。获取到AVCodec结构体后,即可调用`avcodec_alloc_context3`来创建AVCodecContext上下文环境。
 

初始化完A VS tream和编码上下文结构体之后,我们就需要对这些参数进行配置。重点:推流编码器参数和RV1126编码器的参数要完全一样,否则可能会出问题,具体的如下图:

1920 * 1080编码器以及1280* 720编码器和FFMPEG推流器的配置

为确保视频编码的一致性,FFMPEG的参数设置须与右侧VENC编码器完全匹配。关键参数包括:

  1. 分辨率参数(WIDTH/HEIGHT)必须对应:

    • 当RV1126编码器设为1920×1080时,FFMPEG的WIDTH=1920,HEIGHT=1080
    • 当RV1126编码器设为1280×720时,FFMPEG的WIDTH=1280,HEIGHT=720
  2. GOP_SIZE参数需保持相同:

    • 若RV1126的GOP值为25,则FFMPEG的gop_size同样设为25
  3. 时间基(time_base)必须与视频帧率完全一

cpp 复制代码
 //在h264头部添加SPS,PPS
    if (oc->oformat->flags & AVFMT_GLOBALHEADER)// 检查容器是否支持全局头
    {
        c->flags |= AV_CODEC_FLAG_GLOBAL_HEADER;// 启用编码器的全局头标志
    }

AV_CODEC_FLAG_GLOBAL_HEADER ****:****发送视频数据的时候都会在关键帧前面添加SPS/PPS,这个标识符在FFMPEG初始化的时候都需要添加。

2.5. 设置完上述参数之后, 拷贝参数到AVStream编解码器 ,具体的操作如下:

拷贝参数到AVStream,我们封装到open_video自定义函数里面,要先调用avcodec_open2打开编码器,然后再调用avcodec_parameters_from_context把编码器参数传输到AVStream里面

cpp 复制代码
//使能video编码器
int open_video(AVFormatContext *oc, AVCodec *codec, OutputStream *ost, AVDictionary *opt_arg)
{
    AVCodecContext *c = ost->enc;

    //打开编码器(激活视频编码器)
    avcodec_open2(c, codec, NULL);

    //分配video avpacket包,创建视频数据包容器
    ost->packet = av_packet_alloc();

    /* 将AVCodecContext参数复制AVCodecParameters复用器 
    将AVCodecContext的参数(如width/height/pix_fmt)复制到AVStream->codecpar
    复用器(avformat_write_header)依赖这些参数生成容器头部*/
    avcodec_parameters_from_context(ost->stream->codecpar, c);
    return 0;
}

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

这个函数的具体作用是, 打开编解码器

****第一个参数:****AVCodecContext结构体指针

****第二个参数:****AVCodec结构体指针

****第三个参数:****AVDictionary二级指针

2.5.2. int avcodec_parameters_from_context(AVCodecParameters *par, const AVCodecContext *codec);

这个函数的具体作用是,把 AVCodecContext 的参数拷贝到 AVCodecParameters 里面。

****第一个参数:****AVCodecParameters结构体指针

****第二个参数:****AVCodecContext结构体指针

2.6 . 打开I O 文件操作

cpp 复制代码
 //打印输出格式的详细调试信息
    av_dump_format(ffmpeg_config->oc, 0, ffmpeg_config->network_addr, 1);

    if (!(fmt->flags & AVFMT_NOFILE))
    {
        //打开输出文件或网络流
        ret = avio_open(&ffmpeg_config->oc->pb, ffmpeg_config->network_addr, AVIO_FLAG_WRITE);
        if (ret < 0)
        {
            free_stream(ffmpeg_config->oc, &ffmpeg_config->video_stream);
            free_stream(ffmpeg_config->oc, &ffmpeg_config->audio_stream);
            avformat_free_context(ffmpeg_config->oc);
            return -1;
        }
    }

使用avio_open 打开对应的文件,注意这里的文件不仅是指本地的文件也指的是网络流媒体文件,下面是a vio_open的定义。

cpp 复制代码
/**
 * Create and initialize a AVIOContext for accessing the
 * resource indicated by url.
 * @note When the resource indicated by url has been opened in
 * read+write mode, the AVIOContext can be used only for writing.
 *
 * @param s Used to return the pointer to the created AVIOContext.
 * In case of failure the pointed to value is set to NULL.
 * @param url resource to access
 * @param flags flags which control how the resource indicated by url
 * is to be opened
 * @return >= 0 in case of success, a negative value corresponding to an
 * AVERROR code in case of failure
 */
int avio_open(AVIOContext **s, const char *url, int flags);

int avio_open(AVIOContext **s, const char *url, int flags);

****第一个参数:****AVIOContext的结构体指针,它主要是管理数据输入输出的结构体

第二个参数: url地址,这个URL地址既包括本地文件如(xxx.ts、xxx.mp4),也可以是网络流媒体地址,如(rtmp://192.168.22.22:1935/live/01)等

****第三个参数:****flags标识符

#define AVIO_FLAG_READ 1 /**< read-only */

#define AVIO_FLAG_WRITE 2 /**< write-only */

#define AVIO_FLAG_READ_WRITE (AVIO_FLAG_READ|AVIO_FLAG_WRITE) /**< read-write pseudo flag */

2.7 . avformat_write_header 对头部进行初始化 输出模块头部进行初始化

cpp 复制代码
//写入容器头部元数据
    avformat_write_header(ffmpeg_config->oc, NULL);

int avformat_write_header(AVFormatContext *s, AVDictionary **options);

第一个参数: 传递AVFormatContext结构体指针

第二个参数: 传递AVDictionary结构体指针的指针

疑问:为什么一定要在h264头部添加SPS,PPS

cpp 复制代码
 if (oc->oformat->flags & AVFMT_GLOBALHEADER)// 检查容器是否支持全局头
    {
        c->flags |= AV_CODEC_FLAG_GLOBAL_HEADER;// 启用编码器的全局头标志
    }

全局头模式:说明书贴在包装盒外面(类似MP4容器)。打开盒子前就能看到,一次阅读,全程通用,省纸又方便。
非全局头模式:说明书塞在每个零件袋里(类似MPEG-TS容器)。每个袋子都有说明书,但重复印刷浪费纸,还可能看混。
FFmpeg的全局头配置就是帮你选"说明书放哪里",让播放器快速找到参数,避免播放失败或浪费流量。
SPS和PPS就是"玩具说明书"的内容

SPS(序列参数集):相当于玩具的"总说明书",规定整体规格。比如:玩具尺寸(视频分辨率)、颜色模式(色彩格式)、最大承重(码率上限)等。没有它,播放器根本不知道该怎么"组装"视频。
PPS(图像参数集):相当于每个零件的"小说明书",规定具体细节。比如:螺丝型号(量化参数)、弹簧长度(帧率)、是否防锈(去块滤波)等。没有它,每一帧视频都可能"装错"或"卡住"。
为什么需要全局头配置?

省流量:全局头模式下,参数只存一次(像说明书贴盒外),不用每帧重复,减少视频体积,直播更流畅。
防卡顿:播放器一开始就能读到参数,快速启动播放;非全局头模式下,每遇到关键帧(如I帧)都要重新读参数,可能卡顿。
避错误:如果容器需要全局头但没配置(比如MP4没贴说明书),播放器会报错"找不到说明书",直接罢工;如果容器不支持全局头却强行配置(比如MPEG-TS贴说明书),参数反而会丢失,播放异常。
举个实际例子
你看直播时,如果主播用MP4格式(支持全局头),视频参数一开始就加载好,播放丝滑;如果用MPEG-TS格式(不支持全局头),每切换一个画面(关键帧)都要重新加载参数,可能卡一下。而SPS/PPS就是这些参数的"核心内容",没有它们,视频就像没说明书的玩具,根本玩不了。

简单来说,全局头配置是"选对放说明书的位置",SPS/PPS是"说明书的内容",两者配合,才能让视频流畅播放。

全部代码:

cpp 复制代码
int add_stream(OutputStream *ost, AVFormatContext *oc, AVCodec **codec, enum AVCodecID codec_id, int width, int height)
{
    AVCodecContext *c = NULL;//声明编码器上下文指针,用于后续操作编码器参数

    //创建输出码流的AVStream, AVStream是存储每一个视频/音频流信息的结构体
    ost->stream = avformat_new_stream(oc, NULL);
    if (!ost->stream)
    {
        printf("Can't not avformat_new_stream\n");
        return 0;
    }
    else
    {
        printf("Success avformat_new_stream\n");
    }

    //通过codecid找到对应的编码器
    *codec = avcodec_find_encoder(codec_id);
    if (!(*codec))
    {
        printf("Can't not find any encoder");
        return 0;
    }
    else
    {
        printf("Success find encoder");
    }

    //nb_streams 输入视频的AVStream 个数 就是当前有几种Stream,比如视频流、音频流、字幕,这样就算三种了,
    // oc->nb_streams - 1其实对应的应是AVStream 中的 index
    ost->stream->id = oc->nb_streams - 1;
    //通过CODEC分配编码器上下文,关联到ost->enc供后续操作使用
    c = avcodec_alloc_context3(*codec);
    if (!c)
    {
        printf("Can't not allocate context3\n");
        return 0;
    }
    else
    {
        printf("Success allocate context3");
    }

    ost->enc = c;

    switch ((*codec)->type)
    {
    case AVMEDIA_TYPE_AUDIO:

        c->sample_fmt = (*codec)->sample_fmts ? (*codec)->sample_fmts[0] : AV_SAMPLE_FMT_FLTP; //FFMPEG采样格式
        c->bit_rate = 153600;  //FFMPEG音频码率
        c->sample_rate = 48000; //FFMPEG采样率
        c->channel_layout = AV_CH_LAYOUT_STEREO;//FFMPEG声道数2
        c->channels = av_get_channel_layout_nb_channels(c->channel_layout); //FFMPEG采样通道
        ost->stream->time_base = (AVRational){1, c->sample_rate};//FFMPEG音频时间基
        break;

    case AVMEDIA_TYPE_VIDEO:

        //c->codec_id = codec_id;
        c->bit_rate = width * height * 3; //FFMPEG视频码率
        //分辨率必须是2的倍数
        c->width = width; //FFMPEG视频宽度
        c->height = height;//FFMPEG视频高度
        //控制关键帧间隔(如每25帧一个I帧)
        ost->stream->r_frame_rate.den = 1; //FFMPEG帧率,分母
        ost->stream->r_frame_rate.num = 25;//FFMPEG帧率,分子
        ost->stream->time_base = (AVRational){1, 25};//Stream视频时间基,默认情况下等于帧率

        c->time_base = ost->stream->time_base; //编码器时间基
        c->gop_size = GOPSIZE; //GOPSIZE
        c->pix_fmt = AV_PIX_FMT_NV12;//图像格式

        break;

    default:
        break;
    }

    //在h264头部添加SPS,PPS
    if (oc->oformat->flags & AVFMT_GLOBALHEADER)// 检查容器是否支持全局头
    {
        c->flags |= AV_CODEC_FLAG_GLOBAL_HEADER;// 启用编码器的全局头标志
    }

    return 0;
}

//使能video编码器
int open_video(AVFormatContext *oc, AVCodec *codec, OutputStream *ost, AVDictionary *opt_arg)
{
    AVCodecContext *c = ost->enc;

    //打开编码器(激活视频编码器)
    avcodec_open2(c, codec, NULL);

    //分配video avpacket包,创建视频数据包容器
    ost->packet = av_packet_alloc();

    /* 将AVCodecContext参数复制AVCodecParameters复用器 
    将AVCodecContext的参数(如width/height/pix_fmt)复制到AVStream->codecpar
    复用器(avformat_write_header)依赖这些参数生成容器头部*/
    avcodec_parameters_from_context(ost->stream->codecpar, c);
    return 0;
}


int init_rkmedia_ffmpeg_context(RKMEDIA_FFMPEG_CONFIG *ffmpeg_config)
{
    AVOutputFormat *fmt = NULL;
    AVCodec *audio_codec = NULL;
    AVCodec *video_codec = NULL;
    int ret = 0;

    //FLV_PROTOCOL is RTMP TCP
    if (ffmpeg_config->protocol_type == FLV_PROTOCOL)
    {
        //初始化一个FLV的AVFormatContext
        ret = avformat_alloc_output_context2(&ffmpeg_config->oc, NULL, "flv", ffmpeg_config->network_addr); 
        if (ret < 0)
        {
            return -1;
        }
    }
    //TS_PROTOCOL is SRT UDP RTSP
    else if (ffmpeg_config->protocol_type == TS_PROTOCOL)
    {
        //初始化一个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 = ffmpeg_config->oc->oformat; 是FFmpeg中获取输出格式描述符的关键操作,其作用是从已创建的输出上下文(AVFormatContext)中提取对应的输出格式元数据,AVFormatContext的成员变量,指向AVOutputFormat结构体。该结构体是FFmpeg对容器格式的抽象定义在avformat_alloc_output_context2(&ffmpeg_config->oc, NULL, "flv", ...)调用后,FFmpeg内部会为oc->oformat自动关联到libavformat/flvenc.c中的ff_flv_muxer(FLV格式)或libavformat/mpegtsenc.c中的ff_ts_muxer(TS格式)
    /*指定编码器*/
    fmt->video_codec = ffmpeg_config->video_codec;
    fmt->audio_codec = ffmpeg_config->audio_codec;

    if (fmt->video_codec != AV_CODEC_ID_NONE)
    {
        ret = add_stream(&ffmpeg_config->video_stream, ffmpeg_config->oc, &video_codec, fmt->video_codec,ffmpeg_config->width,ffmpeg_config->height);
        if (ret < 0)
        {
            avcodec_free_context(&ffmpeg_config->video_stream.enc);
            free_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);
        }
    }

#if 0
    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);
            free_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);
        }
    }
#endif
    //打印输出格式的详细调试信息
    av_dump_format(ffmpeg_config->oc, 0, ffmpeg_config->network_addr, 1);

    if (!(fmt->flags & AVFMT_NOFILE))
    {
        //打开输出文件或网络流
        ret = avio_open(&ffmpeg_config->oc->pb, ffmpeg_config->network_addr, AVIO_FLAG_WRITE);
        if (ret < 0)
        {
            free_stream(ffmpeg_config->oc, &ffmpeg_config->video_stream);
            free_stream(ffmpeg_config->oc, &ffmpeg_config->audio_stream);
            avformat_free_context(ffmpeg_config->oc);
            return -1;
        }
    }
    //写入容器头部元数据
    avformat_write_header(ffmpeg_config->oc, NULL);
    return 0;
}

注:还是要注意一点 这里的FFMPEG的输出配置一步都不能少 否则就启动不了

相关推荐
Nimsolax2 小时前
Linux网络应用层自定义协议与序列化
linux·网络
egoist20232 小时前
[linux仓库]图解System V共享内存:从shmget到内存映射的完整指南
linux·开发语言·共享内存·system v
葵花日记2 小时前
LINUX——进度条
linux·运维·服务器
hmcjn(小何同学)3 小时前
轻松Linux-10.进程信号
linux·运维·服务器
用户31187945592183 小时前
libopenssl1_0_0-1.0.2p-3.49.1.x86_64安装教程(RPM包手动安装步骤+依赖解决附安装包下载)
linux
驱动探索者3 小时前
linux 学习平台 arm+x86 搭建
linux·arm开发·学习
深思慎考3 小时前
【新版】Elasticsearch 8.15.2 完整安装流程(Linux国内镜像提速版)
java·linux·c++·elasticsearch·jenkins·框架
微电子爱好者3 小时前
TCP和UDP调试工具的介绍和使用
linux·tcp/ip·udp
mxpan3 小时前
VirtualBox中ubuntu1804虚拟机共享文件夹设置
linux·运维·服务器