refactor onvif

This commit is contained in:
xugo
2025-12-03 19:55:40 +08:00
parent 63d91e08df
commit 9ebf461956
23 changed files with 1262 additions and 163 deletions
+7 -3
View File
@@ -141,7 +141,7 @@ build/local:
$(eval dir := $(BUILD_DIR_ROOT)/$(GOOS)_$(GOARCH))
@echo 'Building $(VERSION) $(dir)...'
@rm -rf $(dir)
@GOOS=$(GOOS) GOARCH=$(GOARCH) go build \
@CGO_ENABLED=0 GOOS=$(GOOS) GOARCH=$(GOARCH) go build \
-trimpath \
-ldflags="-s -w \
-X main.buildVersion=$(VERSION) \
@@ -175,7 +175,7 @@ build/windows:
@make build/local GOOS=$(GOOS) GOARCH=$(GOARCH)
docker/build:
@docker build --force-rm=true --push --platform linux/amd64 -t registry.cn-shanghai.aliyuncs.com/ixugo/gowvp:latest -f Dockerfile .
@docker build --force-rm=true --push --platform linux/amd64,linux/arm64 -t registry.cn-shanghai.aliyuncs.com/ixugo/gowvp:latest -f Dockerfile .
docker/save:
@docker save -o $(MODULE_NAME).tar $(IMAGE_NAME)
@@ -190,8 +190,12 @@ docker/build/full: build/clean build/linux
#@docker build --force-rm=true --push --platform linux/amd64,linux/arm64 -t $(IMAGE_NAME) -f Dockerfile_full .
@docker build --force-rm=true --push --platform linux/amd64,linux/arm64 -t registry.cn-shanghai.aliyuncs.com/ixugo/homenvr:latest -f Dockerfile_full .
docker/publish: build/clean build/linux docker/build
@docker build --foroe-rm=true --push --platform linux/amd64,linux/arm64 -t registry.cn-shanghai.aliyuncs.com/ixugo/homenvr:latest -t $(IMAGE_NAME) -f Dockerfile_full .
docker/build/gowvp: build/clean build/linux
@docker build --force-rm=true --push --platform linux/amd64,linux/arm64 -t registry.cn-shanghai.aliyuncs.com/ixugo/gowvp:latest -f Dockerfile .
@docker build --force-rm=true --push --platform linux/amd64 -t registry.cn-shanghai.aliyuncs.com/ixugo/gowvp:latest -f Dockerfile .
# ==================================================================================== #
+9 -2
View File
@@ -1,3 +1,4 @@
**中文** | [English](./README_EN.md)
<p align="center">
<img src="./docs/logo.png" alt="GoWVP Logo" width="550"/>
@@ -5,7 +6,6 @@
<p align="center">
<a href="https://github.com/gowvp/gb28181/releases"><img src="https://img.shields.io/github/v/release/ixugo/goddd?include_prereleases" alt="Version"/></a>
<!-- <a href="https://github.com/ixugo/goddd/blob/master/LICENSE.txt"><img src="https://img.shields.io/dub/l/vibe-d.svg" alt="License"/></a> -->
</p>
# 开箱即用的视频监控平台
@@ -37,7 +37,14 @@ go wvp 是 Go 语言实现的开源 GB28181 解决方案,基于 GB28181-2022
感谢 @panjjo 大佬的开源库 [panjjo/gosip](https://github.com/panjjo/gosip),GoWVP 的 sip 信令基于此库,出于底层封装需要,并非直接依赖该项目,而是源代码放到了 pkg 包中。
流媒体服务基于@夏楚 [ZLMediaKit](https://github.com/ZLMediaKit/ZLMediaKit)
流媒体服务支持两种
+ @夏楚 [ZLMediaKit](https://github.com/ZLMediaKit/ZLMediaKit)
+ **lalmax-pro 有 golang 流媒体的需求请联系微信 [golangxx](https://github.com/ixugo)**
- 对环境没有要求,不需要安装任何静态库,支持跨平台编译
- 支持特色功能定制
- 支持 G711(G711A/G711U) 转 AAC
播放器使用@dexter [jessibuca](https://github.com/langhuihui/jessibuca/tree/v3)
+281
View File
@@ -0,0 +1,281 @@
[中文](./README.md) | **English**
<p align="center">
<img src="./docs/logo.png" alt="GoWVP Logo" width="550"/>
</p>
<p align="center">
<a href="https://github.com/gowvp/gb28181/releases"><img src="https://img.shields.io/github/v/release/ixugo/goddd?include_prereleases" alt="Version"/></a>
</p>
# Out-of-the-Box Video Surveillance Platform
GoWVP is an open-source GB28181 solution implemented in Go, a network video platform based on the GB28181-2022 standard that also supports 2016/2011 versions, with ONVIF/RTMP/RTSP protocol support.
## Live Demo
+ [Online Demo Platform :)](http://gowvp.golang.space:15123/)
![](./docs/demo/play.gif)
|![](./docs/phone/login.webp)|![](./docs/phone/desktop.webp)|![](./docs/phone/gb28181.webp)|![](./docs/phone/discover.webp)|
|-|-|-|-|
## Use Cases
+ Browser-based camera video playback without plugins
+ Support for GB28181-compliant devices (IP cameras, platforms, NVRs, etc.)
+ Support for non-GB28181 devices (RTSP, RTMP, streaming devices, etc.) - maximize your existing equipment
+ Cross-network video preview
+ Deployment via Docker, Docker Compose, or Kubernetes
## Open Source Libraries
Thanks to @panjjo for the open-source library [panjjo/gosip](https://github.com/panjjo/gosip). GoWVP's SIP signaling is based on this library. Due to underlying encapsulation requirements, it's not a direct dependency but rather included in the pkg package.
Two streaming media servers are supported:
+ @夏楚 [ZLMediaKit](https://github.com/ZLMediaKit/ZLMediaKit)
+ **lalmax-pro** - For Go streaming media needs, contact WeChat [golangxx](https://github.com/ixugo)
- No environment requirements, no static library installation needed, cross-platform compilation support
- Custom feature development available
- G711 (G711A/G711U) to AAC transcoding support
Player uses @dexter's [jessibuca](https://github.com/langhuihui/jessibuca/tree/v3)
Project framework based on @ixugo's [goddd](https://github.com/ixugo/goddd)
## FAQ
> Where are the frontend resources? How to load the web interface?
[Click to download www.zip package](https://github.com/gowvp/gb28181_web/releases/latest)
Download (packaged) frontend resources and place them in the project root directory, named `www` to load properly.
> Any learning materials about the code?
[GB/T28181 Open Source Diary[1]: Complete Practice from 0 to Implementing GB28181 Protocol](https://juejin.cn/post/7456722441395568651)
[GB/T28181 Open Source Diary[2]: Setting Up Server, Solving CORS, API Integration](https://juejin.cn/post/7456796962120417314)
[GB/T28181 Open Source Diary[3]: Building Monitoring Dashboard with React Components](https://juejin.cn/post/7457228085826764834)
[GB/T28181 Open Source Diary[4]: Using ESLint for Development](https://juejin.cn/post/7461539078111789108)
[GB/T28181 Open Source Diary[5]: Completing Forms with react-hook-form](https://juejin.cn/post/7461899974198181922)
[GB/T28181 Open Source Diary[6]: Quick Integration of jessibuca.js Player in React](https://juejin.cn/post/7462229773982351410)
[GB/T28181 Open Source Diary[7]: Implementing RTMP Authentication and Playback](https://juejin.cn/post/7463504223177261119)
[GB/T28181 Open Source Diary[8]: Quick Guide to GB28181 Development](https://juejin.cn/post/7468626309699338294)
> Any usage documentation?
**RTMP**
[RTMP Push/Pull Stream Rules](https://juejin.cn/post/7463124448540934194)
[How to Use OBS RTMP Push Stream to GB/T28181 Platform](https://juejin.cn/post/7463350947100786739)
[Hikvision Camera RTMP Push Stream to Open Source GB/T28181 Platform](https://juejin.cn/post/7468191617020313652)
[Dahua Camera RTMP Push Stream to Open Source GB/T28181 Platform](https://juejin.cn/spost/7468194672773021731)
**GB/T28181**
[7 Ways to Register GB28181 Devices](https://juejin.cn/post/7465274924899532838)
> Black screen when playing
Check "Quick Desktop" - "ZLM settings button (top right)" - "GB28181 stream receiving default address"
Ensure this address is accessible by the surveillance device.
Check "Quick Desktop" - "ZLM settings button (top right)" - "Hook IP"
Can ZLM access GoWVP? For Docker combined version, use 127.0.0.1. For separate deployment, use explicit IP address.
> Channel list shows fewer channels than actual count
By design. More than 4 channels should be viewed in the management page, or click "View More" on the right.
> Using nginx reverse proxy, returned playback addresses don't work or snapshots don't load
Configure the following parameters in reverse proxy (replace domain with your actual one):
proxy_set_header X-Forwarded-Host $host;
proxy_set_header X-Forwarded-Prefix "https://gowvp.com";
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
## Documentation
GoWVP [Online API Documentation](https://apifox.com/apidoc/shared-7b67c918-5f72-4f64-b71d-0593d7427b93)
ZLM Documentation [github.com/ZLMediaKit/ZLMediaKit](https://github.com/ZLMediaKit/ZLMediaKit)
// >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
<h1>You've made it this far!</h1>
<h1>Give us a ⭐ star!</h1>
// >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
## Docker
### Video Guide
[How to Build or Run the Project](https://www.bilibili.com/video/BV1QLQeYHEXb)
[How to Deploy with Docker Compose](https://www.bilibili.com/video/BV112QYY3EZX)
[Docker Hub](https://hub.docker.com/r/gospace/gowvp)
**GoWVP & ZLMediaKit Combined Image (Recommended)**
docker-compose.yml
```yml
services:
gowvp:
# If Docker Hub image is unavailable, try:
# registry.cn-shanghai.aliyuncs.com/ixugo/homenvr:latest
image: gospace/gowvp:latest
restart: unless-stopped
# For Linux, uncomment the line below and comment out all ports
# network_mode: host
ports:
# gb28181
- 15123:15123 # Management platform HTTP port
- 15060:15060 # GB28181 SIP TCP port
- 15060:15060/udp # GB28181 SIP UDP port
# zlm
- 1935:1935 # rtmp
- 554:554 # rtsp
- 8080:80 # http
- 8443:443 # https
- 10000:10000
- 8000:8000/udp
- 9000:9000/udp
- 20000-20100:20000-20100 # GB28181 stream receiving ports
- 20000-20100:20000-20100/udp # GB28181 stream receiving UDP ports
volumes:
# Log directory is configs/logs
- ./data:/opt/media/bin/configs
```
**GoWVP & ZLMediaKit Separate Images (More Complex Deployment)**
```yml
services:
gowvp:
image: registry.cn-shanghai.aliyuncs.com/ixugo/gowvp:latest
ports:
- 15123:15123 # Management platform HTTP port
- 15060:15060 # GB28181 SIP TCP port
- 15060:15060/udp # GB28181 SIP UDP port
volumes:
# - ./logs:/app/logs # Uncomment if you need persistent logs
- ./configs:/app/configs
depends_on:
- zlm
zlm:
image: zlmediakit/zlmediakit:master
restart: always
# Recommended: use host mode for Linux
# network_mode: host
ports:
- 1935:1935 # rtmp
- 554:554 # rtsp
- 8080:80 # api
- 8443:443
- 10000:10000
- 10000:10000/udp
- 8000:8000/udp
- 9000:9000/udp
- 20000-20100:20000-20100
- 20000-20100:20000-20100/udp
volumes:
- ./configs:/opt/media/conf
```
## Quick Start
If you're a Go developer familiar with Docker, you can download the source code and run locally.
**Prerequisites**
+ Golang
+ Docker & Docker Compose
+ Make
**Steps**
1. Clone this repository
2. Modify `WebHookIP` in `configs/config.toml` to your LAN IP
3. Run `make build/linux && docker compose up -d`
4. A `zlm.conf` folder is auto-created. Get the API secret from `config.ini` and fill it in `configs/config.toml` under `Secret`
5. Run `docker compose restart`
6. Access `http://localhost:15123` in your browser
## How to Contribute?
1. Fork this project
2. Set your editor's run/debug output directory to the project root
3. Make changes, submit a PR with description of modifications
## Features
- [x] Out-of-the-box with web interface
- [x] RTMP stream distribution support
- [x] RTSP stream distribution support
- [x] Multiple protocol output: HTTP_FLV, Websocket_FLV, HLS, WebRTC, RTSP, RTMP
- [x] LAN/Internet/Multi-layer NAT/Special network environment deployment
- [x] SQLite database for quick deployment
- [x] PostgreSQL/MySQL database support
- [x] Auto offline/reconnect on service restart
- [x] GB/T 28181
- [x] Device registration with 7 connection methods
- [x] UDP and TCP signaling transport modes
- [x] Device time synchronization
- [x] Information queries support
- [x] Device catalog query
- [x] Device info query
- [x] Device basic config query (e.g., timeout 3s × 3 retries = ~9+x seconds for offline detection)
- [x] Live streaming from devices
- [x] UDP and TCP passive stream transport modes
- [x] On-demand streaming to save bandwidth (auto-stop after 30s without viewers)
- [x] H264 and H265 video codec support
- [x] g711a/g711u/aac audio codec support
- [x] Snapshots
- [x] CORS support
- [x] Chinese and English language support
- [x] ONVIF support
- [ ] PTZ control
- [ ] Recording playback
- [ ] Alarm event subscription
- [ ] Alarm event notification handling
## Acknowledgments
Thanks to our sponsors (in no particular order):
[@joestarzxh](https://github.com/joestarzxh)
[@oldweipro](https://github.com/oldweipro)
[@beixiaocai](https://github.com/beixiaocai)
## License
This project is licensed under the **[GNU General Public License v3.0 (GPL-3.0)](https://www.gnu.org/licenses/gpl-3.0.html)**.
- **You are free to use, modify, and distribute** the code of this project, subject to the following conditions:
- **Open Source Requirement**: Any derivative works based on this project (including modified code or software integrating this project) **must also be open-sourced under GPL-3.0**.
- **Retain License & Copyright Notice**: Derivative works must include the original project's `LICENSE` file and copyright notices.
- **Document Modifications**: If you modify the code, you must indicate the changes in the files.
**Note**: If using this project for commercial closed-source software or SaaS services, you must comply with GPL-3.0's copyleft provisions (i.e., related code must be open-sourced).
For the complete license text, see the [LICENSE](./LICENSE) file.
+3 -1
View File
@@ -9,7 +9,7 @@ require (
github.com/gin-gonic/gin v1.11.0
github.com/glebarez/sqlite v1.11.0
github.com/google/wire v0.7.0
github.com/gowvp/onvif v0.0.12
github.com/gowvp/onvif v0.0.14
github.com/ixugo/goddd v1.5.1
github.com/ixugo/netpulse v0.1.3
github.com/jinzhu/copier v0.4.0
@@ -93,3 +93,5 @@ require (
modernc.org/memory v1.8.0 // indirect
modernc.org/sqlite v1.34.4 // indirect
)
// replace github.com/gowvp/onvif => ../onvif
+2 -2
View File
@@ -65,8 +65,8 @@ github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/wire v0.7.0 h1:JxUKI6+CVBgCO2WToKy/nQk0sS+amI9z9EjVmdaocj4=
github.com/google/wire v0.7.0/go.mod h1:n6YbUQD9cPKTnHXEBN2DXlOp/mVADhVErcMFb0v3J18=
github.com/gowvp/onvif v0.0.12 h1:6lUFtnLENB5iHQt2QrIbAQj6tzadWdydLM4g1S0woO4=
github.com/gowvp/onvif v0.0.12/go.mod h1:Dshr55Q/Xgwa9XMQBPBQBMOWj/2Sq+DxLhdNY35uoFc=
github.com/gowvp/onvif v0.0.14 h1:NNrFqzqBHf9Z9MEQOiDpunpagXUHQraRjFmyiXhUwr4=
github.com/gowvp/onvif v0.0.14/go.mod h1:Dshr55Q/Xgwa9XMQBPBQBMOWj/2Sq+DxLhdNY35uoFc=
github.com/ixugo/goddd v1.5.1 h1:1GSFcBMTNz84PSRaUuOin4/6GSda9jc9pAzy78IwRlw=
github.com/ixugo/goddd v1.5.1/go.mod h1:FzEjEd6uWEWan1XWTh8VXdqGtyjMYGow/URNtBY8X7w=
github.com/ixugo/netpulse v0.1.3 h1:760mxad/boWr5hxY2nD0I0yfmQcoNDrlu8KKyk7jOs0=
+1 -1
View File
@@ -65,7 +65,7 @@ func (a *Adapter) OnStreamNotFound(ctx context.Context, app string, stream strin
// QueryCatalog implements ipc.Protocoler.
func (a *Adapter) QueryCatalog(ctx context.Context, device *ipc.Device) error {
panic("unimplemented")
return a.gbs.QueryCatalog(device.DeviceID)
}
// StartPlay implements ipc.Protocoler.
+4 -14
View File
@@ -7,7 +7,6 @@ import (
"github.com/gowvp/gb28181/internal/core/ipc"
"github.com/gowvp/gb28181/internal/core/sms"
"github.com/gowvp/gb28181/pkg/zlm"
"github.com/ixugo/goddd/pkg/orm"
)
@@ -42,19 +41,10 @@ func (a *Adapter) OnStreamNotFound(ctx context.Context, app, stream string) erro
return err
}
_, err = a.sms.AddStreamProxy(svr, zlm.AddStreamProxyRequest{
Vhost: "__defaultVhost__",
App: app,
Stream: stream,
URL: streamURI,
RetryCount: 3,
TimeoutSec: 10,
EnableHLSFMP4: zlm.NewBool(true),
EnableAudio: zlm.NewBool(true),
EnableRTSP: zlm.NewBool(true),
EnableRTMP: zlm.NewBool(true),
AddMuteAudio: zlm.NewBool(true),
AutoClose: zlm.NewBool(true),
_, err = a.sms.AddStreamProxy(svr, sms.AddStreamProxyRequest{
App: app,
Stream: stream,
URL: streamURI,
})
if err == nil {
if err := a.adapter.EditPlaying(ctx, ch.DeviceID, ch.ChannelID, true); err != nil {
+4 -2
View File
@@ -220,6 +220,9 @@ func (a *Adapter) queryAndSaveProfiles(ctx context.Context, device *ipc.Device,
}
channels = append(channels, channel)
}
if len(channels) == 0 {
return fmt.Errorf("没有找到 ONVIF 通道")
}
// 使用统一的 SaveChannels 方法保存(自动处理增删改)
if err := a.adapter.SaveChannels(channels); err != nil {
@@ -255,11 +258,10 @@ func buildPlayURL(rawurl, username, password string) string {
}
func (a *Adapter) Discover(ctx context.Context, w io.Writer) error {
recv, cancel, err := onvif.AllAvailableDevicesAtSpecificEthernetInterfaces()
recv, err := onvif.AllAvailableDevicesAtSpecificEthernetInterfaces()
if err != nil {
return err
}
defer cancel()
for {
select {
+5 -15
View File
@@ -6,7 +6,6 @@ import (
"github.com/gowvp/gb28181/internal/core/ipc"
"github.com/gowvp/gb28181/internal/core/proxy"
"github.com/gowvp/gb28181/internal/core/sms"
"github.com/gowvp/gb28181/pkg/zlm"
)
var _ ipc.Protocoler = (*Adapter)(nil)
@@ -49,20 +48,11 @@ func (a *Adapter) OnStreamNotFound(ctx context.Context, app string, stream strin
if err != nil {
return err
}
resp, err := a.smsCore.AddStreamProxy(svr, zlm.AddStreamProxyRequest{
Vhost: "__defaultVhost__",
App: proxy.App,
Stream: proxy.Stream,
URL: proxy.SourceURL,
RetryCount: 3,
RTPType: proxy.Transport,
TimeoutSec: 10,
EnableHLSFMP4: zlm.NewBool(true),
EnableAudio: zlm.NewBool(true),
EnableRTSP: zlm.NewBool(true),
EnableRTMP: zlm.NewBool(true),
AddMuteAudio: zlm.NewBool(true),
AutoClose: zlm.NewBool(true),
resp, err := a.smsCore.AddStreamProxy(svr, sms.AddStreamProxyRequest{
App: proxy.App,
Stream: proxy.Stream,
URL: proxy.SourceURL,
RTPType: proxy.Transport,
})
if err != nil {
return err
+1
View File
@@ -75,6 +75,7 @@ type Media struct {
IP string `comment:"媒体服务器 IP"`
HTTPPort int `comment:"媒体服务器 HTTP 端口"`
Secret string `comment:"媒体服务器密钥"`
Type string `comment:"媒体服务器类型 zlm/lalmax"`
WebHookIP string `comment:"用于流媒体 webhook 回调"`
RTPPortRange string `comment:"媒体服务器 RTP 端口范围"`
SDPIP string `comment:"媒体服务器 SDP IP"`
+1
View File
@@ -44,6 +44,7 @@ func DefaultConfig() Bootstrap {
WebHookIP: "127.0.0.1",
SDPIP: "127.0.0.1",
RTPPortRange: "20000-20100",
Type: "zlm",
},
Log: Log{
Dir: "./logs",
+12 -1
View File
@@ -131,7 +131,7 @@ func (c Core) AddDevice(ctx context.Context, in *AddDeviceInput) (*Device, error
// 初始化协议连接(失败不影响设备添加)
if protocol, ok := c.protocols[out.GetType()]; ok {
if err := protocol.InitDevice(ctx, &out); err != nil {
slog.WarnContext(ctx, "初始化协议失败", "err", err, "device_id", out.ID)
return nil, reason.ErrBadRequest.SetMsg(err.Error())
}
}
@@ -193,3 +193,14 @@ func (c Core) DelDevice(ctx context.Context, id string) (*Device, error) {
return &dev, nil
}
func (c Core) QueryCatalog(ctx context.Context, deviceID string) error {
device, err := c.GetDeviceByDeviceID(ctx, deviceID)
if err != nil {
return err
}
if err := c.protocols[device.GetType()].QueryCatalog(ctx, device); err != nil {
return reason.ErrBadRequest.SetMsg(err.Error())
}
return nil
}
+33
View File
@@ -0,0 +1,33 @@
package sms
import (
"context"
"github.com/gowvp/gb28181/pkg/zlm"
)
const (
ProtocolZLMediaKit = "zlm"
ProtocolLalmax = "lalmax"
)
// Driver 定义流媒体服务的通用行为
type Driver interface {
// Protocol 返回协议/类型名称,如 "zlm", "srs"
Protocol() string
// Connect 测试连接并获取初始信息 (对应目前的 connection 方法中的部分逻辑)
Connect(ctx context.Context, ms *MediaServer) error
// Setup 下发配置到流媒体服务 (对应目前的 connection 方法中的配置逻辑)
Setup(ctx context.Context, ms *MediaServer, webhookURL string) error
// Ping 主动探测服务是否在线
Ping(ctx context.Context, ms *MediaServer) error
// Stream Operations
OpenRTPServer(ctx context.Context, ms *MediaServer, req *zlm.OpenRTPServerRequest) (*zlm.OpenRTPServerResponse, error)
CloseRTPServer(ctx context.Context, ms *MediaServer, req *zlm.CloseRTPServerRequest) (*zlm.CloseRTPServerResponse, error)
AddStreamProxy(ctx context.Context, ms *MediaServer, req *AddStreamProxyRequest) (*zlm.AddStreamProxyResponse, error)
GetSnapshot(ctx context.Context, ms *MediaServer, req *zlm.GetSnapRequest) ([]byte, error)
}
+32
View File
@@ -0,0 +1,32 @@
package sms
type AddStreamProxyRequest struct {
App string `json:"app"` // 添加的流的应用名,例如 live
Stream string `json:"stream"` // 添加的流的 id 名,例如 test
URL string `json:"url"` // 拉流地址,例如 rtmp://live.hkstv.hk.lxdns.com/live/hks2
RTPType int `json:"rtp_type"` // rtsp 拉流时,拉流方式,0:tcp,1:udp,2:组播
// Vhost string `json:"vhost"` // 添加的流的虚拟主机,例如__defaultVhost__
// RetryCount int `json:"retry_count"` // 拉流重试次数,默认为-1 无限重试
// TimeoutSec float32 `json:"timeout_sec"` // 拉流超时时间,单位秒,float 类型
// EnableHLS *bool `json:"enable_hls,omitempty"` // 是否转换成 hls-mpegts 协议
// EnableHLSFMP4 *bool `json:"enable_hls_fmp4,omitempty"` // 是否转换成 hls-fmp4 协议
// EnableMP4 *bool `json:"enable_mp4,omitempty"` // 是否允许 mp4 录制
// EnableRTSP *bool `json:"enable_rtsp,omitempty"` // 是否转 rtsp 协议
// EnableRTMP *bool `json:"enable_rtmp,omitempty"` // 是否转 rtmp/flv 协议
// EnableTS *bool `json:"enable_ts,omitempty"` // 是否转 http-ts/ws-ts 协议
// EnableFMP4 *bool `json:"enable_fmp4,omitempty"` // 是否转 http-fmp4/ws-fmp4 协议
// HLSDemand *bool `json:"hls_demand,omitempty"` // 该协议是否有人观看才生成
// RTSPDemand *bool `json:"rtsp_demand,omitempty"` // 该协议是否有人观看才生成
// RTMPDemand *bool `json:"rtmp_demand,omitempty"` // 该协议是否有人观看才生成
// TSDemand *bool `json:"ts_demand,omitempty"` // 该协议是否有人观看才生成
// FMP4Demand *bool `json:"fmp4_demand,omitempty"` // 该协议是否有人观看才生成
// EnableAudio *bool `json:"enable_audio,omitempty"` // 转协议时是否开启音频
// AddMuteAudio *bool `json:"add_mute_audio,omitempty"` // 转协议时,无音频是否添加静音 aac 音频
// MP4SavePath *string `json:"mp4_save_path,omitempty"` // mp4 录制文件保存根目录,置空使用默认
// MP4MaxSecond *int `json:"mp4_max_second,omitempty"` // mp4 录制切片大小,单位秒
// MP4AsPlayer *bool `json:"mp4_as_player,omitempty"` // MP4 录制是否当作观看者参与播放人数计数
// HLSSavePath *string `json:"hls_save_path,omitempty"` // hls 文件保存保存根目录,置空使用默认
// ModifyStamp *int `json:"modify_stamp,omitempty"` // 该流是否开启时间戳覆盖(0:绝对时间戳/1:系统时间戳/2:相对时间戳)
// AutoClose *bool `json:"auto_close,omitempty"` // 无人观看是否自动关闭流(不触发无人观看 hook)
}
+109
View File
@@ -0,0 +1,109 @@
package sms
import (
"context"
"fmt"
"github.com/gowvp/gb28181/pkg/lalmax"
"github.com/gowvp/gb28181/pkg/zlm"
)
const (
PullTimeoutMs = 10000
PullRetryNum = 3
)
var _ Driver = (*LalmaxDriver)(nil)
type LalmaxDriver struct {
engine lalmax.Engine
}
// AddStreamProxy implements Driver.
func (l *LalmaxDriver) AddStreamProxy(ctx context.Context, ms *MediaServer, req *AddStreamProxyRequest) (*zlm.AddStreamProxyResponse, error) {
engine := l.withConfig(ms)
resp, err := engine.CtrlStartRelayPull(ctx, lalmax.ApiCtrlStartRelayPullReq{
StreamName: req.Stream,
Url: req.URL,
PullTimeoutMs: PullTimeoutMs,
PullRetryNum: PullRetryNum,
RtspMode: req.RTPType,
})
if err != nil {
return nil, err
}
var result zlm.AddStreamProxyResponse
result.Data.Key = resp.Data.SessionId
return &result, nil
}
// CloseRTPServer implements Driver.
func (l *LalmaxDriver) CloseRTPServer(ctx context.Context, ms *MediaServer, req *zlm.CloseRTPServerRequest) (*zlm.CloseRTPServerResponse, error) {
panic("unimplemented")
}
// Connect implements Driver.
func (l *LalmaxDriver) Connect(ctx context.Context, ms *MediaServer) error {
engine := l.withConfig(ms)
resp, err := engine.GetServerConfig()
if err != nil {
return err
}
// if len(resp.Data) == 0 {
// return fmt.Errorf("Lalmax 服务节点配置为空")
// }
_ = resp
panic("unimplemented")
}
// GetSnapshot implements Driver.
func (l *LalmaxDriver) GetSnapshot(ctx context.Context, ms *MediaServer, req *zlm.GetSnapRequest) ([]byte, error) {
panic("unimplemented")
}
// OpenRTPServer implements Driver.
func (l *LalmaxDriver) OpenRTPServer(ctx context.Context, ms *MediaServer, req *zlm.OpenRTPServerRequest) (*zlm.OpenRTPServerResponse, error) {
engine := l.withConfig(ms)
resp, err := engine.ApiCtrlStartRtpPub(ctx, lalmax.ApiCtrlStartRtpPubReq{
StreamName: req.StreamID,
Port: req.Port,
TimeoutMs: PullTimeoutMs,
IsTcpFlag: 0,
IsWaitKeyFrame: 0,
IsTcpActive: false,
DebugDumpPacket: "",
})
if err != nil {
return nil, err
}
return &zlm.OpenRTPServerResponse{
Port: resp.Data.Port,
}, nil
}
// Ping implements Driver.
func (l *LalmaxDriver) Ping(ctx context.Context, ms *MediaServer) error {
return nil
}
// Protocol implements Driver.
func (l *LalmaxDriver) Protocol() string {
return ProtocolLalmax
}
// Setup implements Driver.
func (l *LalmaxDriver) Setup(ctx context.Context, ms *MediaServer, webhookURL string) error {
panic("unimplemented")
}
func NewLalmaxDriver() *LalmaxDriver {
return &LalmaxDriver{}
}
func (l *LalmaxDriver) withConfig(ms *MediaServer) lalmax.Engine {
url := fmt.Sprintf("http://%s:%d", ms.IP, ms.Ports.HTTP)
return l.engine.SetConfig(lalmax.Config{
URL: url,
Secret: ms.Secret,
})
}
+144
View File
@@ -0,0 +1,144 @@
package sms
import (
"context"
"fmt"
"log/slog"
"github.com/gowvp/gb28181/pkg/zlm"
)
type ZLMDriver struct {
engine zlm.Engine
}
func NewZLMDriver() *ZLMDriver {
return &ZLMDriver{
engine: zlm.NewEngine(),
}
}
func (d *ZLMDriver) Protocol() string {
return ProtocolZLMediaKit
}
func (d *ZLMDriver) withConfig(ms *MediaServer) zlm.Engine {
url := fmt.Sprintf("http://%s:%d", ms.IP, ms.Ports.HTTP)
return d.engine.SetConfig(zlm.Config{
URL: url,
Secret: ms.Secret,
})
}
func (d *ZLMDriver) Connect(ctx context.Context, ms *MediaServer) error {
engine := d.withConfig(ms)
resp, err := engine.GetServerConfig()
if err != nil {
return err
}
if len(resp.Data) == 0 {
return fmt.Errorf("ZLM 服务节点配置为空")
}
// 更新端口信息等
// 注意:这里我们不直接修改数据库,而是修改传入的 ms 对象,调用者负责持久化或使用
zlmConfig := resp.Data[0]
http := ms.Ports.HTTP
ms.Ports.FLV = http
ms.Ports.WsFLV = http
ms.Ports.HTTPS = zlmConfig.HTTPSslport
ms.Ports.RTMP = zlmConfig.RtmpPort
ms.Ports.RTMPs = zlmConfig.RtmpSslport
ms.Ports.RTSP = zlmConfig.RtspPort
ms.Ports.RTSPs = zlmConfig.RtspSslport
ms.Ports.RTPPorxy = zlmConfig.RtpProxyPort
ms.Ports.FLVs = zlmConfig.HTTPSslport
ms.Ports.WsFLVs = zlmConfig.HTTPSslport
ms.HookAliveInterval = 10
ms.Status = true
return nil
}
func (d *ZLMDriver) Setup(ctx context.Context, ms *MediaServer, webhookURL string) error {
engine := d.withConfig(ms)
// 构造配置请求
req := zlm.SetServerConfigRequest{
RtcExternIP: zlm.NewString(ms.IP),
GeneralMediaServerID: zlm.NewString(ms.ID),
HookEnable: zlm.NewString("1"),
HookOnFlowReport: zlm.NewString(""),
HookOnPlay: zlm.NewString(fmt.Sprintf("%s/on_play", webhookURL)),
ProtocolEnableTs: zlm.NewString("0"),
ProtocolEnableFmp4: zlm.NewString("0"),
ProtocolEnableHls: zlm.NewString("0"),
ProtocolEnableHlsFmp4: zlm.NewString("1"),
HookOnPublish: zlm.NewString(fmt.Sprintf("%s/on_publish", webhookURL)),
HookOnStreamNoneReader: zlm.NewString(fmt.Sprintf("%s/on_stream_none_reader", webhookURL)),
GeneralStreamNoneReaderDelayMS: zlm.NewString("30000"),
HookOnStreamNotFound: zlm.NewString(fmt.Sprintf("%s/on_stream_not_found", webhookURL)),
HookOnRecordTs: zlm.NewString(""),
HookOnRtspAuth: zlm.NewString(""),
HookOnRtspRealm: zlm.NewString(""),
HookOnShellLogin: zlm.NewString(""),
HookOnStreamChanged: zlm.NewString(fmt.Sprintf("%s/on_stream_changed", webhookURL)),
HookOnServerKeepalive: zlm.NewString(fmt.Sprintf("%s/on_server_keepalive", webhookURL)),
HookTimeoutSec: zlm.NewString("20"),
HookAliveInterval: zlm.NewString(fmt.Sprint(ms.HookAliveInterval)),
ProtocolContinuePushMs: zlm.NewString("3000"),
RtpProxyPortRange: &ms.RTPPortRange,
FfmpegLog: zlm.NewString("./fflogs/ffmpeg.log"),
}
resp, err := engine.SetServerConfig(&req)
if err != nil {
return err
}
slog.Info("ZLM 服务节点配置设置成功", "changed", resp.Changed)
return nil
}
func (d *ZLMDriver) Ping(ctx context.Context, ms *MediaServer) error {
// 使用 getApiList 或简单的获取配置来探测是否存活
engine := d.withConfig(ms)
// 可以使用更轻量级的接口,这里暂时复用 GetServerConfig
_, err := engine.GetServerConfig()
return err
}
func (d *ZLMDriver) OpenRTPServer(ctx context.Context, ms *MediaServer, req *zlm.OpenRTPServerRequest) (*zlm.OpenRTPServerResponse, error) {
engine := d.withConfig(ms)
return engine.OpenRTPServer(*req)
}
func (d *ZLMDriver) CloseRTPServer(ctx context.Context, ms *MediaServer, req *zlm.CloseRTPServerRequest) (*zlm.CloseRTPServerResponse, error) {
engine := d.withConfig(ms)
return engine.CloseRTPServer(*req)
}
func (d *ZLMDriver) AddStreamProxy(ctx context.Context, ms *MediaServer, req *AddStreamProxyRequest) (*zlm.AddStreamProxyResponse, error) {
engine := d.withConfig(ms)
return engine.AddStreamProxy(zlm.AddStreamProxyRequest{
Vhost: "__defaultVhost__",
App: req.App,
Stream: req.Stream,
URL: req.URL,
RTPType: req.RTPType,
RetryCount: 3,
TimeoutSec: PullTimeoutMs / 1000,
EnableHLSFMP4: zlm.NewBool(true),
EnableAudio: zlm.NewBool(true),
EnableRTSP: zlm.NewBool(true),
EnableRTMP: zlm.NewBool(true),
AddMuteAudio: zlm.NewBool(true),
AutoClose: zlm.NewBool(true),
})
}
func (d *ZLMDriver) GetSnapshot(ctx context.Context, ms *MediaServer, req *zlm.GetSnapRequest) ([]byte, error) {
engine := d.withConfig(ms)
return engine.GetSnap(*req)
}
+96 -118
View File
@@ -20,26 +20,44 @@ import (
type WarpMediaServer struct {
IsOnline bool
LastUpdatedAt time.Time
Config *MediaServer
}
type NodeManager struct {
storer Storer
zlm zlm.Engine
drivers map[string]Driver
cacheServers conc.Map[string, *WarpMediaServer]
quit chan struct{}
}
func NewNodeManager(storer Storer) *NodeManager {
n := NodeManager{
storer: storer,
zlm: zlm.NewEngine(),
quit: make(chan struct{}, 1),
storer: storer,
drivers: make(map[string]Driver),
quit: make(chan struct{}, 1),
}
n.RegisterDriver(ProtocolZLMediaKit, NewZLMDriver())
n.RegisterDriver(ProtocolLalmax, NewLalmaxDriver())
go n.tickCheck()
return &n
}
func (n *NodeManager) RegisterDriver(name string, driver Driver) {
n.drivers[name] = driver
}
func (n *NodeManager) getDriver(name string) (Driver, error) {
if name == "" {
name = "zlm"
}
d, ok := n.drivers[name]
if !ok {
return nil, fmt.Errorf("driver [%s] not found", name)
}
return d, nil
}
func (n *NodeManager) Close() {
close(n.quit)
}
@@ -53,11 +71,29 @@ func (n *NodeManager) tickCheck() {
case <-n.quit:
return
case <-ticker.C:
// TODO: 前期先固定保活,后期优化
const KeepaliveInterval = 2 * 15 * time.Second
n.cacheServers.Range(func(_ string, ms *WarpMediaServer) bool {
isOffline := time.Since(ms.LastUpdatedAt) >= KeepaliveInterval
ms.IsOnline = !isOffline
if time.Since(ms.LastUpdatedAt) < KeepaliveInterval {
ms.IsOnline = true
return true
}
// 尝试主动探测
if ms.Config != nil {
driver, err := n.getDriver(ms.Config.Type)
if err == nil {
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
if err := driver.Ping(ctx, ms.Config); err == nil {
ms.LastUpdatedAt = time.Now()
ms.IsOnline = true
cancel()
return true
}
cancel()
}
}
ms.IsOnline = false
return true
})
}
@@ -99,16 +135,18 @@ func setupSecret(bc *conf.Bootstrap) {
func (n *NodeManager) Run(bc *conf.Bootstrap, serverPort int) error {
ctx := context.Background()
setupSecret(bc)
cfg := bc.Media
setValueFn := func(ms *MediaServer) {
ms.ID = DefaultMediaServerID
ms.IP = cfg.IP
ms.Ports.HTTP = cfg.HTTPPort
ms.Secret = cfg.Secret
ms.Type = "zlm"
ms.Type = cfg.Type
// TODO: 应该读取环境变量
if ms.Type == "" {
ms.Type = ProtocolZLMediaKit
}
ms.Status = false
ms.RTPPortRange = cfg.RTPPortRange
ms.HookIP = cfg.WebHookIP
@@ -136,7 +174,11 @@ func (n *NodeManager) Run(bc *conf.Bootstrap, serverPort int) error {
}
for _, ms := range mediaServers {
go n.connection(ms, serverPort)
go func(ms *MediaServer) {
if err := n.connection(ms, serverPort); err != nil {
slog.Error("Connect media server failed", "id", ms.ID, "err", err)
}
}(ms)
}
return nil
@@ -145,105 +187,40 @@ func (n *NodeManager) Run(bc *conf.Bootstrap, serverPort int) error {
func (n *NodeManager) connection(server *MediaServer, serverPort int) error {
n.cacheServers.Store(server.ID, &WarpMediaServer{
LastUpdatedAt: time.Now(),
Config: server,
})
url := fmt.Sprintf("http://%s:%d", server.IP, server.Ports.HTTP)
engine := n.zlm.SetConfig(zlm.Config{
URL: url,
Secret: server.Secret,
})
log := slog.With("url", url, "id", server.ID)
log.Info("ZLM 服务节点连接中")
resp, err := engine.GetServerConfig()
driver, err := n.getDriver(server.Type)
if err != nil {
log.Error("ZLM 服务节点连接失败", "err", err)
slog.Error("获取驱动失败", "type", server.Type, "err", err)
return err
}
log.Info("ZLM 服务节点连接成功")
if len(resp.Data) == 0 {
log.Error("ZLM 服务节点配置为空")
return fmt.Errorf("配置失败, code[%d] msg[%s]", resp.Code, resp.Msg)
log := slog.With("id", server.ID, "type", server.Type)
log.Info("MediaServer 连接中...")
ctx := context.Background()
if err := driver.Connect(ctx, server); err != nil {
log.Error("MediaServer 连接失败", "err", err)
return err
}
log.Info("MediaServer 连接成功")
zlmConfig := resp.Data[0]
var ms MediaServer
if err := n.storer.MediaServer().Edit(context.Background(), &ms, func(b *MediaServer) {
// b.Ports.FLV = zlmConfig.HTTPPort
// TODO: 映射的端口,会导致获取配置文件的端口不一定能访问
http := server.Ports.HTTP
b.Ports.FLV = http
b.Ports.WsFLV = http // zlmConfig.HTTPSslport
b.Ports.HTTPS = zlmConfig.HTTPSslport
b.Ports.RTMP = zlmConfig.RtmpPort
b.Ports.RTMPs = zlmConfig.RtmpSslport
b.Ports.RTSP = zlmConfig.RtspPort
b.Ports.RTSPs = zlmConfig.RtspSslport
b.Ports.RTPPorxy = zlmConfig.RtpProxyPort
b.Ports.FLVs = zlmConfig.HTTPSslport
b.Ports.WsFLVs = zlmConfig.HTTPSslport
b.HookAliveInterval = 10
b.Status = true
// 更新数据库中的端口信息等
if err := n.storer.MediaServer().Edit(ctx, &MediaServer{}, func(b *MediaServer) {
// 更新字段
b.Ports = server.Ports
b.HookAliveInterval = server.HookAliveInterval
b.Status = server.Status
}, orm.Where("id=?", server.ID)); err != nil {
panic(fmt.Errorf("保存 MediaServer 失败 %w", err))
}
log.Info("ZLM 服务节点配置设置")
log.Info("MediaServer 配置设置...")
hookPrefix := fmt.Sprintf("http://%s:%d/webhook", server.HookIP, serverPort)
req := zlm.SetServerConfigRequest{
RtcExternIP: zlm.NewString(server.IP),
GeneralMediaServerID: zlm.NewString(server.ID),
HookEnable: zlm.NewString("1"),
HookOnFlowReport: zlm.NewString(""),
HookOnPlay: zlm.NewString(fmt.Sprintf("%s/on_play", hookPrefix)),
// HookOnHTTPAccess: zlm.NewString(""),
// 仅开启 hls_fmp4
ProtocolEnableTs: zlm.NewString("0"),
ProtocolEnableFmp4: zlm.NewString("0"),
ProtocolEnableHls: zlm.NewString("0"),
ProtocolEnableHlsFmp4: zlm.NewString("1"),
HookOnPublish: zlm.NewString(fmt.Sprintf("%s/on_publish", hookPrefix)),
HookOnStreamNoneReader: zlm.NewString(fmt.Sprintf("%s/on_stream_none_reader", hookPrefix)),
GeneralStreamNoneReaderDelayMS: zlm.NewString("30000"),
HookOnStreamNotFound: zlm.NewString(fmt.Sprintf("%s/on_stream_not_found", hookPrefix)),
HookOnRecordTs: zlm.NewString(""),
HookOnRtspAuth: zlm.NewString(""),
HookOnRtspRealm: zlm.NewString(""),
// HookOnServerStarted: ,
HookOnShellLogin: zlm.NewString(""),
HookOnStreamChanged: zlm.NewString(fmt.Sprintf("%s/on_stream_changed", hookPrefix)),
// HookOnStreamNotFound: ,
HookOnServerKeepalive: zlm.NewString(fmt.Sprintf("%s/on_server_keepalive", hookPrefix)),
// HookOnSendRtpStopped: ,
// HookOnRtpServerTimeout: ,
// HookOnRecordMp4: ,
HookTimeoutSec: zlm.NewString("20"),
// TODO: 回调时间间隔有问题
HookAliveInterval: zlm.NewString(fmt.Sprint(server.HookAliveInterval)),
// 推流断开后可以在超时时间内重新连接上继续推流,这样播放器会接着播放。
// 置0关闭此特性(推流断开会导致立即断开播放器)
// 此参数不应大于播放器超时时间
// 优化此消息以更快的收到流注销事件
ProtocolContinuePushMs: zlm.NewString("3000"),
RtpProxyPortRange: &server.RTPPortRange,
FfmpegLog: zlm.NewString("./fflogs/ffmpeg.log"),
}
{
resp, err := engine.SetServerConfig(&req)
if err != nil {
log.Error("ZLM 服务节点配置设置失败", "err", err)
return err
}
log.Info("ZLM 服务节点配置设置成功", "changed", resp.Changed)
if err := driver.Setup(ctx, server, hookPrefix); err != nil {
log.Error("MediaServer 配置设置失败", "err", err)
return err
}
return nil
@@ -269,34 +246,35 @@ func (n *NodeManager) findMediaServer(ctx context.Context, in *FindMediaServerIn
// OpenRTPServer 开启RTP服务器
func (n *NodeManager) OpenRTPServer(server *MediaServer, in zlm.OpenRTPServerRequest) (*zlm.OpenRTPServerResponse, error) {
addr := fmt.Sprintf("http://%s:%d", server.IP, server.Ports.HTTP)
e := n.zlm.SetConfig(zlm.Config{
URL: addr,
Secret: server.Secret,
})
return e.OpenRTPServer(in)
driver, err := n.getDriver(server.Type)
if err != nil {
return nil, err
}
return driver.OpenRTPServer(context.Background(), server, &in)
}
// CloseRTPServer 关闭RTP服务器
func (n *NodeManager) CloseRTPServer(in zlm.CloseRTPServerRequest) (*zlm.CloseRTPServerResponse, error) {
return n.zlm.CloseRTPServer(in)
func (n *NodeManager) CloseRTPServer(server *MediaServer, in zlm.CloseRTPServerRequest) (*zlm.CloseRTPServerResponse, error) {
driver, err := n.getDriver(server.Type)
if err != nil {
return nil, err
}
return driver.CloseRTPServer(context.Background(), server, &in)
}
// AddStreamProxy 添加流代理
func (n *NodeManager) AddStreamProxy(server *MediaServer, in zlm.AddStreamProxyRequest) (*zlm.AddStreamProxyResponse, error) {
addr := fmt.Sprintf("http://%s:%d", server.IP, server.Ports.HTTP)
e := n.zlm.SetConfig(zlm.Config{
URL: addr,
Secret: server.Secret,
})
return e.AddStreamProxy(in)
func (n *NodeManager) AddStreamProxy(server *MediaServer, in AddStreamProxyRequest) (*zlm.AddStreamProxyResponse, error) {
driver, err := n.getDriver(server.Type)
if err != nil {
return nil, err
}
return driver.AddStreamProxy(context.Background(), server, &in)
}
func (n *NodeManager) GetSnapshot(server *MediaServer, in zlm.GetSnapRequest) ([]byte, error) {
addr := fmt.Sprintf("http://%s:%d", server.IP, server.Ports.HTTP)
e := n.zlm.SetConfig(zlm.Config{
URL: addr,
Secret: server.Secret,
})
return e.GetSnap(in)
driver, err := n.getDriver(server.Type)
if err != nil {
return nil, err
}
return driver.GetSnapshot(context.Background(), server, &in)
}
+4 -4
View File
@@ -86,9 +86,7 @@ func registerGB28181(g gin.IRouter, api IPCAPI, handler ...gin.HandlerFunc) {
group.POST("", web.WrapH(api.addDevice)) // 添加设备(所有协议,通过 type 区分)
group.DELETE("/:id", web.WrapH(api.delDevice)) // 删除设备(所有协议)
group.GET("/channels", web.WrapH(api.FindChannelsForDevice)) // 设备与通道列表(所有协议)
// GB28181 特有功能
group.POST("/:id/catalog", web.WrapH(api.queryCatalog)) // 刷新通道(GB28181 特有)
group.POST("/:id/catalog", web.WrapH(api.queryCatalog))
}
{
// group := g.Group("/onvif", handler...)
@@ -151,9 +149,11 @@ func (a IPCAPI) delDevice(c *gin.Context, _ *struct{}) (any, error) {
func (a IPCAPI) queryCatalog(c *gin.Context, _ *struct{}) (any, error) {
did := c.Param("id")
if err := a.uc.SipServer.QueryCatalog(did); err != nil {
if err := a.ipc.QueryCatalog(c.Request.Context(), did); err != nil {
return nil, ErrDevice.SetMsg(err.Error())
}
return gin.H{"msg": "ok"}, nil
}
+72
View File
@@ -0,0 +1,72 @@
package lalmax
type ResCode int64
const (
CodeSuccess ResCode = 10000
CodeInvalidParam ResCode = 10001
CodeServerBusy ResCode = 10002
CodeGroupNotFound ResCode = 11001
CodeSessionNotFound ResCode = 11002
CodeStartRelayPullFail ResCode = 11003
CodeStartRelayPushFail ResCode = 11004
CodeStopRelayPushFail ResCode = 11005
CodeGbObServerNotFound ResCode = 12001
CodeDeviceNotRegister ResCode = 12002
CodeDevicePlayError ResCode = 12003
CodeDeviceStopError ResCode = 12004
CodeDevicePtzError ResCode = 12005
CodeDeviceTalkError ResCode = 12006
CodeDeviceRecordListError ResCode = 12007
CodeGetRoomParticipantFail ResCode = 13001
CodeConnectRoomFail ResCode = 13002
CodeListRoomParticipantFail ResCode = 13003
CodeParticipantExist ResCode = 13004
CodeParticipantNotFound ResCode = 13005
CodeStartRoomPublishFail ResCode = 13006
CodeStopRoomPublishFail ResCode = 13007
CodeOnvifObServerNotFound ResCode = 14001
CodeOnvifDiscoverDeviceListFail ResCode = 14002
CodeOnvifGetRtspPlayInfoFail ResCode = 14003
CodeOnvifGetSnapInfoFail ResCode = 14004
CodeOnvifGetPTZCapabilitiesFail ResCode = 14005
CodeOnvifAddDeviceFail ResCode = 14006
CodeOnvifGetDevicesFail ResCode = 14007
CodeOnvifDeleteDeviceFail ResCode = 14008
CodeOnvifPtzDirectionFail ResCode = 14009
CodeOnvifPtzStopFail ResCode = 14010
)
var codeMsgMap = map[ResCode]string{
CodeSuccess: "success",
CodeInvalidParam: "请求参数错误",
CodeServerBusy: "服务繁忙",
CodeGroupNotFound: "group不存在",
CodeSessionNotFound: "session不存在",
CodeStartRelayPullFail: "relay pull 失败",
CodeGbObServerNotFound: "gb 服务没有启动",
CodeDeviceNotRegister: "设备未注册",
CodeDevicePlayError: "gb播放错误",
CodeDeviceStopError: "gb停止错误",
CodeDevicePtzError: "gb ptz操作错误",
CodeGetRoomParticipantFail: "获取房间参与者失败",
CodeConnectRoomFail: "连接房间失败",
CodeListRoomParticipantFail: "获取房间参与者列表失败",
CodeParticipantExist: "参与者已存在",
CodeParticipantNotFound: "参与者不存在",
CodeDeviceTalkError: "对讲操作失败",
CodeOnvifObServerNotFound: "onvif 服务没有启动",
CodeOnvifDiscoverDeviceListFail: "onvif 设备发现失败",
CodeOnvifGetRtspPlayInfoFail: "获取 onvif rtsp 播放地址失败",
CodeOnvifGetSnapInfoFail: "获取 onvif 快照地址失败",
CodeOnvifGetPTZCapabilitiesFail: "获取 onvif ptz 能力失败",
CodeOnvifAddDeviceFail: "添加 onvif 设备失败",
CodeOnvifGetDevicesFail: "获取 onvif 设备列表失败",
CodeOnvifDeleteDeviceFail: "删除 onvif 设备失败",
CodeOnvifPtzDirectionFail: "onvif ptz 方向控制失败",
CodeOnvifPtzStopFail: "onvif ptz 停止控制失败",
}
+281
View File
@@ -0,0 +1,281 @@
package lalmax
type GetServerConfigResponse struct {
ConfVersion string `json:"conf_version"`
CheckSessionDisposeInterval uint32 `json:"check_session_dispose_interval"`
UpdateSessionStateInterval uint32 `json:"update_session_state_interval"`
ManagerChanSize uint32 `json:"manager_chan_size"`
AdjustPts bool `json:"adjust_pts"`
MaxOpenFiles uint64 `json:"max_open_files"`
// 关键帧存储路径
KeyFramePath string `json:"key_frame_path"`
GopCacheConfig GopCacheConfig `json:"gop_cache_config"`
RtmpConfig RtmpConfig `json:"rtmp"`
InSessionConfig InSessionConfig `json:"in_session"`
DefaultHttpConfig DefaultHttpConfig `json:"default_http"`
HttpflvConfig HttpflvConfig `json:"httpflv"`
HlsConfig HlsConfig `json:"hls"`
HttptsConfig HttptsConfig `json:"httpts"`
RtspConfig RtspConfig `json:"rtsp"`
DashConfig DashConfig `json:"dash"`
RtcConfig RtcConfig `json:"rtc"`
Gb28181Config SipConfig `json:"gb28181"`
// OnvifConfig onvif.Config `json:"onvif"`
HttpFmp4Config HttpFmp4Config `json:"httpfmp4"`
RoomConfig RoomConfig `json:"room"`
RecordConfig RecordConfig `json:"record"`
PlaybackConfig PlaybackConfig `json:"playback"`
MetricsConfig MetricsConfig `json:"metrics"`
RelayPushConfig RelayPushConfig `json:"relay_push"`
StaticRelayPullConfig StaticRelayPullConfig `json:"static_relay_pull"`
HttpApiConfig HttpApiConfig `json:"http_api"`
ServerId string `json:"server_id"`
HttpNotifyConfig HttpNotifyConfig `json:"http_notify"`
SimpleAuthConfig SimpleAuthConfig `json:"simple_auth"`
PprofConfig PprofConfig `json:"pprof"`
// LogConfig nazalog.Option `json:"log"`
DebugConfig DebugConfig `json:"debug"`
ReportStatWithFrameRecord bool `json:"report_stat_with_frame_record"`
}
type SipConfig struct {
Enable bool `json:"enable"` // gb28181使能标志
ListenAddr string `json:"listen_addr"` // gb28181监听地址
SipIP string `json:"sip_ip"` // sip 服务器公网IP
SipPort uint16 `json:"sip_port"` // sip 服务器端口,默认 5060
Serial string `json:"serial"` // sip 服务器 id, 默认 34020000002000000001
Realm string `json:"realm"` // sip 服务器域,默认 3402000000
Username string `json:"username"` // sip 服务器账号
Password string `json:"password"` // sip 服务器密码
KeepaliveInterval int `json:"keepalive_interval"` // 心跳包时长
QuickLogin bool `json:"quick_login"` // 快速登陆,有keepalive就认为在线
SipLogClose bool `json:"sip_log_close"` // 关闭sip日志
// MediaConfig MediaConfig `json:"media_config"` // 媒体服务器配置
}
type GopCacheConfig struct {
GopNum int `json:"gop_cache_num"`
SingleGopMaxFrameNum int `json:"single_gop_max_frame_num"`
}
type RtmpConfig struct {
Enable bool `json:"enable"`
Addr string `json:"addr"`
RtmpsEnable bool `json:"rtmps_enable"`
RtmpsAddr string `json:"rtmps_addr"`
RtmpOverQuicEnable bool `json:"rtmp_over_quic_enable"`
RtmpOverQuicAddr string `json:"rtmp_over_quic_addr"`
RtmpsCertFile string `json:"rtmps_cert_file"`
RtmpsKeyFile string `json:"rtmps_key_file"`
RtmpOverKcpEnable bool `json:"rtmp_over_kcp_enable"`
RtmpOverKcpAddr string `json:"rtmp_over_kcp_addr"`
RtmpOverKcpDataShards int `json:"rtmp_over_kcp_data_shards"`
RtmpOverKcpParityShards int `json:"rtmp_over_kcp_parity_shards"`
MergeWriteSize int `json:"merge_write_size"`
PubTimeoutSec uint32 `json:"pub_timeout_sec"`
PullTimeoutSec uint32 `json:"pull_timeout_sec"`
}
type InSessionConfig struct {
AddDummyAudioEnable bool `json:"add_dummy_audio_enable"`
AddDummyAudioWaitAudioMs int `json:"add_dummy_audio_wait_audio_ms"`
}
type DefaultHttpConfig struct {
CommonHttpAddrConfig
}
type HttpflvConfig struct {
CommonHttpServerConfig
}
type HttptsConfig struct {
CommonHttpServerConfig
}
type HlsConfig struct {
CommonHttpServerConfig
UseMemoryAsDiskFlag bool `json:"use_memory_as_disk_flag"`
DiskUseMmapFlag bool `json:"disk_use_mmap_flag"`
UseM3u8MemoryFlag bool `json:"use_m3u8_memory_flag"`
// hls.MuxerConfig
SubSessionTimeoutMs int `json:"sub_session_timeout_ms"`
SubSessionHashKey string `json:"sub_session_hash_key"`
Fmp4HttpServerConfig CommonHttpServerConfig `json:"fmp4"`
}
type DashConfig struct {
CommonHttpServerConfig
UseMemoryAsDiskFlag bool `json:"use_memory_as_disk_flag"`
DiskUseMmapFlag bool `json:"disk_use_mmap_flag"`
UseMpdMemoryFlag bool `json:"use_mpd_memory_flag"`
// dash.Config
}
type RtcConfig struct {
PubTimeoutSec uint32 `json:"pub_timeout_sec"`
CommonHttpServerConfig
// rtc.ICEConfig
}
type RtspConfig struct {
Enable bool `json:"enable"`
Addr string `json:"addr"`
RtspsEnable bool `json:"rtsps_enable"`
RtspsAddr string `json:"rtsps_addr"`
RtspsCertFile string `json:"rtsps_cert_file"`
RtspsKeyFile string `json:"rtsps_key_file"`
OutWaitKeyFrameFlag bool `json:"out_wait_key_frame_flag"`
WsRtspEnable bool `json:"ws_rtsp_enable"`
WsRtspAddr string `json:"ws_rtsp_addr"`
PubTimeoutSec uint32 `json:"pub_timeout_sec"`
PullTimeoutSec uint32 `json:"pull_timeout_sec"`
RtspRemuxerAddSpsPps2KeyFrameFlag bool `json:"add_sps_pps_to_key_frame_flag"`
// rtsp.ServerAuthConfig
}
type Jt1078Config struct {
Enable bool `json:"enable"`
ListenIp string `json:"listen_ip"`
ListenPort int `json:"listen_port"`
PortNum uint16 `json:"port_num"` // 范围 ListenPort至ListenPort+PortNum
PubTimeoutSec uint32 `json:"pub_timeout_sec"`
Intercom struct {
Enable bool
IP string `json:"ip"` // 固定外网ip
Port int `json:"port"` // 固定外网udp端口
AudioPorts [2]int `json:"audio_ports"` // 范围 AudioPorts[0]至AudioPorts[1]
OnJoinURL string `json:"on_join_url"` // 设备对讲连接上了的url回调
OnLeaveURL string `json:"on_leave_url"` // 设备对讲断开了的url回调
} `json:"intercom"`
}
type HttpFmp4Config struct {
CommonHttpServerConfig
}
type RecordConfig struct {
EnableFlv bool `json:"enable_flv"`
FlvOutPath string `json:"flv_out_path"`
EnableMpegts bool `json:"enable_mpegts"`
MpegtsOutPath string `json:"mpegts_out_path"`
EnableFmp4 bool `json:"enable_fmp4"`
Fmp4OutPath string `json:"fmp4_out_path"`
RecordInterval int `json:"record_interval"` // 固定间隔录制一个文件,单位秒
EnableRecordInterval bool `json:"enable_record_interval"` // 是否开启固定间隔录制
}
type PlaybackConfig struct {
CommonHttpServerConfig
}
type MetricsConfig struct {
Enable bool `json:"enable"`
PushgatewayURL string `json:"pushgateway_url"`
JobName string `json:"job_name"`
InstanceName string `json:"instance_name"`
PushInterval int `json:"push_interval"`
}
type RelayPushConfig struct {
Enable bool `json:"enable"`
AddrList []string `json:"addr_list"`
}
type StaticRelayPullConfig struct {
Enable bool `json:"enable"`
Addr string `json:"addr"`
}
type HttpApiConfig struct {
CommonHttpServerConfig
}
type HttpNotifyConfig struct {
Enable bool `json:"enable"`
UpdateIntervalSec int `json:"update_interval_sec"`
OnServerStart string `json:"on_server_start"`
OnUpdate string `json:"on_update"`
OnPubStart string `json:"on_pub_start"`
OnPubStop string `json:"on_pub_stop"`
OnSubStart string `json:"on_sub_start"`
OnSubStop string `json:"on_sub_stop"`
OnPushStart string `json:"on_push_start"`
OnPushStop string `json:"on_push_stop"`
OnRelayPullStart string `json:"on_relay_pull_start"`
OnRelayPullStop string `json:"on_relay_pull_stop"`
OnRtmpConnect string `json:"on_rtmp_connect"`
OnHlsMakeTs string `json:"on_hls_make_ts"`
OnHlsMakeFmp4 string `json:"on_hls_make_fmp4"`
OnReportStat string `json:"on_report_stat"`
OnReportFrameInfo string `json:"on_report_frame_info"`
MaxTaskLen int `json:"max_task_len"` // 最大任务数
ClientSize int `json:"client_size"` // 并发客户端
// NotifyTimeoutSec int `json:"notify_timeout_sec"` // 通知超时时间
DiscardInterval uint32 `json:"discard_interval"` // 丢弃间隔,当队列满的时候,丢弃数量达到此值,下一个一定保留
}
type SimpleAuthConfig struct {
Key string `json:"key"`
DangerousLalSecret string `json:"dangerous_lal_secret"`
PubRtmpEnable bool `json:"pub_rtmp_enable"`
SubRtmpEnable bool `json:"sub_rtmp_enable"`
SubHttpflvEnable bool `json:"sub_httpflv_enable"`
SubHttptsEnable bool `json:"sub_httpts_enable"`
PubRtspEnable bool `json:"pub_rtsp_enable"`
SubRtspEnable bool `json:"sub_rtsp_enable"`
HlsM3u8Enable bool `json:"hls_m3u8_enable"`
PushRtmpEnable bool `json:"push_rtmp_enable"`
PushJt1078Enable bool `json:"push_jt1078_enable"`
PushPsEnable bool `json:"push_ps_enable"`
}
type PprofConfig struct {
CommonHttpServerConfig
}
type DebugConfig struct {
LogGroupIntervalSec int `json:"log_group_interval_sec"`
LogGroupMaxGroupNum int `json:"log_group_max_group_num"`
LogGroupMaxSubNumPerGroup int `json:"log_group_max_sub_num_per_group"`
}
type CommonHttpServerConfig struct {
CommonHttpAddrConfig
Enable bool `json:"enable"`
EnableHttps bool `json:"enable_https"`
EnableHttp3 bool `json:"enable_http3"`
UrlPattern string `json:"url_pattern"`
}
type CommonHttpAddrConfig struct {
HttpListenAddr string `json:"http_listen_addr"`
HttpsListenAddr string `json:"https_listen_addr"`
Http3ListenAddr string `json:"http3_listen_addr"`
HttpsCertFile string `json:"https_cert_file"`
HttpsKeyFile string `json:"https_key_file"`
}
type RoomConfig struct {
Enable bool `json:"enable"`
APIKey string `json:"api_key"`
APISecret string `json:"api_secret"`
}
func (e *Engine) GetServerConfig() (*GetServerConfigResponse, error) {
// var resp GetServerConfigResponse
// if err := e.post(getServerConfig, nil, &resp); err != nil {
// return nil, err
// }
// if err := e.ErrHandle(resp.Code, resp.Msg); err != nil {
// return nil, err
// }
// return &resp, nil
return nil, nil
}
+49
View File
@@ -0,0 +1,49 @@
package lalmax
import (
"bytes"
"context"
"encoding/json"
"net/http"
"time"
)
type Config struct {
URL string
Secret string
}
type Engine struct {
cfg Config
cli *http.Client
}
func NewEngine() Engine {
return Engine{
cli: &http.Client{
Timeout: 5 * time.Second,
Transport: &http.Transport{
MaxIdleConns: 30,
MaxIdleConnsPerHost: 30,
MaxConnsPerHost: 100,
},
},
}
}
func (e Engine) SetConfig(cfg Config) Engine {
e.cfg = cfg
return e
}
func (e *Engine) post(ctx context.Context, path string, data map[string]any, out any) error {
body, _ := json.Marshal(data)
req, _ := http.NewRequestWithContext(ctx, http.MethodPost, e.cfg.URL+path, bytes.NewReader(body))
req.Header.Set("Content-Type", "application/json")
resp, err := e.cli.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
return json.NewDecoder(resp.Body).Decode(out)
}
+75
View File
@@ -0,0 +1,75 @@
package lalmax
import (
"context"
"encoding/json"
)
const (
apiCtrlStartRelayPull = "/api/ctrl/startRelayPull"
apiCtrlStopRelayPull = "/api/ctrl/stopRelayPull"
)
type ApiCtrlStartRelayPullReq struct {
Url string `json:"url"` // 必填项,回源拉流的完整url地址,目前支持rtmp和rtsp
StreamName string `json:"stream_name"` // 选填项,如果不指定,则从`url`参数中解析获取
PullTimeoutMs int `json:"pull_timeout_ms"` // 选填项,pull建立会话的超时时间,单位毫秒。
//. 选填项,pull连接失败或者中途断开连接的重试次数
// -1 表示一直重试,直到收到stop请求,或者开启并触发下面的自动关闭功能
// = 0 表示不重试
// > 0 表示重试次数
// 提示:不开启自动重连,你可以在收到HTTP-Notify on_relay_pull_stop, on_update等消息时决定是否重连
PullRetryNum int `json:"pull_retry_num"`
// 选填项,没有观看者时,自动关闭pull会话,节约资源
// -1 表示不启动该功能
// = 0 表示没有观看者时,立即关闭pull会话
// > 0 表示没有观看者持续多长时间,关闭pull会话,单位毫秒
// 默认值是-1
// 提示:不开启该功能,你可以在收到HTTP-Notify on_sub_stop, on_update等消息时决定是否关闭relay pull
AutoStopPullAfterNoOutMs int `json:"auto_stop_pull_after_no_out_ms"`
//. 选填项,使用rtsp时的连接方式
// 0 tcp
// 1 udp
// 默认值是0
RtspMode int `json:"rtsp_mode"`
//. 选填项,将接收的数据存成文件
DebugDumpPacket string `json:"debug_dump_packet"`
KeepLiveFormGetParameter bool `json:"keep_live_form_get_parameter"`
}
type CommonResp struct {
Code int `json:"code"`
Msg string `json:"msg"`
}
type ApiCtrlStartRelayPullResp struct {
CommonResp
Data struct {
StreamName string `json:"stream_name"`
SessionId string `json:"session_id"`
} `json:"data"`
}
func (e *Engine) CtrlStartRelayPull(ctx context.Context, in ApiCtrlStartRelayPullReq) (*ApiCtrlStartRelayPullResp, error) {
body, err := struct2map(in)
if err != nil {
return nil, err
}
var resp ApiCtrlStartRelayPullResp
if err := e.post(ctx, apiCtrlStartRelayPull, body, &resp); err != nil {
return nil, err
}
return &resp, nil
}
func struct2map(in any) (map[string]any, error) {
b, err := json.Marshal(in)
if err != nil {
return nil, err
}
var out map[string]any
if err := json.Unmarshal(b, &out); err != nil {
return nil, err
}
return out, nil
}
+37
View File
@@ -0,0 +1,37 @@
package lalmax
import "context"
const ctrlStartRtpPub = "/api/ctrl/startRtpPub"
type ApiCtrlStartRtpPubReq struct {
StreamName string `json:"stream_name"`
Port int `json:"port"`
PeerPort int `json:"peer_port"` // 对端收流端口
TimeoutMs int `json:"timeout_ms"`
IsTcpFlag int `json:"is_tcp_flag"`
IsWaitKeyFrame int `json:"is_wait_key_frame"`
DebugDumpPacket string `json:"debug_dump_packet"`
IsTcpActive bool `json:"is_tcp_active"` // Tcp主动模式
}
type ApiCtrlStartRtpPubResp struct {
CommonResp
Data struct {
StreamName string `json:"stream_name"`
SessionId string `json:"session_id"`
Port int `json:"port"`
} `json:"data"`
}
func (e *Engine) ApiCtrlStartRtpPub(ctx context.Context, in ApiCtrlStartRtpPubReq) (*ApiCtrlStartRtpPubResp, error) {
body, err := struct2map(in)
if err != nil {
return nil, err
}
var resp ApiCtrlStartRtpPubResp
if err := e.post(ctx, ctrlStartRtpPub, body, &resp); err != nil {
return nil, err
}
return &resp, nil
}