使用FFMPEG和SDL2实现音视频同步的简易视频播放器

news2024/11/24 20:00:43

程序框架

由于之前都是针对FFMPEG某一个功能做的测试和学习。这里我简单做了一个视频播放器,并简单做了音视频同步。在此记录大致过程。

大致框架如下:

 主线程

1.加载视频文件,查找音视频流信息

2.初始化音视频解码器

3.初始化SDL并设置相关的音视频参数

4.创建解复用线程,音频解码播放线程,视频解码播放线程

5.然后进入SDL窗口的事件循环,等待退出事件

解复用线程

1.循环读文件流,每次从文件流中读取一帧数据

2.根据帧类型放入相应的队列中

音频解码播放线程

1.从音频队列中取数据

2.将取到的数据送至音频解码器中

3.循环从解码器中取解码音频帧

4.将解码数据转换成packed形式,也就是LRLRLR...

5.等待SDL音频回调播放音频完成,回到1

视频解码播放线程

1.从视频队列中取数据

2.将取到的数据送至视频解码器中

3.循环从解码器中取解码视频帧

4.渲染视频帧到SDL窗口中

5.计算视频帧的pts和持续时间

6.根据音频帧和视频帧的差值计算延时

7.延时计算的时长后回到1

同步逻辑

1.如果当前视频帧与音频帧的播放时间差值小于或等于视频帧持续时间,则表示音视频同步,正常延时

2.如果视频帧比音频帧快,且大于视频帧一帧的时长,延时2倍的正常延时

3.如果视频帧比音频帧慢,且大于视频帧一帧的时长,则立即播放下一帧

源码

#include <stdio.h>

#include "SDL.h"

extern "C"
{
#include <libavformat/avformat.h>
#include "libavutil/time.h"
}

char av_error[AV_ERROR_MAX_STRING_SIZE] = { 0 };
#define av_err2str(errnum) av_make_error_string(av_error, AV_ERROR_MAX_STRING_SIZE, errnum)

#define MAX_VIDEO_PIC_NUM  1  //最大缓存解码图片数

//队列
typedef struct PacketQueue {
	AVPacketList* first_pkt, * last_pkt;
	int nb_packets;
	SDL_mutex* mutex;
} PacketQueue;

//音视频同步时钟模式
enum {
	AV_SYNC_AUDIO_MASTER, /* default choice */
	AV_SYNC_VIDEO_MASTER,
	AV_SYNC_EXTERNAL_CLOCK, /* synchronize to an external clock */
};

static  Uint8* audio_chunk;
static  Uint32  audio_len;
static  Uint8* audio_pos;

SDL_Window* sdlWindow = nullptr;
SDL_Renderer* sdlRender = nullptr;
SDL_Texture* sdlTtexture = nullptr;


AVFormatContext* ifmt_ctx = NULL;
AVPacket* pkt;
AVFrame* video_frame, * audio_frame;
int ret;
int videoindex = -1, audioindex = -1;
//const char* in_filename = "D:/工程/音视频分析/source/间谍过家家16.mp4";  //beautlWorld.mp4";
const char* in_filename = "D:/工程/音视频分析/source/beautlWorld.mp4";

int frame_width = 1280;
int frame_height = 720;

//视频解码
AVCodec* video_codec = nullptr;
AVCodecContext* video_codecContent = nullptr;

typedef struct video_pic
{
	AVFrame frame; 
	
	float clock; //显示时钟
	float duration; //持续时间
	int frame_NUM; //帧号
}video_pic;

video_pic v_pic[MAX_VIDEO_PIC_NUM]; //视频解码最多保存四帧数据
int pic_count = 0; //以存储图片数量

//音频解码
AVCodec* audio_codec = nullptr;
AVCodecContext* audio_codecContent = nullptr;

//视频帧队列
PacketQueue video_pkt_queue;
PacketQueue audio_pkt_queue;

//同步时钟设置为音频为主时钟
int av_sync_type = AV_SYNC_AUDIO_MASTER;

int64_t audio_callback_time;
float audio_clock;
float video_clock;

//SDL音频参数结构
SDL_AudioSpec wanted_spec;

int initSdl();
void closeSDL();
void  fill_audio_pcm2(void* udata, Uint8* stream, int len);

