其它教程请见流媒体教程专栏
上一期我们介绍了音视频的一些基础理论,现在结合 m7s 先开发一个简单的性能采集插件,这个插件不涉及音视频相关理论,没有相关基础也能开发,前提技术熟练掌握 go 即可。我们的目标是开发一款 prometheus 采集插件,将 m7s 的运行中的数据输出到 prometheus 。
认识插件
m7s 使用 go 开发,利用了 go 的包导入机制来加载插件,即导入第三方包后,按照 变量初始化->init()->main() 的顺序执行。开发插件的时候,无需改动 m7s 的引擎代码,只需要在 monibuca 的启动文件 main.go 使用下划线导入 _ import 插件即可,非常方便。
另外 m7s 提供了常见的音视频相关的库:
- stream,track 提供音视频流/轨数据,方便插件二次利用
- codec h264,h265,mp4 等相关解封包
- ringbuffer 缓存环
- ps,ts 等格式包解析
- rtsp,rtmp,flv,webrtc,gb28181,hls 等传输协议的解析
- datatrack 数据轨,可以用来做任意数据传输,不限于音视频
- 等等
开发插件时,我们可以利用引擎提供的这些库,避免重复造轮子。m7s 还引入了状态机,提供了音视频流的四种状态: 等待发布->正在发布->延迟关闭->流已销毁。 对于状态机性能采集插件暂不涉及,下期开发关于音视频的插件再介绍,敬请期待。
源码分析
这里我们使用最新的 v4 版。
git clone --depth 1 https://github.com/langhuihui/monibuca
# 欢迎大家star
打开 main.go 可以发现代码非常简洁,除了导入依赖包外,就只有如下代码:
func main() {
fmt.Println("start monibuca version:", version)
conf := flag.String("c", "config.yaml", "config file")
flag.Parse()
ctx, cancel := context.WithCancel(context.WithValue(context.Background(), "version", version))
go util.WaitTerm(cancel)
engine.Run(ctx, *conf)
}
这就是 m7s 插件设计的妙处,让我们跳转到 engine.Run(ctx, *conf) 的实现,这里是引擎的初始化,以及插件配置赋值,下面这段代码即插件配置的赋值。
for name, plugin := range Plugins {
plugin.RawConfig = cg.GetChild(name)
if plugin.RawConfig != nil {
if b, err := yaml.Marshal(plugin.RawConfig); err == nil {
plugin.Yaml = string(b)
}
}
plugin.assign()
}
继续进入 plugin.assign() ,摘录部分代码如下:
if opt == Engine {
opt.registerHandler()
return
}
if opt.RawConfig == nil {
opt.RawConfig = config.Config{}
} else if opt.RawConfig["enable"] == false {
opt.Warn("disabled")
return
} else if opt.RawConfig["enable"] == true {
//移除这个属性防止反序列化报错
delete(opt.RawConfig, "enable")
}
t := reflect.TypeOf(opt.Config).Elem()
// 用全局配置覆盖没有设置的配置
for _, fname := range MergeConfigs {
if _, ok := t.FieldByName(fname); ok {
if v, ok := Engine.RawConfig[strings.ToLower(fname)]; ok {
if !opt.RawConfig.Has(fname) {
opt.RawConfig.Set(fname, v)
} else if opt.RawConfig.HasChild(fname) {
opt.RawConfig.GetChild(fname).Merge(Engine.RawConfig.GetChild(fname))
}
}
}
}
opt.registerHandler() //注册http接口,详见下文
opt.run() //运行插件,注册事件OnEvent
下面让我分析一下:
opt == Engine可见 engine 亦是插件的一部分,m7s 是一个完全插件结构化的流媒体框架。opt.RawConfig == nil做到了零配置启动opt.RawConfig["enable"]则实现了插件的按需启动for _, fname := range MergeConfigs则实现了按需配置,不需要完整的配置字段
插件 hello world
让我查看一下官网文档,插件只需要实现 OnEvent 接口即可。
既然是采集插件,那我们首先要知道当前节点位置,先实现位置查询的功能。 在 monibuca 目录下新建一个 plugin-exporter 目录,新建一个 main.go ,代码如下:
package exporter
import (
. "m7s.live/engine/v4"
"m7s.live/engine/v4/log"
)
type ExporterConfig struct {
NodeAddr string //节点位置
}
func (p *ExporterConfig) OnEvent(event any) {
switch event.(type) {
default:
log.Info("Exporter NodeAddr:" + p.NodeAddr)
}
}
var plugin = InstallPlugin(new(ExporterConfig))
修改 config.yaml 添加如下配置,注意需要小写
exporter:
nodeaddr: 北京
小写的原因是 config/config.go Unmarshal 里会把结构体的字段名转小写,如下图:
如果 yaml 文件里配置字段大写,那么在 nameMap 里就找不到对应的结构体字段了。
在 monibuca main.go 里引入我们的插件,_ "monibuca/plugin-exporter" 运行,则输出如下:
插件HTTP接口
按照官网文档所述,只需要实现 ServeHTTP(w http.ResponseWriter, r *http.Request) 则会自动注册插件的 http 接口。文档描述如下:
路由为
/myplugin/api/abc路径的HTTP API,即下划线_会转换成/
按照文档说明,那么我们的接口路由为 /exporter/api/info
在上述代码中添加如下接口,返回当前节点位置信息:
func (p *ExporterConfig) API_info(w http.ResponseWriter, r *http.Request) {
if err := json.NewEncoder(w).Encode(p); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
}
}
打开 http://localhost:8080/ 可以看到我们的插件 http 接口已经注册成功,和文档描述一致。
访问接口,成功返回当前节点的位置信息
那么插件的 HTTP 接口是如何自动注册的呢?还是藏在 plugin.assign() 中,让我们查看一下 opt.register():
func (opt *Plugin) registerHandler() {
t := reflect.TypeOf(opt.Config)
v := reflect.ValueOf(opt.Config)
// 注册http响应
for i, j := 0, t.NumMethod(); i < j; i++ {
name := t.Method(i).Name
if handler, ok := v.Method(i).Interface().(func(http.ResponseWriter, *http.Request)); ok {
patten := "/"
if name != "ServeHTTP" {
patten = strings.ToLower(strings.ReplaceAll(name, "_", "/"))
}
opt.handleFunc(patten, handler)
}
}
}
可见这里使用了反射,判断函数类型,最后在 opt.handleFunc 注册,有兴趣的同学可以继续查看源码,我这里就不分析了。另外欢迎大家加群,一起讨论,加群链接。