# 插件开发指南

## 1. 准备工作

### 开发工具
- Visual Studio Code
- Goland
- Cursor
### 安装gRPC
```shell
$ go install google.golang.org/protobuf/cmd/protoc-gen-go@v1.28
$ go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@v1.2
```

### 安装gRPC-Gateway
```shell
$ 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. 创建插件

```go
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 加载。
### 传入默认配置
例如：
```go
const defaultConfig = m7s.DefaultYaml(`tcp:
  listenaddr: :5554`)

var _ = m7s.InstallPlugin[MyPlugin](defaultConfig)
```
## 3. 实现事件回调（可选）
### 初始化回调
```go
func (config *MyPlugin) OnInit() (err error) {
    // 初始化一些东西
    return
}
```
用于插件的初始化，此时插件的配置已经加载完成，可以在这里做一些初始化工作。返回错误则插件初始化失败，插件将进入禁用状态。

### 接受 TCP 请求回调

```go
func (config *MyPlugin) OnTCPConnect(conn *net.TCPConn) task.ITask {
	
}
```
当配置了 tcp 监听端口后，收到 tcp 连接请求时，会调用此回调。

### 接受 UDP 请求回调
```go
func (config *MyPlugin) OnUDPConnect(conn *net.UDPConn) task.ITask {

}
```
当配置了 udp 监听端口后，收到 udp 连接请求时，会调用此回调。

### 接受 QUIC 请求回调
```go
func (config *MyPlugin) OnQUICConnect(quic.Connection) task.ITask {

}
```
当配置了 quic 监听端口后，收到 quic 连接请求时，会调用此回调。

## 4. HTTP 接口回调
### 延续 v4 的回调
```go
func (config *MyPlugin) API_test1(rw http.ResponseWriter, r *http.Request) {
	        // do something
}
```
可以通过`http://ip:port/myplugin/api/test1`来访问`API_test1`方法。

### 通过配置映射表
这种方式可以实现带参数的路由，例如：
```go
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 中。
```go
type Pusher struct {
  pullCtx m7s.PullJob
}

func (c *Pusher) GetPullJob() *m7s.PullJob {
	return &c.pullCtx
}

func NewPusher(_ config.Push) m7s.IPusher {
	return &Pusher{}
}
var _ = m7s.InstallPlugin[MyPlugin](NewPusher)

```

### 实现拉流客户端
拉流客户端就是想要实现一个 IPuller，然后将创建 IPuller 的方法传入 InstallPlugin 中。
下面这个 Puller 继承了 m7s.HTTPFilePuller，可以实现基本的文件和 HTTP拉流。具体拉流逻辑需要覆盖 Run 方法。
```go
type Puller struct {
	m7s.HTTPFilePuller
}

func NewPuller(_ config.Pull) m7s.IPuller {
	return &Puller{}
}
var _ = m7s.InstallPlugin[MyPlugin](NewPuller)
```

## 6. 实现gRPC服务
实现 gRPC 可以自动生成对应的 restFul 接口，方便调用。
### 在`pb`目录下创建`myplugin.proto`文件
```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中加入
```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 目录下运行命令行:
```shell
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 文件
```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服务
```go
package plugin_myplugin
import (
    "m7s.live/v5"
	"m7s.live/v5/plugin/myplugin/pb"
)

var _ = m7s.InstallPlugin[MyPlugin](&pb.Api_ServiceDesc, pb.RegisterApiHandler)

type MyPlugin struct {
	pb.UnimplementedApiServer
	m7s.Plugin
	Foo string
}

```
### 额外的 restFul 接口
和 v4 相同
```go
func (config *MyPlugin)  API_test1(rw http.ResponseWriter, r *http.Request) {
	        // do something
}
```
就可以通过 get 请求`/myplugin/api/test1`来调用`API_test1`方法。

## 5. 发布流

```go

publisher, err = p.Publish(streamPath, connectInfo)
```
后面两个入参是可选的

得到 `publisher` 后，就可以通过调用 `publisher.WriteAudio`、`publisher.WriteVideo` 来发布音视频数据。
### 定义音视频数据
如果先有的音视频数据格式无法满足需求，可以自定义音视频数据格式。
但需要满足转换格式的要求。即需要实现下面这个接口：
```go
IAVFrame interface {
    GetAllocator() *util.ScalableMemoryAllocator
    SetAllocator(*util.ScalableMemoryAllocator)
    Parse(*AVTrack) error                                          // get codec info, idr
    ConvertCtx(codec.ICodecCtx) (codec.ICodecCtx, IAVFrame, error) // convert codec from source stream
    Demux(codec.ICodecCtx) (any, error)                            // demux to raw format
    Mux(codec.ICodecCtx, *AVFrame)                                 // mux from raw format
    GetTimestamp() time.Duration
    GetCTS() time.Duration
    GetSize() int
    Recycle()
    String() string
    Dump(byte, io.Writer)
}
```
> 音频和视频需要定义两个不同的类型

其中 `Parse` 方法用于解析音视频数据，`ConvertCtx` 方法用于转换音视频数据格式的上下文，`Demux` 方法用于解封装音视频数据，`Mux` 方法用于封装音视频数据，`Recycle` 方法用于回收资源。
- GetAllocator 方法用于获取内存分配器。(嵌入 RecyclableMemory 会自动实现)
- SetAllocator 方法用于设置内存分配器。(嵌入 RecyclableMemory 会自动实现)
- Parse方法主要从数据中识别关键帧，序列帧等重要信息。
- ConvertCtx 会在需要转换协议的时候调用，传入原始的协议上下文，返回新的协议上下文（即自定义格式的上下文）。
- Demux 会在需要解封装音视频数据的时候调用，传入协议上下文，返回解封装后的音视频数据，用于给其他格式封装使用。
- Mux 会在需要封装音视频数据的时候调用，传入协议上下文和解封装后的音视频数据，用于封装成自定义格式的音视频数据。
- Recycle 方法会在嵌入 RecyclableMemory 时自动实现，无需手动实现。
- String 方法用于打印音视频数据的信息。
- GetSize 方法用于获取音视频数据的大小。
- GetTimestamp 方法用于获取音视频数据的时间戳(单位：纳秒)。
- GetCTS 方法用于获取音视频数据的Composition Time Stamp(单位：纳秒)。PTS = DTS+CTS
- Dump 方法用于打印音视频数据的二进制数据。

### 6. 订阅流
```go
var suber *m7s.Subscriber
suber, err = p.Subscribe(ctx,streamPath)
go m7s.PlayBlock(suber, handleAudio, handleVideo)
```
这里需要注意的是 handleAudio, handleVideo 是处理音视频数据的回调函数，需要自己实现。
handleAudio/Video 的入参是一个你需要接受到的音视频格式类型,返回 error，如果返回的 error 不是 nil，则订阅中止。

## 7. 接入 Prometheus
只需要实现 Collector 接口，系统会自动收集所有插件的指标信息。
```go
func (p *MyPlugin) Describe(ch chan<- *prometheus.Desc) {
  
}

func (p *MyPlugin) Collect(ch chan<- prometheus.Metric) {
  
}

```