//fltp转为packed形式
void fltp_convert_to_f32le(float* f32le, float* fltp_l, float* fltp_r, int nb_samples, int channels) 
{
	for (int i = 0; i < nb_samples; i++)
	{
		f32le[i * channels] = fltp_l[i];
		f32le[i * channels + 1] = fltp_r[i];
	}
}

//将一个压缩数据包放入相应的队列中
void put_AVPacket_into_queue(PacketQueue *q, AVPacket* packet)
{
	SDL_LockMutex(q->mutex);
	AVPacketList* temp = nullptr;
	temp = (AVPacketList*)av_malloc(sizeof(AVPacketList));
	if (!temp)
	{
		printf("malloc a AVPacketList error\n");
		return;
	}

	temp->pkt =  *packet;
	temp->next = NULL;

	if (!q->last_pkt)
		q->first_pkt = temp;
	else
		q->last_pkt->next = temp;

	q->last_pkt = temp;
	q->nb_packets++;

	SDL_UnlockMutex(q->mutex);
}

static void packet_queue_get(PacketQueue* q,AVPacket *pkt2)
{	
	while (true)
	{
		AVPacketList* pkt1;
		//一直取,直到队列中有数据,就返回
		pkt1 = q->first_pkt;
		if (pkt1)
		{
			SDL_LockMutex(q->mutex);
			q->first_pkt = pkt1->next;

			if (!q->first_pkt)
				q->last_pkt = NULL;

			q->nb_packets--;
			SDL_UnlockMutex(q->mutex);

			*pkt2 = pkt1->pkt;

			av_free(pkt1);

			
			break;
			
		}
		else
			SDL_Delay(1);
	
	}
	
	return;
} 

int delCunt = 0;
int video_play_thread(void * data)
{
	AVPacket video_packt = {0};
	//取数据
	while (true)
	{
		//取数据包
		
		packet_queue_get(&video_pkt_queue, &video_packt);
		
		ret = avcodec_send_packet(video_codecContent, &video_packt);
		if (ret < 0) {
			fprintf(stderr, "Error sending a packet for decoding\n", av_err2str(ret));
			continue;
		}

		while (ret >= 0)
		{
			ret = avcodec_receive_frame(video_codecContent, video_frame);
			if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF)
				break;
			else if (ret < 0)
			{
				fprintf(stderr, "Error during decoding\n");
				break;
			}
			//printf(" frame num %3d\n", video_codecContent->frame_number);
			fflush(stdout);

			video_clock = av_q2d(video_codecContent->time_base) * video_codecContent->ticks_per_frame * 1000 * video_codecContent->frame_number;
			//printf("视频帧pts: %f ms\n", video_clock);
			float duration = av_q2d(video_codecContent->time_base) * video_codecContent->ticks_per_frame * 1000;

			SDL_UpdateYUVTexture(sdlTtexture, nullptr,
				video_frame->data[0], video_frame->linesize[0],
				video_frame->data[1], video_frame->linesize[1],
				video_frame->data[2], video_frame->linesize[2]);


			//清理渲染器缓冲区
			SDL_RenderClear(sdlRender);
			//将纹理拷贝到窗口渲染平面上
			SDL_RenderCopy(sdlRender, sdlTtexture, NULL, nullptr);
			//翻转缓冲区,前台显示
			SDL_RenderPresent(sdlRender);

			//延时处理
			float delay = duration;
			float diff = video_clock - audio_clock;
			if (fabs(diff) <= duration) //时间差在一帧范围内表示正常,延时正常时间
				delay = duration;
			else if (diff > duration) //视频时钟比音频时钟快,且大于一帧的时间,延时2倍
				delay *= 2;
			else if (diff < -duration) //视频时钟比音频时钟慢,且超出一帧时间,立即播放当前帧
				delay = 1;

			printf("帧数:%d 延时: %f\n", video_codecContent->frame_number, delay);
			
			SDL_Delay(delay);
		}
	}

}

