- Refactor frame converter implementation - Update mp4 track to use ICodex - General refactoring and code improvements 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
17 KiB
插件开发指南
1. 准备工作
开发工具
- Visual Studio Code
- Goland
- Cursor
- CodeBuddy
- Trae
- Qoder
- Claude Code
- Kiro
- Windsurf
安装gRPC
$ go install google.golang.org/protobuf/cmd/protoc-gen-go@latest
$ go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest
安装gRPC-Gateway
$ go install github.com/grpc-ecosystem/grpc-gateway/v2/protoc-gen-grpc-gateway@latest
$ go install google.golang.org/protobuf/cmd/protoc-gen-go@latest
$ go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest
创建工程
- 创建一个go 工程,例如:
MyPlugin - 创建目录
pkg,用来存放可导出的代码 - 创建目录
pb,用来存放gRPC的proto文件 - 创建目录
example, 用来测试插件
也可以直接在 monibuca 项目的 plugin 中创建一个目录
xxx, 用来存放插件代码
2. 创建插件
package plugin_myplugin
import (
"m7s.live/v5"
)
var _ = m7s.InstallPlugin[MyPlugin]()
type MyPlugin struct {
m7s.Plugin
Foo string
}
- MyPlugin 结构体就是插件定义,Foo 是插件的一个属性,可以在配置文件中配置。
- 必须嵌入
m7s.Plugin结构体,这样插件就具备了插件的基本功能。 m7s.InstallPlugin[MyPlugin](...)用来注册插件,这样插件就可以被 monibuca 加载。
传入默认配置
例如:
const defaultConfig = m7s.DefaultYaml(`tcp:
listenaddr: :5554`)
var _ = m7s.InstallPlugin[MyPlugin](m7s.PluginMeta{
DefaultYaml: defaultConfig,
})
3. 实现事件回调(可选)
初始化回调
func (config *MyPlugin) Start() (err error) {
// 初始化一些东西
return
}
用于插件的初始化,此时插件的配置已经加载完成,可以在这里做一些初始化工作。返回错误则插件初始化失败,插件将进入禁用状态。
接受 TCP 请求回调
func (config *MyPlugin) OnTCPConnect(conn *net.TCPConn) task.ITask {
}
当配置了 tcp 监听端口后,收到 tcp 连接请求时,会调用此回调。
接受 UDP 请求回调
func (config *MyPlugin) OnUDPConnect(conn *net.UDPConn) task.ITask {
}
当配置了 udp 监听端口后,收到 udp 连接请求时,会调用此回调。
接受 QUIC 请求回调
func (config *MyPlugin) OnQUICConnect(quic.Connection) task.ITask {
}
当配置了 quic 监听端口后,收到 quic 连接请求时,会调用此回调。
4. HTTP 接口回调
延续 v4 的回调
func (config *MyPlugin) API_test1(rw http.ResponseWriter, r *http.Request) {
// do something
}
可以通过http://ip:port/myplugin/api/test1来访问API_test1方法。
通过配置映射表
这种方式可以实现带参数的路由,例如:
func (config *MyPlugin) RegisterHandler() map[string]http.HandlerFunc {
return map[string]http.HandlerFunc{
"/test1/{streamPath...}": config.test1,
}
}
func (config *MyPlugin) test1(rw http.ResponseWriter, r *http.Request) {
streamPath := r.PathValue("streamPath")
// do something
}
5. 实现推拉流客户端
实现推流客户端
推流客户端需要实现 IPusher 接口,然后将创建 IPusher 的方法传入 InstallPlugin 中。
type Pusher struct {
task.Task
pushJob m7s.PushJob
}
func (c *Pusher) GetPushJob() *m7s.PushJob {
return &c.pushJob
}
func NewPusher(_ config.Push) m7s.IPusher {
return &Pusher{}
}
var _ = m7s.InstallPlugin[MyPlugin](m7s.PluginMeta{
NewPusher: NewPusher,
})
实现拉流客户端
拉流客户端需要实现 IPuller 接口,然后将创建 IPuller 的方法传入 InstallPlugin 中。 下面这个 Puller 继承了 m7s.HTTPFilePuller,可以实现基本的文件和 HTTP拉流。具体拉流逻辑需要覆盖 Start 方法。
type Puller struct {
m7s.HTTPFilePuller
}
func NewPuller(_ config.Pull) m7s.IPuller {
return &Puller{}
}
var _ = m7s.InstallPlugin[MyPlugin](m7s.PluginMeta{
NewPuller: NewPuller,
})
6. 实现gRPC服务
实现 gRPC 可以自动生成对应的 restFul 接口,方便调用。
在pb目录下创建myplugin.proto文件
syntax = "proto3";
import "google/api/annotations.proto";
import "google/protobuf/empty.proto";
package myplugin;
option go_package="m7s.live/v5/plugin/myplugin/pb";
service api {
rpc MyMethod (MyRequest) returns (MyResponse) {
option (google.api.http) = {
post: "/myplugin/api/bar"
body: "foo"
};
}
}
message MyRequest {
string foo = 1;
}
message MyResponse {
string bar = 1;
}
以上的定义只中包含了实现对应 restFul 的路由,可以通过 post 请求/myplugin/api/bar来调用MyMethod方法。
生成gRPC代码
- 可以使用 vscode 的 task.json中加入
{
"type": "shell",
"label": "build pb myplugin",
"command": "protoc",
"args": [
"-I.",
"-I${workspaceRoot}/pb",
"--go_out=.",
"--go_opt=paths=source_relative",
"--go-grpc_out=.",
"--go-grpc_opt=paths=source_relative",
"--grpc-gateway_out=.",
"--grpc-gateway_opt=paths=source_relative",
"myplugin.proto"
],
"options": {
"cwd": "${workspaceRoot}/plugin/myplugin/pb"
}
},
- 或者在 pb 目录下运行命令行:
protoc -I. -I$ProjectFileDir$/pb --go_out=. --go_opt=paths=source_relative --go-grpc_out=. --go-grpc_opt=paths=source_relative --grpc-gateway_out=. --grpc-gateway_opt=paths=source_relative myplugin.proto
把其中的 $ProjectFileDir$ 替换成包含全局 pb 的目录,全局 pb 文件就在 monibuca 项目的 pb 目录下。
实现gRPC服务
创建 api.go 文件
package plugin_myplugin
import (
"context"
"m7s.live/m7s/v5"
"m7s.live/m7s/v5/plugin/myplugin/pb"
)
func (config *MyPlugin) MyMethod(ctx context.Context, req *pb.MyRequest) (*pb.MyResponse, error) {
return &pb.MyResponse{Bar: req.Foo}, nil
}
注册gRPC服务
package plugin_myplugin
import (
"m7s.live/v5"
"m7s.live/v5/plugin/myplugin/pb"
)
var _ = m7s.InstallPlugin[MyPlugin](m7s.PluginMeta{
ServiceDesc: &pb.Api_ServiceDesc,
RegisterGRPCHandler: pb.RegisterApiHandler,
})
type MyPlugin struct {
pb.UnimplementedApiServer
m7s.Plugin
Foo string
}
额外的 restFul 接口
和 v4 相同
func (config *MyPlugin) API_test1(rw http.ResponseWriter, r *http.Request) {
// do something
}
就可以通过 get 请求/myplugin/api/test1来调用API_test1方法。
7. 发布流
publisher, err := p.Publish(ctx, streamPath)
ctx 参数是必需的,streamPath 参数是必需的。
写入音视频数据
旧的 WriteAudio 和 WriteVideo 方法已被更结构化的写入器模式取代,使用泛型实现:
创建写入器
// 音频写入器
audioWriter := m7s.NewPublishAudioWriter[*AudioFrame](publisher, allocator)
// 视频写入器
videoWriter := m7s.NewPublishVideoWriter[*VideoFrame](publisher, allocator)
// 组合音视频写入器
writer := m7s.NewPublisherWriter[*AudioFrame, *VideoFrame](publisher, allocator)
写入帧
// 设置时间戳并写入音频帧
writer.AudioFrame.SetTS32(timestamp)
err := writer.NextAudio()
// 设置时间戳并写入视频帧
writer.VideoFrame.SetTS32(timestamp)
err := writer.NextVideo()
写入自定义数据
// 对于自定义数据帧
err := publisher.WriteData(data IDataFrame)
定义音视频数据
如果现有的音视频数据格式无法满足需求,可以自定义音视频数据格式。 但需要满足转换格式的要求。即需要实现下面这个接口:
IAVFrame interface {
GetSample() *Sample
GetSize() int
CheckCodecChange() error
Demux() error // demux to raw format
Mux(*Sample) error // mux from origin format
Recycle()
String() string
}
音频和视频需要定义两个不同的类型
其中各方法的作用如下:
- GetSample 方法用于获取音视频数据的Sample对象,包含编解码上下文和原始数据。
- GetSize 方法用于获取音视频数据的大小。
- CheckCodecChange 方法用于检查编解码器是否发生变化。
- Demux 方法用于解封装音视频数据到裸格式,用于给其他格式封装使用。
- Mux 方法用于从原始格式封装成自定义格式的音视频数据。
- Recycle 方法用于回收资源,会在嵌入 RecyclableMemory 时自动实现。
- String 方法用于打印音视频数据的信息。
内存管理
新的模式包含内置的内存管理:
util.ScalableMemoryAllocator- 用于高效的内存分配- 通过
Recycle()方法进行帧回收 - 自动内存池管理
8. 订阅流
var suber *m7s.Subscriber
suber, err = p.Subscribe(ctx,streamPath)
go m7s.PlayBlock(suber, handleAudio, handleVideo)
这里需要注意的是 handleAudio, handleVideo 是处理音视频数据的回调函数,需要自己实现。 handleAudio/Video 的入参是一个你需要接受到的音视频格式类型,返回 error,如果返回的 error 不是 nil,则订阅中止。
9. 使用 H26xFrame 处理裸流数据
9.1 理解 H26xFrame 结构
H26xFrame 结构体用于处理 H.264/H.265 裸流数据:
type H26xFrame struct {
pkg.Sample
}
主要特性:
- 继承自
pkg.Sample- 包含编解码上下文、内存管理和时间戳信息 - 使用
Raw.(*pkg.Nalus)存储 NALU(网络抽象层单元)数据 - 支持 H.264 (AVC) 和 H.265 (HEVC) 格式
- 使用高效的内存分配器实现零拷贝操作
9.2 创建 H26xFrame 进行发布
import (
"m7s.live/v5"
"m7s.live/v5/pkg/format"
"m7s.live/v5/pkg/util"
"time"
)
// 创建支持 H26xFrame 的发布器 - 多帧发布
func publishRawH264Stream(streamPath string, h264Frames [][]byte) error {
// 获取发布器
publisher, err := p.Publish(streamPath)
if err != nil {
return err
}
// 创建内存分配器
allocator := util.NewScalableMemoryAllocator(1 << util.MinPowerOf2)
defer allocator.Recycle()
// 创建 H26xFrame 写入器
writer := m7s.NewPublisherWriter[*format.RawAudio, *format.H26xFrame](publisher, allocator)
// 设置 H264 编码器上下文
writer.VideoFrame.ICodecCtx = &format.H264{}
// 发布多帧
// 注意:这只是演示一次写入多帧,实际情况是逐步写入的,即从视频源接收到一帧就写入一帧
startTime := time.Now()
for i, frameData := range h264Frames {
// 为每帧创建 H26xFrame
frame := writer.VideoFrame
// 设置正确间隔的时间戳
frame.Timestamp = startTime.Add(time.Duration(i) * time.Second / 30) // 30 FPS
// 写入 NALU 数据
nalus := frame.GetNalus()
// 假如 frameData 中只有一个 NALU,否则需要循环执行下面的代码
p := nalus.GetNextPointer()
mem := frame.NextN(len(frameData))
copy(mem, frameData)
p.PushOne(mem)
// 发布帧
if err := writer.NextVideo(); err != nil {
return err
}
}
return nil
}
// 连续流发布示例
func continuousH264Publishing(streamPath string, frameSource <-chan []byte, stopChan <-chan struct{}) error {
// 获取发布器
publisher, err := p.Publish(streamPath)
if err != nil {
return err
}
defer publisher.Dispose()
// 创建内存分配器
allocator := util.NewScalableMemoryAllocator(1 << util.MinPowerOf2)
defer allocator.Recycle()
// 创建 H26xFrame 写入器
writer := m7s.NewPublisherWriter[*format.RawAudio, *format.H26xFrame](publisher, allocator)
// 设置 H264 编码器上下文
writer.VideoFrame.ICodecCtx = &format.H264{}
startTime := time.Now()
frameCount := 0
for {
select {
case frameData := <-frameSource:
// 为每帧创建 H26xFrame
frame := writer.VideoFrame
// 设置正确间隔的时间戳
frame.Timestamp = startTime.Add(time.Duration(frameCount) * time.Second / 30) // 30 FPS
// 写入 NALU 数据
nalus := frame.GetNalus()
mem := frame.NextN(len(frameData))
copy(mem, frameData)
// 发布帧
if err := writer.NextVideo(); err != nil {
return err
}
frameCount++
case <-stopChan:
// 停止发布
return nil
}
}
}
9.3 处理 H26xFrame(转换器模式)
type MyTransform struct {
m7s.DefaultTransformer
Writer *m7s.PublishWriter[*format.RawAudio, *format.H26xFrame]
}
func (t *MyTransform) Go() {
defer t.Dispose()
for video := range t.Video {
if err := t.processH26xFrame(video); err != nil {
t.Error("process frame failed", "error", err)
break
}
}
}
func (t *MyTransform) processH26xFrame(video *format.H26xFrame) error {
// 复制帧元数据
copyVideo := t.Writer.VideoFrame
copyVideo.ICodecCtx = video.ICodecCtx
*copyVideo.BaseSample = *video.BaseSample
nalus := copyVideo.GetNalus()
// 处理每个 NALU 单元
for nalu := range video.Raw.(*pkg.Nalus).RangePoint {
p := nalus.GetNextPointer()
mem := copyVideo.NextN(nalu.Size)
nalu.CopyTo(mem)
// 示例:过滤或修改特定 NALU 类型
if video.FourCC() == codec.FourCC_H264 {
switch codec.ParseH264NALUType(mem[0]) {
case codec.NALU_IDR_Picture, codec.NALU_Non_IDR_Picture:
// 处理视频帧 NALU
// 示例:应用转换、滤镜等
case codec.NALU_SPS, codec.NALU_PPS:
// 处理参数集 NALU
}
} else if video.FourCC() == codec.FourCC_H265 {
switch codec.ParseH265NALUType(mem[0]) {
case h265parser.NAL_UNIT_CODED_SLICE_IDR_W_RADL:
// 处理 H.265 IDR 帧
}
}
// 推送处理后的 NALU
p.PushOne(mem)
}
return t.Writer.NextVideo()
}
9.4 H.264/H.265 常见 NALU 类型
H.264 NALU 类型
const (
NALU_Non_IDR_Picture = 1 // 非 IDR 图像(P 帧)
NALU_IDR_Picture = 5 // IDR 图像(I 帧)
NALU_SEI = 6 // 补充增强信息
NALU_SPS = 7 // 序列参数集
NALU_PPS = 8 // 图像参数集
)
// 从第一个字节解析 NALU 类型
naluType := codec.ParseH264NALUType(mem[0])
H.265 NALU 类型
// 从第一个字节解析 H.265 NALU 类型
naluType := codec.ParseH265NALUType(mem[0])
9.5 内存管理最佳实践
// 使用内存分配器进行高效操作
allocator := util.NewScalableMemoryAllocator(1 << 20) // 1MB 初始大小
defer allocator.Recycle()
// 处理多帧时重用同一个分配器
writer := m7s.NewPublisherWriter[*format.RawAudio, *format.H26xFrame](publisher, allocator)
9.6 错误处理和验证
func processFrame(video *format.H26xFrame) error {
// 检查编解码器变化
if err := video.CheckCodecChange(); err != nil {
return err
}
// 验证帧数据
if video.Raw == nil {
return fmt.Errorf("empty frame data")
}
// 安全处理 NALU
nalus, ok := video.Raw.(*pkg.Nalus)
if !ok {
return fmt.Errorf("invalid NALUs format")
}
// 处理帧...
return nil
}
10. 接入 Prometheus
只需要实现 Collector 接口,系统会自动收集所有插件的指标信息。
func (p *MyPlugin) Describe(ch chan<- *prometheus.Desc) {
}
func (p *MyPlugin) Collect(ch chan<- prometheus.Metric) {
}
## 插件合并说明
### Monitor 插件合并到 Debug 插件
从 v5 版本开始,Monitor 插件的功能已经合并到 Debug 插件中。这种合并简化了插件结构,并提供了更统一的调试和监控体验。
#### 功能变更
- Monitor 插件的所有功能现在可以通过 Debug 插件访问
- 任务监控 API 路径从 `/monitor/api/*` 变更为 `/debug/api/monitor/*`
- 数据模型和数据库结构保持不变
- Session 和 Task 的监控逻辑完全迁移到 Debug 插件
#### 使用方法
以前通过 Monitor 插件访问的 API 现在应该通过 Debug 插件访问:
旧路径
GET /monitor/api/session/list GET /monitor/api/search/task/{sessionId}
新路径
GET /debug/api/monitor/session/list GET /debug/api/monitor/task/{sessionId}
#### 配置变更
不再需要单独配置 Monitor 插件,只需配置 Debug 插件即可。Debug 插件会自动初始化监控功能。
```yaml
debug:
enable: true
# 其他 debug 配置项