news 2026/10/20 1:05:01

FFmpeg实战:用ARM盒子实现5路RTMP推流全攻略

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
FFmpeg实战:用ARM盒子实现5路RTMP推流全攻略

做直播服务端的人,多多少少都有个痛点:机器一多,机房功耗和机柜空间就跟着爆炸。我年初接到一个项目,需要在边缘节点同时推5路RTMP流,甲方要求设备尽量小、功耗尽量低、预算还要压到千元以内。第一反应肯定是上主流迷你主机,但算上系统盘、电源适配器、散热改造,一套下来体积和成本都不便宜。后来我换了思路,把手里的MEDAI V2盒子拿出来试了试——这块118g的铝合金小盒子,居然真把5路RTMP推流任务扛下来了,连续跑了半个月没掉过链子。

这篇文章不吹硬件参数,重点讲两件事:一是FFmpeg处理音视频的核心原理,别停留在“会用命令”的层面;二是怎么把FFmpeg的5路推流任务塞进一个巴掌大的ARM盒子里,包括资源预算、系统调优和实际踩坑。如果你正打算做类似的边缘推流节点,或者想搞明白FFmpeg内部到底在干什么,这篇应该能帮你少走不少弯路。

1. 为什么选FFmpeg做推流,又被谁逼着换硬件

1.1 每个转码推流任务背后都是一条FFmpeg流水线

先说说FFmpeg在这个项目里的定位。RTMP推流听起来简单,就是把摄像头或视频文件塞给服务器。但实际场景里,源端可能是IPC的RTSP流,也可能是本地文件循环播放,甚至可能是多个信号源混合画面。不同源端的编码格式不一样,目标服务器的协议要求也不一样——这时候FFmpeg就是那个万能转换器。

它的核心能力可以拆成四层:

  • 解封装(Demuxer):读入RTSP、HTTP-FLV、MP4、TS等封装格式,把里面的视频流、音频流拆分出来;
  • 解码(Decoder):把H.264/H.265等压缩码流还原成原始图像数据;
  • 滤镜(Filter):对原始图像做缩放、裁剪、叠加、帧率转换等处理;
  • 编码与推流(Encoder + Muxer):重新编码成目标格式,再通过RTMP等协议封装发送到服务器。

一条最典型的软编码推流命令长这样:

ffmpeg -re -i input.mp4 \ -c:v libx264 -preset veryfast -b:v 2M \ -c:a aac -b:a 128k \ -f flv rtmp://your-server/live/stream

这里-re是让FFmpeg按原文件播放速度读取,避免推流速度超过实时速率;-c:v libx264指定视频编码器;-f flv指定输出封装格式为FLV,因为RTMP协议走的是FLV封装。

在PC工作站上,这条命令跑一路二路毫无压力,问题在于跑五路。软编码x264在1080p分辨率下,每路大概要占满1.5到2个CPU核心;五路同时编码,8核16线程的桌面CPU都开始吃力,功耗直接拉到一两百瓦。这还是在服务器机房环境,放到用户现场根本不现实。

1.2 项目场景复盘:从工作站到ARM盒子

最初我是在一台旧工作站上跑推流测试,用testsrc测试源同时起5个FFmpeg进程,CPU占用轻松飙到85%以上,风扇直接起飞。这还只是模拟源,如果接真实摄像头,解码和滤镜的开销还会更高。甲方给的约束是:整机功耗最好低于15W,体积控制在手掌大小,能塞进弱电箱。

这让我想到了手里的MEDAI V2盒子:铝合金被动散热外壳,整机118g,CPU是四核A76加四核A55的大小核架构,板载4GB内存,最关键是带独立的H.264/H.265硬件编码器。硬件编码器的存在意味着FFmpeg可以不用x264软编码,而是把编码任务扔给VPU,CPU只负责解封装、滤镜和控制逻辑,这样五路推流的CPU占用能压到极低。后来实测下来,5路720p硬编码推流,整机CPU占用才38%左右,内存占用约1.8GB,功耗在9到12W之间波动。

