流媒体服务新手入门教程04--性能采集插件开发1

266 阅读4分钟

其它教程请见流媒体教程专栏

上一期我们介绍了音视频的一些基础理论,现在结合 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()
}

image.png

继续进入 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 里会把结构体的字段名转小写,如下图: image.png 如果 yaml 文件里配置字段大写,那么在 nameMap 里就找不到对应的结构体字段了。

在 monibuca main.go 里引入我们的插件,_ "monibuca/plugin-exporter" 运行,则输出如下:

image.png

插件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 接口已经注册成功,和文档描述一致。

image.png

访问接口,成功返回当前节点的位置信息 image.png

那么插件的 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 注册,有兴趣的同学可以继续查看源码,我这里就不分析了。另外欢迎大家加群,一起讨论,加群链接