go2rtc 的 Motion JPEG 全链路实战:MJPEG 拉流、推流、快照抓取与终端 ASCII 艺术流
【免费下载链接】go2rtcUltimate camera streaming application项目地址: https://gitcode.com/GitHub_Trending/go/go2rtc
导读
本文围绕 go2rtc 内部的 internal/mjpeg/README.md 展开,系统讲解这个以“终极摄像头流媒体应用”为定位的项目中 MJPEG(Motion JPEG)模块的完整能力:如何把摄像头转换为同时含 H264 与 MJPEG 双编码的流、如何通过api/stream.mjpeg输出 MJPEG 流、如何用api/frame.jpeg抓取 JPEG 快照、如何把摄像头画面以 ASCII 艺术形式输出到服务器终端,以及如何反向把 MJPEG 流推入 go2rtc。读完本文,你将掌握 go2rtc 中 MJPEG 的“消费(拉取)—服务(输出)—摄取(输入)”三大场景的全部配置与 API 用法,并理解其背后的源码实现原理。
一、模块概览:一个模块,三种职责
从 internal/mjpeg/mjpeg.go 的Init()可以看到,该模块注册了四条核心 HTTP API 路由:
api/frame.jpeg—— 抓取 JPEG 快照(单帧)api/stream.mjpeg—— 输出或接收 MJPEG 流(按 HTTP 方法区分)api/stream.ascii—— 输出终端 ASCII 艺术流api/stream.y4m—— 输出 YUV4MPEG 原始帧流
同时它还注册了 WebSocket 的mjpeg消息类型(ws.HandleFunc("mjpeg", handlerWS)),用于 Web UI 中直接在浏览器播放 MJPEG 画面。
对应官方文档 internal/mjpeg/README.md 的定位,本模块承担三种职责:
- 提供与接收 MJPEG 格式的流(双向);
- 负责接收 JPEG 格式的快照(单帧抓取);
- 支持以“动态 ASCII 艺术”格式把流输出到服务器控制台(终端)——这是 go2rtc 的特色功能之一,纯粹“图一乐”,但非常适合向朋友演示“无 GUI 也能看摄像头”。
值得一提:
api/frame.jpeg与api/stream.ascii两条路由都指向handlerStream之外的独立处理函数,其中frame.jpeg由handlerKeyframe处理,而stream.ascii与stream.mjpeg共用handlerStream(根据 URL 后缀.mjpeg/.ascii分发到不同 Writer),见 internal/mjpeg/mjpeg.go。
二、MJPEG 客户端:如何让 go2rtc“拿到” MJPEG 流
关键前提:对于一个 MJPEG 格式的流,你的视频源必须包含 MJPEG 编码器(codec)。只要源里有 MJPEG 编码,就可以通过 API 接收MJPEG 流或JPEG 快照。
获取 MJPEG 流的常见途径有四种:
- 部分摄像头在 RTSP 流内直接提供 MJPEG 编码(例如大华 Dahua 摄像头的第二路流),相关协议见 internal/rtsp/README.md;
- 部分摄像头提供带 MJPEG 流的 HTTP 链接,协议支持见 internal/http/README.md;
- 部分摄像头提供 HTTP 快照链接,go2rtc 可以把快照序列转换为 MJPEG 流,同样见 internal/http/README.md;
- 通过 FFmpeg 集成把摄像头的 H264/H265 流转码为 MJPEG,集成说明见 internal/ffmpeg/README.md。
2.1 配置示例:一个流同时拥有 H264 与 MJPEG 两种编码
官方文档给出的标准配置如下,camera1同时列出两个源,go2rtc 会按顺序尝试,最终该流将同时具备 H264 和 MJPEG 两种编码:
streams: camera1: - rtsp://rtsp:12345678@192.168.1.123/av_stream/ch0 - ffmpeg:camera1#video=mjpeg其中第一条直接对接摄像头的 RTSP 主码流(H264),第二条通过ffmpeg:前缀引用同名的camera1并指定#video=mjpeg进行转码。这样下游消费者(浏览器、ffplay、curl)就可以按需选择 H264 或 MJPEG。
2.2 客户端侧源码佐证:Consumer 如何消费 MJPEG
从 pkg/mjpeg/consumer.go 可以看到,MJPEG 消费者(NewConsumer())声明它只接受视频方向的两种编码:
JPEG(core.CodecJPEG)RAW(core.CodecRAW,原始 YUV 帧)
在AddTrack中,若轨道编码是 RTP 封装的,则调用RTPDepay(RTP 解包);若是 RAW 原始帧,则调用Encoder在内存中把 YUV 帧直接编码为 JPEG(见 pkg/mjpeg/helpers.go,其内部通过jpeg.Encode完成,并支持跳过空帧)。这也是 go2rtc 能“无 FFmpeg 进程”直接消费 JPEG 流的原因——纯 Go 实现,零外部依赖。
三、MJPEG 服务端:四个输出 API
3.1 mpjpeg:输出 MJPEG 流
MJPEG 在 FFmpeg 生态里叫mpjpeg,因为它本质是携带 HTTP 头(multipart)的 JPEG 帧序列。go2rtc 提供标准输出接口:
ffplay http://192.168.1.123:1984/api/stream.mjpeg?src=camera1用浏览器或 ffplay 打开即可看到实时画面。底层实现中,pkg/mjpeg/writer.go 会把Content-Type设置为multipart/x-mixed-replace; boundary=frame,这是浏览器 MJPEG 播放的标准 MIME 类型;每一帧都以--frame\r\nContent-Type: image/jpeg\r\nContent-Length: N的边界头封装后写出,并在写完后显式Flush()以保持实时性(见 pkg/mjpeg/writer.go)。
源码注释中保留了一条 Chrome 的历史 bug 记录(
bugs.chromium.org/p/chromium/issues/detail?id=527446):Chrome 播放 MJPEG 时总是显示倒数第二帧,这也是代码中每帧都 flush 的原因之一。
3.2 jpeg:抓取 JPEG 快照
抓取单帧快照使用:
curl http://192.168.1.123:1984/api/frame.jpeg?src=camera1返回的 HTTP 响应会携带Content-Type: image/jpeg、Cache-Control: no-cache等头(见 internal/mjpeg/mjpeg.go),可直接保存或喂给其他系统。
支持的查询参数(均直接透传给 FFmpeg 转码):
| 参数 | 别名 | 取值与说明 |
|---|---|---|
width | w | 输出宽度,如width=640 |
height | h | 输出高度,如h=480 |
rotate | 无 | 90、180、270或-90,旋转快照 |
hardware | hw | 硬件加速转码,详见项目 Wiki 的 Hardware-acceleration 章节 |
cache | 无 | 如1m、10s,获取缓存快照 |
参数解析逻辑在 internal/ffmpeg/jpeg.go 的parseQuery中:
width/height通过scale=W:H过滤器实现;rotate通过transpose过滤器实现:90→transpose=1(顺时针 90°),180→ 两次 transpose,-90/270→transpose=2(逆时针 90°);hardware交给hardware.MakeHardware生成对应硬解/硬编参数。
关于cache参数的三个重要行为(官方文档明确):
- 快照仅在请求携带
cache参数时才会被缓存; - 当缓存快照的“年龄”未超过
cache指定的时间(如10s)时,直接复用缓存结果; cache参数不会校验缓存图片尺寸与请求中指定的尺寸是否一致——即第一次以某个分辨率请求后,后续即使换尺寸,只要在缓存窗口内仍会返回旧尺寸。
源码对应 internal/mjpeg/mjpeg.go:缓存以map[string]cacheEntry形式保存(键为src),命中条件为time.Since(entry.timestamp) < timeout。
快照的底层原理:handlerKeyframe使用magic.NewKeyframe()(pkg/magic/keyframe.go)注册为消费者。它会根据源编码分流处理:
- 源为H264/H265:只取关键帧(
h264.IsKeyframe/h265.IsKeyframe),解包成 Annex-B 裸流后交给ffmpeg.JPEGWithQuery调用 FFmpeg 转成 JPEG; - 源为JPEG:直接经
mjpeg.FixJPEG修正 JPEG 头后返回; - 源为RAW:用
mjpeg.Encoder编码为 JPEG。
其中FixJPEG(pkg/mjpeg/helpers.go)会处理各类“非标准” JPEG:对缺少 DHT 表的AVI1头 JPEG 注入默认 Huffman 表(InjectDHT),对头损坏的 JPEG 用 Go 标准库重新编码修复,确保快照能被下游(如 Telegram 服务器)正常识别。
3.3 ascii:把摄像头流输出到终端
这是 go2rtc 最具趣味性的功能:把 MJPEG 流转成动态 ASCII 艺术输出到服务器终端,无需任何 GUI 就能“看”摄像头。官方定位是“just for fun”——可以跟朋友炫耀“我能把摄像头流到服务器控制台”。
官方文档中有一段演示视频(YouTube 链接),其画面其实是该格式多种参数组合再加上音频的效果;需要说明的是,该格式本身不支持音频。
使用要点(Tips):
- 该功能只对 MJPEG 编码有效(必要时需先转码);
- 建议选择低帧率(FPS);
- 选择适合你终端大小的宽高;
- 不同终端支持的颜色数不同(8、256、rgb);
text参数需要URL 编码(如空格编码为%20);- 可以流式播放任意摄像头或磁盘上的文件。
go2rtc.yaml 配置示例—— 转码为 MJPEG、终端尺寸 210x59(16:9)、帧率 10:
streams: gamazda: ffmpeg:gamazda.mp4#video=mjpeg#hardware#width=210#height=59#raw=-r 10其中#raw=-r 10把 FFmpeg 输出帧率限制为 10fps,#hardware启用硬件加速,width/height直接决定 ASCII 画布的字符网格大小。
API 参数:
| 参数 | 取值 | 说明与示例 |
|---|---|---|
color | 空 /8/256/rgb/ SGR 序列 | 前景色。8与256为对应色数的自动映射,rgb为 24 位真彩;也可直接传 ANSI SGR 码,如30(黑)、37(白)、38;5;226(黄) |
back | 同上 | 背景色,如40(黑)、47(白)、48;5;226(黄) |
text | 空 / 单字符 /block/ 亮度递增字符列表 | 字符集。空为默认亮度渐变串;block使用块元素(░▒▓█家族);ox这类多字符列表按亮度升序映射 |
curl 示例(官方原文):
% curl "http://192.168.1.123:1984/api/stream.ascii?src=gamazda" % curl "http://192.168.1.123:1984/api/stream.ascii?src=gamazda&color=256" % curl "http://192.168.1.123:1984/api/stream.ascii?src=gamazda&back=256&text=%20" % curl "http://192.168.1.123:1984/api/stream.ascii?src=gamazda&back=8&text=%20%20" % curl "http://192.168.1.123:1984/api/stream.ascii?src=gamazda&text=helloworld"实现原理(pkg/ascii/ascii.go):NewWriter收到每帧 JPEG 后先jpeg.Decode解码,再逐像素取 RGB,按亮度灰度映射到字符集,配合 ANSI 转义序列上色;开场发送\033[2J(清屏),每帧开头发送\033[H(光标归位)实现动画刷新。颜色映射细节:
color=8:在 8 色 xterm 调色板中搜索最接近的颜色(xterm256color(r,g,b,8));color=256:映射到 256 色表(\033[38;5;Nm);color=rgb:直接输出 24 位真彩(\033[38;2;R;G;Bm);- 默认
text字符集为` .::--~~==++**##%%$@`(按亮度从暗到亮),block则映射到块元素" ░░▒▒▓▓█"(见 pkg/ascii/ascii.go)。
3.4 yuv4mpegpipe:输出原始 YUV 帧流
输出带 YUV4MPEG 头的原始 YUV 帧流:
ffplay http://192.168.1.123:1984/api/stream.y4m?src=camera1该接口由apiStreamY4M实现(internal/mjpeg/mjpeg.go),直接使用 pkg/y4m 的y4m.NewConsumer()作为消费者,把帧以 YUV4MPEG 容器格式原样输出。适合需要原始帧做进一步处理(如 AI 分析、自定义编码)的场景。
四、Streaming ingest:把 MJPEG 流推入 go2rtc
api/stream.mjpeg接口同时支持GET(输出)与POST(输入),路由分发逻辑见 internal/mjpeg/mjpeg.go:r.Method != "POST"时走输出outputMjpeg,否则走输入inputMjpeg。
官方给出的推流示例——用 FFmpeg 把本地视频转成 MJPEG 并推送到 go2rtc:
ffmpeg -re -i BigBuckBunny.mp4 -c mjpeg -f mpjpeg http://localhost:1984/api/stream.mjpeg?dst=camera1关键点:
- 使用
-f mpjpeg输出格式(multipart JPEG,自带 HTTP 边界头); - 目标地址带
dst=camera1查询参数,指定要推入的流名称; - go2rtc 侧
inputMjpeg用mpjpeg.Open(r.Body)打开请求体作为 Producer,挂到streams.Get(dst)对应的流上(见 internal/mjpeg/mjpeg.go)。
底层 multipart 解析:pkg/mpjpeg的Next(pkg/mpjpeg/multipart.go)逐行读取边界,解析 MIME 头中的Content-Length,按长度读满一帧 JPEG 体,再封装为带 90kHz 时间戳的 RTP 包写入接收者(pkg/mpjpeg/producer.go)。该 Producer 声明只接收JPEG编码(ClockRate 90000、RAW 负载类型),即推入的必须是 MJPEG 格式数据。
值得一提的是,multipart 解析中对某些实现拙劣的摄像头 MJPEG 服务器做了兼容处理(源码注释提到 Foscam G2 的 MJPEG 实现问题,见 pkg/mpjpeg/multipart.go),遇到连续
--边界时会跳过空边界继续解析。
五、小结与使用建议
go2rtc 的 MJPEG 模块在 internal/mjpeg 提供 API 层、在 pkg/mjpeg、pkg/mpjpeg、pkg/ascii、pkg/y4m 提供编解码与容器实现,构成了完整的闭环:
- 拉取:RTSP/HTTP 源里带 MJPEG 编码即可直接消费,H264/H265 源可用
ffmpeg:#video=mjpeg转码; - 输出:
stream.mjpeg(浏览器/播放器)、frame.jpeg(快照)、stream.ascii(终端 ASCII 艺术)、stream.y4m(原始帧); - 摄取:
POST api/stream.mjpeg?dst=...接收外部 MJPEG 推流。
实际部署时建议:抓快照优先用cache参数降低反复转码开销(注意其不校验尺寸的语义);终端 ASCII 播放务必用低 FPS 并匹配终端尺寸;H264/H265 源抓 JPEG 快照依赖 FFmpeg 转码,需确保环境已正确配置 FFmpeg(见 internal/ffmpeg/README.md)。
【免费下载链接】go2rtcUltimate camera streaming application项目地址: https://gitcode.com/GitHub_Trending/go/go2rtc
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考