# Monibuca engine v4: the core of an m7s streaming server

> The m7s v4 engine is the Go core of the Monibuca streaming server: it owns the publisher and subscriber lifecycle, the media format caches, the HTTP API and the configuration system, while protocols such as RTMP, RTSP, WebRTC or GB28181 are supposed to arrive as plugins. Read it as a framework for building a streaming server, not as a server you can run on its own.

**Monibuca/engine** — Monibuca 核心引擎，包含流媒体核心转发逻辑，需要配合功能插件一起组合运行

- Repository: https://github.com/Monibuca/engine
- Stars: 979 · Forks: 207
- Language: Go
- License: MIT
- Published: 2026-10-20 · Updated: 2026-10-20 · Language: en
- Canonical page: https://hysenlabs.com/projects/monibuca-engine

## An engine that expects plugins to do the talking

The repository is one part of a larger product. The README describes it as the engine part of m7s, the part holding the core forwarding logic of a streaming server, and it is explicit that a plugin introduction mechanism is included while the remaining functionality is implemented by plugins. That single sentence sets the scope for anyone evaluating the project: this is a framework core, not an end-user server.

The engine's own feature list is long, and it is best read as a catalogue of the mechanisms that a plugin author can rely on rather than a list of protocols you can enable. It provides a plugin mechanism with unified management of plugin startup, configuration parsing and event dispatch. It forwards H264, H265, AAC and G711. It offers reusable pre-packaging for AVCC, RTP, AnnexB and ADTS formats, which are the container shapes different protocols expect. It provides a multi-track mechanism supporting large streams and encrypted stream extensions, a data track mechanism usable for things such as room text chat, timestamp synchronisation and rate limiting, an RTP packet reordering mechanism, a subscriber frame catch-up and skip mechanism, and the publish and subscribe infrastructure for pushing and pulling to outside systems.

The Go module tells you the shape of the codebase. The module path is m7s.live/engine/v4, it targets go 1.20, and the dependency list is the protocol toolchain: gortsplib and mediacommon for RTSP, pion/webrtc for WebRTC, pion/rtp, quic-go, gomedia for media containers, yaml.v3 for configuration, zap for logging and naza for shared utilities. The top level of the repository shows how the pieces are split, with publisher.go, subscriber.go, stream.go, puller.go, pusher.go, plugin.go, events.go, http.go, io.go and summary.go sitting next to track/, util/, config/, log/, codec/, common/ and lang/ directories.

A consequence worth stating early: nothing in this repository speaks RTMP or RTSP to a client by itself. The engine formats and moves media between publishers and subscribers, and the plugins do the talking on the wire.

## The stream state machine and the rules that move a stream between states

A stream in this engine is a small state machine, and the README draws it as a diagram. A stream is created in a waiting-for-publisher state. A publisher arriving moves it to publishing. A publisher disconnecting sends it back to waiting for a publisher, which is what allows the reconnect mechanism to work: a publisher that drops and comes back finds its stream still there. A close, a timeout, or the departure of the last subscriber moves it to closed.

The most interesting state is the third one, waiting to close. When the last subscriber leaves a publishing stream, the engine does not tear it down immediately. It parks the stream in waiting to close, and only closes it if a timeout expires or the publisher disconnects. If a new subscriber arrives first, the stream returns to publishing. The configuration key delayclosetimeout controls exactly how long that grace period lasts, and setting it to 0 disables the mechanism so the connection is simply kept open and the decision to close is left to the subscribers.

A separate setting, waittimeout, is described as the time to wait for a publisher and is used when subscribing to a stream that has not been published yet. A subscriber can therefore ask for a stream before it exists and wait for it to appear.

Two other states of the publisher lifecycle are configurable in the same way. publishtimeout is the default expiry of a published stream: if the publisher does not restore the stream within that window, the stream is deleted. waitclosetimeout is the wait after a publisher disconnects, and 0 there means the decision is left to the subscribers rather than to the engine. Setting these to zero is not always free, because a stream with no publisher and no timeout lingers, and the summary and statistics views keep counting it.

## Configuration layering and the priority order between URL, plugin and global

Configuration is YAML, and the engine ships a default document that documents every key it understands. The first block covers the global section, including the logging language and level, the HTTP listener, and the publish and subscribe groups. The HTTP defaults put the gateway on port 8080 and offer a separate TLS port at 8443, with empty certificate and key paths, CORS enabled, and empty username and password fields for basic authentication on the API:

```yaml
global:
  disableall: false # 是否禁用所有插件
  loglang: zh # 日志语言，可选值：zh,en
  loglevel: info # 日志级别，可选值：debug,info,warn,error,panic,fatal
  http:
    listenaddr: :8080 # 网关地址，用于访问API
    listenaddrtls: :8443  # 用于HTTPS方式访问API的端口配置
    certfile: ""
    keyfile: ""
    cors: true  # 是否自动添加cors头
    username: ""  # 用户名和密码，用于API访问时的基本身份认证
    password: ""
```