int audio_play_thread(void* data)
{
	AVPacket audio_packt = { 0 };

	while (true)
	{
		packet_queue_get(&audio_pkt_queue, &audio_packt);

		ret = avcodec_send_packet(audio_codecContent, &audio_packt);
		if (ret < 0) {
			fprintf(stderr, "Error submitting the packet to the decoder\n");
			exit(1);
		}


		/* read all the output frames (in general there may be any number of them */
		while (ret >= 0) {
			//接到解码后的<AVPacket数据>,读取到AVFrame中
			ret = avcodec_receive_frame(audio_codecContent, audio_frame);
			if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF)
				break;
			else if (ret < 0) {
				fprintf(stderr, "Error during decoding\n");
				break;
			}
			/*下面是得到解码后的裸流数据进行处理,根据裸流数据的特征做相应的处理,如AAC解码后是PCM,h264解码后是YUV等。*/
			int data_size = av_get_bytes_per_sample(audio_codecContent->sample_fmt);
			if (data_size < 0) {
				/* This should not occur, checking just for paranoia */
				fprintf(stderr, "Failed to calculate data size\n");
				break;
			}

			//转换为packed模式
			int pcm_buffer_size = data_size * audio_frame->nb_samples * audio_codecContent->channels;
			uint8_t* pcm_buffer = (uint8_t*)malloc(pcm_buffer_size);
			memset(pcm_buffer, 0, pcm_buffer_size);

			fltp_convert_to_f32le((float*)pcm_buffer, (float*)audio_frame->data[0], (float*)audio_frame->data[1],
				audio_frame->nb_samples, audio_codecContent->channels);
			//使用SDL播放
			//Set audio buffer (PCM data)
			audio_chunk = pcm_buffer;//(Uint8*)audio_frame->data[0];
			audio_chunk = pcm_buffer;
			audio_len = pcm_buffer_size;
			audio_pos = audio_chunk;

			audio_clock = audio_frame->pts * av_q2d(audio_codecContent->time_base) * 1000 ;
			//printf("音频帧pts: %f ms\n", audio_clock);

			while (audio_len > 0)//Wait until finish
				SDL_Delay(1);

			//SDL_Delay(1000);
			free(pcm_buffer);
		}
	}

	return 0;
}


int open_file_thread(void* data)
{
	//读取
	while (av_read_frame(ifmt_ctx, pkt) >= 0)
	{
		if (pkt->stream_index == videoindex) {

			//加入视频队列
			put_AVPacket_into_queue(&video_pkt_queue, pkt);
		}
		else if (pkt->stream_index == audioindex)
		{
			//加入音频队列
			put_AVPacket_into_queue(&audio_pkt_queue, pkt);
		}
		else
			av_packet_unref(pkt);
	}

	return 0;
}

