青云传·流影诀:读懂 Monibuca RTMP 的 net-stream.go

本篇以《诛仙》青云门人物为引,对照 Monibuca v5 plugin/rtmp/pkg/net-stream.go 的源码设计,把"RTMP 流级会话"讲成一门可运行的修行功法。

摘要

本文以《诛仙》青云门人物为线索,逐层拆解 Monibuca v5 中 plugin/rtmp/pkg/net-stream.go 的设计:NetStream 通过嵌入 *NetConnection 复用连接能力,用 StreamID 标识连接内的子会话;Begin / BeginPlay 负责开流授权,PublishWriter 与 RTMPSubscriber 分别承担推流写入与拉流零拷贝转发,closeStream 则完成资源回收而不关闭 TCP 连接。全文配有可运行示例与单元测试,帮助读者理解 RTMP 流级会话的职责边界。

一、世界观设定:RTMP 是一座青云剑阵

  • 道玄真人 → NetConnection:掌管"连接级命令"(connect / createStream),定乾坤、开山门。
  • 张小凡 → NetStream:流级会话,拿着 StreamID 这把"烧火棍",在连接里开辟一条专属灵脉。
  • 陆雪琪 → Subscriber:拉流者,天琊出鞘,只取池中倒影(RingBuffer 读取)。
  • 田不易 → Publisher:推流者,赤焰炼体,把音视频真气写入灵泉(RingBuffer 写入)。
  • 苏茹 → RingBuffer:温柔包容,万流归宗,不丢帧、不复写。
  • 曾书书 → Chunk:碎碎念,把大帧切成小块传。

net-stream.go 不负责握手(那是 handshake.go),不负责拆包细节(那是 chunk.go),它只做一件事:

把"一条流"的生命周期,安放在一条 TCP 连接里。

二、源码主角:NetStream 结构

go 复制代码
type NetStream struct {
    *NetConnection      // 继承连接:能发 chunk、能回 AMF 命令
    StreamID     uint32  // 这条流在连接里的唯一编号
}

逐层拆解设计意图

  1. 组合而非继承
  • NetStream 嵌入 *NetConnection,复用底层写 chunk、发消息的能力。
  • 设计意图:连接是"通道",流是"通道里的子频道",避免重复持有 bufio、chunkSize。
  1. StreamID 是灵脉编号
  • RTMP 里一个 TCP 连接可复用多个流(虽然常见场景只有 1 条)。
  • StreamID 决定 chunk header 里的 message stream id。

小凡拜入大竹峰:nc.createStream() 分配 ID → 小凡持棍入场。

三、开流仪式:Begin 与 BeginPlay

  1. 流开始信号(User Control Message)
go 复制代码
func (ns *NetStream) Begin() {
    ns.SendStreamID(RTMP_USER_STREAM_BEGIN, ns.StreamID)
}
  • 发一个 StreamBegin 控制消息。
  • 语义:"弟子已入门,可以传功了"。
  • 对应 RTMP 规范:Event type = 0x0000。
  1. 允许播放(拉流侧)
go 复制代码
func (ns *NetStream) BeginPlay() {
    ns.Begin()
    ns.SendMessage(RTMP_MSG_AMF0_COMMAND, newPlayResponseMessageData(
        ns.StreamID, NetStream_Play_Start, Level_Status,
    ))
}

逐行看设计:

  • Begin():先通知控制面。
  • Play_Start:AMF 状态消息,告诉客户端"可以收帧了"。
  • 之后 Subscriber 才会把 RingBuffer 里的音视频,经 Mux() 切成 RTMP chunk 发出去。

雪琪拔剑:不是立刻出招,先亮剑鸣------"我要看了"。

四、推流侧:田不易的赤焰炼体

当客户端发 publish 命令:

go 复制代码
// 伪代码逻辑(对应 index.go + net-stream.go 协作)
pub, _ := plugin.Publish(streamPath)
ns.Writers[ns.StreamID] = &PublishWriter{
    Publisher: pub,
    ns:        ns,
}

PublishWriter 实现:

  • 收到 RTMP 视频消息 → Parse 出 H264/AVCC
  • 收到音频消息 → 解 AAC 头
  • 调用 publisher.WriteVideo / WriteAudio

关键点(源码里容易忽略):

go 复制代码
// 视频帧要算 delta timestamp
t := packet.Timestamp - lastVideoTime
lastVideoTime = packet.Timestamp
packet.Timestamp = t

设计意图:

  • RTMP 传的是"相对时间戳",Monibuca 内部用绝对时间。
  • net-stream.go 是"协议时钟"和"内部时钟"的翻译官。

田不易每一式都记"距上一式过了多久",苏茹才能把招式排进灵泉。

五、拉流侧:雪琪的天琊映流

go 复制代码
func (ns *NetStream) Subscribe(sub *Subscriber) {
    sender := &RTMPSubscriber{...}
    ns.senders[ns.StreamID] = sender
    sub.OnEvent = func(frame AVFrame) {
        chunk := frame.Mux(videoCtx) // 转成 RTMP 能发的样子
        ns.SendMessage(SEND_VIDEO_MESSAGE, chunk)
    }
    sub.PlayBlock()
}

设计拆解:

  • Subscriber 从 RingBuffer 读帧(不拷内存,零拷贝引用)。
  • Mux() 把内部帧转 RTMP 格式(AVCC + tag)。
  • SendMessage 交给 NetConnection 切 chunk 发出去。

雪琪不炼功,只"映":池水(RingBuffer)里有啥,天琊就映啥。

六、关闭流:剑收人退

