news 2026/9/9 15:26:40

Qt + FFmpeg 视频播放器开发指南:架构、解码与音视频同步实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Qt + FFmpeg 视频播放器开发指南:架构、解码与音视频同步实战

简介:Qt 配合 FFmpeg 实现跨平台视频播放器是不少客户端开发者的常见需求。这份资源面向已有 Qt 基础、想在项目中接入 FFmpeg 以兼容更多音视频格式的开发人员,通过一个可运行的播放器工程演示了完整集成思路:从 FFmpeg 库的编译链接,到继承 QAbstractMediaService 自定义解码服务,再到界面层播放、暂停、快进、进度跳转、音量控制与异常反馈的实现。压缩包共 270 个文件、34.26MB,以 180 个头文件、22 个 lib 与 18 个 dll 动态库、16 个 a/def 静态导入库为主体,构成 FFmpeg 链接与调用基础;4 个 cpp 与 1 个 ui 文件对应播放器主逻辑和界面设计,qrc 资源、ico 图标、mp4 示例视频及 pro 工程文件让读者可以直接用 Qt Creator 打开工程对照学习。这份资料还覆盖了 avcodec_find_decoder、avcodec_open2、AVFormatContext 打开文件、AVStream 获取流、YUV/RGB 渲染到 QVideoWidget 等关键调用点,并给出元数据显示与错误处理框架,方便二次开发。目前已有 1070 人学习,适合希望深度掌握 Qt 与 FFmpeg 媒体管道、并快速落地到实际项目的读者。 我最早接触 Qt 和 FFmpeg,是想给项目里加一个简单又好用的视频播放模块。Qt 自带的 QMediaPlayer 说实话够用,但碰到一些非常规格式、或者想对解码后的帧做实时处理时,就会明显感觉使不上劲。后来花了几个晚上用 FFmpeg 的库直接对接 Qt,把解码、显示、音频输出、同步这些环节一个一个打通,总算是完完整整跑起来了。这篇文章就把这个过程中最有参考价值的东西整理出来,包含架构选型、工程配置、代码骨架、同步思路和踩坑记录,给正准备用 Qt + FFmpeg 做播放器的朋友一份可以少走弯路的参考。

1. 播放器方案选型与整体架构设计

我之前也纠结过一个问题:既然 Qt 已经提供了 QMediaPlayer,为什么还要绕一圈去调 FFmpeg?这个问题的答案,直接决定了整个项目的技术路线。

1.1 QMediaPlayer 与 FFmpeg 方案的边界

QMediaPlayer 的优势是接入简单,几行代码就能拖动一个窗口播放本地视频。但它有一个绕不开的短板:解码能力受限于 Qt Multimedia 后端的实现。在 Windows 上依赖系统自带的 DirectShow 或 WMF,在 Linux 上依赖 GStreamer,一旦遇到系统没安装对应解码器或格式较冷门的文件,就直接罢工。而且 QMediaPlayer 不给你访问原始帧数据的机会,想做滤镜、截图、逐帧分析、定制渲染,基本没门。

FFmpeg 是一个纯粹的编解码库,自带 libavcodec、libavformat、libswscale、libswresample 等模块,支持的封装格式和编码格式覆盖面非常广。它不管界面的事,把解码出来的原始数据交给你,剩下怎么显示、怎么播放、怎么同步,完全由自己掌控。这既是优势也是成本:界面、线程、音频输出、同步逻辑全都要自己搭。

1.2 整体模块划分

我最终的播放器架构分成了四个核心模块:

  • 解封装与解码模块:基于 FFmpeg 的 libavformat 读取容器格式,通过 libavcodec 解码出原始视频帧和音频采样。
  • 视频显示模块:把解码出的 YUV 帧转换为 RGB 图像,交给 Qt 的 QLabel 或 QOpenGLWidget 渲染。
  • 音频输出模块:把 PCM 数据喂给 Qt 的 QAudioSink,实现出声。
  • 同步控制模块:以音频时钟为参考,控制视频帧的显示节奏,实现音画同步。

这几个模块放在独立的解码线程中,避免阻塞 UI 线程。UI 线程只负责接收 QImage 并显示,音频数据则通过队列传递给音频输出接口。这样拆分的最大好处是职责清晰,后续想加暂停、定位、截图功能时,只需要在对应模块上扩展。