如果你也想做这类轻量化推流节点,选型时最该关注的就是有没有硬件编码器,其次再关注CPU算力。没有VPU的盒子,就算CPU再强也会被软编码拖垮,散热和功耗根本压不住。

1.3 我的选型检查清单

给同样想用ARM盒子做推流节点的朋友一个自查清单,避免买到手才发现没法用:

  • 编码器支持:查芯片资料或ffmpeg -encoders输出,确认有h264_rkmpp、h264_v4l2m2m等硬件编码器;
  • 内存容量:五路推流建议4GB起步,2GB版本跑两三路还好,五路容易触发OOM;
  • 接口带宽:确认有线网口是千兆,至少也得是USB3.0转千兆,百兆网口推不了高码率多路流;
  • 供电方式:优先选Type-C PD或DC 12V供电,别选那种自带电源适配器还巨大无比的“便携”设备;
  • 系统生态:最好有现成的Ubuntu或Debian ARM镜像,省去自己移植系统的精力。

2. FFmpeg核心机制拆解:从命令行深入到数据流

2.1 一切都是AVFrame的搬运与加工

很多人用了很久FFmpeg,却说不清它内部怎么流转数据。我就用大白话讲一遍:FFmpeg把输入文件当成一个容器,容器里面有多个流(Stream),每个流由一帧一帧的数据组成。解码器把压缩帧解成原始图像/音频帧,滤镜链对这些帧做处理,编码器再把处理后的帧压缩成目标格式,最后通过muxer打包推出去。

整个流程可以类比成一条食品加工流水线:拆开包装(解封装)、清洗切块(解码)、调味摆盘(滤镜)、重新真空包装(编码)、贴标签发货(推流)。中间任何一环出问题,整条链路就卡住。

理解了这个模型,你就能明白为什么有些命令会有那么多参数:

ffmpeg -rtsp_transport tcp -i rtsp://camera/stream \ -vf "scale=1280:720,fps=25" \ -c:v h264_rkmpp -b:v 2M -g 50 -bf 0 \ -c:a aac -b:a 64k -ar 44100 \ -f flv -flvflags no_duration_filesize \ rtmp://server/live/cam1

上面的-vf是指定滤镜链:先缩放分辨率到1280x720,再强制帧率25fps。-g 50设置GOP(关键帧间隔)为50帧,也就是每2秒一个关键帧,方便拉流端快速起播。-bf 0关闭B帧,因为RTMP播放器对B帧的兼容性参差不齐,关闭后延迟会更低。

2.2 RTMP推流的底层:FLV over TCP

RTMP本身不是文件格式,而是一个基于TCP的实时消息协议。市面上的“RTMP推流”绝大多数指的是:把FLV格式的数据块通过RTMP协议分包发给服务器。

FFmpeg里对应的操作就是用flv作为输出封装格式:

-f flv rtmp://host/live/stream

-f flv告诉FFmpeg输出封装用FLV;rtmp://这个URL则让FFmpeg自动选择RTMP协议处理器。二者缺一不可——如果你写成-f flv file.flv就变成写本地文件,如果URL是rtmp但输出格式没指定,FFmpeg可能猜不出来。

推流时最影响稳定性的一个是-flvflags参数。默认情况下,FFmpeg写FLV时会在文件头写入时长和文件大小,这个信息对点播文件没问题,但对直播推流就是灾难。加-flvflags no_duration_filesize可以去掉这类元数据,让接收端(比如SRS、Nginx-RTMP)不用等文件结束就能正常解析。

另一个关键参数是是否开启实时推流节奏。用文件循环推流时,必须加-re;如果用实时源(比如RTSP摄像头),可以不加重读,因为摄像头本身的数据到达速率就是实时的。我曾经见过有人用摄像头源还加了-re,结果画面越来越慢,就是因为-re强制了文件读取速率,与实时源冲突。

2.3 滤镜链的分支与合并要比想象中灵活