The publish group controls what a publisher may send and how long its stream survives without a connection. The subscribe group controls the same for subscribers, including how a subscriber behaves when the stream it wants does not exist yet:

```yaml
  publish:
      pubaudio: true # 是否发布音频流
      pubvideo: true # 是否发布视频流
      kickexist: false # 剔出已经存在的发布者，用于顶替原有发布者
      publishtimeout: 10s # 发布流默认过期时间，超过该时间发布者没有恢复流将被删除
  subscribe:
      submode: 0 # 订阅模式，0为跳帧追赶模式，1为不追赶（多用于录制），2为时光回溯模式
      syncmode: 0 # 音视频同步模式，0按照时间戳同步，1按照写入时间同步（在时间戳不正确的时候)
      iframeonly: false # 只订阅关键帧
      waittimeout: 10s # 等待发布者的超时时间，用于订阅尚未发布的流
```

The layering rules are explicit. With no configuration file at all, the hardcoded defaults in the code apply. With a file present, its values override those defaults. Within the http, publish and subscribe groups, the resolution order is: parameters carried in the publish or subscribe request win first, then the matching plugin's own configuration section, and only then the global configuration. That order is what lets a single engine serve several plugins with different publish rules while still allowing a per-request override, for example a subscriber asking for a specific track through the ats, vts and dts track arguments.

## Authentication in three layers: secret keys, plugin hooks and global hooks

The engine supports authentication at three levels, and the ordering between them is documented.

The default scheme is a shared key in the publish or subscribe configuration. A publisher or subscriber then adds two arguments to the URL, secret and expire. The secret is MD5 of the key, the stream path and the expiry concatenated, and the expiry is a hexadecimal UNIX timestamp, since the value travels in a URL. The README gives a worked example, computing the secret for a key and a stream path, and a timestamp conversion table for a specific moment, so the scheme is reproducible from the documentation alone. The key, the argument names secretargname and expireargname, and the expiry itself are all configuration entries, which means the argument names can be changed without touching code.

The second level is a per-plugin interface. A plugin's publisher or subscriber can implement one of two interfaces, and the engine calls it when a publish or subscribe happens:

```go
type AuthSub interface {
	OnAuth(*util.Promise[ISubscriber]) error
}

type AuthPub interface {
	OnAuth(*util.Promise[IPublisher]) error
}
```

Returning an error from OnAuth means the check failed. The Promise type is there to allow asynchronous checks, so a plugin can consult a remote service and resolve or reject the promise later instead of blocking the publish path.

The third level is global. Two package-level function variables can be assigned by plugins, and the engine calls them on publish and subscribe:

```go
var OnAuthSub func(p *util.Promise[ISubscriber]) error
var OnAuthPub func(p *util.Promise[IPublisher]) error
```

The README is candid about the two sharp edges here. When a per-plugin implementation and a global one both exist, the per-plugin one takes priority. And the global variables can be overwritten more than once, so if two plugins both assign them, the plugin authors have to merge the logic themselves. That is a real coordination problem for a multi-plugin deployment, and it is the kind of detail that only shows up once more than one plugin wants to authenticate.

## The built-in HTTP API on a shared port and the middleware hook

The engine serves its own control plane over HTTP, on the same port that plugins can share, and the endpoint list is part of the public contract. There are read endpoints for a single stream at /api/stream, for engine information at /api/sysInfo, for a system summary at /api/summary and for the plugin list at /api/plugins. There are management endpoints: closing a stream at /api/closestream, stopping a push at /api/stop/push, and stopping one subscriber at /api/stop/subscribe. There are configuration endpoints: reading a plugin configuration at /api/getconfig, modifying and saving one at /api/modifyconfig, and hot-reloading one at /api/updateconfig, where the name parameter selects a plugin and an empty name means the global configuration. There are replay endpoints that read an mp4 or a ts file from disk and publish it again as a live stream, at /api/replay/mp4 and /api/replay/ts. And there are listing endpoints for remote pull and push sessions at /api/list/pull and /api/list/push.

Two details of that surface are easy to miss. The configuration read endpoint returns the global configuration when the name parameter is absent or empty, so an unauthenticated caller can read global settings unless the basic authentication fields are filled in. The modify endpoint takes a YAML string in the request body and can persist it, which makes the API a write surface for configuration, not just a read surface for statistics.

For custom handling, the HTTPConfig interface exposes a middleware registration function:

```go
type Middleware func(string, http.Handler) http.Handler
type HTTPConfig interface {
	GetHTTPConfig() *HTTP
	Listen(ctx context.Context) error
	Handle(string, http.Handler)
	AddMiddleware(Middleware)
}
```

Middleware is a standard net/http wrapper, so it can add headers, wrap the response writer or reject requests. The ordering rule is explicit: middleware must be added before FirstConfig, which is before Listen. Add it later and it is silently too late, which is an easy mistake to make in a plugin whose init runs after the engine has already started listening.

## Media caches, buffers and the switches that trade memory for features