提示:如果对实时性要求不高,只是想在 Qt 里放一个能用的视频窗口,QMediaPlayer 确实更省事。但如果你想深入理解播放器原理,或者对格式兼容性、帧处理有更高要求,FFmpeg 方案是非常值得投入时间的选择。

2. 开发环境准备与工程构建

在动手写代码之前,先把环境搭好。这部分看起来琐碎,但很多新手会在这里卡住,特别是链接库那一堆依赖关系。

2.1 版本选型与下载

我使用的是 Qt 5.15.2 和 FFmpeg 4.4(后来也试过 FFmpeg 6.x,接口基本兼容)。Qt 5.15.2 是 LTS 版本,稳定,资料多,适合开发桌面应用。FFmpeg 4.4 也是 LTS 分支,API 变动相对小,网上能查到的资料大部分都适用于这个版本。

如果是在 Windows 上开发,FFmpeg 官方不直接提供 Windows 编译版本,一般去 gyan.dev 或 BtbN 的 GitHub Releases 下载已编译好的 shared 版本。shared 版本里面包含 dll、lib 和头文件,适合做二次开发。

Qt 的话,官方安装包在国内下载可能会慢一些,可以走 Qt 的国内镜像站,比如清华 TUNA 的镜像源。装的时候注意勾选 Qt 5.15.2 对应的 MSVC 编译器版本,别只装 Qt 库却忘了装编译器工具链。

2.2 CMake 工程配置

整个项目用 CMake 构建,比 qmake 更灵活,也更符合现代 C++ 项目的习惯。核心配置是这样的:

cmake_minimum_required(VERSION 3.16) project(QtFfmpegPlayer) set(CMAKE_CXX_STANDARD 17) set(CMAKE_AUTOMOC ON) set(CMAKE_AUTORCC ON) find_package(Qt5 COMPONENTS Widgets Multimedia REQUIRED) set(FFMPEG_ROOT "D:/libs/ffmpeg-4.4") set(FFMPEG_INCLUDE_DIR "${FFMPEG_ROOT}/include") set(FFMPEG_LIB_DIR "${FFMPEG_ROOT}/lib") add_executable(QtFfmpegPlayer main.cpp MainWindow.ui PlayerCore.h PlayerCore.cpp ) target_include_directories(QtFfmpegPlayer PRIVATE ${FFMPEG_INCLUDE_DIR}) target_link_directories(QtFfmpegPlayer PRIVATE ${FFMPEG_LIB_DIR}) target_link_libraries(QtFfmpegPlayer PRIVATE Qt5::Widgets Qt5::Multimedia avformat avcodec avutil swscale swresample )

有几个点需要特别强调。

第一,FFmpeg 的头文件和库目录一定要正确指定,否则编译时会报找不到头文件。

第二,链接顺序有讲究。在 MSVC 环境下,avformat 依赖 avcodec 和 avutil,avcodec 依赖 avutil,swscale 和 swresample 也依赖 avutil,所以通常把 avutil 放在最后,让链接器能正确解析符号依赖。如果你在链接时遇到一堆 unresolved external symbol,多半就是顺序问题。

第三,Debug 和 Release 版本的库不能混用。Qt 的 debug 库后缀是 d,FFmpeg 的库如果只有 release 版本,那整个项目也统一用 release 构建,否则会碰到运行时崩溃或解码异常的问题。

2.3 动态库运行时配置

编译通过只是第一步。程序运行时,由于 FFmpeg 是动态链接的,必须让系统能找到对应的 dll 文件。两个做法:

直接把 FFmpeg 的 bin 目录下的所有 dll 复制到 exe 所在目录。这是最简单也最粗暴的方法,发布时也用得上。

在 Qt Creator 里,把 FFmpeg 的 bin 目录路径加入到 PATH 环境变量或者构建环境的 PATH 设置中,方便开发调试。

网上经常看到有人运行时提示找不到 avformat-*.dll,或者代码能编译但一运行就崩溃,一部分原因就是 dll 没跟着跑起来。另有一个高频报错是 Windows 下提示 no qt platform plugin could be initialized,这个多半是 Qt 的 platforms 插件目录没有随程序正确发布导致的,后面在常见问题里我会再详细说。

