ffmpeg播放器实现详解 - 框架搭建
liuian 2025-09-14 22:27 3 浏览
ffplay是ffmpeg源码中一个自带的开源播放器实例,同时支持本地视频文件的播放以及在线流媒体播放,功能非常强大。
FFplay: FFplay is a very simple and portable media player using the FFmpeg libraries and the SDL library. It is mostly used as a testbed for the various FFmpeg APIs
ffplay中的代码充分调用了ffmpeg中的函数库,因此,想学习ffmpeg的使用,或基于ffmpeg开发一个自己的播放器,ffplay都是一个很好的切入点。 ffplay源码编译见[公众号:断点实验室]的前述文章 [ffplay源码编译]。 由于ffmpeg本身的开发文档比较少,且ffplay播放器源码的实现相对复杂,除了基础的ffmpeg组件调用外,还包含视频帧的渲染、音频帧的播放、音视频同步策略及线程调度等问题。
因此,这里我们以ffmpeg官网推荐的一个ffplay播放器简化版本的开发例程为基础,在此基础上循序渐进由浅入深,最终探讨实现一个视频播放器的完整逻辑。
ffplay播放器简化版本开发例程可在ffmpeg官网[documentation]页面的右下角找到,点击An FFmpeg and SDL Tutorial即可打开找到对应的源码。
这里对其中部分难以理解的代码进行了修改,并对几乎所有代码逐行注释,方便大家理解
1、项目编译环境搭建
这里仍以Ubuntu 16.04 LTS为基础进行讲述,由于ffmpeg支持多个主流平台,且api接口在各个平台是一致的,因此其他平台也可参照本文内容,后续会将代码移植到windows等其他平台,方便大家调试。
源码的编译除了ffmpeg环境外,还需要SDL-1.x版本的支持,用于提供视频帧的渲染及音频帧的播放。
1.1 sdl库编译
SDL(Simple DirectMedia Layer)是一个跨平台的多媒体和游戏开发包,提供2D,音频,事件驱动,多线程和定时器等服务,它使用C语言写成,提供了多种控制图像、声音、输出的函数,让开发者只要用相同或是相似的代码就可以开发出跨多个平台(Linux、Windows、Mac OS X等)的应用软件。
SDL: Simple DirectMedia Layer is a cross-platform development library designed to provide low level access to audio, keyboard, mouse, joystick, and graphics hardware via OpenGL and Direct3D. It is used by video playback software, emulators, and popular games including Valve's award winning catalog and many Humble Bundle games.
可通过下面的链接下载SDL-1.2.15源码,注意,例程中依赖的SDL版本与ffplay中有所不同
https://www.libsdl.org/download-1.2.php
下载完成后解压进入sdl源码目录,可通过下面的配置方法生成Makefile文件
./configure --prefix=/usr/local/3rdparty/sdl
生成Makefile文件后,输入make命令即可开始编译过程,编译完成后,执行make install命令进行安装
make
make install
安装完成后,会在configure指定的目录下找到sdl的目录,由于sdl以库文件的方式提供支持,因此在sdl/bin目录下没有对应的可执行文件。
1.2 sdl环境变量配置
sdl编译完成后,还需要让系统能够找到对应的安装位置。打开/etc/profile配置文件,在该文件底部添加sdl的环境变量
#SDL ENVIRONMENT
export C_INCLUDE_PATH=/usr/local/3rdparty/sdl/include/SDL:$C_INCLUDE_PATH
export LD_LIBRARY_PATH=/usr/local/3rdparty/sdl/lib:$LD_LIBRARY_PATH
export PKG_CONFIG_PATH=/usr/local/3rdparty/sdl/lib/pkgconfig:$PKG_CONFIG_PATH
1.3 项目源码编译
项目源码可采用如下Makefile脚本进行编译
tutorial01: tutorial01.c
gcc -o tutorial01 -g3 tutorial01.c -I${FFMPEG_INCLUDE} -I${SDL_INCLUDE} \
-L${FFMPEG_LIB} -lavutil -lavformat -lavcodec -lswscale -lswresample -lz -lm \
`sdl-config --cflags --libs`
clean:
rm -rf tutorial01
rm -rf *.ppm
执行make命令开始编译,编译完成后,可在源码目录生成名为[tutorial01]的可执行文件。
1.4 验证
与ffplay的使用方法类似,执行[tutorial01 url]命令,可以看到在源码目录生成的后缀名为.ppm的图像
./tutorial01 rtmp://58.200.131.2:1935/livetv/hunantv
ppm图像在linux平台下可直接打开,看到有ppm图像生成,即可确定项目能够正常工作,输入Ctrl+C结束程序运行。
ppm格式的图像平时不太常用,大家没有必要做深入研究,这里仅用于对编译结果的验证。
PPM: A PPM file is a 24-bit color image formatted using a text format. It stores each pixel with a number from 0 to 65536, which specifies the color of the pixel. PPM files also store the image height and width, whitespace data, and the maximum color value. The portable pixmap format (PPM), the portable graymap format (PGM) and the portable bitmap format (PBM) are image file formats designed to be easily exchanged between platforms.
领取C++音视频开发学习资料:点击→音视频开发(资料文档+视频教程+面试题)(FFmpeg+WebRTC+RTMP+RTSP+HLS+RTP)
2 源码分析
上述例程除了生成几张图片外,好像什么也做不了,似乎离一个功能完整的视频播放器还有很远的距离。 尽管如此,例程依然包含了ffmpeg视频开发用到的几乎所有关键的api与数据结构。后面的内容会在此基础上不断的完善,直至实现一个完整的视频播放器。
2.1 流程
下面给出例程的流程图,流程非常简单,所有代码都运行在主线程中,流程涉及api及数据结构的含义都在例程源码中有详细的注释。
2.2 源码中涉及的api及组件
由于篇幅的限制,这里先简要介绍每个组件及api的含义,后续文章中会深入介绍每个组件及api的使用方法
组件:
- AVFormatContext 保存文件容器封装信息及码流参数的结构体
- AVCodecContext 解码器上下文对象,解码器依赖的相关环境、状态、资源以及参数集的接口指针
- AVCodec 保存编解码器信息的结构体,提供编码与解码的公共接口
- AVPacket 负责保存压缩编码数据相关信息的结构体,每帧图像由一到多个packet包组成
- AVFrame 保存音视频解码后的数据,如状态信息、编解码器信息、宏块类型表,QP表,运动矢量表等数据
- SwsContext 描述转换器参数的结构体
api :
- av_register_all 注册所有ffmpeg支持的多媒体格式及编解码器
- avformat_open_input 打开视频文件,读文件头内容,取得文件容器的封装信息及码流参数并存储在pFormatCtx中
- avformat_find_stream_info 取得文件中保存的码流信息,并填充到pFormatCtx->stream 字段
- avcodec_find_decoder 根据视频流对应的解码器上下文查找对应的解码器,返回对应的解码器
- avcodec_alloc_context3 复制编解码器上下文对象,用于保存从视频流中抽取的帧
- avcodec_open2 打开解码器
- av_frame_alloc 为解码后的视频信息结构体分配空间并完成初始化操作
- av_read_frame 从文件中依次读取每个图像编码数据包,并存储在AVPacket数据结构中
- avcodec_decode_video2 解码完整的一帧数据,若一个packet无法解码一个完整的视频帧,则在ffmpeg后台维护的缓存队列会持续等待多个packet,直到能够解码出一个完整的视频帧为止
3 ffmpeg能帮我们做什么
视频开发涉及到多种视频格式的编解码,多种文件格式及传输协议的解封装等操作,很难一下子全部掌握。 ffmpeg通过其封装的api及组件,为我们屏蔽了不同视频封装格式及编码格式的差异,以统一的api接口提供给开发者使用,开发者不需要了解每种编码方式及封装方式具体的技术细节,只需要调用ffmpeg提供的api就可以完成解封装和解码的操作了。 至于视频帧的渲染及音频帧的播放,ffmpeg就无能为力了,因此需要借助类似sdl库等其他组件完成,后面的章节会为大家介绍继续介绍。
4 源码清单
// tutorial01.c
// Code based on a tutorial by Martin Bohme (boehme@inb.uni-luebeckREMOVETHIS.de)
// Tested on Gentoo, CVS version 5/01/07 compiled with GCC 4.1.1
// With updates from https://github.com/chelyaev/ffmpeg-tutorial
// Updates tested on:
// LAVC 54.59.100, LAVF 54.29.104, LSWS 2.1.101
// on GCC 4.7.2 in Debian February 2015
//
// Updates tested on:
// Mac OS X 10.11.6
// Apple LLVM version 8.0.0 (clang-800.0.38)
//
// A small sample program that shows how to use libavformat and libavcodec to read video from a file.
//
// Use
//
// $ gcc -o tutorial01 tutorial01.c -lavutil -lavformat -lavcodec -lswscale -lz -lm
//
// to build (assuming libavutil/libavformat/libavcodec/libswscale are correctly installed your system).
//
// Run using
//
// $ tutorial01 myvideofile.mpg
//
// to write the first five frames from "myvideofile.mpg" to disk in PPM format.
// comment by breakpointlab@outlook.com
#include <libavcodec/avcodec.h>
#include <libavformat/avformat.h>
#include <libswscale/swscale.h>
#include <libavutil/imgutils.h>
#include <stdio.h>
// compatibility with newer API
#if LIBAVCODEC_VERSION_INT < AV_VERSION_INT(55,28,1)
#define av_frame_alloc avcodec_alloc_frame
#define av_frame_free avcodec_free_frame
#endif
//保存PPM文件
void SaveFrame(AVFrame *pFrame, int width, int height, int iFrame) {
FILE *pFile;//定义文件对象
char szFilename[32];//定义输出文件名
// Open file,打开文件
sprintf(szFilename, "frame%d.ppm", iFrame);//格式化输出文件名
pFile = fopen(szFilename, "wb");//打开输出文件
if (pFile == NULL) {//检查输出文件是否打开成功
return;
}
// Write header indicated how wide & tall the image is,向输出文件中写入文件头
fprintf(pFile, "P6\n%d %d\n255\n", width, height);
// Write pixel data,write the file one line a time,一次一行循环写入RGB24像素值
int y;
for (y = 0; y < height; y++) {
fwrite(pFrame->data[0]+y*pFrame->linesize[0], 1, width*3, pFile);
}
// Close file,关闭文件
fclose(pFile);
}
int main(int argc, char *argv[]) {
/*--------------参数定义-------------*/
// Initalizing these to NULL prevents segfaults!
AVFormatContext *pFormatCtx = NULL;//保存文件容器封装信息及码流参数的结构体
AVCodecContext *pCodecCtxOrig = NULL;//解码器上下文对象,解码器依赖的相关环境、状态、资源以及参数集的接口指针
AVCodecContext *pCodecCtx = NULL;//编码器上下文对象,用于PPM文件输出
AVCodec *pCodec = NULL;//保存编解码器信息的结构体,提供编码与解码的公共接口,可以看作是编码器与解码器的一个全局变量
AVPacket packet;//负责保存压缩编码数据相关信息的结构体,每帧图像由一到多个packet包组成
AVFrame *pFrame = NULL;//保存音视频解码后的数据,如状态信息、编解码器信息、宏块类型表,QP表,运动矢量表等数据
AVFrame *pFrameRGB = NULL;//保存输出24-bit RGB的PPM文件数据
struct SwsContext *sws_ctx = NULL;//描述转换器参数的结构体
int numBytes;//RGB24格式数据长度
uint8_t *buffer = NULL;//解码数据输出缓存指针
int i,videoStream;//循环变量,视频流类型标号
int frameFinished;//解码操作是否成功标识
/*-------------参数初始化------------*/
if (argc<2) {//检查输入参数个数是否正确
printf("Please provide a movie file\n");
return -1;
}
// Register all available formats and codecs,注册所有ffmpeg支持的多媒体格式及编解码器
av_register_all();
/*-----------------------
* Open video file,打开视频文件,读文件头内容,取得文件容器的封装信息及码流参数并存储在pFormatCtx中
* read the file header and stores information about the file format in the AVFormatContext structure
* The last three arguments are used to specify the file format, buffer size, and format options
* but by setting this to NULL or 0, libavformat will auto-detect these
-----------------------*/
if (avformat_open_input(&pFormatCtx, argv[1], NULL, NULL) != 0) {
return -1; // Couldn't open file.
}
/*-----------------------
* 取得文件中保存的码流信息,并填充到pFormatCtx->stream 字段
* check out & Retrieve the stream information in the file
* then populate pFormatCtx->stream with the proper information
* pFormatCtx->streams is just an array of pointers, of size pFormatCtx->nb_streams
-----------------------*/
if (avformat_find_stream_info(pFormatCtx, NULL) < 0) {
return -1; // Couldn't find stream information.
}
// Dump information about file onto standard error,打印pFormatCtx中的码流信息
av_dump_format(pFormatCtx, 0, argv[1], 0);
// Find the first video stream.
videoStream=-1;//视频流类型标号初始化为-1
for (i=0;i<pFormatCtx->nb_streams;i++) {//遍历文件中包含的所有流媒体类型(视频流、音频流、字幕流等)
if(pFormatCtx->streams[i]->codec->codec_type==AVMEDIA_TYPE_VIDEO) {//若文件中包含有视频流
videoStream = i;//用视频流类型的标号修改标识,使之不为-1
break;//退出循环
}
}
if (videoStream==-1) {//检查文件中是否存在视频流
return -1; // Didn't find a video stream.
}
// Get a pointer to the codec context for the video stream,根据流类型标号从pFormatCtx->streams中取得视频流对应的解码器上下文
pCodecCtxOrig = pFormatCtx->streams[videoStream]->codec;
/*-----------------------
* Find the decoder for the video stream,根据视频流对应的解码器上下文查找对应的解码器,返回对应的解码器(信息结构体)
* The stream's information about the codec is in what we call the "codec context.
* This contains all the information about the codec that the stream is using
-----------------------*/
pCodec = avcodec_find_decoder(pCodecCtxOrig->codec_id);
if (pCodec == NULL) {//检查解码器是否匹配
fprintf(stderr, "Unsupported codec!\n");
return -1; // Codec not found.
}
// Copy context,复制编解码器上下文对象,用于保存从视频流中抽取的帧
pCodecCtx = avcodec_alloc_context3(pCodec);//创建AVCodecContext结构体对象pCodecCtx
if (avcodec_copy_context(pCodecCtx, pCodecCtxOrig) != 0) {//复制编解码器上下文对象
fprintf(stderr, "Couldn't copy codec context");
return -1; // Error copying codec context.
}
// Open codec,打开解码器
if (avcodec_open2(pCodecCtx, pCodec, NULL) < 0) {
return -1; // Could not open codec.
}
// Allocate video frame,为解码后的视频信息结构体分配空间并完成初始化操作(结构体中的图像缓存按照下面两步手动安装)
pFrame = av_frame_alloc();
// Allocate an AVFrame structure,为转换PPM文件的结构体分配空间并完成初始化操作
pFrameRGB = av_frame_alloc();
if (pFrameRGB == NULL) {//检查初始化操作是否成功
return -1;
}
// Determine required buffer size and allocate buffer,根据像素格式及图像尺寸计算内存大小
numBytes = av_image_get_buffer_size(AV_PIX_FMT_RGB24, pCodecCtx->width, pCodecCtx->height, 1);
buffer = (uint8_t *) av_malloc(numBytes * sizeof(uint8_t));//为转换后的RGB24图像配置缓存空间
// Assign appropriate parts of buffer to image planes in pFrameRGB Note that pFrameRGB is an AVFrame, but AVFrame is a superset of AVPicture
// 为AVFrame对象安装图像缓存,将out_buffer缓存挂到pFrameYUV->data指针结构上
av_image_fill_arrays(pFrameRGB->data, pFrameRGB->linesize, buffer, AV_PIX_FMT_RGB24, pCodecCtx->width, pCodecCtx->height, 1);
// Initialize SWS context for software scaling,设置图像转换像素格式为AV_PIX_FMT_RGB24
sws_ctx = sws_getContext(pCodecCtx->width, pCodecCtx->height, pCodecCtx->pix_fmt, pCodecCtx->width, pCodecCtx->height, AV_PIX_FMT_RGB24, SWS_BILINEAR, NULL, NULL, NULL);
/*--------------循环解码-------------*/
i = 0;// Read frames(2 packet) and save first five frames to disk,
/*-----------------------
* read in a packet and store it in the AVPacket struct
* ffmpeg allocates the internal data for us,which is pointed to by packet.data
* this is freed by the av_free_packet()
-----------------------*/
while (av_read_frame(pFormatCtx, &packet) >= 0) {//从视频文件或网络流媒体中依次读取每个图像编码数据包,并存储在AVPacket数据结构中
// Is this a packet from the video stream,检查数据包类型
if (packet.stream_index == videoStream) {
/*-----------------------
* Decode video frame,解码完整的一帧数据,并将frameFinished设置为true
* 可能无法通过只解码一个packet就获得一个完整的视频帧frame,可能需要读取多个packet才行
* avcodec_decode_video2()会在解码到完整的一帧时设置frameFinished为真
* Technically a packet can contain partial frames or other bits of data
* ffmpeg's parser ensures that the packets we get contain either complete or multiple frames
* convert the packet to a frame for us and set frameFinisned for us when we have the next frame
-----------------------*/
avcodec_decode_video2(pCodecCtx, pFrame, &frameFinished, &packet);
// Did we get a video frame,检查是否解码出完整一帧图像
if (frameFinished) {
// Convert the image from its native format to RGB,//将解码后的图像转换为RGB24格式
sws_scale(sws_ctx, (uint8_t const * const *) pFrame->data, pFrame->linesize, 0, pCodecCtx->height, pFrameRGB->data, pFrameRGB->linesize);
if (++i <= 5) {// Save the frame to disk,将前5帧图像存储到磁盘上
SaveFrame(pFrameRGB, pCodecCtx->width, pCodecCtx->height, i);
}
}
}
// Free the packet that was allocated by av_read_frame,释放AVPacket数据结构中编码数据指针
av_packet_unref(&packet);
}
/*--------------参数撤销-------------*/
// Free the RGB image buffer
av_free(buffer);
av_frame_free(&pFrameRGB);
// Free the YUV frame.
av_frame_free(&pFrame);
// Close the codecs.
avcodec_close(pCodecCtx);
avcodec_close(pCodecCtxOrig);
// Close the video file.
avformat_close_input(&pFormatCtx);
return 0;
}
相关推荐
- python入门到脱坑函数—定义函数_如何定义函数python
-
Python函数定义:从入门到精通一、函数的基本概念函数是组织好的、可重复使用的代码块,用于执行特定任务。在Python中,函数可以提高代码的模块性和重复利用率。二、定义函数的基本语法def函数名(...
- javascript函数的call、apply和bind的原理及作用详解
-
javascript函数的call、apply和bind本质是用来实现继承的,专业点说法就是改变函数体内部this的指向,当一个对象没有某个功能时,就可以用这3个来从有相关功能的对象里借用过来...
- JS中 call()、apply()、bind() 的用法
-
其实是一个很简单的东西,认真看十分钟就从一脸懵B到完全理解!先看明白下面:例1obj.objAge;//17obj.myFun()//小张年龄undefined例2shows(...
- Pandas每日函数学习之apply函数_apply函数python
-
apply函数是Pandas中的一个非常强大的工具,它允许你对DataFrame或Series中的数据应用一个函数,可以是自定义的函数,也可以是内置的函数。apply可以作用于DataF...
- Win10搜索不习惯 换个设定就好了_window10搜索用不了怎么办
-
Windows10的搜索功能是真的方便,这点用惯了Windows10的小伙伴应该都知道,不过它有个小问题,就是Windows10虽然会自动联网搜索,但默认使用微软自家的Bing搜索引擎和Edge...
- 面试秘籍:call、bind、apply的区别,面试官为什么总爱问这三位?
-
引言你有没有发现,每次JavaScript面试,面试官总爱问你call、bind和apply的区别?好像这三个方法成了通关密码,掌握了它们,就能顺利过关。其实不难理解,面试官问这些问题,不...
- 记住这8招,帮你掌握“追拍“摄影技法—摄影早自习第422日
-
杨海英同学提问:请问叶梓老师,我练习追拍时,总也不能把运动的人物拍清晰,速度一般掌握在1/40-1/60,请问您如何把追拍拍的清晰?这跟不同的运动形式有关系吗?请您给讲讲要点,谢谢您!摄影:Damia...
- [Sony] 有点残酷的测试A7RII PK FS7
-
都是好机!手中利器!主要是最近天天研究fs5,想知道fs5与a7rii后期匹配问题,苦等朋友的fs5月底到货,于是先拿手里现有的fs7小测一下,十九八九也能看到fs5的影子,另外也了解一下fs5k标配...
- AndroidStudio_Android使用OkHttp发起Http请求
-
这个okHttp的使用,其实网络上有很多的案例的,但是,如果以前没用过,copy别人的直接用的话,可以发现要么导包导不进来,要么,人家给的代码也不完整,这里自己整理一下.1.引入OkHttp的jar...
- ESL-通过事件控制FreeSWITCH_es事务控制
-
通过事件提供的最底层控制机制,允许我们有效地利用工具箱,适时选择使用其中的单个工具。FreeSWITCH是一个核心交换与混合矩阵,它周围有几十个模块提供各种功能特性。我们完全控制了所有的即时信息,这些...
- 【调试】perf和火焰图_perf生成火焰图
-
简介perf是linux上的性能分析工具,perf可以对event进行统计得到event的发生次数,或者对event进行采样,得到每次event发生时的相关数据(cpu、进程id、运行栈等),利用这些...
- 文本检索控件也玩安卓?dtSearch Engine发布Android测试版
-
dtSearchEngineforLinux(原生64-bit/32-bitC++和JavaAPIs)和dtSearchEngineforWin&.NET(原生64-bi...
- 网站后台莫名增加N个管理员,记一次SQL注入攻击
-
网站没流量,但却经常被SQL注入光顾。最近,网站真的很奇怪,网站后台不光莫名多了很多“管理员”,所有的Wordpres插件还会被自动暂停,导致一些插件支持的页面,如WooCommerce无法正常访问、...
- 多元回归树分析Multivariate Regression Trees,MRT
-
多元回归树(MultivariateRegressionTrees,MRT)是单元回归树的拓展,是一种对一系列连续型变量递归划分成多个类群的聚类方法,是在决策树(decision-trees)基础...
- JMETER性能测试_JMETER性能测试指标
-
jmeter为性能测试提供了一下特色:jmeter可以对测试静态资源(例如js、html等)以及动态资源(例如php、jsp、ajax等等)进行性能测试jmeter可以挖掘出系统最大能处...
- 一周热门
-
-
【验证码逆向专栏】vaptcha 手势验证码逆向分析
-
Python实现人事自动打卡,再也不会被批评
-
Psutil + Flask + Pyecharts + Bootstrap 开发动态可视化系统监控
-
一个解决支持HTML/CSS/JS网页转PDF(高质量)的终极解决方案
-
再见Swagger UI 国人开源了一款超好用的 API 文档生成框架,真香
-
网页转成pdf文件的经验分享 网页转成pdf文件的经验分享怎么弄
-
C++ std::vector 简介
-
飞牛OS入门安装遇到问题,如何解决?
-
系统C盘清理:微信PC端文件清理,扩大C盘可用空间步骤
-
10款高性能NAS丨双十一必看,轻松搞定虚拟机、Docker、软路由
-
- 最近发表
-
- python入门到脱坑函数—定义函数_如何定义函数python
- javascript函数的call、apply和bind的原理及作用详解
- JS中 call()、apply()、bind() 的用法
- Pandas每日函数学习之apply函数_apply函数python
- Win10搜索不习惯 换个设定就好了_window10搜索用不了怎么办
- 面试秘籍:call、bind、apply的区别,面试官为什么总爱问这三位?
- 记住这8招,帮你掌握“追拍“摄影技法—摄影早自习第422日
- [Sony] 有点残酷的测试A7RII PK FS7
- AndroidStudio_Android使用OkHttp发起Http请求
- ESL-通过事件控制FreeSWITCH_es事务控制
- 标签列表
-
- python判断字典是否为空 (50)
- crontab每周一执行 (48)
- aes和des区别 (43)
- bash脚本和shell脚本的区别 (35)
- canvas库 (33)
- dataframe筛选满足条件的行 (35)
- gitlab日志 (33)
- lua xpcall (36)
- blob转json (33)
- python判断是否在列表中 (34)
- python html转pdf (36)
- 安装指定版本npm (37)
- idea搜索jar包内容 (33)
- css鼠标悬停出现隐藏的文字 (34)
- linux nacos启动命令 (33)
- gitlab 日志 (36)
- adb pull (37)
- python判断元素在不在列表里 (34)
- python 字典删除元素 (34)
- vscode切换git分支 (35)
- python bytes转16进制 (35)
- grep前后几行 (34)
- hashmap转list (35)
- c++ 字符串查找 (35)
- mysql刷新权限 (34)