音视频开发新手项目FFMPEG+SDL播放器`

本文详细介绍了如何使用FFMPEG库进行视频解码,并配合SDL库实现一个基础的视频播放器,包括文件读取、解码、图像转换和SDL界面显示。适合初学者学习FFmpeg的视频处理流程。
摘要由CSDN通过智能技术生成
在这里插入代码片

提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档


前言

音视频开发新手项目FFMPEG+SDL播放器`

雷神的代码,做了些解释,方便大家一起学习

提示:以下是本篇文章正文内容,下面案例可供参考

一、代码如下

/**

  • 最简单的基于FFmpeg的视频播放器2(SDL升级版)
  • Simplest FFmpeg Player 2(SDL Update)
  • 雷霄骅 Lei Xiaohua
  • leixiaohua1020@126.com
  • 中国传媒大学/数字电视技术
  • Communication University of China / Digital TV Technology
  • http://blog.csdn.net/leixiaohua1020
  • 本程序实现了视频文件的解码和显示(支持HEVC,H.264,MPEG2等)。
  • 是最简单的FFmpeg视频解码方面的教程。
  • 通过学习本例子可以了解FFmpeg的解码流程。
  • 本版本中使用SDL消息机制刷新视频画面。
  • This software is a simplest video player based on FFmpeg.
  • Suitable for beginner of FFmpeg.

*/

#include <stdio.h>

#define __STDC_CONSTANT_MACROS//是一种解决方法,允许 C++ 程序使用 C99 标准中指定但不在C++ 标准。

extern "C"
{
#include "libavcodec/avcodec.h"
#include "libavformat/avformat.h"
#include "libswscale/swscale.h"
#include "SDL2/SDL.h"
};


//Refresh Event
#define SFM_REFRESH_EVENT  (SDL_USEREVENT + 1)//定义一个事件用来刷新

#define SFM_BREAK_EVENT  (SDL_USEREVENT + 2)
/*(SDL_USEREVENT + 1) 是一个SDL库中预定义的事件类型的索引。
SDL_USEREVENT 是SDL库为用户自定义事件保留的一个范围,
而 (SDL_USEREVENT + 1) 表示在用户自定义事件范围内向后移动了一个位置,因此它是一个新的用户自定义事件类型的索引。
这样做是为了确保自定义事件类型不会与SDL库内部事件类型冲突。
*/

int thread_exit=0;

int sfp_refresh_thread(void *opaque){//定义一个线程事件用来刷新
	thread_exit=0;
	while (!thread_exit) {
		SDL_Event event;
		event.type = SFM_REFRESH_EVENT;//定义一个事件用来刷新
		SDL_PushEvent(&event);//SDL_PushEvent函数是SDL库中的一个函数,用于将一个自定义的事件推入事件队列中。
		SDL_Delay(40);//让当前线程暂停执行40毫秒,然后再继续执行后续的代码。
	}
	thread_exit=0;
	//Break
	SDL_Event event;
	event.type = SFM_BREAK_EVENT;//定义一个事件用来退出
	SDL_PushEvent(&event);

	return 0;
}