int main(int argc, char * argv[]) 
{
	if ((ret = avformat_open_input(&ifmt_ctx, in_filename, 0, 0)) < 0) {
		printf("Could not open input file.");
		return -1;
	}

	if ((ret = avformat_find_stream_info(ifmt_ctx, 0)) < 0) {
		printf("Failed to retrieve input stream information");
		return -1;
	}

	videoindex = -1;
	for (int i = 0; i < ifmt_ctx->nb_streams; i++) { //nb_streams:视音频流的个数
		if (ifmt_ctx->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_VIDEO)
			videoindex = i;
		else if (ifmt_ctx->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_AUDIO)
			audioindex = i;
	}

	printf("\nInput Video===========================\n");
	av_dump_format(ifmt_ctx, 0, in_filename, 0);  // 打印信息
	printf("\n======================================\n");

	//根据编解码器id查询解码器并返回解码器结构
	video_codec = avcodec_find_decoder(ifmt_ctx->streams[videoindex]->codecpar->codec_id);

	//分配解码器上下文
	video_codecContent = avcodec_alloc_context3(video_codec);
	//拷贝视频流信息到视频编码器上下文中
	avcodec_parameters_to_context(video_codecContent, ifmt_ctx->streams[videoindex]->codecpar);

	frame_width = ifmt_ctx->streams[videoindex]->codecpar->width;
	frame_height = ifmt_ctx->streams[videoindex]->codecpar->height;
	//打开视频解码器和关联解码器上下文
	if (avcodec_open2(video_codecContent, video_codec, nullptr))
	{
		printf("could not open codec!\n");
		return -1;
	}

	//根据编解码器id查询解码器并返回解码器结构
	audio_codec = avcodec_find_decoder(ifmt_ctx->streams[audioindex]->codecpar->codec_id);

	//分配解码器上下文
	audio_codecContent = avcodec_alloc_context3(audio_codec);
	//拷贝音频流信息到音频编码器上下文中
	avcodec_parameters_to_context(audio_codecContent, ifmt_ctx->streams[audioindex]->codecpar);
	//打开音频解码器和关联解码器上下文
	if (avcodec_open2(audio_codecContent, audio_codec, nullptr))
	{
		printf("could not open codec!\n");
		return -1;
	}

	//申请一个AVPacket结构
	pkt = av_packet_alloc();

	//申请一个AVFrame 结构用来存放解码后的数据
	video_frame = av_frame_alloc();
	audio_frame = av_frame_alloc();


	//初始化SDL
	initSdl();

	video_pkt_queue.mutex = SDL_CreateMutex();
	audio_pkt_queue.mutex = SDL_CreateMutex();

	//设置SDL音频播放参数
	wanted_spec.freq = audio_codecContent->sample_rate; //采样率
	wanted_spec.format = AUDIO_F32LSB; // audio_codecContent->sample_fmt;  这里需要转换为sdl的采样格式
	wanted_spec.channels = audio_codecContent->channels; //通道数
	wanted_spec.silence = 0;
	wanted_spec.samples = audio_codecContent->frame_size;   //每一帧的采样点数量
	wanted_spec.callback = fill_audio_pcm2; //音频播放回调

	//打开系统音频设备
	if (SDL_OpenAudio(&wanted_spec, NULL) < 0) {
		printf("can't open audio.\n");
		return -1;
	}
	//Play
	SDL_PauseAudio(0);

	SDL_CreateThread(open_file_thread, "open_file", nullptr);
	SDL_CreateThread(video_play_thread, "video_play", nullptr);
	SDL_CreateThread(audio_play_thread, "audio_play", nullptr);
	
	bool quit = false;
	SDL_Event e;
	while (quit == false)
	{
		while (SDL_PollEvent(&e) != 0)
		{
			if (e.type == SDL_QUIT)
			{
				quit = true;
				break;
			}
		}
	}


	SDL_DestroyMutex(video_pkt_queue.mutex);
	SDL_DestroyMutex(audio_pkt_queue.mutex);

	//释放ffmpeg指针
	avformat_close_input(&ifmt_ctx);
	avcodec_free_context(&video_codecContent);
	avcodec_free_context(&audio_codecContent);
	av_frame_free(&audio_frame);
	av_frame_free(&video_frame);
	av_packet_free(&pkt);
	return 0;
}



//sdl初始化
int initSdl()
{
	bool success = true;

	if (SDL_Init(SDL_INIT_VIDEO))
	{
		printf("init sdl error:%s\n", SDL_GetError());
		success = false;
	}

	//创建window
	sdlWindow = SDL_CreateWindow("decode video", SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED, frame_width, frame_height, SDL_WINDOW_SHOWN);
	if (sdlWindow == nullptr)
	{
		printf("create window error: %s\n", SDL_GetError());
		success = false;
	}

	//创建渲染器
	sdlRender = SDL_CreateRenderer(sdlWindow, -1, 0);
	if (sdlRender == nullptr)
	{
		printf("init window Render error: %s\n", SDL_GetError());
		success = false;
	}

	//构建合适的纹理
	sdlTtexture = SDL_CreateTexture(sdlRender, SDL_PIXELFORMAT_IYUV, SDL_TEXTUREACCESS_STREAMING, frame_width, frame_height);

	return success;
}

//sdl音频回调
void  fill_audio_pcm2(void* udata, Uint8* stream, int len) {


	//获取当前系统时钟
	audio_callback_time = av_gettime();

	//SDL 2.0
	SDL_memset(stream, 0, len);

	if (audio_len == 0)
		return;
	len = (len > audio_len ? audio_len : len);

	SDL_MixAudio(stream, audio_pos, len, SDL_MIX_MAXVOLUME);
	audio_len -= len;


}

void closeSDL()
{
	
	SDL_DestroyWindow(sdlWindow);
	sdlWindow = nullptr;
	SDL_DestroyRenderer(sdlRender);
	sdlRender = nullptr;
	SDL_DestroyTexture(sdlTtexture);
	sdlTtexture = nullptr;
}

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

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

相关文章

题解校验码—CRC循环校验码与海明校验码