如果只是单路转码,滤镜链还体现不出威力。但5路推流场景里,我用了大量滤镜组合:有的需要把16:9画面裁成1:1竖屏,有的需要叠加台标和时间戳,还有的需要把两路视频并排合成一路。

Windows媒体服务器常见的滤镜链式处理,在FFmpeg里也能做:

# 画中画:主画面加右下角小窗 ffmpeg -i main.mp4 -i sub.mp4 \ -filter_complex "[0:v]scale=1920:1080[bg];[1:v]scale=320:180[fg];[bg][fg]overlay=W-w-10:H-h-10[out]" \ -map "[out]" -c:v h264_rkmpp -f flv rtmp://server/live/pip

filter_complex是复杂滤镜链,可以处理多路输入输出。这里先对主画面缩放,再对子画面缩放,最后用overlay滤镜把子画面叠到主画面右下角。这种能力在直播运营场景非常实用——比如同时推主摄像头信号和产品特写信号。

需要提醒的是,硬件编码器对输入分辨率有限制。某些VPU要求宽度和高度必须是2的倍数或4的倍数,你用scale=1281:720这种非对齐参数,硬件编码器会直接报错。所以写滤镜时尽量保持偶数分辨率,或加force_original_aspect_ratio=decrease后手动补齐。

3. MEDAI V2盒子实测:从刷机到5路RTMP推流的完整链路

3.1 拿到手先做的事:刷系统与确认编码器能力

我手里这块MEDAI V2是某方案商出的边缘AI盒子,按照官方文档刷入了Ubuntu 22.04 ARM版本。这里给个实操步骤,ARM盒子刷机大同小异:

  1. 用Type-C数据线连接盒子的烧录口,安装厂家提供的驱动工具;
  2. 下载官方Ubuntu镜像,用烧录工具写入eMMC;
  3. 首次开机后通过串口或SSH连接,修改默认密码并扩展分区;
  4. 执行apt update && apt install ffmpeg,或从源码编译带硬件编码器的FFmpeg。

装完后第一件事是确认FFmpeg能看到硬件编码器:

ffmpeg -hide_banner -encoders | grep rkmpp

如果输出里有h264_rkmpp和hevc_rkmpp,说明硬件编码器已识别。某些精简版系统可能没有启用VPU驱动,需要额外安装librga和rockchip-mpp相关库。

我见过不少人在这一步卡住:系统装好了,但ffmpeg -encoders里只有软编码器。原因多半是系统里跑的FFmpeg是发行版默认版本,不带rockchip的mpp补丁。解决办法有两个:一是从源码编译,加上--enable-version3 --enable-libvpx --enable-rkmpp等配置项;二是直接找方案商要编译好的FFmpeg二进制,一般都会带硬件编解码支持。

3.2 5路推流运行时的进程规划

五路推流不能简单粗暴地起五个ffmpeg命令,那样每个进程都要重复加载解码器、打开输入源,资源浪费严重。我的做法是写一个启动脚本,用nohup或systemd管理,但每个推流任务独立进程,方便单路异常时单独重启。

每一路的命令行核心参数是一致的,区别只在输入源、输出地址和流名:

ffmpeg -hide_banner -loglevel warn \ -rtsp_transport tcp -i "rtsp://192.168.1.20:554/stream1" \ -vf "scale=1280:720,fps=25" \ -c:v h264_rkmpp -b:v 2M -g 50 -bf 0 \ -c:a aac -b:a 96k -ar 44100 -ac 2 \ -f flv -flvflags no_duration_filesize \ rtmp://your-server/live/camera01 \ -y

这里解释几个容易忽略的细节:

  • -rtsp_transport tcp:拉取摄像头RTSP流时强制用TCP,避免UDP丢包导致的画面花屏。摄像头和盒子在同一局域网时TCP是最稳的选择;
  • -b:v 2M:码率单位是bit/s,这里设置视频码率2Mbps。五路加起来就是10Mbps左右,千兆网口余量充足;
  • -g 50:2秒一个关键帧,播放端拖动和起播都会更快。追求更低延迟可以改成25,但码率会有所上升;
  • -bf 0:关B帧。B帧会引入帧重排延迟,直播场景里一般建议关掉。