注意:FFmpeg 在 Windows 下的 Debug 构建版本并不好找,多数预编译包只提供 release 版本。如果项目必须用 Debug 模式调试,建议在 CMake 里做一次判断,Debug 模式也链接 release 库,并确保全局使用同样的运行时库设置,以免出现运行时崩溃。

3. FFmpeg 解封装与解码流程实现

环境没问题之后,就开始写核心逻辑了。解封装和解码是播放器的前半段,就是把视频文件中压缩的数据还原成原始的图像和声音。

3.1 打开媒体文件与查找流信息

第一步是打开文件,拿到格式上下文,并从容器中探测出视频流、音频流和对应的解码器。

extern "C" { #include <libavformat/avformat.h> #include <libavcodec/avcodec.h> #include <libswscale/swscale.h> #include <libswresample/swresample.h> #include <libavutil/imgutils.h> } bool PlayerCore::openFile(const QString& filePath) { // 1. 打开输入文件 if (avformat_open_input(&m_formatCtx, filePath.toStdString().c_str(), nullptr, nullptr) != 0) { return false; } // 2. 读取媒体信息,找到视频流和音频流 if (avformat_find_stream_info(m_formatCtx, nullptr) < 0) { return false; } // 3. 查找最佳视频流 m_videoStreamIndex = av_find_best_stream(m_formatCtx, AVMEDIA_TYPE_VIDEO, -1, -1, nullptr, 0); m_audioStreamIndex = av_find_best_stream(m_formatCtx, AVMEDIA_TYPE_AUDIO, -1, -1, nullptr, 0); if (m_videoStreamIndex < 0) { // 既有视频也有音频,才算是完整的播放器需求 return false; } m_videoStream = m_formatCtx->streams[m_videoStreamIndex]; // 4. 打开视频解码器 const AVCodec* videoDecoder = avcodec_find_decoder(m_videoStream->codecpar->codec_id); m_videoCodecCtx = avcodec_alloc_context3(videoDecoder); avcodec_parameters_to_context(m_videoCodecCtx, m_videoStream->codecpar); if (avcodec_open2(m_videoCodecCtx, videoDecoder, nullptr) < 0) { return false; } // 5. 音频解码器按同样步骤打开 // ... return true; }

av_find_best_stream 这个函数很好用,它会自动选择最合适的流,省去自己遍历 streams 数组去判断编码类型的麻烦。注意旧版本的 FFmpeg 用的是 codec,新版本用 codecpar,如果你看的老博客用的是旧的赋值方式,编译时会报字段不存在的错误。

3.2 读取数据包与解码

打开解码器之后,就进入主循环:循环读取压缩数据包,把包交给解码器,解码器输出原始帧,然后再把帧做后续处理。

AVPacket* packet = av_packet_alloc(); AVFrame* frame = av_frame_alloc(); while (av_read_frame(m_formatCtx, packet) >= 0) { if (packet->stream_index == m_videoStreamIndex) { // 发送给视频解码器 if (avcodec_send_packet(m_videoCodecCtx, packet) == 0) { while (avcodec_receive_frame(m_videoCodecCtx, frame) == 0) { // 拿到一个解码后的视频帧 processVideoFrame(frame); } } } else if (m_audioStreamIndex >= 0 && packet->stream_index == m_audioStreamIndex) { // 音频包走同样的流程,解码后交给音频输出 if (avcodec_send_packet(m_audioCodecCtx, packet) == 0) { while (avcodec_receive_frame(m_audioCodecCtx, frame) == 0) { processAudioFrame(frame); } } } av_packet_unref(packet); }

这里有一个关键的编码方式设计:FFmpeg 新一代的 API 是 send/receive 模式,即发送一个压缩包进去,解码器可能输出 0 个、1 个或多个原始帧。必须用 while 循环把解码器内部缓冲的所有帧都取出来,不能只取一次。

3.3 视频转格式:YUV 到 RGB

FFmpeg 解码出来的视频帧一般是 YUV420P 格式,但 Qt 的 QImage 显示最方便的是 RGB32 格式。这个转换用 FFmpeg 自带的 swscale 模块完成,效率很高。

void PlayerCore::processVideoFrame(AVFrame* frame) { // 第一次处理时初始化格式转换上下文 if (m_swsCtx == nullptr) { m_swsCtx = sws_getContext( frame->width, frame->height, AV_PIX_FMT_YUV420P, frame->width, frame->height, AV_PIX_FMT_RGB32, SWS_BILINEAR, nullptr, nullptr, nullptr); m_rgbBuffer.resize(av_image_get_buffer_size(AV_PIX_FMT_RGB32, frame->width, frame->height, 1)); } uint8_t* rgbData[1] = { m_rgbBuffer.data() }; int rgbLineSize[1] = { frame->width * 4 }; sws_scale(m_swsCtx, frame->data, frame->linesize, 0, frame->height, rgbData, rgbLineSize); QImage image(rgbData[0], frame->width, frame->height, rgbLineSize[0], QImage::Format_RGB32); // 通过信号把图片发给 UI 线程 emit frameReady(image.copy()); }

QImage 默认是浅拷贝,只保存像素指针,不复制数据,所以这里必须调用 copy() 做一次深拷贝,否则传给 UI 线程后原始缓冲区被复用,图像内容就乱了。这是我最早写播放器时踩过的一个坑,图像花屏闪烁,找了好久才发现是这个原因。

4. 音频输出与音视频同步

视频图像能一帧一帧出来之后,接下来要解决两个问题:声音怎么放出来,以及画面和声音怎么对上。

4.1 Qt 的音频输出接口实现

我用了 Qt Multimedia 里的 QAudioOutput 来播放 PCM 数据。这个类会把 QByteArray 或 QIODevice 里的数据写入音频设备,底层是跨平台的,省去自己调 Windows/ALSA/CoreAudio 的麻烦。

初始化代码:

// 假设声道数是 2,采样率 44100,采用 16 位有符号整数 QAudioFormat fmt; fmt.setSampleRate(44100); fmt.setChannelCount(2); fmt.setSampleFormat(QAudioFormat::UInt8); // 注意这里 m_audioOutput = new QAudioOutput(fmt); m_audioIO = m_audioOutput->start();

编码细节需要说明一下:不同平台和不同 FFmpeg 解码出的音频格式并不一样,很多音频文件解码出来是 AV_SAMPLE_FMT_FLTP,即浮点 planar 格式,而 QAudioOutput 最通用的是有符号 16 位交错格式。因此你需要用 swresample 做一次音频重采样,把解码出的 AVFrame 转成目标格式后再塞给音频输出。如果不做转换,轻则无声,重则杂音剧烈。

音频重采样示例:

// 初始化转换器 SwrContext* swr = swr_alloc_set_opts(nullptr, outChannelLayout, AV_SAMPLE_FMT_S16, outSampleRate, inChannelLayout, (AVSampleFormat)frame->format, (int)frame->sample_rate, 0, nullptr); swr_init(swr); // 转换数据 uint8_t* outBuffer = nullptr; int outSamples = swr_get_out_samples(swr, frame->nb_samples); av_samples_alloc(&outBuffer, nullptr, outChannels, outSamples, AV_SAMPLE_FMT_S16, 0); int converted = swr_convert(swr, &outBuffer, outSamples, (const uint8_t**)frame->extended_data, frame->nb_samples); // 然后 m_audioIO->write((const char*)outBuffer, converted * outChannels * 2);

4.2 音视频同步的常用策略

播放器最难的部分不是解码,而是同步。我采用的是最经典、也最实用的方案:以音频时钟为主时钟,视频向音频对齐。具体思路是这样的:

  • 维护一个全局的音频时钟,即当前正在播放的音频已经播到了第几秒。
  • 视频帧在显示之前,比较自己的 PTS(展示时间戳)和音频时钟的差值。
  • 如果视频领先音频超过一定阈值,就等待,比如休眠几十毫秒再判断。
  • 如果视频落后音频,就放弃显示这一帧,直接跳过,让画面快点追上声音。

计算视频帧 PTS 秒数的方式:

double pts = frame->pts * av_q2d(m_videoStream->time_base);

音频时钟的获取,我用的方案是:记录音频输出启动时的 QAudioOutput 的 processedUSecs 减去已写入的数据累计时间。更简单一点,可以用一个计数器累加每次写入音频设备的样本数,再除以采样率得到当前播放位置。当然启动和暂停的时候这个时钟会复杂一点,但基本的播放下,累加写入样本数的方式已经够用。

有了时钟后,视频显示逻辑可以简化成伪代码:

double audioClock = getAudioClock(); double videoPts = getVideoPts(frame); if (videoPts > audioClock) { // 画面快了,等一下 double delayMs = (videoPts - audioClock) * 1000; QThread::msleep(qMin((int)delayMs, 100)); // 最多睡100ms,避免卡死UI } else if (audioClock - videoPts > 0.1) { // 画面落后太多,跳过这帧 av_frame_unref(frame); continue; }

同步算法的具体阈值可以根据实际体验调整。阈值太小会觉得卡顿,太大会觉得不同步。我最后用的是声音领先的画面超前 30ms 内不丢帧,画面落后 50ms 内强制显示的策略,试了几轮视频下来体感正常。

4.3 暂停与跳转的同步处理

如果停了播放,又原地开始,音频和视频的时间基准就不一致了。最简单的处理方式是:暂停时记住当前的音频时钟偏移量,恢复时将偏移量加到所有帧的 PTS 比较逻辑上。跳转时则调用 av_seek_frame 或者 avformat_seek_file 进行关键帧定位,跳转后清空解码器内部缓冲,同时重置音频时钟。

跳转一次之后,如果直接读到非关键帧,解码器无法立即输出完整画面,这是正常现象。FFmpeg 在 seek 后会自动往关键帧跳,不过视频可能会有几十毫秒到几百毫秒的黑屏或花屏过渡,这符合正常的播放器行为。

5. 工程构建中的高频报错与排查记录

最后把这几个月的实践中碰到最多的坑总结一下。这些报错在论坛和问答平台里几乎天天能看到,很多人卡住很久,我把原因和解决办法直接列出来。

5.1 高频问题排查表

报错现象可能原因解决办法
运行时提示 no qt platform plugin could be initialized程序目录缺少 platforms/qwindows.dll用 windeployqt 部署插件目录,或在开发环境检查 PATH 是否包含 Qt 的 plugins 目录
编译时报 avformat.h 找不到FFmpeg 头文件路径未指定检查 CMake 的 include_directories 路径,确认是 include 目录,而不是 bin 目录
链接报 unresolved external symbol库链接顺序不对,或库版本与编译器不匹配FFmpeg 库放在依赖库之前;确认库用的编译器架构和当前一致
程序运行后立刻崩溃FFmpeg 是 release 库但工程是 debug 模式统一用 release 模式编译,或换用带 debug 符号的库
有图像没声音音频格式未转换,或采样率不匹配检查 QAudioFormat 参数,用 swresample 把 PCM 转成 S16 交错格式
画面播放速度过快缺少同步逻辑,帧直接全速显示引入音频时钟,显示前判断 PTS 差值,等待或丢帧
视频出现花屏、闪烁QImage 浅拷贝导致缓冲区被覆盖信号发出前调用 QImage::copy() 深拷贝
视频结束后 UI 卡死解码循环和 UI 线程直接交互保证解码、读取、处理都在子线程,只通过信号和 UI 通信

5.2 发布部署的注意事项

程序开发完要发布到其他机器时,需要把 Qt 的运行库和 FFmpeg 的 dll 打包完整。Qt 官方提供的 windeployqt 工具可以自动分析 exe 依赖并拷贝对应的 Qt 插件和运行库,但 FFmpeg 的 dll 必须手动一起复制进去。我用 windeployqt 部署完,又手动把 FFmpeg 的 bin 目录下所有 dll 放到 exe 同一目录下,测试了几台没有 Qt 环境的新电脑,都能正常运行。

有次我在项目里还遇到过一个比较隐蔽的问题:在 Windows 上 FFmpeg 如果打开了硬件解码(比如 cuda、dxva2),发布时需要额外带上对应依赖,而且目标机器还得有对应显卡驱动,否则会运行报错。我没有在通用版本里启用硬件解码,只在特定需求的版本中做了条件编译。

5.3 调试技巧与性能优化

开发过程中有一些很有效的排查手段:

用 av_err2str 函数把 FFmpeg 返回的错误码转换成字符串错误信息,可以快速判断是文件打开失败、流未找到,还是解码器内出现异常。

视频转 RGB 时,SWS_BILINEAR 是通用选择,如果需要更高性能,可以试试 SWS_FAST_BILINEAR,质量差一点点但速度快不少。如果做实时预览或处理高分辨率视频,建议用 QOpenGLWidget 配合着色器直接做 YUV 到 RGB 的转换,比 sws_scale 再交给 QImage 要快得多。

解码线程是纯计算密集型任务,在 Qt 的信号槽中传递 QImage 虽然方便,但频繁的拷贝会影响性能。我后续优化时把视频帧队列改为使用无锁队列直接传递 QImage,把 UI 刷新通过定时器定时从队列取图,这样能显著减少 UI 线程的压力。

从整体体验来看,Qt + FFmpeg 这个组合除了开发初期搭建环境需要一点耐心之外,后面基本是一马平川,非常自由,几乎所有音视频层的需求都能自己掌控。你完全可以基于这套架构继续扩展,比如加字幕、做帧截图、加滤镜、支持网络流播放,这也是我当初选择 FFmpeg 而不是其他方案的原因所在。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/9 15:25:45

股票行情查询接口整理与使用教程

股票行情查询接口整理与使用教程说明&#xff1a;本文基于公开文档与网上可查的接口写法整理&#xff0c;未对每个接口做真实请求实测&#xff0c;接口可用性、字段与限流以各官方文档为准&#xff0c;集成到生产环境前请自行发请求验证。写在前面做量化、写看盘小工具、或在业…

作者头像 李华
网站建设 2026/9/9 15:25:17

Pycopy:极简Python方言,如何在STM32上省下每一KB内存

简介&#xff1a;这是Pycopy极简高效Python方言的项目资源包&#xff0c;面向希望在云、台式机、受限系统和微控制器上使用可扩展Python运行时的开发者与嵌入式工程师。Pycopy由MicroPython项目演进而来&#xff0c;在保留完整Python 3.4语法的基础上引入Python 3.5的异步特性&…

作者头像 李华
网站建设 2026/9/9 15:24:05

三菱PLC五大功能指令详解:SUM、BON、DECO、ENCO、ZRST实战指南

做三菱PLC项目这些年&#xff0c;我总结了一个规律&#xff1a;程序写到一定复杂度&#xff0c;真正决定效率的不是那几个常开常闭触点&#xff0c;而是功能指令用得好不好。尤其是在设备联调、上位机对接、数据统计这种场景里&#xff0c;SUM、BON、DECO、ENCO、ZRST这五条指令…

作者头像 李华
网站建设 2026/9/9 15:23:50

Unity UGUI特效方案:UIEffect组件化实践与性能优化

简介&#xff1a;面向Unity开发者的UGUI特效功能资源&#xff0c;聚焦UGUI界面中可用的轻量级视觉特效实现&#xff0c;适合在游戏UI或应用界面开发中希望快速提升界面表现力的初中级开发者。资源共158个文件&#xff0c;压缩包约53.35MB&#xff0c;核心包括34个C#脚本、5个Sh…

作者头像 李华
网站建设 2026/9/9 15:23:04

易语言1200例源码实战:从索引建起到吃透经典示例的完整指南

简介&#xff1a;《易语言源代码1200例》是一套面向易语言入门与进阶开发者的源码合集&#xff0c;覆盖鼠标限制、Windows API调用、外挂开发、锁屏、映射等典型应用场景&#xff0c;帮助用户通过实例掌握中文编程的语法结构、事件处理、系统交互与算法逻辑。资源以RAR压缩包形…

作者头像 李华
网站建设 2026/9/9 15:22:55

虚拟机安装配置实战:解决VMware蓝屏、网络问号与虚拟化冲突

先说一个我见过最多的场景&#xff1a;教程看了十几篇&#xff0c;VMware也装好了&#xff0c;结果点“开启此虚拟机”&#xff0c;屏幕一黑&#xff0c;等来的不是Ubuntu桌面&#xff0c;而是各种看不懂的英文报错&#xff0c;或者直接Windows蓝屏。再搜一圈&#xff0c;又看到…

作者头像 李华