码距 一个编码系统的码距是任意两个码字的最小距离。 例如个编码系统采用三位长度的二进制编码&#xff0c;若该系统有四种编码分别为&#xff1a;000&#xff0c;011&#xff0c;100&#xff0c;111&#xff0c;此编码系统中000与111的码距为3&#xff1b;011与000的码距为2…

POE:性价比最高的 AI 整合网站

创作不易&#xff0c;如果本文对你有帮助&#xff0c;胖友记得一键三连 &#x1f62d;。更多 AI 优质内容推荐请关注主页 “AI” 专栏&#xff0c;笔者会不定期更新觉得自己用下来还不错的 AI 相关产品。 1.介绍 Poe 是一款同时整合了 ChatGPT、Sage、GPT-4、Claude、Claude-in…

经典神经网络(2)AlexNet及其在Fashion-MNIST数据集上的应用

2、深度卷积神经网络AlexNet ImageNet 数据集&#xff1a;一个开源的图片数据集&#xff0c;包含超过 1400万张图片和图片对应的标签&#xff0c;包含2万多个类别。 自从2010 年以来&#xff0c;ImageNet 每年举办一次比赛&#xff0c;即&#xff1a;ImageNet 大规模视觉识别挑…

数组排序算法

数组排序算法 一、冒泡排序算法二、直接选择排序三、插入排序四、反转排序 一、冒泡排序算法 冒泡排序算法&#xff1a; 类似气泡上涌的动作&#xff0c;会将数据在数组中从小到大或者从大到小不断向前移动。 基本思想&#xff1a; 冒泡排序的基本思想是对比相邻的两个元素值&…

并发编程(二) — 内存可见性问题

目录 前言 内存可见性问题 synchronized volatile CAS算法 CAS算法原理 CAS算法应用场景 CAS算法代码实现 参考目录 前言 在谈共享变量的内存可见性问题之前&#xff0c;先谈谈线程安全问题 &#xff0c;线程安全问题是指当多个线程同时读写一个共享资源并且没有任何同…

c高级day4作业

有m1.txt m2.txt m3.txt m4.txt&#xff0c;分别创建出对应的目录&#xff0c;m1 m2 m3 m4 并把文件移动到对应的目录下使用break关键字打印九九乘法表&#xff0c;提示&#xff1a;printf "%d * %d %d" $i $j $((i*j)) #!/bin/bash for i in m1 m2 m3 m4 do#文件夹…

只限今日免费,Midjourney 5.1震撼更新!逼真到给跪,中国情侣细节惊艳,3D视频大片马上来

来源 | 新智元 微信号&#xff1a;AI-era 【导读】全新升级的Midjourney让全网又疯狂了&#xff0c;创造力解禁&#xff0c;出图更逼真。重要的是&#xff0c;限时免费到今天&#xff0c;要玩的抓紧了。 一个月前&#xff0c;Midjourney V5画的一对中国完美情侣在网上爆火&am…

涅槃重生,BitKeep如何闯出千万用户新起点

在全球&#xff0c;BitKeep钱包现在已经有超过千万用户在使用。 当我得知这个数据的时候&#xff0c;有些惊讶&#xff0c;也有点意料之中。关注BitKeep这几年&#xff0c;真心看得出这家公司的发展之迅速。还记得2018年他们推出第一个版本时&#xff0c;小而美&#xff0c;简洁…

python和pycharm的安装(安装包免费下载共享)

说明&#xff1a; 本文内容包括Python和Pycharm安装。 一、python安装&#xff1a; python是一门编程语言&#xff0c;安装python是为了能在电脑上使用这门语言。 1、python下载 下载链接&#xff1a;https://pan.baidu.com/s/1mWsJjO8HngNQtINCzu0bBA 提取码&#xff1a;9…

又又又发现了一个 AI 插件神器 TeamSmart

简介 TeamSmart AI 是最近比较火的 Chrome 插件&#xff0c;它是基于 ChatGPT 集成的 AI 助手团队工具 对&#xff0c;没错&#xff0c;是一个团队。这个团队里面有许多不同角色的成员&#xff0c;每隔成员都有自己的专业领域&#xff0c;比如商业、市场营销、灵魂写手、程序…

批量查看域名历史软件-网站老域名批量查询注册