每一路进程内存大约在300MB左右,加上系统和其他服务,4GB内存刚好够用。我实测5路同时跑,free -h显示used 1.8GB / total 3.8GB,还有不少余量。

3.3 用Systemd守护进程,掉线自动拉起

命令行跑起来容易,但生产环境最怕进程半夜挂掉,第二天早上发现直播已经断了几个小时。所以我没有用裸nohup,而是给每路推流写了一个systemd服务单元。

拿其中一路举例,/etc/systemd/system/rtmp-camera01.service内容如下:

[Unit] Description=RTMP Push Camera 01 After=network-online.target Wants=network-online.target [Service] Type=simple ExecStart=/usr/local/bin/ffmpeg -hide_banner -loglevel warn -rtsp_transport tcp -i "rtsp://192.168.1.20:554/stream1" -vf "scale=1280:720,fps=25" -c:v h264_rkmpp -b:v 2M -g 50 -bf 0 -c:a aac -b:a 96k -ar 44100 -ac 2 -f flv -flvflags no_duration_filesize rtmp://your-server/live/camera01 -y Restart=always RestartSec=5 StartLimitIntervalSec=60 StartLimitBurst=5 [Install] WantedBy=multi-user.target

Restart=always表示进程异常退出后自动拉起,间隔5秒。配合StartLimitBurst=5防止无限重启导致日志爆炸。这里有一个坑:systemd默认对单个服务的重启次数有限制,如果FFmpeg因为配置错误反复崩溃,systemd会放弃重启,这时候你需要systemctl reset-failed rtmp-camera01.service再手动启动。

五路推流就写五个service文件,然后用systemctl enable rtmp-camera0*.service开机自启。再补充一个简单的监控脚本,每30秒检查进程是否存活、推流地址是否可连通,发现异常就调用systemctl restart。这套方案扛住了两周连续运行,没有一次需要人工干预。

4. 性能压测与参数调优:怎样把5路推流压在118g的盒子里

4.1 用testsrc模拟源做基准压测

正式接摄像头之前,最好先用FFmpeg内置的testsrc测试源做压测,排除摄像头本身不稳定造成的干扰。比如这样可以模拟一路1080p50的测试视频:

ffmpeg -re -f lavfi -i testsrc2=size=1920x1080:rate=50 \ -c:v h264_rkmpp -b:v 4M -g 50 -bf 0 \ -f flv rtmp://server/live/test

注意,testsrc2是纯视频源,不带音频。要模拟完整音视频流可以加sine音频源,用-filter_complex合成一路。实际项目中我用testsrc做基准压测,把五路同时推流的CPU占用、内存、推流码率都记录下来,然后再切换到真实摄像头源对比。

压测时最值得关注的数据是:CPU占用是否稳定、推流码率是否在目标值上下波动、接收端拉流是否出现花屏和卡顿。我当时记录的结果是:5路720p推流,CPU占用大约35%到42%之间波动,系统内存稳定在1.7GB到2.0GB,推流码率每路稳定在1.9Mbps到2.1Mbps。

4.2 核心调优参数:延迟与稳定性的平衡

如果把FFmpeg当成“万能瑞士军刀”,那调优就是选对刀刃。我实际踩坑后发现,以下几组参数对推流稳定性影响最大:

帧率与分辨率对齐

很多IPC摄像头实际帧率是25fps,但它们的视频流描述里可能写的30fps,或者关键帧间隔极不规律。进FFmpeg后如果不加fps滤镜,输出时间基准就是乱的,可能导致接收端音视频不同步。我习惯在滤镜链里强制fps=25,并用-vsync cfr锁定恒定帧率输出。

码率控制模式

-b:v是目标码率,但编码器可能为了追码率而突然降低画质,或者码率超标。更稳的做法是用-maxrate和-bufsize约束峰值:

-c:v h264_rkmpp -b:v 2M -maxrate 2.5M -bufsize 4M

maxrate 2.5M限制瞬时峰值不超过2.5Mbps,bufsize 4M是码率控制缓冲。这样既保证平均码率稳定,又不会因为画面剧烈变化导致网络瞬时拥塞。

日志与调试

生产环境里FFmpeg的日志默认打到stderr,systemd会把它写进journald,时间久了日志文件会非常大。加-loglevel warn只保留警告和错误,减少不必要输出。调试时再用-loglevel debug或者-loglevel info看内部状态。

这套参数组合下,我实测五路推流的最差表现是重启某一路时接收端有1秒左右黑屏,其余四路全程正常。单独一路崩溃的恢复时间约8到10秒(含systemd重启等待和推流握手时间),在可接受范围内。

4.3 温度、功耗与长时间稳定性

铝合金被动散热盒子最怕长时间高负载运行导致芯片降频。我用传感器读取CPU温度,5路推流运行一小时后温度稳定在68到72摄氏度。这个温度对于A76大核来说属于安全区间,没有触发降频。

不过有个细节——盒子的供电适配器标称12V 2A,但我实测峰值功耗达到13W,已经接近适配器上限。建议如果你要长期满负载跑,换一个12V 3A或更大余量的电源,避免适配器过热或电压跌落导致锁频甚至重启。

另外建议在部署现场给盒子留出空气流通空间,别把它埋进封闭的弱电箱里。虽然铝合金外壳本身就是散热器,但热量总得有地方散。我在机箱外壳上贴了一小条导热硅脂垫加强接触,温度又下降了4度左右,这个小改动成本低效果明显。

5. 推流异常排查实录:从时间戳到RTMP握手的坑

5.1 画面卡顿但CPU负载不高:问题在网络而非编码

我刚开始压测时遇到一个奇怪现象:5路同时推流,CPU不高,内存不紧张,但接收端画面时常卡顿。用iftop和tcpdump抓包分析,发现推流带宽在某一瞬间突然飙到6Mbps以上,然后服务器侧出现TCP重传。

经过排查,根因是编码器输出码率不均匀。硬编码器在画面剧烈变化时(比如测试画面里的快速移动区域),瞬时码率可能冲到目标码率的1.5倍以上。而我在接收端服务器上设置的带宽限制较低,网络出现瓶颈。

解决方案是加-maxrate和-bufsize约束瞬时码率,同时在输入滤镜链里加gblur等轻量模糊滤镜平滑画面细节,避免编码器为追求细节疯狂提码率。最终画面卡顿彻底消失。

5.2 时间戳错乱导致播放器快进快退

另一路信号的问题是:拉流端播放时偶尔会自动快进几秒,或者音频明显比视频快。排查一圈发现,问题不在编码器,而在输入源——摄像头RTSP流的关键帧间隔不规律,有的间隔3秒,有的间隔1秒,导致FFmpeg输出的时间戳序列不连续。

解决方法是滤镜里加settb=AVTB和setpts=PTS,把输入时间基准归一化到FFmpeg标准时间基准:

-vf "settb=AVTB,setpts=PTS,scale=1280:720,fps=25"

另外在推流参数里显式指定-use_wallclock_as_timestamps 1,让输入使用墙钟时间生成时间戳,不依赖源端的时间基准。经过这两个调整后,时间戳抖动问题消失。

5.3 RTMP握手失败:URL路径与服务器配置对照

调试中最隐蔽的坑是RTMP握手失败。FFmpeg日志显示Server returned error,但服务器端日志没有任何异常。最后定位到是URL里的应用名与服务器配置不一致。

RTMP URL格式是rtmp://ip:port/app/stream_name,其中app是服务器里配置的应用路径。如果服务器上配置的应用名是live,但URL写成rtmp://ip:port/live2/stream,就会握手失败。这类问题靠表面看很难发现,排查时一定要同时看FFmpeg端和服务器端的日志,对照应用名是否一致。