int main(int argc, char* argv[])
{

	AVFormatContext	*pFormatCtx;//封装格式上下文结构体,也是统领全局的结构体,保存了视频文件封装格式相关信息
	int				i, videoindex;
	AVCodecContext	*pCodecCtx;//编码器上下文结构体,保存了视频(音频)编解码相关信息。
	AVCodec			*pCodec;//每种视频(音频)编解码器
	AVFrame	*pFrame,*pFrameYUV;//存储一帧解码后像素(采样)数据
	uint8_t *out_buffer;//uint8_t 是C/C++语言中的一种数据类型,表示8位的无符号整数,取值范围为0到255。声明了一个缓冲区
	AVPacket *packet;//存储一帧压缩编码数据。也就是解码前的数据
	int ret, got_picture;

	//------------SDL----------------
	int screen_w,screen_h;//解码视频的宽和高
	SDL_Window *screen; 
	SDL_Renderer* sdlRenderer;
	SDL_Texture* sdlTexture;
	SDL_Rect sdlRect;
	/*    SDL_Window *screen;:声明了一个指向SDL_Window类型的指针变量,通常用于表示应用程序的窗口。在SDL中,SDL_Window是窗口的抽象表示,你可以使用它来创建、管理和控制窗口。

    SDL_Renderer* sdlRenderer;:声明了一个指向SDL_Renderer类型的指针变量,用于处理图形渲染相关的操作。SDL_Renderer用于将图形渲染到窗口上,你可以使用它来绘制几何形状、图像等。

    SDL_Texture* sdlTexture;:声明了一个指向SDL_Texture类型的指针变量,表示在渲染器上进行绘制的纹理。SDL_Texture是SDL中用于表示图像数据的对象,你可以将图像加载到纹理中,然后在渲染器上进行绘制。

    SDL_Rect sdlRect;:声明了一个SDL_Rect类型的变量,用于表示矩形区域的位置和大小。SDL_Rect通常用于指定在窗口或者纹理上进行绘制的位置和大小。*/
	SDL_Thread *video_tid;
	SDL_Event event;

	struct SwsContext *img_convert_ctx;//struct SwsContext 是FFmpeg(或Libav)库中的一个结构体,用于图像转换和缩放的上下文。

	char filepath[]="屌丝男士.mov";

	av_register_all();//注册所有组件。
	avformat_network_init();/*在使用FFmpeg库进行网络相关的操作(比如从网络流媒体中获取数据)之前,
需要调用 avformat_network_init() 函数进行网络初始化。这个函数会初始化FFmpeg库中的网络模块,确保网络相关的功能可以正常使用。*/
	pFormatCtx = avformat_alloc_context();//用于分配一个空的 AVFormatContext 结构体,并将其指针赋值给 pFormatCtx。

	if(avformat_open_input(&pFormatCtx,filepath,NULL,NULL)!=0){//打开视频文件并解析为AVFormatContext格式,赋值给pFormatCtx,
		printf("Couldn't open input stream.\n");//原型为int avformat_open_input(AVFormatContext **ps, const char *url, AVInputFormat *fmt, AVDictionary **options);
		return -1;//fmt:要强制使用的输入格式。通常传入 NULL 表示自动检测输入文件的格式。options:一些附加选项,比如设置超时、设置缓冲区大小等。
	}
	if(avformat_find_stream_info(pFormatCtx,NULL)<0){//获取视频文件信息。int avformat_find_stream_info(AVFormatContext *ic, AVDictionary **options);
		printf("Couldn't find stream information.\n");//提取出流的基本信息,比如编解码器信息、帧率、分辨率等。这些信息会填充到 AVFormatContext 结构体中的相应字段中
		return -1;
	}
	videoindex=-1;
	for(i=0; i<pFormatCtx->nb_streams; i++) //nb_streams :输入视频的AVStream 个数,本段函数就是找到视频流的开始编号
		if(pFormatCtx->streams[i]->codec->codec_type==AVMEDIA_TYPE_VIDEO){//AVMEDIA_TYPE_VIDEO 是一个枚举值,表示音视频流的类型之一,具体指的是视频流。
			videoindex=i;
			break;
			/*AVMEDIA_TYPE_AUDIO:音频流
			AVMEDIA_TYPE_SUBTITLE:字幕流
			AVMEDIA_TYPE_ATTACHMENT:附件流(比如封面、章节等)
			AVMEDIA_TYPE_DATA:数据流(比如标签、元数据等)*/
		}
	if(videoindex==-1){
		printf("Didn't find a video stream.\n");
		return -1;
	}
	pCodecCtx=pFormatCtx->streams[videoindex]->codec;//获取视频流的解码上下文并存储在之前定义的编码器上下文结构体
	pCodec=avcodec_find_decoder(pCodecCtx->codec_id);//根据编码器上下文结构体信息查找解码器并赋值给pCodec。
	if(pCodec==NULL){
		printf("Codec not found.\n");
		return -1;
	}
	if(avcodec_open2(pCodecCtx, pCodec,NULL)<0){//打开解码器。使用给定的解码器(pCodec)初始化并打开解码器上下文(pCodecCtx)
		printf("Could not open codec.\n");
		return -1;
	}
	pFrame=av_frame_alloc();//分配内存
	pFrameYUV=av_frame_alloc();
	out_buffer=(uint8_t *)av_malloc(avpicture_get_size(PIX_FMT_YUV420P, pCodecCtx->width, pCodecCtx->height));//计算图像大小分配内存
	//out_buffer = (uint8_t*)av_malloc(av_image_get_buffer_size(AV_PIX_FMT_YUV420P, pCodecCtx->width, pCodecCtx->height,1));
	avpicture_fill((AVPicture *)pFrameYUV, out_buffer, PIX_FMT_YUV420P, pCodecCtx->width, pCodecCtx->height);//用于将一组图像数据填充到一个 AVPicture 结构体中。
	/*typedef struct AVPicture {
    uint8_t *data[AV_NUM_DATA_POINTERS];
    int linesize[AV_NUM_DATA_POINTERS];
}	AVPicture;
	data:用于存储图像数据的指针数组,通常有三个元素,分别用于存储 Y、U 和 V 三个分量的数据指针。
    linesize:用于存储图像数据每行的大小的数组,通常也有三个元素,分别对应于三个分量的每行大小。
*/
	img_convert_ctx = sws_getContext(pCodecCtx->width, pCodecCtx->height, pCodecCtx->pix_fmt, 
		pCodecCtx->width, pCodecCtx->height, PIX_FMT_YUV420P, SWS_BICUBIC, NULL, NULL, NULL); //用于创建一个图像转换上下文(SwsContext),该上下文用于执行图像格式转换和缩放操作
	/*
	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);
	srcW:源图像的宽度。
    srcH:源图像的高度。
    srcFormat:源图像的像素格式。
    dstW:目标图像的宽度。
    dstH:目标图像的高度。
    dstFormat:目标图像的像素格式。
    flags:转换标志,用于指定转换时的一些选项。
    srcFilter:源图像的滤波器。
    dstFilter:目标图像的滤波器。
    param:保留参数,暂时没有使用。

在这行代码中,sws_getContext 函数被用于创建一个图像转换上下文 img_convert_ctx,用于将视频帧从 pCodecCtx->pix_fmt(视频流的原始像素格式)转换为 YUV420P 格式,以便后续处理。
*/

	if(SDL_Init(SDL_INIT_VIDEO | SDL_INIT_TIMER)) {  //初始化视频和定时器子系统,使用了 SDL 库中的 SDL_Init 函数来初始化 SDL 库,并检查初始化是否成功。
		printf( "Could not initialize SDL - %s\n", SDL_GetError()); 
		return -1;
	} 
	//SDL 2.0 Support for multiple windows
	screen_w = pCodecCtx->width;
	screen_h = pCodecCtx->height;
	screen = SDL_CreateWindow("Simplest ffmpeg player's Window", SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED,
		screen_w, screen_h,SDL_WINDOW_OPENGL);//创建一个窗口并返回一个指向该窗口的指针。
	/*
	SDL_Window* SDL_CreateWindow(const char* title, int x, int y, int w, int h, Uint32 flags);
	title:窗口的标题。
	x:窗口的左上角 x 坐标,SDL_WINDOWPOS_UNDEFINED 表示将窗口放置在屏幕的中央。
	y:窗口的左上角 y 坐标,SDL_WINDOWPOS_UNDEFINED 表示将窗口放置在屏幕的中央。
	w:窗口的宽度。
	h:窗口的高度。
	flags:窗口的标志,控制窗口的行为,比如是否可调整大小、是否全屏等。在这里使用了 SDL_WINDOW_OPENGL,表示创建一个支持 OpenGL 渲染的窗口。
*/
	if(!screen) {  
		printf("SDL: could not create window - exiting:%s\n",SDL_GetError());  
		return -1;
	}
	sdlRenderer = SDL_CreateRenderer(screen, -1, 0);  //这行代码调用了SDL库中的SDL_CreateRenderer函数,用于创建一个与指定窗口相关联的渲染器。
	/*
	SDL_Renderer* SDL_CreateRenderer(SDL_Window* window, int index, Uint32 flags);
	window:指定要与渲染器关联的窗口。
	index:指定渲染器所支持的驱动程序索引。通常传入-1表示使用第一个支持的渲染器。
	flags:指定渲染器的标志。在这里传入0表示使用默认标志。*/
	//IYUV: Y + U + V  (3 planes)
	//YV12: Y + V + U  (3 planes)
	sdlTexture = SDL_CreateTexture(sdlRenderer, SDL_PIXELFORMAT_IYUV, SDL_TEXTUREACCESS_STREAMING,pCodecCtx->width,pCodecCtx->height);  
	/*调用了SDL库中的SDL_CreateTexture函数,用于创建一个纹理对象。
	SDL_Texture* SDL_CreateTexture(SDL_Renderer* renderer, Uint32 format, int access, int w, int h);
     renderer:指定要与纹理关联的渲染器。
    format:指定纹理的像素格式。在这里使用了SDL_PIXELFORMAT_IYUV,表示纹理的像素格式是IYUV(即YUV420格式)。
    access:指定纹理的访问方式。在这里使用了SDL_TEXTUREACCESS_STREAMING,表示纹理支持频繁更新,比如通过像素数据流进行更新。
    w:指定纹理的宽度。
    h:指定纹理的高度。*/
	sdlRect.x=0;
	sdlRect.y=0;
	sdlRect.w=screen_w;
	sdlRect.h=screen_h;

	packet=(AVPacket *)av_malloc(sizeof(AVPacket));

	video_tid = SDL_CreateThread(sfp_refresh_thread,NULL,NULL);
	/*SDL 库中用于创建线程的函数,用于在应用程序中启动一个新的线程执行指定的任务。
	SDL_Thread* SDL_CreateThread(SDL_ThreadFunction fn, const char* name, void* data);
	fn:指定新线程要执行的函数,通常是一个函数指针。
	name:指定新线程的名称,可以是一个字符串。
	data:指定传递给新线程的数据,通常是一个指针。这个函数会返回一个 SDL_Thread* 类型的指针,指向新创建的线程对象*/
	//------------SDL End------------
	//Event Loop
	
	for (;;) {
		//Wait
		SDL_WaitEvent(&event);
		/* SDL 库中用于等待事件的函数,它会一直阻塞当前线程,直到一个事件发生并被取出处理。
		int SDL_WaitEvent(SDL_Event *event);
		参数 event 是一个指向 SDL_Event 结构体的指针,用于存储发生的事件。
		调用 SDL_WaitEvent 函数后,程序会一直阻塞,直到某个事件发生。一旦有事件发生,函数就会返回,
		同时将事件信息填充到 event 参数所指向的结构体中,并返回一个非零值表示成功。如果发生错误或者函数被中断,它会返回零。		  */
		if(event.type==SFM_REFRESH_EVENT){
			//------------------------------
			if(av_read_frame(pFormatCtx, packet)>=0){//av_read_frame 函数会从输入媒体中读取下一个音频或视频帧,并将其存储到 pkt 所指向的 AVPacket 结构体中。这个函数会自动选择适当的流并读取其下一个帧。
				if(packet->stream_index==videoindex){
					ret = avcodec_decode_video2(pCodecCtx, pFrame, &got_picture, packet);//解码视频帧数据。
					if(ret < 0){//int avcodec_decode_video2(AVCodecContext *avctx, AVFrame *picture, int *got_picture_ptr, const AVPacket *avpkt);
						/*    avctx:指向已经初始化的视频解码器上下文(AVCodecContext)的指针。
							picture:指向要存储解码后视频帧的 AVFrame 结构体的指针。
							got_picture_ptr:一个指向整数的指针,用于存储解码是否成功的标志。如果解码成功,会将该标志设置为非零值,否则设置为零值。
							avpkt:指向包含待解码视频数据的 AVPacket 结构体的指针。
							调用 avcodec_decode_video2 函数会将 avpkt 所指向的视频数据解码,
							并将解码后的视频帧存储到 picture 所指向的 AVFrame 结构体中。解码是否成功会通过 got_picture_ptr 指针进行标志。
							成功解码后,picture 中将包含解码后的视频帧数据,可以用于后续的处理或显示。
							函数返回值为解码后的帧的字节数,或者出现错误时返回负数。*/
						printf("Decode Error.\n");
						return -1;
					}
					if(got_picture){
						/*将输入图像数据转换为目标像素格式,并进行可选的图像缩放和滤波操作。
						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:指向已经初始化的图像转换上下文(SwsContext)的指针。
						srcSlice:指向源图像数据的数组,通常是一个指向每个图像平面数据指针的数组。
						srcStride:指向源图像每行大小的数组,通常是一个指向每个图像平面行大小的数组。
						srcSliceY:指定源图像数据的起始行。
						srcSliceH:指定源图像数据的行数。
						dst:指向目标图像数据的数组,通常是一个指向每个图像平面数据指针的数组。
						dstStride:指向目标图像每行大小的数组,通常是一个指向每个图像平面行大小的数组。
						调用 sws_scale 函数会将 srcSlice 中指定的源图像数据转换为目标像素格式,并存储到 dst 指定的目标图像数据数组中。
						转换后的图像数据可以进行可选的缩放和滤波操作,具体取决于初始化图像*/
						sws_scale(img_convert_ctx, (const uint8_t* const*)pFrame->data, pFrame->linesize, 0, pCodecCtx->height, pFrameYUV->data, pFrameYUV->linesize);
						//SDL---------------------------
						SDL_UpdateTexture( sdlTexture, NULL, pFrameYUV->data[0], pFrameYUV->linesize[0] );
						/*将 YUV 数据更新到纹理对象中,用于显示视频帧。
						int SDL_UpdateTexture(SDL_Texture* texture, const SDL_Rect* rect, const void* pixels, int pitch);
						texture:指定要更新的纹理对象。
						rect:指定要更新的纹理区域的矩形区域。如果为 NULL,则更新整个纹理。
						pixels:指向包含要更新到纹理的像素数据的指针。
						pitch:指定像素数据的行大小(即每行像素数据的字节数)。*/
						SDL_RenderClear( sdlRenderer );  
						/*用于清除渲染器的绘图区域。
						int SDL_RenderClear(SDL_Renderer* renderer);
						参数 renderer 是要清除的渲染器。
						调用 SDL_RenderClear 函数会清除与指定渲染器相关联的绘图区域,
						将其填充为渲染器的默认清除颜色。这样做通常是为了准备绘制新的一帧图像或场景。*/
						//SDL_RenderCopy( sdlRenderer, sdlTexture, &sdlRect, &sdlRect );  
						SDL_RenderCopy( sdlRenderer, sdlTexture, NULL, NULL);  
						/*用于将纹理的内容复制到渲染器的绘图区域中。
						    renderer:指定要进行绘制的渲染器。
							texture:指定要复制的纹理对象。
							srcrect:指定要从纹理中复制的矩形区域。如果为NULL,则表示复制整个纹理。
							dstrect:指定纹理将要复制到渲染器中的目标矩形区域。如果为NULL,则表示纹理将会被复制到渲染器的整个绘图区域中。*/
						SDL_RenderPresent( sdlRenderer );  //将渲染器中的绘制结果显示到屏幕
						//SDL End-----------------------
					}
				}
				av_free_packet(packet);//释放被 pkt 指向的 AVPacket 结构体所占用的内存空间。在解码或处理完音视频帧数据后,通常需要调用这个函数来释放 AVPacket 结构体的内存,以避免内存泄漏。
			}else{
				//Exit Thread
				thread_exit=1;
			}
		}else if(event.type==SDL_QUIT){
			thread_exit=1;
		}else if(event.type==SFM_BREAK_EVENT){
			break;
		}

	}

	sws_freeContext(img_convert_ctx);//用于释放图像转换上下文内存的函数。

	SDL_Quit();//用于清理和关闭 SDL 环境的函数。
	//--------------
	av_frame_free(&pFrameYUV);
	av_frame_free(&pFrame);
	avcodec_close(pCodecCtx);
	avformat_close_input(&pFormatCtx);

	return 0;
}

  • 20
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
### 回答1: FFmpeg是一个开源的跨平台多媒体处理工具,可以用来编码、解码、转码和播放各种音频和视频文件。SDL(Simple DirectMedia Layer)是一个跨平台的多媒体库,可以用来处理音频、视频和输入设备。 FFmpegSDL可以结合使用来实现一个简单的媒体播放器。首先,我们需要使用FFmpeg来解码音频和视频文件。通过FFmpeg的解码功能,我们可以将音频和视频数据解析出来,然后就可以利用SDL将它们播放出来。 在SDL中,我们可以创建一个音频流和一个视频流,并将解码后的音频和视频数据分别写入其中。SDL会负责将这些数据渲染到音频设备和视频窗口,从而实现播放效果。我们可以通过控制音频和视频流的缓冲区大小和时钟同步来实现音视频的同步播放。 另外,我们还可以利用FFmpeg的一些其他功能来提升播放器的性能和功能。例如,可以使用FFmpeg的过滤器功能来实现音频和视频的裁剪、旋转、缩放等操作。也可以利用FFmpeg的网络协议支持来播放网络上的音频和视频流。 总之,FFmpegSDL可以组成一个简单但功能强大的媒体播放器。通过使用FFmpeg的解码功能和SDL的渲染功能,我们可以实现音视频的解码和播放。而且,FFmpeg提供了许多其他的功能,比如过滤器、网络协议支持等,可以让我们的播放器更加灵活和强大。 ### 回答2: ffmpeg sdl播放器是一种基于ffmpegSDL开发音视频播放器ffmpeg是一种开源的多媒体处理库,可以对音视频数据进行解码、编码、转码等操作。SDL是一种跨平台的多媒体开发库,可以实现多媒体的显示、音频播放、事件处理等功能。 使用ffmpeg sdl播放器可以实现对各种音视频格式的播放。首先,它可以将各种格式的音视频文件进行解码,将数据转换成可供显示和播放的格式。然后,通过SDL库可以将解码后的音频数据进行声音的播放,并将视频数据进行显示。 此外,ffmpeg sdl播放器还支持音视频的同步播放。它会根据视频帧的时间戳来计算音频数据的播放时间,从而实现音视频的同步播放。同时,它还能够处理音视频的各种事件,如播放暂停、快进、快退等。 ffmpeg sdl播放器还具有良好的扩展性和可定制性。开发者可以根据自己的需求,进行特定功能的定制和扩展,如添加字幕显示、视频特效等功能。 总的来说,ffmpeg sdl播放器是一个功能强大、灵活性高的音视频播放器。它可以支持各种音视频格式,实现音视频的同步播放,并且具有良好的扩展性。无论是在桌面应用还是移动应用中,ffmpeg sdl播放器都是一个理想的选择。 ### 回答3: ffmpeg sdl播放器是一款基于FFmpegSDL(Simple DirectMedia Layer)库开发视频播放器。通过FFmpeg库,它可以解码各种视频格式,并且还支持音频解码功能。SDL库则提供了跨平台的图形、声音、事件处理等功能。 这款播放器具有以下特点和功能: 1. 跨平台性能:FFmpegSDL都是跨平台的库,因此该播放器可以在多个操作系统(如Windows、Mac OS、Linux等)上运行,并且具有良好的性能和稳定性。 2. 支持多种视频格式:FFmpeg库提供了广泛的视频格式支持,如AVI、MP4、MKV等,因此该播放器可以播放多种常见的视频文件。 3. 支持多种音频格式:除了视频,该播放器还支持音频解码,可以播放多种音频格式,如MP3、AAC、FLAC等。 4. 播放控制和界面:播放器提供基本的播放控制,如播放、暂停、快进、快退等功能,同时还有播放进度条和音量控制条,用户可以根据需求进行设置。 5. 良好的用户体验:播放器界面简洁易用,具有良好的用户体验,适合不同年龄段的用户使用。 总之,ffmpeg sdl播放器是一款功能强大且易于使用的视频播放器,它支持多种视频和音频格式,提供基本的播放控制和界面,可以在不同操作系统上运行。无论是观看电影、视频教程,还是听音乐,这款播放器都能满足用户的需求。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值