Telegram 频道视频自动发布
给频道做视频自动发布时踩到的两个硬限制:Bot API 单请求 50MB(实测 46MB 过、62MB 413),以及 HLS 下载静默截断。换 MTProto 用户账号后单文件 2GB 原样直发,文中附方案对比与实测数据。
技术 · 2026-10-24 · 约 7 分钟阅读 · 标签:Telegram、MTProto、工程实践、Bot API、FFmpeg
起因:一条 68 分钟的视频,只发出去了 2 分 19 秒
流水线本身很简单:定时抓取内容源 → 入库去重 → 下载视频 → 发到自己的 Telegram 频道。这套东西内部叫 呱呱 Guagua(最早的代号是 xclib,刚改的名),跑在一台 arm64 小主机上的一只容器里。跑了几天都正常,直到频道里出现几条「没有视频、只有文字和链接」的帖子。
排查下来不是抓取的问题,是出口的体积限制——而且它不会报错给你看,只会悄悄降级。
这一篇把踩过的四个坑和最后换出口的方案记下来,都是实测数据,不是文档抄来的。
坑一:Bot API 的 50MB,比你想的更紧
Bot API 上传单文件上限 50MB,这是常识。但真正的限制是一次 HTTP 请求的体积,也就是整个 multipart body。
实测(同一个 bot,同一个频道,2 段视频的媒体组):
| 请求总体积 | 结果 | | --- | --- | | 31 MB | ✅ 成功 | | 46 MB | ✅ 成功 | | 62 MB | ❌ 413 Request Entity Too Large | | 78 MB | ❌ 同上 | | 94 MB | ❌ 同上 |
所以阈值大约在 50MB 上下:「一条消息里放几个文件」不影响额度,总字节数才影响。
这直接否决了一个看似优雅的方案:
超过 48MB 就无损切片,几段用
sendMediaGroup塞进同一条消息,画质一点都不掉。
一次请求多段 → 413;改成逐段 sendVideo → 请求能过,但一条帖子变成三四条消息,体验直接崩掉。
而「一条消息」这个目标一旦锁死,可用的字节预算就是 48MB(50MB 留 2MB 余量)。一个 48MB 的单文件,和两个 24MB 的文件放在同一条消息里,画质完全一样——总字节一样,切片只是多了一个文件、多了一次点击。
结论:只要出口是 Bot API,「一条消息」和「48MB」是绑死的,画质没有操作空间。
坑二:下载截断,静默失败
画质问题还没解决,又发现新问题:某条 5 分钟的视频,发出去只有 2 分 19 秒。
原因在 HLS:ffmpeg 拉 m3u8 时如果中途断流,它不会报错,会老老实实把已经下载的部分封装成一个完整可播放的 mp4。文件能播、时长正常、ffprobe 也没意见——只是内容少了一半。
修法是加一道完整性校验:
- 先请求 m3u8 播放列表,把
#EXTINF全部加起来,得到「应有的总时长」 - 下载完成后
ffprobe拿实际时长 - 实际 < 应有 × 97% → 删掉重下(最多 3 次)
- 3 次还不完整 → 直接报错,宁可不发,也不发半截
这段代码只有二十来行,但它是「发出去的东西完整」这条底线的唯一保证。
坑三:压缩救不了画质,只会二次伤害
中间试过按目标体积反推码率的自适应压缩:
- 由
目标体积 / 时长反推可用视频码率 - 按每像素比特数
bpp = 码率 / (宽 × 高 × 25)判断,bpp ≥ 0.06才算不糊 - 从这个条件倒推该用 1080p 还是 720p、540p、480p、360p
- 编码用 CRF 22 +
maxrate双约束,压完核对体积,超了就降一档再来
结果很现实:8 分钟片子压到 48MB 还能看,68 分钟的片子压到 48MB,就是 360p 以下的马赛克。源片本身码率就不高的情况下,二次压缩等于二次伤害。
所以压缩只应该当兜底,不能当方案。
换出口:把「发帖」从 Bot API 换成 MTProto
问题的根不在流水线,在出口。同一件事换条路走,限制就消失了:
- Bot API:单文件 50MB,一次请求 50MB
- MTProto(用户账号):单文件 2GB(Premium 4GB),一条消息发完,服务端不重编码
很多人以为「用账号发」意味着帖子署名变成个人。其实不会:频道帖永远署名频道本身,只有账号在群组里发言才显示个人名字。所以在读者眼里,换出口是零感知的。
实现上用 telethon:
- 独立 session 文件(落在容器数据卷),不和其他 Telegram 客户端抢登录
- 通过 HTTP 代理连 Telegram 数据中心(
python-socks) - 发视频用
send_file+DocumentAttributeVideo(显式带时长、分辨率、supports_streaming),缩略图单独生成后一起传 - 超时按体积估算:
max(1200s, 体积MB × 8 + 600)
两个实现细节值得记一笔
1. 数字 chat_id 需要 access_hash
-100xxxxxxxxxx 这种 channel id 不能直接丢给 send_file,会报 Cannot find any entity corresponding to ...。解决办法是先用可见的用户名把频道解析成 InputPeer:
peer = await client.get_input_entity("@channel_username")
拿不到用户名时再退化成 PeerChannel(abs(id) - 10**12)。
2. 登录不需要碰服务器终端
一次性登录做成了面板里的一个页面:填手机号 → 收验证码(发到 Telegram 客户端,不是短信)→ 有二步验证就再填一次 → session 落盘。之后全自动,掉线了在面板里能直接看到状态、重新登录。
密码和 API 凭据只存在本机的 env_file(600 权限),不进仓库、不进数据库、不打日志。
结果
- 一条 68 分钟 / 1.17GB 的视频,一条消息发完,时长和源完全一致
- 一条 46 分钟的视频,定时任务自动发的,同样一条消息
- 两条都没有切片、没有重编码
- 本地不存副本:传完即删,磁盘占用从 3.2GB 归档 + 0.5GB 残留清到接近 0
整条链路现在是:抓取 → 完整性校验 → 原文件直发 → 删除本地,中间的切片、相册、压缩逻辑全部退居兜底。
顺带把项目从内部代号 xclib 正式改成了 呱呱 Guagua——名字跟频道调性对得上,也终于好念了。
顺手清掉的噪音
- 分类只有第一页时,源站返回
410 Gone。这本来算正常情况,但之前被当成错误写进了状态栏,面板上一直挂着一条红字。改成静默跳过,并且在抓取成功时清空last_error——错误提示要能代表真实故障,否则就是狼来了。 - 面板加了 Basic Auth,密码单独放
env_file,不写进镜像也不写进代码。 - 删掉两条曾经用到的容器挂载(媒体库方案放弃了),重建后健康检查恢复正常。
取舍
| 方案 | 单文件上限 | 一条消息 | 画质 | | --- | --- | --- | --- | | Bot API | 50MB | 受请求体积限制 | 长片必须压缩 | | 自建 Bot API server(--local) | 2000MB | ✅ | 原样 | | MTProto 用户账号 | 2GB | ✅ | 原样 |
自建 telegram-bot-api --local 也能解锁 2GB,还能保留 bot 身份,但它必须直连 Telegram 数据中心(--proxy 只管 webhook,不管 MTProto),在网络受限的环境里很别扭。MTProto 这条路直接复用现有代理,代价只有一个:需要一次账号登录,以及 session 掉线时得重登。
对一个「画质不能妥协、一条消息必须发完」的场景,这个代价很划算。
呱呱 Guagua 为私有自建工具,不公开仓库地址与部署信息。本文只记录工程结论与实测数据。