From 41b709a6f0a1425a4f53af20dd8eec0aa3dbabc6 Mon Sep 17 00:00:00 2001 From: xugo Date: Mon, 17 Mar 2025 02:19:21 +0800 Subject: [PATCH] =?UTF-8?q?=E6=9B=B4=E6=96=B0=E9=95=9C=E5=83=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Dockerfile | 4 +- Makefile | 11 ++- README.md | 82 +++++++++++-------- cmd/server/main.go | 24 ++++++ configs/config.toml | 63 +++++++++----- docker-compose.debug.yml | 27 +++++- docker-compose.yml | 20 +++-- .../core/gb28181/store/gb28181cache/cache.go | 3 +- .../core/gb28181/store/gb28181cache/device.go | 21 ++++- internal/core/sms/media_server.go | 3 +- internal/core/sms/media_server.model.go | 2 +- internal/core/sms/media_server.param.go | 38 ++++----- internal/core/sms/node_manager.go | 4 +- internal/web/api/api.go | 2 + internal/web/api/sms.go | 23 +++++- pkg/gbs/devices.go | 2 + pkg/zlm/zlm.go | 5 +- 17 files changed, 235 insertions(+), 99 deletions(-) diff --git a/Dockerfile b/Dockerfile index 24d4d9f..ad68a98 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,7 @@ FROM alpine:latest +ARG TARGETARCH + ENV TZ=Asia/Shanghai RUN apk --no-cache add ca-certificates \ @@ -7,7 +9,7 @@ RUN apk --no-cache add ca-certificates \ WORKDIR /app -ADD ./build/linux_amd64/bin ./ +ADD ./build/linux_${TARGETARCH}/bin ./ ADD ./configs/config.toml /app/configs/config.toml ADD ./www /app/www diff --git a/Makefile b/Makefile index df3f506..ea195b2 100644 --- a/Makefile +++ b/Makefile @@ -158,6 +158,12 @@ BUILD_LINUX_AMD64_DIR := ./build/linux_amd64 build/linux: $(eval GOARCH := amd64) $(eval GOOS := linux) + $(eval dir := $(BUILD_DIR_ROOT)/$(GOOS)_$(GOARCH)) + @make build/local GOOS=$(GOOS) GOARCH=$(GOARCH) + + $(eval GOARCH := arm64) + $(eval GOOS := linux) + $(eval dir := $(BUILD_DIR_ROOT)/$(GOOS)_$(GOARCH)) @make build/local GOOS=$(GOOS) GOARCH=$(GOARCH) ## build/windows: 构建 windows 应用 @@ -178,7 +184,10 @@ docker/push: @docker push $(IMAGE_NAME) docker/build/full: - @docker build --force-rm=true --platform linux/amd64 -t $(IMAGE_NAME) -f Dockerfile_full . + @docker build --force-rm=true --push --platform linux/amd64,linux/arm64 -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 . # ==================================================================================== # diff --git a/README.md b/README.md index 8bb2462..f8d136f 100644 --- a/README.md +++ b/README.md @@ -36,8 +36,6 @@ go wvp 是 Go 语言实现的开源 GB28181 解决方案,基于 GB28181-2022 项目框架基于 @ixugo [goweb](https://github.com/ixugo/goweb) -Java 语言 WVP @648540858 [wvp-GB28181-pro](https://github.com/648540858/wvp-GB28181-pro) - ## QA > 怎么没有前端资源? 如何加载网页呢? @@ -92,9 +90,54 @@ ZLM使用文档 [github.com/ZLMediaKit/ZLMediaKit](https://github.com/ZLMediaKit ## Docker +### 视频指南 + +[如何构建或运行项目](https://www.bilibili.com/video/BV1QLQeYHEXb) + +[如何用 docker compose 部署项目](https://www.bilibili.com/video/BV112QYY3EZX) + + + + [docker hub](https://hub.docker.com/r/gospace/gowvp) -** gowvp & zlmediakit 融合镜像** +** gowvp & zlmediakit 分开镜像(推荐)** + +```yml +services: + gowvp: + image: registry.cn-shanghai.aliyuncs.com/ixugo/gowvp:latest + ports: + - 15123:15123 # 管理平台 http 端口 + - 15060:15060 # gb28181 sip tcp 端口 + - 15060:15060/udp # gb28181 sip udp 端口 + volumes: + # - ./logs:/app/logs # 如果需要持久化日志,请取消注释 + - ./configs:/app/configs + depends_on: + - zlm + zlm: + image: zlmediakit/zlmediakit:master + restart: always + # 推荐 linux 主机使用 host 模式 + # 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 + - 20050-20100:20050-20100 + - 20050-20100:20050-20100/udp + volumes: + - ./configs:/opt/media/conf + +``` + +** gowvp & zlmediakit 融合镜像(不推荐)** docker-compose.yml ```yml services: @@ -119,39 +162,6 @@ services: - ./zlm.conf:/opt/media/conf ``` -** gowvp & zlmediakit 分开镜像** - -```yml -services: - gowvp: - image: registry.cn-shanghai.aliyuncs.com/ixugo/gowvp:latest - ports: - - 15123:15123 # 管理平台 http 端口 - - 15060:15060 # gb28181 sip tcp 端口 - - 15060:15060/udp # gb28181 sip udp 端口 - volumes: - - ./logs:/app/logs - - ./configs:/app/configs - zlm: - image: zlmediakit/zlmediakit:master - restart: always - # 推荐 linux 主机使用 host 模式 - # 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 - - 20050-20100:20050-20100 - - 20050-20100:20050-20100/udp - volumes: - - ./conf:/opt/media/conf -``` - diff --git a/cmd/server/main.go b/cmd/server/main.go index 0c4c189..8dcb4c2 100644 --- a/cmd/server/main.go +++ b/cmd/server/main.go @@ -8,6 +8,7 @@ import ( "os" "os/signal" "path/filepath" + "regexp" "strconv" "syscall" "time" @@ -45,6 +46,7 @@ func main() { var bc conf.Bootstrap // 获取配置目录绝对路径 fileDir, _ := abs(*configDir) + os.MkdirAll(fileDir, 0o755) filePath := filepath.Join(fileDir, "config.toml") configIsNotExistWrite(filePath) if err := conf.SetupConfig(&bc, filePath); err != nil { @@ -76,6 +78,14 @@ func main() { })) } + secret, err := getSecret(*configDir) + if err == nil { + slog.Info("发现 zlm 配置,已赋值,未回写配置文件", "secret", secret) + bc.Media.Secret = secret + } else { + slog.Info("未发现 zlm 配置,请检查 config.ini 文件", "err", err) + } + handler, cleanUp, err := wireApp(&bc, log) if err != nil { slog.Error("程序构建失败", "err", err) @@ -125,3 +135,17 @@ func configIsNotExistWrite(path string) { } } } + +// 读取 config.ini 文件,通过正则表达式,获取 secret 的值 +func getSecret(configDir string) (string, error) { + content, err := os.ReadFile(filepath.Join(system.Getwd(), configDir, "config.ini")) + if err != nil { + return "", err + } + re := regexp.MustCompile(`secret=(\w+)`) + matches := re.FindStringSubmatch(string(content)) + if len(matches) < 2 { + return "", fmt.Errorf("secret not found") + } + return matches[1], nil +} diff --git a/configs/config.toml b/configs/config.toml index 1526577..5714994 100644 --- a/configs/config.toml +++ b/configs/config.toml @@ -1,16 +1,25 @@ -version = 1 [Server] - rtmpSecret = "123" - [Server.HTTP] - Port = 15123 - JwtSecret = "" - Timeout = "60s" + Debug = false + # rtmp 推流秘钥 + RTMPSecret = '123' - [Server.HTTP.Pprof] + # 对外提供的服务,建议由 nginx 代理 + [Server.HTTP] + # http 端口 + Port = 15123 + # 请求超时时间 + Timeout = '1m0s' + # jwt 秘钥,空串时,每次启动程序将随机赋值 + JwtSecret = '' + + [Server.HTTP.PProf] + # 是否启用 pprof, 建议设置为 true Enabled = true + # 访问白名单 AccessIps = ['::1', '127.0.0.1'] [Data] + # 数据库支持 sqlite 和 postgres 两种,使用 sqlite 时 dsn 应当填写文件存储路径 [Data.Database] Dsn = './configs/data.db' MaxIdleConns = 1 @@ -18,20 +27,6 @@ version = 1 ConnMaxLifetime = '6h0m0s' SlowThreshold = '200ms' -[Sip] - Port = 15060 - ID = "3402000000200000001" - Domain = "3402000000" - Password = "" - -[Media] - IP = "127.0.0.1" - HTTPPort = 8080 - Secret = "s1kPE7bzqKeHUaVcp8dCA0jeB8yxyFq4" - WebHookIP = "192.168.1.10" - RTPPortRange = "20000-20500" - SDPIP = "192.168.1.10" - [Log] # 日志存储目录,不能使用特殊符号 Dir = './logs' @@ -42,4 +37,28 @@ version = 1 # 多久时间,分割一个新的日志文件 RotationTime = '12h0m0s' # 多大文件,分割一个新的日志文件(MB) - RotationSize = 50 \ No newline at end of file + RotationSize = 50 + +[Sip] + # 服务监听的 tcp/udp 端口号 + Port = 15060 + # gb/t28181 20 位国标 ID + ID = '3402000000200000001' + # 域 + Domain = '3402000000' + # 注册密码 + Password = '' + +[Media] + # 媒体服务器 IP + IP = '127.0.0.1' + # 媒体服务器 HTTP 端口 + HTTPPort = 8080 + # 媒体服务器密钥 + Secret = '' + # 用于流媒体 webhook 回调 + WebHookIP = '192.168.1.10' + # 媒体服务器 RTP 端口范围 + RTPPortRange = '20000-20500' + # 媒体服务器 SDP IP + SDPIP = '192.168.1.10' \ No newline at end of file diff --git a/docker-compose.debug.yml b/docker-compose.debug.yml index c8b9008..41c6fcb 100644 --- a/docker-compose.debug.yml +++ b/docker-compose.debug.yml @@ -5,4 +5,29 @@ services: context: . dockerfile: ./Dockerfile ports: - - 18082:18081 + - 15123:15123 # 管理平台 http 端口 + - 15060:15060 # gb28181 sip tcp 端口 + - 15060:15060/udp # gb28181 sip udp 端口 + volumes: + - ./logs:/app/logs + - ./configs:/app/configs + depends_on: + - zlm + zlm: + image: zlmediakit/zlmediakit:master + restart: always + # 推荐 linux 主机使用 host 模式 + # 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 + - 20050-20100:20050-20100 + - 20050-20100:20050-20100/udp + volumes: + - ./conf:/opt/media/conf diff --git a/docker-compose.yml b/docker-compose.yml index 67b59dd..9aa8416 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,21 +1,24 @@ services: gowvp: - image: gb28181 - build: - context: . - dockerfile: ./Dockerfile + image: registry.cn-shanghai.aliyuncs.com/ixugo/gowvp:latest ports: - 15123:15123 # 管理平台 http 端口 - 15060:15060 # gb28181 sip tcp 端口 - 15060:15060/udp # gb28181 sip udp 端口 volumes: - - ./logs:/app/logs + # - ./logs:/app/logs # 如果需要持久化日志,请取消注释 - ./configs:/app/configs + networks: + - gowvp-network + depends_on: + - zlm zlm: image: zlmediakit/zlmediakit:master restart: always # 推荐 linux 主机使用 host 模式 # network_mode: host + networks: + - gowvp-network ports: - 1935:1935 # rtmp - 554:554 # rtsp @@ -28,4 +31,9 @@ services: - 20050-20100:20050-20100 - 20050-20100:20050-20100/udp volumes: - - ./conf:/opt/media/conf + - ./configs:/opt/media/conf + +# 如果不使用 host 模式,可以使用下面的配置 +networks: + gowvp-network: + driver: bridge diff --git a/internal/core/gb28181/store/gb28181cache/cache.go b/internal/core/gb28181/store/gb28181cache/cache.go index ed638bc..1c03754 100644 --- a/internal/core/gb28181/store/gb28181cache/cache.go +++ b/internal/core/gb28181/store/gb28181cache/cache.go @@ -88,8 +88,9 @@ func (c *Cache) Change(deviceID string, changeFn func(*gb28181.Device), changeFn dev2.LastKeepaliveAt = dev.KeepaliveAt.Time dev2.LastRegisterAt = dev.RegisteredAt.Time dev2.Expires = dev.Expires + dev2.Password = dev.Password + dev2.Address = dev.Address changeFn2(dev2) - if !dev2.IsOnline { if err := c.Storer.Channel().BatchEdit(context.TODO(), "is_online", false, orm.Where("did=?", dev.ID)); err != nil { slog.Error("更新通道离线状态失败", "error", err) diff --git a/internal/core/gb28181/store/gb28181cache/device.go b/internal/core/gb28181/store/gb28181cache/device.go index 4fedb86..2a03a25 100644 --- a/internal/core/gb28181/store/gb28181cache/device.go +++ b/internal/core/gb28181/store/gb28181cache/device.go @@ -2,6 +2,7 @@ package gb28181cache import ( "context" + "log/slog" "github.com/gowvp/gb28181/internal/core/gb28181" "github.com/gowvp/gb28181/pkg/gbs" @@ -12,7 +13,7 @@ import ( var _ gb28181.DeviceStorer = &Device{} -type Device Cache +type Device = Cache // Add implements gb28181.DeviceStorer. func (d *Device) Add(ctx context.Context, dev *gb28181.Device) error { @@ -47,7 +48,23 @@ func (d *Device) Del(ctx context.Context, dev *gb28181.Device, opts ...orm.Query // Edit implements gb28181.DeviceStorer. func (d *Device) Edit(ctx context.Context, dev *gb28181.Device, changeFn func(*gb28181.Device), opts ...orm.QueryOption) error { - return d.Storer.Device().Edit(ctx, dev, changeFn, opts...) + if err := d.Storer.Device().Edit(ctx, dev, changeFn, opts...); err != nil { + return err + } + dev2, ok := d.devices.Load(dev.DeviceID) + if !ok { + panic("edit device not found") + } + // 密码修改,设备需要重新注册 + if dev2.Password != dev.Password && dev.Password != "" { + slog.Info("修改密码,设备离线") + d.Change(dev.DeviceID, func(d *gb28181.Device) { + d.Password = dev.Password + d.IsOnline = false + }, func(d *gbs.Device) { + }) + } + return nil } // Find implements gb28181.DeviceStorer. diff --git a/internal/core/sms/media_server.go b/internal/core/sms/media_server.go index 7be86cc..e040548 100755 --- a/internal/core/sms/media_server.go +++ b/internal/core/sms/media_server.go @@ -54,7 +54,7 @@ func (c *Core) AddMediaServer(ctx context.Context, in *AddMediaServerInput) (*Me } // EditMediaServer Update object information -func (c *Core) EditMediaServer(ctx context.Context, in *EditMediaServerInput, id string) (*MediaServer, error) { +func (c *Core) EditMediaServer(ctx context.Context, in *EditMediaServerInput, id string, serverPort int) (*MediaServer, error) { var out MediaServer if err := c.storer.MediaServer().Edit(ctx, &out, func(b *MediaServer) { if err := copier.Copy(b, in); err != nil { @@ -63,6 +63,7 @@ func (c *Core) EditMediaServer(ctx context.Context, in *EditMediaServerInput, id }, orm.Where("id=?", id)); err != nil { return nil, web.ErrDB.Withf(`Edit err[%s]`, err.Error()) } + c.connection(&out, serverPort) return &out, nil } diff --git a/internal/core/sms/media_server.model.go b/internal/core/sms/media_server.model.go index aa776f2..2d4a293 100755 --- a/internal/core/sms/media_server.model.go +++ b/internal/core/sms/media_server.model.go @@ -13,7 +13,7 @@ type MediaServer struct { CreatedAt orm.Time `gorm:"column:created_at;notNull;default:CURRENT_TIMESTAMP" json:"created_at"` UpdatedAt orm.Time `gorm:"column:updated_at;notNull;default:CURRENT_TIMESTAMP" json:"updated_at"` HookIP string `gorm:"column:hook_ip;notNull;default:''" json:"hook_ip"` - SDPIP string `gorm:"column:sdpip;notNull;default:''" json:"sdpip"` + SDPIP string `gorm:"column:sdp_ip;notNull;default:''" json:"sdp_ip"` StreamIP string `gorm:"column:stream_ip;notNull;default:''" json:"stream_ip"` Ports MediaServerPorts `gorm:"column:ports;notNull;default:'{}';type:jsonb" json:"ports"` AutoConfig bool `gorm:"column:auto_config;notNull;default:FALSE" json:"auto_config"` diff --git a/internal/core/sms/media_server.param.go b/internal/core/sms/media_server.param.go index 90defa9..c26733f 100755 --- a/internal/core/sms/media_server.param.go +++ b/internal/core/sms/media_server.param.go @@ -30,25 +30,25 @@ type FindMediaServerInput struct { } type EditMediaServerInput struct { - IP string `json:"ip"` - HookIP string `json:"hook_ip"` - SDPIP string `json:"sdpip"` - StreamIP string `json:"stream_ip"` - Ports MediaServerPorts `json:"ports"` - AutoConfig bool `json:"auto_config"` - Secret string `json:"secret"` - HookAliveInterval int `json:"hook_alive_interval"` - RTPEnable bool `json:"rtpenable"` - Status bool `json:"status"` - RTPPortRange string `json:"rtpport_range"` - SendRTPPortRange string `json:"send_rtpport_range"` - RecordAssistPort int `json:"record_assist_port"` - LastKeepaliveAt orm.Time `json:"last_keepalive_at"` - IsDefault bool `json:"is_default"` - RecordDay int `json:"record_day"` - RecordPath string `json:"record_path"` - Type string `json:"type"` - TranscodeSuffix string `json:"transcode_suffix"` + IP string `json:"ip"` + HookIP string `json:"hook_ip"` + SDPIP string `json:"sdpip"` + // StreamIP string `json:"stream_ip"` + // Ports MediaServerPorts `json:"ports"` + // AutoConfig bool `json:"auto_config"` + Secret string `json:"secret"` + // HookAliveInterval int `json:"hook_alive_interval"` + // RTPEnable bool `json:"rtpenable"` + // Status bool `json:"status"` + // RTPPortRange string `json:"rtpport_range"` + // SendRTPPortRange string `json:"send_rtpport_range"` + // RecordAssistPort int `json:"record_assist_port"` + // LastKeepaliveAt orm.Time `json:"last_keepalive_at"` + // IsDefault bool `json:"is_default"` + // RecordDay int `json:"record_day"` + // RecordPath string `json:"record_path"` + // Type string `json:"type"` + // TranscodeSuffix string `json:"transcode_suffix"` } type AddMediaServerInput struct { diff --git a/internal/core/sms/node_manager.go b/internal/core/sms/node_manager.go index 4efa4da..dc910e6 100644 --- a/internal/core/sms/node_manager.go +++ b/internal/core/sms/node_manager.go @@ -126,10 +126,10 @@ func (n *NodeManager) connection(server *MediaServer, serverPort int) { log.Info("ZLM 服务节点连接中") - for { + for i := range 10 { resp, err := engine.GetServerConfig() if err != nil { - log.Error("ZLM 服务节点连接失败", "err", err) + log.Error("ZLM 服务节点连接失败", "err", err, "retry", i) time.Sleep(10 * time.Second) continue } diff --git a/internal/web/api/api.go b/internal/web/api/api.go index c6c4e4d..1ee4288 100644 --- a/internal/web/api/api.go +++ b/internal/web/api/api.go @@ -23,6 +23,7 @@ var startRuntime = time.Now() func setupRouter(r *gin.Engine, uc *Usecase) { uc.GB28181API.uc = uc + uc.SMSAPI.uc = uc go stat.LoadTop(system.Getwd(), func(m map[string]any) { _ = m }) @@ -70,6 +71,7 @@ func setupRouter(r *gin.Engine, uc *Usecase) { registerGB28181(r, uc.GB28181API) registerProxy(r, uc.ProxyAPI) registerConfig(r, uc.ConfigAPI) + registerSms(r, uc.SMSAPI) } type playOutput struct { diff --git a/internal/web/api/sms.go b/internal/web/api/sms.go index 0f9b35b..9f72aa4 100755 --- a/internal/web/api/sms.go +++ b/internal/web/api/sms.go @@ -13,6 +13,7 @@ import ( type SmsAPI struct { smsCore sms.Core + uc *Usecase } func NewSMSCore(db *gorm.DB, cfg *conf.Bootstrap) sms.Core { @@ -31,10 +32,11 @@ func registerSms(g gin.IRouter, api SmsAPI, handler ...gin.HandlerFunc) { { group := g.Group("/media_servers", handler...) group.GET("", web.WarpH(api.findMediaServer)) - group.GET("/:id", web.WarpH(api.getMediaServer)) group.PUT("/:id", web.WarpH(api.editMediaServer)) - group.POST("", web.WarpH(api.addMediaServer)) - group.DELETE("/:id", web.WarpH(api.delMediaServer)) + + // group.GET("/:id", web.WarpH(api.getMediaServer)) + // group.POST("", web.WarpH(api.addMediaServer)) + // group.DELETE("/:id", web.WarpH(api.delMediaServer)) } } @@ -52,7 +54,20 @@ func (a SmsAPI) getMediaServer(c *gin.Context, _ *struct{}) (any, error) { func (a SmsAPI) editMediaServer(c *gin.Context, in *sms.EditMediaServerInput) (any, error) { mediaServerID := c.Param("id") - return a.smsCore.EditMediaServer(c.Request.Context(), in, mediaServerID) + out, err := a.smsCore.EditMediaServer(c.Request.Context(), in, mediaServerID, a.uc.Conf.Server.HTTP.Port) + if err != nil { + return nil, err + } + if mediaServerID == "local" { + a.uc.Conf.Media.IP = out.IP + a.uc.Conf.Media.SDPIP = out.SDPIP + a.uc.Conf.Media.Secret = out.Secret + a.uc.Conf.Media.WebHookIP = out.HookIP + if err := conf.WriteConfig(a.uc.Conf, a.uc.Conf.ConfigPath); err != nil { + return nil, web.ErrServer.Msg(err.Error()) + } + } + return out, err } func (a SmsAPI) addMediaServer(c *gin.Context, in *sms.AddMediaServerInput) (any, error) { diff --git a/pkg/gbs/devices.go b/pkg/gbs/devices.go index 43992f2..7227151 100644 --- a/pkg/gbs/devices.go +++ b/pkg/gbs/devices.go @@ -29,6 +29,7 @@ type Device struct { IsOnline bool Address string + Password string conn sip.Connection source net.Addr @@ -63,6 +64,7 @@ func NewDevice(conn sip.Connection, d *gb28181.Device) *Device { LastKeepaliveAt: d.KeepaliveAt.Time, LastRegisterAt: d.RegisteredAt.Time, IsOnline: d.IsOnline, + Password: d.Password, } return &c diff --git a/pkg/zlm/zlm.go b/pkg/zlm/zlm.go index e712f35..80320cc 100644 --- a/pkg/zlm/zlm.go +++ b/pkg/zlm/zlm.go @@ -48,8 +48,9 @@ func (e Engine) SetConfig(cfg Config) Engine { } func (e *Engine) post(path string, data map[string]any, out any) error { - bodyMap := map[string]any{ - "secret": e.cfg.Secret, + bodyMap := make(map[string]any) + if e.cfg.Secret != "" { + bodyMap["secret"] = e.cfg.Secret } maps.Copy(bodyMap, data) body, _ := json.Marshal(bodyMap)