【FFmpeg之如何新增一个硬件解码器】
FFmpeg之如何新增一个硬件解码器
- 前言
- 一、config配置
- 二、解码器定义
- 1.目录结构
- 2.数据结构
- 三、解码流程
- 1、初始化mediacodec_decode_init
- 2、帧接收mediacodec_receive_frame
- 2.1 解码上下文MediaCodecH264DecContext
- 2.2 发包AVPacket到解码器 -- ff_mediacodec_dec_send
- 2.3 接收解码后数据AVFrame -- ff_mediacodec_dec_receive
- 3、刷新缓冲区mediacodec_decode_flush
- 4、关闭解码器mediacodec_decode_close
- 四、回顾与总结
前言
最近在鸿蒙上开发音视频相关功能,在适配好SDL2之后,接入FFmpeg软解即可播放音视频,然对于4k大码率的视频,播放时却非常卡顿。于是乎琢磨着在鸿蒙上加一个FFmpeg的硬解码,PS:鸿蒙官方目前只提供解码相关 SDK。不过应该如何入手呢?想到这个鸿蒙跟安卓还是有点类似,解码都是异步回调机制,于是先捋一遍安卓下硬解码流程吧。
一、config配置
首先MediaCodec是Android平台提供的底层音视频编解码API,支持解码的格式有
- 视频:H.264(AVC)、H.265(HEVC)、VP8、VP9、MPEG-4、AV1
- 音频:AAC、MP3、Opus
下面以H.264格式为例,在configure文件中有以下两行:
h264_mediacodec_decoder_deps="mediacodec"
h264_mediacodec_decoder_select="h264_mp4toannexb_bsf h264_parser"
第一行"mediacodec"表示该解码器依赖 Android 的 MediaCodec API,在配置阶段,configure 脚本会通过配置的NDK路径,检测系统中是否存在这些依赖项;
第二行声明该解码器的关联组件,当 h264_mediacodec_decoder 被启用时,configure 会自动启用以下组件:
- h264_mp4toannexb_bsf:将 H.264 码流从 MP4 封装格式(AVCC)转换为 Annex B 格式。【这个后面会用到,稍后再讲~】
- h264_parser:H.264 码流解析器,用于解析码流中的 NALU 单元。.
在命令行指定一下相关参数即可开启硬件解码:
./configure \--target-os=android \--arch=arm64 \--enable-cross-compile \--sysroot=$ANDROID_NDK/toolchains/llvm/prebuilt/linux-x86_64/sysroot \--enable-mediacodec \--enable-decoder=h264_mediacodec
生成的MakeFile文件中有一行
OBJS-$(CONFIG_H264_MEDIACODEC_DECODER) += mediacodecdec.o
二、解码器定义
1.目录结构
在libavcodec目录下,mediacodec相关的文件有12个,如下:
2.数据结构
首先我们可以看下mediacodecdec.c中DECLARE_MEDIACODEC_VDEC相关的宏定义,
#define DECLARE_MEDIACODEC_VCLASS(short_name) \
static const AVClass ff_##short_name##_mediacodec_dec_class = { \.class_name = #short_name "_mediacodec", /* 注册到 FFmpeg 的类名 */ \.item_name = av_default_item_name, /* 默认的对象名称生成器 */ \.option = ff_mediacodec_vdec_options, /* 解码器配置选项指针 */ \.version = LIBAVUTIL_VERSION_INT, /* 版本号对齐校验 */ \
};#define DECLARE_MEDIACODEC_VDEC(short_name, full_name, codec_id, bsf) \
DECLARE_MEDIACODEC_VCLASS(short_name) /* 先声明 AVClass */ \
const FFCodec ff_ ## short_name ## _mediacodec_decoder = { \.p.name = #short_name "_mediacodec", /* 解码器名称 */ \CODEC_LONG_NAME(full_name " Android MediaCodec decoder"), /* 长描述 */ \.p.type = AVMEDIA_TYPE_VIDEO, /* 媒体类型:视频 */ \.p.id = codec_id, /* FFmpeg 编解码ID */ \.p.priv_class = &ff_##short_name##_mediacodec_dec_class, /* 私有类指针 */ \.priv_data_size = sizeof(MediaCodecH264DecContext), /* 私有数据区大小 */ \.init = mediacodec_decode_init, /* 初始化回调函数 */ \FF_CODEC_RECEIVE_FRAME_CB(mediacodec_receive_frame), /* 帧接收回调 */ \.flush = mediacodec_decode_flush, /* 冲刷缓冲区回调 */ \.close = mediacodec_decode_close, /* 关闭解码器回调 */ \.p.capabilities = AV_CODEC_CAP_DELAY | /* 支持延迟输出 */ \AV_CODEC_CAP_AVOID_PROBING | /* 避免格式探测 */ \AV_CODEC_CAP_HARDWARE, /* 硬件加速标志 */ \.caps_internal = FF_CODEC_CAP_NOT_INIT_THREADSAFE, /* 非线程安全初始化 */ \.bsfs = bsf, /* 关联的比特流过滤器 */ \.hw_configs = mediacodec_hw_configs, /* 硬件配置信息表 */ \.p.wrapper_name = "mediacodec", /* 封装器名称 */ \
};#if CONFIG_H264_MEDIACODEC_DECODER
/* 实例化 H.264 解码器结构体 */
DECLARE_MEDIACODEC_VDEC(h264, /* 短名称 */"H.264", /* 标准名称 */AV_CODEC_ID_H264, /* FFmpeg 编码ID */"h264_mp4toannexb") /* MP4 到 Annex-B 格式转换器 */
#endif
其中先声明一个AVClass 结构体,这个是FFmpeg 的类系统核心,用于统一管理编解码器的元数据,注册FFCodec解码器;
然后就是在解码器中注册四个回调函数:mediacodec_decode_init(初始化)、mediacodec_receive_frame(帧接收)、mediacodec_decode_flush(刷新缓冲区)、mediacodec_decode_close(关闭解码器)
最后可以看到capabilities属性中的AV_CODEC_CAP_HARDWARE标志,这个就是开启硬件加速,比较关键,如果没有这个标志那么就是用的软解了。
三、解码流程
1、初始化mediacodec_decode_init
mediacodec_decode_init里面主要做了两件事:
一、设置FFAMediaFormat媒体格式的一些属性,如MIME类型、context的宽和高等:
// h264对应的MIME为"video/avc"ff_AMediaFormat_setString(format, "mime", codec_mime);ff_AMediaFormat_setInt32(format, "width", avctx->width);ff_AMediaFormat_setInt32(format, "height", avctx->height);
最后通过调用NDK里面的相关函数设置(没有NDK的话就通过jni去调java的? 没细究 0.o)
二、ff_mediacodec_dec_init,其中首先通过
s->codec_name = ff_AMediaCodecList_getCodecNameByType(mime, profile, 0, avctx);
s->codec = ff_AMediaCodec_createCodecByName(s->codec_name, s->use_ndk_codec);
获取解码器名称并创建解码器,然后再配置解码器并启动
status = ff_AMediaCodec_configure(s->codec, format, s->surface, NULL, 0);
status = ff_AMediaCodec_start(s->codec);
上述函数均有NDK和JNI两套调用逻辑。
2、帧接收mediacodec_receive_frame
mediacodec_receive_frame是从解码器中获取解码后的视频帧,里面的核心流程是异步处理输入和输出缓冲区(通过队列管理)。当ff_mediacodec_dec_send被调用时,AVPacket数据会被放入输入队列,等待解码器处理。解码后的数据则从输出队列中取出,即ff_mediacodec_dec_receive函数负责从输出队列获取解码后的帧AVFrame。
2.1 解码上下文MediaCodecH264DecContext
不过在这之前首先来看下解码器上下文MediaCodecH264DecContext这个关键类的数据结构设计:
// H264 MediaCodec解码器上下文:
typedef struct MediaCodecH264DecContext {//AVClass 集成到FFmpeg的类系统中,用于日志记录、私有选项配置及参数解析AVClass *avclass;//MediaCodecDecContext指向通用的MediaCodec解码器上下文,h264只是其中的一个特化MediaCodecDecContext *ctx;//当输入数据包过大,无法一次性写入硬件缓冲区时,剩余数据暂存于此,等待后续处理AVPacket buffered_pkt;//延迟刷新解码器的标志,确保所有已提交数据被处理后再执行刷新int delay_flush;int amlogic_mpeg2_api23_workaround; // ?// NDK API更高效,减少Java层交互开销,适用于高性能需求场景int use_ndk_codec;
} MediaCodecH264DecContext;//通用的MediaCodec解码器上下文,管理硬件解码器状态:
typedef struct MediaCodecDecContext {AVCodecContext *avctx; //FFmpeg编解码上下文,用于日志和配置信息atomic_int refcount;atomic_int hw_buffer_count;char *codec_name;FFAMediaCodec *codec;FFAMediaFormat *format;void *surface; // FFANativeWindow * 渲染表面(用于零拷贝)int started;int draining;int flushing;int eos;int width;int height;int stride;int slice_height;int color_format;int crop_top;int crop_bottom;int crop_left;int crop_right;int display_width;int display_height;uint64_t output_buffer_count;ssize_t current_input_buffer;bool delay_flush;atomic_int serial;bool use_ndk_codec;
} MediaCodecDecContext;
在提交缓冲区给解码器解码的过程中,一般来说可以通过Surface(上面的void *surface就是指向渲染表面)或DMA 缓冲区共享技术实现零拷贝,减少 CPU 与 GPU 间的数据传输避免解码过慢。
2.2 发包AVPacket到解码器 – ff_mediacodec_dec_send
// 尝试获取输入缓冲区索引
index = ff_AMediaCodec_dequeueInputBuffer(codec, input_dequeue_timeout_us);
// 获取输入缓冲区的内存地址data和容量size
data = ff_AMediaCodec_getInputBuffer(codec, index, &size);
// 提交输入缓冲区到解码器
status = ff_AMediaCodec_queueInputBuffer(codec, index, 0, size, pts, 0);
2.3 接收解码后数据AVFrame – ff_mediacodec_dec_receive
//: 从解码器输出队列中获取缓冲区索引
index = ff_AMediaCodec_dequeueOutputBuffer(codec, &info, output_dequeue_timeout_us);
...
if (info.size) {// Surface 模式:通过 ANativeWindow 直接渲染到 Surface,无需拷贝数据。if (s->surface) {if ((ret = mediacodec_wrap_hw_buffer(avctx, s, index, &info, frame)) < 0) {av_log(avctx, AV_LOG_ERROR, "Failed to wrap MediaCodec buffer\n");return ret;}// ByteBuffer 模式:将 MediaCodec 的 ByteBuffer 数据复制到 AVFrame->data} else {data = ff_AMediaCodec_getOutputBuffer(codec, index, &size);if (!data) {av_log(avctx, AV_LOG_ERROR, "Failed to get output buffer\n");return AVERROR_EXTERNAL;}if ((ret = mediacodec_wrap_sw_buffer(avctx, s, data, size, index, &info, frame)) < 0) {av_log(avctx, AV_LOG_ERROR, "Failed to wrap MediaCodec buffer\n");return ret;}}s->output_buffer_count++;return 0;
} else {status = ff_AMediaCodec_releaseOutputBuffer(codec, index, 0);if (status < 0) {av_log(avctx, AV_LOG_ERROR, "Failed to release output buffer\n");}
}
...
if (ff_AMediaCodec_infoOutputBuffersChanged(codec, index)) {ff_AMediaCodec_cleanOutputBuffers(codec); // 清理旧缓冲区
}
3、刷新缓冲区mediacodec_decode_flush
mediacodec_decode_flush在视频播放中,当用户跳转进度时,需要清空之前的解码数据,这时候就会调用flush函数。
static void mediacodec_decode_flush(AVCodecContext *avctx)
{MediaCodecH264DecContext *s = avctx->priv_data;//av_packet_unref是FFmpeg中释放AVPacket资源的函数,//这里是释放MediaCodecH264DecContext 中缓存的未完全提交到硬件解码器的 AVPacket 数据包av_packet_unref(&s->buffered_pkt);//清空解码器的输入/输出缓冲区若,解码器正在处理数据(Executing 状态),flush会强制停止当前操作ff_mediacodec_dec_flush(avctx, s->ctx);
}
一般来说一下四种场景会调用flush:
a、视频播放器跳转进度:
用户拖动进度条时,需清空当前解码队列,避免旧数据与新位置的数据混合。
b、处理解码错误
当解码器因数据错误进入异常状态时,通过刷新重置其状态,恢复解码能力。
c、格式动态切换
切换分辨率或码率时,需先清空原有数据,再重新配置解码器。
d、结束流或重新初始化
在流结束或重新初始化解码器前,确保资源正确释放。
4、关闭解码器mediacodec_decode_close
通过引用计数管理声明周期,计数为0时依次删除MediaCodec、MediaFormat和Surface等对象,最后删除MediaCodecDecContext。
static void ff_mediacodec_dec_unref(MediaCodecDecContext *s)
{...// 原子操作:引用计数减1,若原值为1(减后为0),则释放资源if (atomic_fetch_sub(&s->refcount, 1) == 1) {ff_AMediaCodec_delete(s->codec); ff_AMediaFormat_delete(s->format);ff_mediacodec_surface_unref(s->surface, NULL);}
}
四、回顾与总结
综上所述,FFmpeg添加一个硬件解码器的关键步骤如下:
步骤 | 关键操作 |
---|---|
1. 配置编译 | 修改 configure 和 Makefile,添加新解码器选项 |
2. 定义结构体 | 注册 AVCodec,实现编解码器上下文 |
3. 初始化与配置 | 创建 MediaCodec 实例,设置格式参数 |
4. 数据传递 | 实现 send_packet 和 receive_frame,适配硬件缓冲区 |
5. 资源管理 | 处理刷新、关闭和引用计数,确保无内存泄漏 |
整体来说,硬件解码核心流程并不复杂,主要是要对NDK中的接口调用以及处理,相比于软件解码来说主要是在数据在CPU和GPU之间传输的不同,虽然硬件解码后支持GPU直接渲染,无需数据回传,不过若需要CPU处理(如滤镜),还需将数据从显存拷贝到系统内存。因此现代播放器常结合两者,优先尝试硬件解码,失败时回退到软件解码。鸿蒙平台和安卓很类似,无非是NDK不同罢了,不过NDK里面怎么写的那就不得而知了。(-_->
相关文章:

【FFmpeg之如何新增一个硬件解码器】
FFmpeg之如何新增一个硬件解码器 前言一、config配置二、解码器定义1.目录结构2.数据结构 三、解码流程1、初始化mediacodec_decode_init2、帧接收mediacodec_receive_frame2.1 解码上下文MediaCodecH264DecContext2.2 发包AVPacket到解码器 -- ff_mediacodec_dec_send2.3 接收…...

P3385 【模板】负环
P3385 【模板】负环 - 洛谷 题目描述 给定一个 n 个点的有向图,请求出图中是否存在从顶点 1 出发能到达的负环。 负环的定义是:一条边权之和为负数的回路。 输入格式 本题单测试点有多组测试数据。 输入的第一行是一个整数 T,表示测试数…...

破解透明物体抓取难题,地瓜机器人CASIA 推出几何和语义融合的单目抓取方案|ICRA 2025
概述 近日,全球机器人领域顶会ICRA 2025(IEEE机器人与自动化国际会议)公布论文录用结果,地瓜机器人主导研发的DOSOD开放词汇目标检测算法与MODEST单目透明物体抓取算法成功入选。前者通过动态语义理解框架提升复杂场景识别准确率…...

深度学习编译器(整理某survey)
一、深度学习框架 TensorFlow PyTorch MXNet ONNX:定义了一个统一的表示,DL models的格式方便不同框架之间的转换模型 二、深度学习硬件 通用硬件(CPU、GPU):通过硬件和软件优化支持深度学习工作负载 GPU:通过多核架构实现高…...

【计算机网络入门】应用层
目录 1.网络应用模型 1.1 C/S模型(客户端服务器模型) 1.2 P2P模型(对等模型) 2. DNS系统 2.1 域名 2.2 域名解析流程 3. FTP文件传输协议 4. 电子邮件系统 4.1 SMTP协议 4.2 pop3协议 4.3 IMAP协议 4.4 基于万维网的电…...

@PostConstruct注解的作用
PostConstruct 注解功能是在一个类的所有依赖被注入完成后,才会被执行的方法。这种方法通常用于类的初始化,初始化过程中可以进行一些资源加载、连接建立、或其他必要的配置工作。PostConstruct 方法仅被调用一次,通常修饰符是public或者prot…...

HTML + CSS 题目
1.说说你对盒子模型的理解? 一、是什么 对一个文档进行布局的时候,浏览器渲染引擎会根据标准之一的css基础盒模型,将所有元素表示为一个个矩形的盒子。 一个盒子由四个部分组成: content,padding,border,margin 下…...

通过多线程获取RV1126的AAC码流
目录 一RV1126多线程获取音频编码AAC码流的流程 1.1AI模块的初始化并使能 1.2AENC模块的初始化 1.3绑定AI模块和AENC模块 1.4多线程获取每一帧AAC码流 1.5每个AAC码流添加ADTSHeader头部 1.6写入具体每一帧AAC的…...

sql sqlserver的进程资源查看,杀掉多余进程
主要是由三个表组成 sys.sysprocesses、sys.dm_exec_sessions、sys.dm_exec_requests 后面两个在2008版本后使用,主要使用sys.dm_exec_sessions SELECT spid AS 会话ID, -- 进程(会话)的ID blocked AS 被阻塞的会话…...
自然语言处理:朴素贝叶斯
介绍 大家好,博主又来和大家分享自然语言处理领域的知识了。按照博主的分享规划,本次分享的核心主题本应是自然语言处理中的文本分类。然而,在对分享内容进行细致梳理时,我察觉到其中包含几个至关重要的知识点,即朴素…...
Pytorch实现之LSRGAN,轻量化SRGAN超分辨率SAR
简介 简介:在SRGAN的基础上设计了一个轻量化的SRGAN模型结构,通过DSConv+CA与残差结构的设计来减少参数量,同时利用SeLU激活函数构造。与多类SRGAN改进不同的是,很少使用BN层。 论文题目:Lightweight Super-Resolution Generative Adversarial Network for SAR Images(…...

学习记录-缺陷
目录 一、缺陷的判定标准 二、缺陷产生的原因 三、缺陷的生存周期 四、软件缺陷描述及提交要素 1.缺陷的核心内容 2.缺陷的提交要素 五、软件缺陷类型 一、缺陷的判定标准 二、缺陷产生的原因 三、缺陷的生存周期 注入bug > 发现bug > 清除bug 四、软件缺陷描述及提…...

文件压缩与解压工具7-Zip的安装和使用(免费)
一.介绍 7-Zip 是一款开源的文件压缩与解压缩工具,支持多种压缩格式,如 7z、ZIP、RAR、TAR 等。它具有高压缩比,尤其是其独有的 7z 格式,并集成了文件管理器。7-Zip 适用于 Windows 系统,提供命令行版本,…...
如何实现对用户密码的加密
摘要算法: 同样的明文,经过同样的摘要算法,得到的结果是一样的 验证方法:验证经过摘要算法处理后的结果,如果密文一样那么就认为明文是一样的 //数据库存储的一定是密文,用户输入的是明文;把…...

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

审批流AntV框架蚂蚁数据可视化X6饼图(注释详尽)
大家好,这次使用的是AntV的蚂蚁数据可视化X6框架,类似于审批流的场景等,代码如下: X6框架参考网址:https://x6.antv.vision/zh/examples/showcase/practices#bpmn 可以进入该网址,直接复制下方代码进行调试…...

用Python之requests库调用大模型API实现多轮对话
文章目录 1. 多轮对话实现概述2. 多轮对话全上下文实现3. 多轮对话最近上下文的链式实现4. 总结 1. 多轮对话实现概述 多轮对话功能可以让大模型“拥有记忆”,满足如追问、信息采集等需要连续交流的场景。 AI大模型API 不会记录您的对话历史记录。如果您需要让大模…...

《异步江湖:XHR、Promise 与 Event Loop 的恩怨情仇》
XMLHttpRequest XMLHttpRequest(简称 XHR)是浏览器提供的一个 JavaScript 对象,用于在客户端和服务器之间发送 HTTP 请求。它是实现 AJAX(Asynchronous JavaScript and XML) 技术的核心工具,允许网页在不…...

【极客时间】浏览器工作原理与实践-2 宏观视角下的浏览器 (6讲) - 2.5 渲染流程(上):HTML、CSS和JavaScript,是如何变成页面的?
https://time.geekbang.org/column/article/118205 2.5 渲染流程(上):HTML、CSS和JavaScript,是如何变成页面的? 2.4讲了导航相关的流程,那导航被提交后又会怎么样呢? 就进入了渲染阶段。 这…...

蓝桥杯第15届真题解析
由硬件框图可以知道我们要配置LED 和按键、lcd,解决lcd引脚冲突 LED 先配置LED的八个引脚为GPIO_OutPut,锁存器PD2也是,然后都设置为起始高电平,生成代码时还要去解决引脚冲突问题 按键 按键配置,由原理图按键所对引…...

【Axure高保真原型】引导弹窗
今天和大家中分享引导弹窗的原型模板,载入页面后,会显示引导弹窗,适用于引导用户使用页面,点击完成后,会显示下一个引导弹窗,直至最后一个引导弹窗完成后进入首页。具体效果可以点击下方视频观看或打开下方…...

UE5 学习系列(三)创建和移动物体
这篇博客是该系列的第三篇,是在之前两篇博客的基础上展开,主要介绍如何在操作界面中创建和拖动物体,这篇博客跟随的视频链接如下: B 站视频:s03-创建和移动物体 如果你不打算开之前的博客并且对UE5 比较熟的话按照以…...
sqlserver 根据指定字符 解析拼接字符串
DECLARE LotNo NVARCHAR(50)A,B,C DECLARE xml XML ( SELECT <x> REPLACE(LotNo, ,, </x><x>) </x> ) DECLARE ErrorCode NVARCHAR(50) -- 提取 XML 中的值 SELECT value x.value(., VARCHAR(MAX))…...

uniapp微信小程序视频实时流+pc端预览方案
方案类型技术实现是否免费优点缺点适用场景延迟范围开发复杂度WebSocket图片帧定时拍照Base64传输✅ 完全免费无需服务器 纯前端实现高延迟高流量 帧率极低个人demo测试 超低频监控500ms-2s⭐⭐RTMP推流TRTC/即构SDK推流❌ 付费方案 (部分有免费额度&#x…...

Ascend NPU上适配Step-Audio模型
1 概述 1.1 简述 Step-Audio 是业界首个集语音理解与生成控制一体化的产品级开源实时语音对话系统,支持多语言对话(如 中文,英文,日语),语音情感(如 开心,悲伤)&#x…...
CRMEB 框架中 PHP 上传扩展开发:涵盖本地上传及阿里云 OSS、腾讯云 COS、七牛云
目前已有本地上传、阿里云OSS上传、腾讯云COS上传、七牛云上传扩展 扩展入口文件 文件目录 crmeb\services\upload\Upload.php namespace crmeb\services\upload;use crmeb\basic\BaseManager; use think\facade\Config;/*** Class Upload* package crmeb\services\upload* …...
A2A JS SDK 完整教程:快速入门指南
目录 什么是 A2A JS SDK?A2A JS 安装与设置A2A JS 核心概念创建你的第一个 A2A JS 代理A2A JS 服务端开发A2A JS 客户端使用A2A JS 高级特性A2A JS 最佳实践A2A JS 故障排除 什么是 A2A JS SDK? A2A JS SDK 是一个专为 JavaScript/TypeScript 开发者设计的强大库ÿ…...

淘宝扭蛋机小程序系统开发:打造互动性强的购物平台
淘宝扭蛋机小程序系统的开发,旨在打造一个互动性强的购物平台,让用户在购物的同时,能够享受到更多的乐趣和惊喜。 淘宝扭蛋机小程序系统拥有丰富的互动功能。用户可以通过虚拟摇杆操作扭蛋机,实现旋转、抽拉等动作,增…...

消防一体化安全管控平台:构建消防“一张图”和APP统一管理
在城市的某个角落,一场突如其来的火灾打破了平静。熊熊烈火迅速蔓延,滚滚浓烟弥漫开来,周围群众的生命财产安全受到严重威胁。就在这千钧一发之际,消防救援队伍迅速行动,而豪越科技消防一体化安全管控平台构建的消防“…...
WEB3全栈开发——面试专业技能点P4数据库
一、mysql2 原生驱动及其连接机制 概念介绍 mysql2 是 Node.js 环境中广泛使用的 MySQL 客户端库,基于 mysql 库改进而来,具有更好的性能、Promise 支持、流式查询、二进制数据处理能力等。 主要特点: 支持 Promise / async-await…...