mirror of
https://github.com/gowvp/gb28181.git
synced 2026-04-22 23:17:19 +08:00
349 lines
12 KiB
Go
Executable File
349 lines
12 KiB
Go
Executable File
// Code generated by godddx, DO AVOID EDIT.
|
|
package api
|
|
|
|
import (
|
|
"fmt"
|
|
"io"
|
|
"log/slog"
|
|
"os"
|
|
"path/filepath"
|
|
"sort"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
"github.com/gowvp/gb28181/internal/core/bz"
|
|
"github.com/gowvp/gb28181/internal/core/gb28181"
|
|
"github.com/gowvp/gb28181/internal/core/push"
|
|
"github.com/gowvp/gb28181/internal/core/sms"
|
|
"github.com/gowvp/gb28181/pkg/zlm"
|
|
"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)
|
|
os.MkdirAll(coverPath, 0o755)
|
|
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 GB28181API struct {
|
|
gb28181Core gb28181.Core
|
|
uc *Usecase
|
|
}
|
|
|
|
func NewGB28181API(core gb28181.Core) GB28181API {
|
|
return GB28181API{gb28181Core: core}
|
|
}
|
|
|
|
func NewGB28181Core(store gb28181.Storer, uni uniqueid.Core) gb28181.Core {
|
|
return gb28181.NewCore(store, uni)
|
|
}
|
|
|
|
func registerGB28181(g gin.IRouter, api GB28181API, handler ...gin.HandlerFunc) {
|
|
g.Any("/gb28181/snapshot", func(c *gin.Context) {
|
|
b, err := io.ReadAll(c.Request.Body)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
os.WriteFile(orm.GenerateRandomString(10)+".jpg", b, 0o644)
|
|
c.JSON(200, gin.H{"msg": "ok"})
|
|
})
|
|
{
|
|
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)) // 添加设备
|
|
group.DELETE("/:id", web.WrapH(api.delDevice)) // 删除设备
|
|
group.POST("/:id/catalog", web.WrapH(api.queryCatalog)) // 刷新通道
|
|
group.GET("/channels", web.WrapH(api.FindChannelsForDevice)) // 设备与通道列表
|
|
}
|
|
|
|
{
|
|
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.GET("/:id", web.WrapH(api.getChannel))
|
|
// group.POST("", web.WrapH(api.addChannel))
|
|
// group.DELETE("/:id", web.WrapH(api.delChannel))
|
|
}
|
|
}
|
|
|
|
// >>> device >>>>>>>>>>>>>>>>>>>>
|
|
|
|
func (a GB28181API) findDevice(c *gin.Context, in *gb28181.FindDeviceInput) (any, error) {
|
|
items, total, err := a.gb28181Core.FindDevice(c.Request.Context(), in)
|
|
return gin.H{"items": items, "total": total}, err
|
|
}
|
|
|
|
func (a GB28181API) getDevice(c *gin.Context, _ *struct{}) (any, error) {
|
|
deviceID := c.Param("id")
|
|
return a.gb28181Core.GetDevice(c.Request.Context(), deviceID)
|
|
}
|
|
|
|
func (a GB28181API) editDevice(c *gin.Context, in *gb28181.EditDeviceInput) (any, error) {
|
|
deviceID := c.Param("id")
|
|
return a.gb28181Core.EditDevice(c.Request.Context(), in, deviceID)
|
|
}
|
|
|
|
func (a GB28181API) addDevice(c *gin.Context, in *gb28181.AddDeviceInput) (any, error) {
|
|
return a.gb28181Core.AddDevice(c.Request.Context(), in)
|
|
}
|
|
|
|
func (a GB28181API) delDevice(c *gin.Context, _ *struct{}) (any, error) {
|
|
did := c.Param("id")
|
|
return a.gb28181Core.DelDevice(c.Request.Context(), did)
|
|
}
|
|
|
|
func (a GB28181API) queryCatalog(c *gin.Context, _ *struct{}) (any, error) {
|
|
did := c.Param("id")
|
|
if err := a.uc.SipServer.QueryCatalog(did); err != nil {
|
|
return nil, ErrDevice.SetMsg(err.Error())
|
|
}
|
|
return gin.H{"msg": "ok"}, nil
|
|
}
|
|
|
|
func (a GB28181API) FindChannelsForDevice(c *gin.Context, in *gb28181.FindDeviceInput) (any, error) {
|
|
items, total, err := a.gb28181Core.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 GB28181API) findChannel(c *gin.Context, in *gb28181.FindChannelInput) (any, error) {
|
|
items, total, err := a.gb28181Core.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 GB28181API) editChannel(c *gin.Context, in *gb28181.EditChannelInput) (any, error) {
|
|
cid := c.Param("id")
|
|
return a.gb28181Core.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 GB28181API) play(c *gin.Context, _ *struct{}) (*playOutput, error) {
|
|
channelID := c.Param("id")
|
|
|
|
var app, appStream, host, stream, session, mediaServerID string
|
|
|
|
// 国标逻辑
|
|
if strings.HasPrefix(channelID, bz.IDPrefixGBChannel) {
|
|
// 防止错误的配置,无法收到流
|
|
if a.uc.Conf.Media.SDPIP == "127.0.0.1" {
|
|
return nil, reason.ErrUsedLogic.SetMsg("请先配置流媒体 SDP 收流地址")
|
|
}
|
|
// a.uc.SipServer.
|
|
ch, err := a.gb28181Core.GetChannel(c.Request.Context(), channelID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
app = "rtp"
|
|
appStream = ch.ID
|
|
|
|
mediaServerID = sms.DefaultMediaServerID
|
|
|
|
} else if strings.HasPrefix(channelID, bz.IDPrefixRTMP) {
|
|
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 strings.HasPrefix(channelID, bz.IDPrefixRTSP) {
|
|
proxy, err := a.uc.ProxyAPI.proxyCore.GetStreamProxy(c.Request.Context(), channelID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
app = proxy.App
|
|
appStream = proxy.Stream
|
|
mediaServerID = sms.DefaultMediaServerID
|
|
} else {
|
|
return nil, reason.ErrNotFound.SetMsg("不支持的播放通道")
|
|
}
|
|
|
|
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
|
|
|
|
out := playOutput{
|
|
App: app,
|
|
Stream: appStream,
|
|
Items: []streamAddrItem{
|
|
{
|
|
Label: "默认线路",
|
|
WSFLV: fmt.Sprintf("ws://%s:%d/proxy/sms/%s.live.flv", host, httpPort, stream) + "?" + session,
|
|
HTTPFLV: fmt.Sprintf("http://%s:%d/proxy/sms/%s.live.flv", host, httpPort, stream) + "?" + session,
|
|
RTMP: fmt.Sprintf("rtmp://%s:%d/%s", host, svr.Ports.RTMP, stream) + "?" + session,
|
|
RTSP: fmt.Sprintf("rtsp://%s:%d/%s", host, svr.Ports.RTSP, stream) + "?" + session,
|
|
WebRTC: fmt.Sprintf("webrtc://%s:%d/proxy/sms/index/api/webrtc?app=%s&stream=%s&type=play", host, httpPort, app, stream) + "&" + session,
|
|
HLS: fmt.Sprintf("http://%s:%d/proxy/sms/%s/hls.fmp4.m3u8", host, httpPort, stream) + "?" + session,
|
|
},
|
|
// {
|
|
// Label: "SSL 线路",
|
|
// WSFLV: fmt.Sprintf("wss://%s:%d/%s.live.flv", host, svr.Ports.HTTP, stream) + session,
|
|
// HTTPFLV: fmt.Sprintf("https://%s:%d/%s.live.flv", host, svr.Ports.HTTP, stream) + session,
|
|
// RTMP: fmt.Sprintf("rtmps://%s:%d/%s", host, svr.Ports.RTMPs, stream) + session,
|
|
// RTSP: fmt.Sprintf("rtsps://%s:%d/%s", host, svr.Ports.RTSPs, stream) + session,
|
|
// WebRTC: fmt.Sprintf("webrtc://%s:%d/index/api/webrtc?app=%s&stream=%s&type=play", host, svr.Ports.HTTPS, app, stream) + "&" + session,
|
|
// HLS: fmt.Sprintf("https://%s:%d/%s/hls.fmp4.m3u8", host, svr.Ports.HTTPS, stream) + "?" + session,
|
|
// },
|
|
},
|
|
}
|
|
|
|
prefix := c.Request.Header.Get("X-Forwarded-Prefix")
|
|
if prefix != "" {
|
|
wsPrefix := strings.Replace(strings.Replace(prefix, "https", "wss", 1), "http", "ws", 1)
|
|
out.Items[0].WSFLV = fmt.Sprintf("%s/proxy/sms/%s.live.flv", wsPrefix, stream) + "?" + session
|
|
out.Items[0].HTTPFLV = fmt.Sprintf("%s/proxy/sms/%s.live.flv", prefix, stream) + "?" + session
|
|
out.Items[0].HLS = fmt.Sprintf("%s/proxy/sms/%s/hls.fmp4.m3u8", prefix, stream) + "?" + session
|
|
rtcPrefix := strings.Replace(strings.Replace(prefix, "https", "webrtc", 1), "http", "webrtc", 1)
|
|
out.Items[0].WebRTC = fmt.Sprintf("%s/proxy/sms/index/api/webrtc?app=%s&stream=%s&type=play", rtcPrefix, app, stream) + "&" + session
|
|
|
|
host := c.Request.Header.Get("X-Forwarded-Host")
|
|
if host != "" {
|
|
out.Items[0].RTMP = fmt.Sprintf("rtmp://%s:%d/%s", host, svr.Ports.RTMP, stream) + "?" + session
|
|
out.Items[0].RTSP = fmt.Sprintf("rtsp://%s:%d/%s", host, svr.Ports.RTSP, stream) + "?" + session
|
|
}
|
|
}
|
|
|
|
// 取一张快照
|
|
go func() {
|
|
body, err := a.uc.SMSAPI.smsCore.GetSnapshot(svr, zlm.GetSnapRequest{
|
|
URL: out.Items[0].RTSP,
|
|
TimeoutSec: 10,
|
|
ExpireSec: 15,
|
|
})
|
|
if err != nil {
|
|
slog.ErrorContext(c.Request.Context(), "get snapshot", "err", err)
|
|
} else {
|
|
writeCover(a.uc.Conf.ConfigDir, channelID, body)
|
|
}
|
|
}()
|
|
return &out, nil
|
|
}
|
|
|
|
type refreshSnapshotInput struct {
|
|
// 指定获取多少秒内创建的快照
|
|
WithinSeconds int64 `json:"within_seconds"`
|
|
// 取快照的链接地址
|
|
URL string `json:"url"`
|
|
}
|
|
|
|
func (a GB28181API) 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, zlm.GetSnapRequest{
|
|
URL: in.URL,
|
|
TimeoutSec: 10,
|
|
ExpireSec: 28800,
|
|
})
|
|
if err != nil {
|
|
slog.ErrorContext(c.Request.Context(), "get snapshot", "err", err)
|
|
// return nil, reason.ErrBadRequest.Msg(err.Error())
|
|
} else {
|
|
if hook.MD5FromBytes(img) != "" {
|
|
writeCover(a.uc.Conf.ConfigDir, channelID, img)
|
|
}
|
|
}
|
|
}
|
|
|
|
return gin.H{"link": fmt.Sprintf("%s/channels/%s/snapshot?token=%s", prefix, channelID, token)}, nil
|
|
}
|
|
|
|
func (a GB28181API) getSnapshot(c *gin.Context) {
|
|
channelID := c.Param("id")
|
|
body, err := readCover(a.uc.Conf.ConfigDir, channelID)
|
|
if err != nil {
|
|
reason.ErrNotFound.SetMsg(err.Error())
|
|
return
|
|
}
|
|
c.Data(200, "image/jpeg", body)
|
|
}
|