mirror of
https://github.com/gowvp/gb28181.git
synced 2026-04-22 15:07:10 +08:00
refactor onvif
This commit is contained in:
@@ -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 .
|
||||
|
||||
|
||||
# ==================================================================================== #
|
||||
|
||||
@@ -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
@@ -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/)
|
||||
|
||||

|
||||
|
||||
|||||
|
||||
|-|-|-|-|
|
||||
|
||||
## 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.
|
||||
@@ -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
|
||||
|
||||
@@ -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=
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"`
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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,
|
||||
})
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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 停止控制失败",
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
Reference in New Issue
Block a user