Files
gb28181/internal/web/api/ipc.go
T
2026-01-08 00:34:09 +08:00

463 lines
14 KiB
Go
Executable File

// Code generated by godddx, DO AVOID EDIT.
package api
import (
"context"
"fmt"
"io"
"log/slog"
"os"
"path/filepath"
"slices"
"sort"
"strconv"
"strings"
"time"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
"github.com/gowvp/gb28181/internal/adapter/onvifadapter"
"github.com/gowvp/gb28181/internal/core/bz"
"github.com/gowvp/gb28181/internal/core/ipc"
"github.com/gowvp/gb28181/internal/core/push"
"github.com/gowvp/gb28181/internal/core/sms"
"github.com/gowvp/gb28181/pkg/zlm"
"github.com/gowvp/gb28181/protos"
"github.com/ixugo/goddd/domain/uniqueid"
"github.com/ixugo/goddd/pkg/hook"
"github.com/ixugo/goddd/pkg/orm"
"github.com/ixugo/goddd/pkg/reason"
"github.com/ixugo/goddd/pkg/web"
)
var ErrDevice = reason.NewError("ErrDevice", "设备错误")
const (
coverDir = "cover"
)
// TODO: 快照不会删除,只会覆盖,设备删除时也不会删除快照,待实现
func writeCover(dataDir, channelID string, body []byte) error {
coverPath := filepath.Join(dataDir, coverDir)
if err := os.MkdirAll(coverPath, 0o777); err != nil {
return err
}
return os.WriteFile(filepath.Join(coverPath, channelID+".jpg"), body, 0o644)
}
func readCoverPath(dataDir, channelID string) string {
coverPath := filepath.Join(dataDir, coverDir)
return filepath.Join(coverPath, channelID+".jpg")
}
func readCover(dataDir, channelID string) ([]byte, error) {
return os.ReadFile(readCoverPath(dataDir, channelID))
}
type IPCAPI struct {
ipc ipc.Core
uc *Usecase
}
func NewIPCAPI(core ipc.Core) IPCAPI {
return IPCAPI{ipc: core}
}
func NewIPCCore(store ipc.Storer, uni uniqueid.Core, protocols map[string]ipc.Protocoler) ipc.Core {
return ipc.NewCore(store, uni, protocols)
}
func registerGB28181(g gin.IRouter, api IPCAPI, handler ...gin.HandlerFunc) {
// GB28181 协议特有的回调接口
g.Any("/gb28181/snapshot", func(c *gin.Context) {
b, err := io.ReadAll(c.Request.Body)
if err != nil {
panic(err)
}
if err := os.WriteFile(orm.GenerateRandomString(10)+".jpg", b, 0o644); err != nil {
slog.ErrorContext(c.Request.Context(), "write cover", "err", err)
}
c.JSON(200, gin.H{"msg": "ok"})
})
// 统一的设备管理 API(支持所有协议)
{
group := g.Group("/devices", handler...)
group.GET("", web.WrapH(api.findDevice)) // 设备列表(所有协议)
group.GET("/:id", web.WrapH(api.getDevice)) // 设备详情(所有协议)
group.PUT("/:id", web.WrapH(api.editDevice)) // 修改设备(所有协议)
group.POST("", web.WrapH(api.addDevice)) // 添加设备(所有协议,通过 type 区分)
group.DELETE("/:id", web.WrapH(api.delDevice)) // 删除设备(所有协议)
group.GET("/channels", web.WrapH(api.FindChannelsForDevice)) // 设备与通道列表(所有协议)
group.POST("/:id/catalog", web.WrapH(api.queryCatalog))
}
{
// group := g.Group("/onvif", handler...)
g.GET("/onvif/discover", api.discover) // ONVIF 设备发现(ONVIF 特有)
// group.GET("/discover", api.discover) // ONVIF 设备发现(ONVIF 特有)
}
// 统一的通道管理 API(支持所有协议)
{
group := g.Group("/channels", handler...)
group.GET("", web.WrapH(api.findChannel)) // 通道列表(所有协议)
group.PUT("/:id", web.WrapH(api.editChannel)) // 修改通道(所有协议)
group.POST("/:id/play", web.WrapH(api.play)) // 播放(所有协议)
group.POST("/:id/snapshot", web.WrapH(api.refreshSnapshot)) // 图像抓拍(所有协议)
group.GET("/:id/snapshot", api.getSnapshot) // 获取图像(所有协议)
group.POST("/:id/zones", web.WrapH(api.addZone)) // 添加区域(所有协议)
group.GET("/:id/zones", web.WrapH(api.getZones)) // 获取区域(所有协议)
}
}
// >>> device >>>>>>>>>>>>>>>>>>>>
func (a IPCAPI) findDevice(c *gin.Context, in *ipc.FindDeviceInput) (any, error) {
items, total, err := a.ipc.FindDevice(c.Request.Context(), in)
return gin.H{"items": items, "total": total}, err
}
func (a IPCAPI) getDevice(c *gin.Context, _ *struct{}) (any, error) {
deviceID := c.Param("id")
return a.ipc.GetDevice(c.Request.Context(), deviceID)
}
func (a IPCAPI) editDevice(c *gin.Context, in *ipc.EditDeviceInput) (any, error) {
deviceID := c.Param("id")
return a.ipc.EditDevice(c.Request.Context(), in, deviceID)
}
// addDevice 添加设备(支持所有协议类型)
// 通过 type 字段区分协议: "GB28181" 或 "ONVIF"
//
// 示例1 - 添加 GB28181 设备:
//
// POST /devices
// { "type": "GB28181", "device_id": "34020000001320000001", "name": "摄像头1" }
//
// 示例2 - 添加 ONVIF 设备:
//
// POST /devices
// { "type": "ONVIF", "ip": "192.168.1.100", "port": 80, "username": "admin", "password": "12345" }
func (a IPCAPI) addDevice(c *gin.Context, in *ipc.AddDeviceInput) (any, error) {
in.Type = strings.ToUpper(in.Type)
if !slices.Contains([]string{ipc.TypeGB28181, ipc.TypeOnvif}, in.Type) {
return nil, reason.ErrBadRequest.SetMsg("不支持的设备类型")
}
return a.ipc.AddDevice(c.Request.Context(), in)
}
func (a IPCAPI) delDevice(c *gin.Context, _ *struct{}) (any, error) {
did := c.Param("id")
return a.ipc.DelDevice(c.Request.Context(), did)
}
func (a IPCAPI) queryCatalog(c *gin.Context, _ *struct{}) (any, error) {
did := c.Param("id")
if err := a.ipc.QueryCatalog(c.Request.Context(), did); err != nil {
return nil, ErrDevice.SetMsg(err.Error())
}
return gin.H{"msg": "ok"}, nil
}
func (a IPCAPI) FindChannelsForDevice(c *gin.Context, in *ipc.FindDeviceInput) (any, error) {
items, total, err := a.ipc.FindChannelsForDevice(c.Request.Context(), in)
// 按照在线优先排序
sort.SliceStable(items, func(i, j int) bool {
return items[i].IsOnline && !items[j].IsOnline
})
return gin.H{"items": items, "total": total}, err
}
// >>> channel >>>>>>>>>>>>>>>>>>>>
func (a IPCAPI) findChannel(c *gin.Context, in *ipc.FindChannelInput) (any, error) {
items, total, err := a.ipc.FindChannel(c.Request.Context(), in)
return gin.H{"items": items, "total": total}, err
}
// func (a GB28181API) getChannel(c *gin.Context, _ *struct{}) (any, error) {
// channelID := c.Param("id")
// return a.gb28181Core.GetChannel(c.Request.Context(), channelID)
// }
func (a IPCAPI) editChannel(c *gin.Context, in *ipc.EditChannelInput) (any, error) {
cid := c.Param("id")
return a.ipc.EditChannel(c.Request.Context(), in, cid)
}
// func (a GB28181API) addChannel(c *gin.Context, in *gb28181.AddChannelInput) (any, error) {
// return a.gb28181Core.AddChannel(c.Request.Context(), in)
// }
// func (a GB28181API) delChannel(c *gin.Context, _ *struct{}) (any, error) {
// channelID := c.Param("id")
// return a.gb28181Core.DelChannel(c.Request.Context(), channelID)
// }
func (a IPCAPI) play(c *gin.Context, _ *struct{}) (*playOutput, error) {
channelID := c.Param("id")
var app, appStream, host, stream, session, mediaServerID string
// 国标逻辑
if bz.IsGB28181(channelID) {
// 防止错误的配置,无法收到流
if a.uc.Conf.Media.SDPIP == "127.0.0.1" {
return nil, reason.ErrUsedLogic.SetMsg("请先配置流媒体 SDP 收流地址")
}
// a.uc.SipServer.
ch, err := a.ipc.GetChannel(c.Request.Context(), channelID)
if err != nil {
return nil, err
}
app = "rtp"
appStream = ch.ID
mediaServerID = sms.DefaultMediaServerID
} else if bz.IsRTMP(channelID) {
pu, err := a.uc.MediaAPI.pushCore.GetStreamPush(c.Request.Context(), channelID)
if err != nil {
return nil, err
}
if pu.Status != push.StatusPushing {
return nil, reason.ErrNotFound.SetMsg("未推流")
}
app = pu.App
appStream = pu.Stream
mediaServerID = pu.MediaServerID
if !pu.IsAuthDisabled && pu.Session != "" {
session = "session=" + pu.Session
}
} else if bz.IsRTSP(channelID) {
proxy, err := a.uc.ProxyAPI.proxyCore.GetStreamProxy(c.Request.Context(), channelID)
if err != nil {
return nil, err
}
app = proxy.App
// TODO: 2025-12-03, 防止旧版本无法播放,强制为 live 先
app = "live" // proxy.App
appStream = proxy.Stream
mediaServerID = sms.DefaultMediaServerID
} else if bz.IsOnvif(channelID) {
app = "live"
appStream = channelID
mediaServerID = sms.DefaultMediaServerID
} else {
return nil, reason.ErrNotFound.SetMsg("不支持的播放通道")
}
if !a.uc.SMSAPI.smsCore.IsOnline(mediaServerID) {
return nil, reason.ErrNotFound.SetMsg("Oops! 流媒体服务离线或IP有误")
}
svr, err := a.uc.SMSAPI.smsCore.GetMediaServer(c.Request.Context(), mediaServerID)
if err != nil {
return nil, err
}
stream = app + "/" + appStream
host = c.Request.Host
if l := strings.Split(c.Request.Host, ":"); len(l) == 2 {
host = l[0]
}
httpPort := a.uc.Conf.Server.HTTP.Port
// 播放规则
// https://github.com/zlmediakit/ZLMediaKit/wiki/%E6%92%AD%E6%94%BEurl%E8%A7%84%E5%88%99
prefix := c.Request.Header.Get("X-Forwarded-Prefix")
if prefix == "" {
prefix = "http://" + host + ":" + strconv.Itoa(httpPort)
}
if h := c.Request.Header.Get("X-Forwarded-Host"); h != "" {
host = h
}
item := a.uc.SMSAPI.smsCore.GetStreamLiveAddr(svr, prefix, host, app, stream)
out := playOutput{
App: app,
Stream: appStream,
Items: []sms.StreamLiveAddr{item},
}
// 取一张快照
go func() {
rtsp := fmt.Sprintf("rtsp://%s:%d/%s", "127.0.0.1", svr.Ports.RTSP, stream) + "?" + session
for range 2 {
time.Sleep(3 * time.Second)
body, err := a.uc.SMSAPI.smsCore.GetSnapshot(svr, sms.GetSnapRequest{
GetSnapRequest: zlm.GetSnapRequest{
URL: rtsp,
TimeoutSec: 10,
ExpireSec: 15,
},
Stream: channelID,
})
if err != nil {
slog.ErrorContext(c.Request.Context(), "get snapshot", "err", err)
continue
}
if err := writeCover(a.uc.Conf.ConfigDir, channelID, body); err != nil {
slog.ErrorContext(c.Request.Context(), "write cover", "err", err)
}
break
}
if a.uc.Conf.Server.AI.Disabled || a.uc.AIWebhookAPI.ai == nil {
return
}
if _, ok := a.uc.AIWebhookAPI.aiTasks.LoadOrStore(channelID, struct{}{}); !ok {
resp, err := a.uc.AIWebhookAPI.ai.StartCamera(context.Background(), &protos.StartCameraRequest{
CameraId: appStream,
CameraName: appStream,
RtspUrl: rtsp,
DetectFps: 5,
Labels: []string{"person", "car", "cat", "dog"},
Threshold: 0.65,
RetryLimit: 10,
CallbackUrl: fmt.Sprintf("http://127.0.0.1:%d/ai", a.uc.Conf.Server.HTTP.Port),
CallbackSecret: "Basic 1234567890",
})
if err != nil {
slog.Error("start camera", "err", err)
return
}
slog.Debug("start camera", "resp", resp,
"msg", resp.GetMessage(),
"source_width", resp.GetSourceWidth(),
"source_height", resp.GetSourceHeight(),
"source_fps", resp.GetSourceFps(),
)
}
}()
return &out, nil
}
type refreshSnapshotInput struct {
// 指定获取多少秒内创建的快照
WithinSeconds int64 `json:"within_seconds"`
// 取快照的链接地址
URL string `json:"url"`
}
func (a IPCAPI) refreshSnapshot(c *gin.Context, in *refreshSnapshotInput) (any, error) {
channelID := c.Param("id")
path := readCoverPath(a.uc.Conf.ConfigDir, channelID)
token := c.GetString("token")
prefix := web.GetBaseURL(c.Request)
if v := c.Request.Header.Get("X-Forwarded-Prefix"); v != "" {
prefix = v
}
// 获取文件的修改时间
fileInfo, err := os.Stat(path)
if err == nil {
if fileInfo.ModTime().Unix() > time.Now().Unix()-in.WithinSeconds {
return gin.H{"link": fmt.Sprintf("%s/channels/%s/snapshot?token=%s", prefix, channelID, token)}, nil
}
}
if in.URL != "" {
svr, err := a.uc.SMSAPI.smsCore.GetMediaServer(c.Request.Context(), sms.DefaultMediaServerID)
if err != nil {
return nil, err
}
img, err := a.uc.SMSAPI.smsCore.GetSnapshot(svr, sms.GetSnapRequest{
GetSnapRequest: zlm.GetSnapRequest{
URL: in.URL,
TimeoutSec: 10,
ExpireSec: 28800,
},
Stream: channelID,
})
if err != nil {
slog.ErrorContext(c.Request.Context(), "get snapshot", "err", err)
// return nil, reason.ErrBadRequest.Msg(err.Error())
} else {
if hook.MD5FromBytes(img) != "" {
if err := writeCover(a.uc.Conf.ConfigDir, channelID, img); err != nil {
slog.ErrorContext(c.Request.Context(), "write cover", "err", err)
}
}
}
}
return gin.H{"link": fmt.Sprintf("%s/channels/%s/snapshot?token=%s", prefix, channelID, token)}, nil
}
func (a IPCAPI) addZone(c *gin.Context, in *ipc.AddZoneInput) (gin.H, error) {
channelID := c.Param("id")
if len(in.Labels) == 0 {
in.Labels = []string{"person", "car", "cat", "dog"}
}
zones, err := a.ipc.AddZone(c.Request.Context(), in, channelID)
return gin.H{"items": zones}, err
}
func (a IPCAPI) getZones(c *gin.Context, _ *struct{}) (any, error) {
channelID := c.Param("id")
return a.ipc.GetZones(c.Request.Context(), channelID)
}
func (a IPCAPI) getSnapshot(c *gin.Context) {
channelID := c.Param("id")
body, err := readCover(a.uc.Conf.ConfigDir, channelID)
if err != nil {
web.Fail(c, reason.ErrNotFound.SetMsg(err.Error()))
return
}
c.Data(200, "image/jpeg", body)
}
func (a IPCAPI) discover(c *gin.Context) {
p := a.ipc.GetProtocol(ipc.TypeOnvif)
onvifAdapter, ok := p.(*onvifadapter.Adapter)
if !ok {
web.Fail(c, reason.ErrNotFound.SetMsg("不支持的协议"))
return
}
se := web.NewSSE(64, time.Minute)
go func() {
defer func() {
se.Publish(web.Event{
ID: uuid.NewString(),
Event: "end",
})
se.Close()
}()
w := IOWriter{fn: func(b []byte) (int, error) {
se.Publish(web.Event{
ID: uuid.NewString(),
Event: "discover",
Data: b,
})
return len(b), nil
}}
if err := onvifAdapter.Discover(c.Request.Context(), w); err != nil {
slog.ErrorContext(c.Request.Context(), "discover", "err", err)
}
}()
se.ServeHTTP(c.Writer, c.Request)
}
type IOWriter struct {
fn func(b []byte) (int, error)
}
func (w IOWriter) Write(b []byte) (int, error) {
return w.fn(b)
}