The engine keeps several caches and buffers, and each one is a configuration switch with a cost. The AVCC cache and the RTP cache are both enabled by default, because the AVCC shape is what RTMP expects and the RTP shape is what RTSP, WebSocket and GB28181 expect. The RTP reordering buffer, sized at 50 packets by default, is what smooths out packets that arrive out of order. The event bus has its own buffer, and the configuration comment is blunt about the failure mode: when there are many events the bus can block threads, so the buffer has to be increased. The memory pool, off by default, is the engine's memory reuse mechanism, and the README also mentions a separate cache time setting used for time-shifting playback.

Rate limiting appears twice. The publish group carries a speed limit with a default of 500ms, intended for streams read from a file, which would otherwise be consumed far faster than real time; the comment notes that if the timestamps in the source file are wrong, the only option is to turn the limit off. The subscription side has a write buffer size that reduces the number of write syscalls at the possible cost of real-time behaviour.

Subscriber behaviour is where the trade-offs are most visible. The submode value selects between catching up by skipping frames, not catching up at all, which the comment says is mostly used for recording, and a time-shifting mode. The sync mode chooses between synchronising audio and video by timestamp or by write time, the second being the fallback for sources with incorrect timestamps. An iframe-only option limits a subscriber to key frames.

All of this is a fine-grained control surface, and it is also a lot of surface area to get wrong. A configuration that seems reasonable in isolation, such as a long publisher expiry with a disabled close delay, produces streams that linger on the server and inflate the statistics the summary endpoint reports.

## Licence, branch layout and the gap between the v4 branch and the last tag

The licence is MIT, which permits use inside closed-source and commercial products provided the copyright and licence text travel with redistributed copies. This describes the published terms rather than legal advice.

The maintenance picture needs care, because the repository structure and the release list point in different directions. The default branch is v4, and the module path carries a /v4 suffix, so the v4 line is the current one. The last push was on 2026-07-13. The most recent releases, however, are v3.5.0 from 2022-02-02 and v2.1.3 from 2020-06-12. In other words, the v4 development line has no published release tag to pin against in this repository.

That has a concrete consequence for adoption. A plugin author who wants a stable dependency has nothing versioned to depend on for the v4 API, so the practical choices are to track the v4 branch and accept that it can move, or to vendor the engine into your own repository and own the merge. The trade is real: tracking the branch gives you upstream fixes, while vendoring freezes an API that upstream is still changing.

The dependency set raises a second question. The go.mod file pins go 1.20 and pulls in quic-go, pion/webrtc and gortsplib, so a project that only needs the core lifecycle and HTTP API still compiles against a sizeable set of media and transport libraries, and any of them can force a Go version bump on your build. The go.sum and the pinned versions in go.mod are the place to check that before you commit to the dependency.

## Conclusion

Adopt the m7s v4 engine if you are writing streaming plugins and want a tested core for the publisher and subscriber lifecycle, the HTTP control plane and the config system. Do not adopt it as a ready streaming server, because the repository deliberately contains no protocol implementations, and do not expect tagged releases: the newest published tag is v3.5.0 from 2022 while development happens on the v4 branch, whose last push was on 2026-07-13. Before building on it, verify that the plugin you need exists for v4, since the API surface of the v4 line has no release tag to pin against.

## FAQ

### What does the Monibuca engine do on its own?

It provides the core forwarding logic of an m7s streaming server: the plugin mechanism, the publisher and subscriber lifecycle, media format caches, the HTTP API, authentication hooks and the configuration system. The README states that the other functionality is implemented by plugins.

### How do I enable authentication for publishing and subscribing in the m7s engine?

Set a key in the publish and subscribe configuration, then pass secret and expire as URL arguments. The secret is MD5 of the key, the stream path and the expiry, and the expiry is a hexadecimal UNIX timestamp.

### Which HTTP API endpoints does the m7s engine expose?

Among others, /api/stream for one stream, /api/sysInfo for engine information, /api/summary for the system summary, /api/plugins for the plugin list, /api/modifyconfig to change a configuration and /api/updateconfig to hot reload one. The default gateway listens on port 8080 with an optional TLS port at 8443.

### How does configuration resolve when a plugin and a request both set a value?

The http, publish and subscribe groups follow a fixed priority: a parameter inside the publish or subscribe request wins first, then the matching plugin configuration, and only then the global configuration.

### What happens to a stream when the last subscriber leaves in the m7s engine?

The stream moves into a waiting-to-close state instead of closing immediately, and it returns to publishing if a new subscriber arrives first. The delayclosetimeout setting controls that grace period, and setting it to 0 keeps the connection open.

## Sources

- [Issues](https://github.com/Monibuca/engine/issues)
- [License: MIT](https://github.com/Monibuca/engine/blob/v4/LICENSE)
- [Monibuca/engine on GitHub](https://github.com/Monibuca/engine)
- [README](https://github.com/Monibuca/engine/blob/v4/README.md)
- [Releases](https://github.com/Monibuca/engine/releases)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/monibuca-engine