未注册备案域名批量扫描软件 未注册备案域名批量扫描软件是专门用于批量扫描未备案的域名的一种工具。它可以快速识别未备案的域名&#xff0c;并帮助用户抓住还未被注册的值得备案的域名&#xff0c;以便用户及时注册备案并使用。 该软件主要具有以下几个优点&#xff1a; 高…

【小程序】输入框检验姓名、身份证(正则表达式)并提交

目标 输入绑定姓名、身份证号并进行校验若未填或校验不通过则显示绑定失败的轻提示若通过校验并提交则显示绑定成功 使用Vant Weapp (gitee.io)库。 思路与代码 html&#xff1a; wx:model绑定输入框输入的值data-key是一个属性&#xff0c;在js中的e.currentTarget.datase…

【新星计划-2023】ARP“攻击”与“欺骗”的原理讲解

网络管理员在网络维护阶段需要处理各种各样的故障&#xff0c;出现最多的就是网络通信问题。除物理原因外&#xff0c;这种现象一般是ARP攻击或ARP欺骗导致的。无论是ARP攻击还是ARP欺骗&#xff0c;它们都是通过伪造ARP应答来实现的。 一、ARP攻击原理 一般情况下&#xff0…

TypeScript语言编译命令

1. 安装 npm install -g typescript2. 编译 tsc工具是TypeScript编译器的控制台接口&#xff0c;它可以将TypeScript文件编译成JavaScript文件&#xff1b; 编译文件&#xff1a; tsc [options] [file ...]查看编译命令的帮助信息&#xff1a; tsc --help或者 tsc -h或者 tsc…

微服架构基础设施环境平台搭建 -(一)基础环境准备

微服架构基础设施环境平台搭建 -&#xff08;一&#xff09;基础环境准备 通过采用微服相关架构构建一套以KubernetesDocker为自动化运维基础平台&#xff0c;以微服务为服务中心&#xff0c;在此基础之上构建业务中台&#xff0c;并通过Jekins自动构建、编译、测试、发布的自动…

【Java AWT 图形界面编程】IntelliJ IDEA 乱码问题最佳配置方案 ( 配置文件编码 | 配置编译器编码参数 | 配置运行时编码参数 )

文章目录 一、IntelliJ IDEA 乱码问题二、IntelliJ IDEA 乱码问题最佳配置方案1、文件编码设置成 UTF-82、编译器编码参数设置成 UTF-83、 配置运行时编码参数为 GBK 一、IntelliJ IDEA 乱码问题 在 IntelliJ IDEA 中开发 AWT / Swing 图形界面程序 , 经常遇到乱码问题 ; 文件…

博文的跑路笔记

HTML CSS Flex布局 使用flex布局 容器 .box {display: flex; }行内元素 .box {display: inline-flex; }flex布局后&#xff0c;float、vertical-align、clear失效。 容器属性 flex-direction&#xff1a;主轴方向 属性值 row&#xff1a;子元素起点在左&#xff0c;左到右。…

初级面试问到rabbitMQ,看这一篇文章就够了!

一、rabbitMQ的基础要点回顾 1.使用场景 1&#xff09;解耦&#xff1a;使用消息队列避免模块间的直接调用。将所需共享的数据放在消息队列中&#xff0c;对于新增的业务模块&#xff0c;只要对该类消息感兴趣就可以订阅该消息&#xff0c;对原有系统无影响&#xff0c;降低了…

防范化解灾害风险,科技筑牢安全城墙

气象灾害&#xff1a;大气圈变异活动会对人类生命财产和国民经济及国防建设等造成的直接或间接损害。我国气象灾害种类繁多&#xff0c;不仅包括台风、暴雨、冰雹、大风、雷暴、暴风雪等天气灾害&#xff0c;还包括干旱、洪涝、持续高温、雪灾等气候灾害。此外&#xff0c;与气…

Docker中部署监控

Docker概念 一、部署Prometheus+grafana环境 1.1 、部署Prometheus+grafana环境 docker pull registry.cn-hangzhou.aliyuncs.com/lhrbest/lhrprometheus:1.0 docker tag registry.cn-hangzhou.aliyuncs.com/lhrbest/lhrprometheus:1.0 lhrbest/lhrprometheus:1.01.2 、创建镜…