mirror of
https://github.com/gowvp/gb28181.git
synced 2026-04-22 23:17:19 +08:00
576 lines
17 KiB
Go
Executable File
576 lines
17 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/owl/internal/adapter/onvifadapter"
|
|
"github.com/gowvp/owl/internal/core/bz"
|
|
"github.com/gowvp/owl/internal/core/ipc"
|
|
"github.com/gowvp/owl/internal/core/push"
|
|
"github.com/gowvp/owl/internal/core/sms"
|
|
"github.com/gowvp/owl/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)
|
|
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)) // 获取区域(所有协议)
|
|
group.POST("/:id/ai/enable", web.WrapH(api.enableAI)) // 启用 AI 检测
|
|
group.POST("/:id/ai/disable", web.WrapH(api.disableAI)) // 禁用 AI 检测
|
|
}
|
|
}
|
|
|
|
// >>> 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)
|
|
}
|
|
|
|
var (
|
|
ErrAIGlobalDisabled = reason.NewError("ErrAIGlobalDisabled", "AI 功能已在全局配置中禁用")
|
|
ErrAIServiceNotReady = reason.NewError("ErrAIServiceNotReady", "AI 服务未初始化或连接失败")
|
|
ErrChannelNotSupported = reason.NewError("ErrChannelNotSupported", "不支持的通道类型")
|
|
)
|
|
|
|
// enableAI 启用指定通道的 AI 检测功能,需要先确保全局 AI 服务已启用且连接正常
|
|
func (a IPCAPI) enableAI(c *gin.Context, _ *struct{}) (gin.H, error) {
|
|
channelID := c.Param("id")
|
|
ctx := c.Request.Context()
|
|
|
|
// 检查全局 AI 配置
|
|
if a.uc.Conf.Server.AI.Disabled {
|
|
return nil, ErrAIGlobalDisabled
|
|
}
|
|
if a.uc.AIWebhookAPI.ai == nil {
|
|
return nil, ErrAIServiceNotReady
|
|
}
|
|
|
|
// 更新数据库中的 AI 启用状态
|
|
channel, err := a.ipc.SetAIEnabled(ctx, channelID, true)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// 构建 RTSP 地址
|
|
rtspURL, err := a.buildRTSPURL(ctx, channelID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// 启动 AI 检测任务
|
|
resp, err := a.uc.AIWebhookAPI.StartAIDetection(ctx, channel, rtspURL)
|
|
if err != nil {
|
|
slog.ErrorContext(ctx, "start camera AI", "err", err)
|
|
return nil, reason.ErrUsedLogic.SetMsg("启动 AI 检测失败: " + err.Error())
|
|
}
|
|
|
|
return gin.H{
|
|
"enabled": true,
|
|
"message": resp.GetMessage(),
|
|
"source_width": resp.GetSourceWidth(),
|
|
"source_height": resp.GetSourceHeight(),
|
|
"source_fps": resp.GetSourceFps(),
|
|
}, nil
|
|
}
|
|
|
|
// disableAI 禁用指定通道的 AI 检测功能,会同时停止正在运行的检测任务
|
|
func (a IPCAPI) disableAI(c *gin.Context, _ *struct{}) (gin.H, error) {
|
|
channelID := c.Param("id")
|
|
ctx := c.Request.Context()
|
|
|
|
// 检查全局 AI 配置
|
|
if a.uc.Conf.Server.AI.Disabled {
|
|
return nil, ErrAIGlobalDisabled
|
|
}
|
|
if a.uc.AIWebhookAPI.ai == nil {
|
|
return nil, ErrAIServiceNotReady
|
|
}
|
|
|
|
// 更新数据库中的 AI 启用状态
|
|
if _, err := a.ipc.SetAIEnabled(ctx, channelID, false); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// 停止 AI 检测任务
|
|
if err := a.uc.AIWebhookAPI.StopAIDetection(ctx, channelID); err != nil {
|
|
slog.ErrorContext(ctx, "stop camera AI", "err", err)
|
|
}
|
|
|
|
return gin.H{
|
|
"enabled": false,
|
|
"message": "AI 检测已停止",
|
|
}, nil
|
|
}
|
|
|
|
// buildRTSPURL 根据通道类型构建对应的 RTSP 播放地址
|
|
func (a IPCAPI) buildRTSPURL(ctx context.Context, channelID string) (string, error) {
|
|
svr, err := a.uc.SMSAPI.smsCore.GetMediaServer(ctx, sms.DefaultMediaServerID)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
var app, stream string
|
|
|
|
if bz.IsGB28181(channelID) {
|
|
app = "rtp"
|
|
stream = channelID
|
|
} else if bz.IsOnvif(channelID) {
|
|
app = "live"
|
|
stream = channelID
|
|
} else if bz.IsRTSP(channelID) {
|
|
proxy, err := a.uc.ProxyAPI.proxyCore.GetStreamProxy(ctx, channelID)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
app = "live"
|
|
stream = proxy.Stream
|
|
} else if bz.IsRTMP(channelID) {
|
|
pu, err := a.uc.MediaAPI.pushCore.GetStreamPush(ctx, channelID)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
app = pu.App
|
|
stream = pu.Stream
|
|
} else {
|
|
return "", ErrChannelNotSupported
|
|
}
|
|
|
|
return fmt.Sprintf("rtsp://%s:%d/%s/%s", "127.0.0.1", svr.Ports.RTSP, app, stream), nil
|
|
}
|