go 复制代码
case "closeStream":
    if s, ok := ns.senders[cmd.StreamId]; ok {
        s.Stop()
        delete(ns.senders, cmd.StreamId)
    }
  • 客户端断流 → 回收 Subscriber / Publisher。
  • 底层 Task 取消,RingBuffer 引用释放。
  • TCP 不断,还能再开新流。

小凡收棍,雪琪还剑,山门不关,只熄一盏灯。

七、可运行示例:本地起一个 RTMP 流,用代码"观想"

下面代码不依赖私有库,用标准 net 模拟"客户端发 publish",重点看 NetStream 职责边界。

  1. 最小可运行:模拟 NetStream 生命周期
go 复制代码
package main

import (
    "fmt"
    "sync"
)

// ---- 极简复刻 net-stream.go 核心 ----

type NetConnection struct {
    mu       sync.Mutex
    streamID uint32
}

func (nc *NetConnection) SendStreamID(event uint16, id uint32) {
    fmt.Printf("[NetConnection] UserControl event=%d streamID=%d\n", event, id)
}

func (nc *NetConnection) SendAMFStatus(code string) {
    fmt.Printf("[NetConnection] AMF status: %s\n", code)
}

type NetStream struct {
    *NetConnection
    StreamID uint32
}

func (ns *NetStream) Begin() {
    ns.SendStreamID(0x0000, ns.StreamID) // StreamBegin
}

func (ns *NetStream) BeginPlay() {
    ns.Begin()
    ns.SendAMFStatus("NetStream.Play.Start")
}

func (ns *NetStream) Publish() {
    ns.Begin()
    ns.SendAMFStatus("NetStream.Publish.Start")
}

func main() {
    nc := &NetConnection{streamID: 1}
    ns := &NetStream{NetConnection: nc, StreamID: 100}

    fmt.Println("== 拉流弟子陆雪琪入场 ==")
    ns.BeginPlay()

    fmt.Println("== 推流师长田不易入场 ==")
    ns.Publish()
}

运行输出:

text 复制代码
== 拉流弟子陆雪琪入场 ==
[NetConnection] UserControl event=0 streamID=100
[NetConnection] AMF status: NetStream.Play.Start
== 推流师长田不易入场 ==
[NetConnection] UserControl event=0 streamID=100
[NetConnection] AMF status: NetStream.Publish.Start

这就是 net-stream.go 的"骨架观想":不碰 socket,只管流级语义。

八、单元测试:验证 StreamID 与生命周期

go 复制代码
// netstream_test.go
package rtmp

import "testing"

func TestNetStream_BeginPlay(t *testing.T) {
    nc := &NetConnection{}
    ns := &NetStream{NetConnection: nc, StreamID: 42}

    // 用 channel 拦截发送,验证调用顺序
    var events []string
    nc.hook = func(s string) { events = append(events, s) }

    ns.BeginPlay()

    if events[0] != "StreamBegin:42" {
        t.Fatalf("want StreamBegin:42, got %v", events)
    }
    if events[1] != "Play.Start" {
        t.Fatalf("want Play.Start, got %v", events)
    }
}

实战建议:源码里 SendMessage 是 IO 操作,测试时用接口 mock,别真连 1935。

九、把源码知识点收进"青云口诀"

原码概念 青云比喻 关键设计
NetStream 张小凡的灵脉 连接内的子会话
StreamID 烧火棍编号 chunk 路由依据
Begin() 入门鸣钟 UserControl StreamBegin
BeginPlay() 雪琪拔剑 拉流授权
PublishWriter 田不易炼体 推流写入 RingBuffer
RTMPSubscriber 天琊映水 零拷贝读取+封装
closeStream 收功散场 回收资源

十、总结:这门功法到底在练什么

net-stream.go 不是"最复杂的文件",却是最懂边界的文件:

  • 不抢 handshake 的活(连接建立)
  • 不抢 chunk.go 的活(切包)
  • 只做三件事:
    1. 给流发"身份证"(StreamID)
    2. 给客户端发"许可"(Play / Publish Start)
    3. 把 Publisher / Subscriber 挂到连接上

青云之巅,小凡悟了:

真正的高手,不是什么都自己干,而是让每条灵脉各司其职。

相关推荐
All The Way North-2 天前
Transformers 5.19.0 源码阅读:AutoTokenizer.decode() 原生批处理与 BPE 空格清理陷阱
transformers·tokenizer·版本更新·decode·源码解读·autotokenizer·v 5.19
All The Way North-3 天前
Transformers 5.19.0 首发源码研读:Tokenizer.call() 核心机制与实战验证
huggingface·transformers·qwen·tokenizer·源码解读·autotokenizer·v 5.19
mont evergreen19 天前
RTMP 里的那根“连接线”:读一份 NetConnection 源码
插件·rtmp·源码解读
mont evergreen20 天前
RTMP 连接单元测试之旅
单元测试·音视频·源码解读
__zRainy__3 个月前
ClaudeCode 源码深度剖析:从零读懂 Agent 架构与 MVP 最小骨架实现
架构·agent·源码解读·claude code
落地加湿器5 个月前
从Hermes cli的源代码中学习skill
人工智能·python·学习·智能体·源码解读
行者-全栈开发6 个月前
开源项目深度解析:Vue.js 响应式原理与源码拆解
proxy·响应式原理·源码解读·技术实操·vue.js 源码·依赖收集·开源项目解析
IT 行者7 个月前
Claude Code 源码解读 06:权限系统与 Hooks——安全与自动化的基石
ai编程·源码解读·claude code
靴子学长7 个月前
GRPO 深度解析 (TRL 源码视角)
大模型·强化学习·算法设计·大模型推理·源码解读