还有一次失败是因为服务器开启了鉴权,要求URL带上?token=xxx,但FFmpeg命令行里没有传。这不是FFmpeg本身的问题,而是推流地址拼接时漏掉了鉴权参数。RTMP是明文协议,鉴权参数也会直接出现在URL里,注意不要在公网环境用,否则容易泄露密钥。

6. 把5路推流固化成工程:监控、重连与后续扩展

6.1 推流节点的监控与自愈体系

单一盒子运行5路推流,最怕的是某路源端摄像头掉线,导致FFmpeg进程挂掉。针对这个问题,我的做法是在systemd之外再加一层应用级监控——一个简单的shell脚本,定时检查每路进程的存活时间和输出流状态。

脚本核心逻辑是:如果某一路的FFmpeg进程不存在,就调用systemctl restart;如果进程存在但推流码率异常(比如连续30秒没有输出),就kill掉进程让它重启。这个自愈机制很有必要,因为有些情况下FFmpeg进程还活着,但网络已经断开,推流名存实亡。

#!/bin/bash # 简单自愈脚本,每30秒执行一次 for stream in camera01 camera02 camera03 camera04 camera05; do if ! pgrep -f "stream=${stream}" >/dev/null 2>&1; then systemctl restart rtmp-${stream} fi done

放到crontab或systemd.timer里定期执行。这个脚本虽然粗糙,但比没有监控好太多。如果想更精细,可以用Prometheus + node_exporter采集FFmpeg进程指标,再配合Grafana看板,但118g盒子上跑这套组件有点重,轻量脚本够用就行。

6.2 多目标同时推流与平台分发

有些场景需要把同一路画面同时推到多个平台,比如自家服务器一份、第三方直播平台一份。FFmpeg支持同时输出多个目标:

ffmpeg -re -i input.mp4 \ -c:v h264_rkmpp -b:v 2M \ -f flv rtmp://server1/live/stream \ -f flv rtmp://server2/live/stream

注意,每增加一个输出目标,编码器输出就要多拷贝一份,网络带宽也要增加一份。五路源各推两个目标,总带宽就是10路码率,MEDAI V2的千兆网口依然扛得住,但CPU会因为多路muxer封装略有上升。

我实际测试中,双输出时的CPU占用率比单输出高出约8%到12%,对于5路双输出就是40%到45%的CPU占用,比较接近盒子上限了,不建议再多扩展。

6.3 这套方案还能怎么玩

5路RTMP推流只是MEDAI V2结合FFmpeg的一种玩法。同样的架构稍加改动,还能做视频轮播、画中画合成、竖屏裁剪、甚至拉流后接入AI分析。盒子的NPU还能跑轻量模型,实现画面异常检测,在推流的同时把告警推给上层平台。

如果你手头有类似的小型ARM盒子,不妨先跑通单路推流,再渐进扩展到多路,过程中重点观察资源水位。FFmpeg的命令参数看似多,实际核心就那几个:输入格式、滤镜、编码器、输出格式。把这些摸透了,换什么硬件都能快速上手。

我个人的感受是:硬件选型决定了能耗和体积的上限,而FFmpeg的掌握深度决定了你能解锁多少种可能。118g的盒子能做到5路推流,靠的正是FFmpeg对硬件编码器的充分适配,以及系统层面不浪费每一分资源。这套组合,如果你也有类似场景,可以直接复制过去,省掉大半个月的踩坑时间。

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

STM32+ESP8266+华为云IoT+Qt:智能家居设备上云全流程实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/20 1:04:07

低功耗AI开发板落地指南:从功耗预算到模型压缩

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/20 1:03:50

机器人嵌入式高薪岗位:四大稀缺能力与跃迁路径

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/20 1:03:33

-0x7780错误码解析:TLS握手失败的定位与排查指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/20 1:03:27

掌控板+Shortcuts实现本地语音控制(不联网)

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/20 1:03:15

ESP32与RC522 RFID读卡实战:从原理到门禁系统开发

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华