mirror of
https://github.com/gowvp/gb28181.git
synced 2025-09-27 03:45:57 +08:00
226 lines
7.2 KiB
Go
226 lines
7.2 KiB
Go
package api
|
||
|
||
import (
|
||
"expvar"
|
||
"fmt"
|
||
"log/slog"
|
||
"net/http"
|
||
"path/filepath"
|
||
"runtime"
|
||
"runtime/debug"
|
||
"sort"
|
||
"strings"
|
||
"time"
|
||
|
||
"github.com/gin-contrib/gzip"
|
||
"github.com/gin-gonic/gin"
|
||
"github.com/gowvp/gb28181/internal/core/media"
|
||
"github.com/gowvp/gb28181/plugin/stat"
|
||
"github.com/gowvp/gb28181/plugin/stat/statapi"
|
||
"github.com/ixugo/goweb/pkg/system"
|
||
"github.com/ixugo/goweb/pkg/web"
|
||
)
|
||
|
||
var startRuntime = time.Now()
|
||
|
||
func setupRouter(r *gin.Engine, uc *Usecase) {
|
||
go stat.LoadTop(system.Getwd(), func(m map[string]any) {
|
||
_ = m
|
||
})
|
||
r.Use(
|
||
// 格式化输出到控制台,然后记录到日志
|
||
// 此处不做 recover,底层 http.server 也会 recover,但不会输出方便查看的格式
|
||
gin.CustomRecovery(func(c *gin.Context, err any) {
|
||
slog.Error("panic", "err", err, "stack", string(debug.Stack()))
|
||
c.AbortWithStatus(http.StatusInternalServerError)
|
||
}),
|
||
web.Metrics(),
|
||
web.Logger(slog.Default(), func(_ *gin.Context) bool {
|
||
// true:记录请求响应报文
|
||
return uc.Conf.Server.Debug
|
||
}),
|
||
)
|
||
go web.CountGoroutines(10*time.Minute, 20)
|
||
|
||
const staticPrefix = "/web"
|
||
const staticDir = "www"
|
||
admin := r.Group(staticPrefix, gzip.Gzip(gzip.DefaultCompression))
|
||
admin.Static("/", filepath.Join(system.Getwd(), staticDir))
|
||
r.NoRoute(func(c *gin.Context) {
|
||
// react-router 路由指向前端资源
|
||
if strings.HasPrefix(c.Request.URL.Path, staticPrefix) {
|
||
c.File(filepath.Join(system.Getwd(), staticDir, "index.html"))
|
||
return
|
||
}
|
||
c.JSON(404, "来到了无人的荒漠")
|
||
})
|
||
// 访问根路径时重定向到前端资源
|
||
r.GET("/", func(ctx *gin.Context) {
|
||
ctx.Redirect(http.StatusPermanentRedirect, filepath.Join(staticPrefix, "index.html"))
|
||
})
|
||
|
||
auth := web.AuthMiddleware(uc.Conf.Server.HTTP.JwtSecret)
|
||
r.GET("/health", web.WarpH(uc.getHealth))
|
||
r.GET("/app/metrics/api", web.WarpH(uc.getMetricsAPI))
|
||
|
||
registerVersionAPI(r, uc.Version, auth)
|
||
statapi.Register(r)
|
||
registerZLMWebhookAPI(r, uc.WebHookAPI)
|
||
// TODO: 待增加鉴权
|
||
registerMediaAPI(r, uc.MediaAPI)
|
||
|
||
// TODO: 临时播放接口,待重构
|
||
r.POST("/channels/:id/play", web.WarpH(func(c *gin.Context, _ *struct{}) (*playOutput, error) {
|
||
channelID := c.Param("id")
|
||
|
||
// TODO: 目前仅开发到 rtsp,待扩展 rtsp/gb 等
|
||
if !strings.HasPrefix(channelID, media.RTMPIDPrefix) {
|
||
return nil, web.ErrNotFound.Msg("不支持的播放通道")
|
||
}
|
||
|
||
push, err := uc.MediaAPI.mediaCore.GetStreamPush(c.Request.Context(), channelID)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
if push.Status != media.StatusPushing {
|
||
return nil, web.ErrNotFound.Msg("未推流")
|
||
}
|
||
|
||
svr, err := uc.SMSAPI.smsCore.GetMediaServer(c.Request.Context(), push.MediaServerID)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
stream := push.App + "/" + push.Stream
|
||
host := c.Request.Host
|
||
if l := strings.Split(c.Request.Host, ":"); len(l) == 2 {
|
||
host = l[0]
|
||
}
|
||
var session string
|
||
if !push.IsAuthDisabled && push.Session != "" {
|
||
session = "session=" + push.Session
|
||
}
|
||
|
||
// 播放规则
|
||
// https://github.com/zlmediakit/ZLMediaKit/wiki/%E6%92%AD%E6%94%BEurl%E8%A7%84%E5%88%99
|
||
return &playOutput{
|
||
App: push.App,
|
||
Stream: push.Stream,
|
||
Items: []streamAddrItem{
|
||
{
|
||
Label: "默认线路",
|
||
WSFLV: fmt.Sprintf("ws://%s:%d/%s.live.flv", host, svr.Ports.HTTP, stream) + "?" + session,
|
||
HTTPFLV: fmt.Sprintf("http://%s:%d/%s.live.flv", host, svr.Ports.HTTP, 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/index/api/webrtc?app=%s&stream=%s&type=play", host, svr.Ports.HTTP, push.App, push.Stream) + "&" + session,
|
||
HLS: fmt.Sprintf("http://%s:%d/%s/hls.fmp4.m3u8", host, svr.Ports.HTTP, 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, push.App, push.Stream) + "&" + session,
|
||
HLS: fmt.Sprintf("https://%s:%d/%s/hls.fmp4.m3u8", host, svr.Ports.HTTPS, stream) + "?" + session,
|
||
},
|
||
},
|
||
}, nil
|
||
}))
|
||
}
|
||
|
||
type playOutput struct {
|
||
App string `json:"app"`
|
||
Stream string `json:"stream"`
|
||
Items []streamAddrItem `json:"items"`
|
||
}
|
||
type streamAddrItem struct {
|
||
Label string `json:"label"`
|
||
WSFLV string `json:"ws_flv"`
|
||
HTTPFLV string `json:"http_flv"`
|
||
RTMP string `json:"rtmp"`
|
||
RTSP string `json:"rtsp"`
|
||
WebRTC string `json:"webrtc"`
|
||
HLS string `json:"hls"`
|
||
}
|
||
|
||
type getHealthOutput struct {
|
||
Version string `json:"version"`
|
||
StartAt time.Time `json:"start_at"`
|
||
GitBranch string `json:"git_branch"`
|
||
GitHash string `json:"git_hash"`
|
||
}
|
||
|
||
func (uc *Usecase) getHealth(_ *gin.Context, _ *struct{}) (getHealthOutput, error) {
|
||
return getHealthOutput{
|
||
Version: uc.Conf.BuildVersion,
|
||
GitBranch: strings.Trim(expvar.Get("git_branch").String(), `"`),
|
||
GitHash: strings.Trim(expvar.Get("git_hash").String(), `"`),
|
||
StartAt: startRuntime,
|
||
}, nil
|
||
}
|
||
|
||
type getMetricsAPIOutput struct {
|
||
RealTimeRequests int64 `json:"real_time_requests"` // 实时请求数
|
||
TotalRequests int64 `json:"total_requests"` // 总请求数
|
||
TotalResponses int64 `json:"total_responses"` // 总响应数
|
||
RequestTop10 []KV `json:"request_top10"` // 请求TOP10
|
||
StatusCodeTop10 []KV `json:"status_code_top10"` // 状态码TOP10
|
||
Goroutines any `json:"goroutines"` // 协程数量
|
||
NumGC uint32 `json:"num_gc"` // gc 次数
|
||
SysAlloc uint64 `json:"sys_alloc"` // 内存占用
|
||
StartAt string `json:"start_at"` // 运行时间
|
||
}
|
||
|
||
func (uc *Usecase) getMetricsAPI(_ *gin.Context, _ *struct{}) (*getMetricsAPIOutput, error) {
|
||
req := expvar.Get("request").(*expvar.Int).Value()
|
||
reqs := expvar.Get("requests").(*expvar.Int).Value()
|
||
resps := expvar.Get("responses").(*expvar.Int).Value()
|
||
urls := expvar.Get(`requestURLs`).(*expvar.Map)
|
||
status := expvar.Get(`statusCodes`).(*expvar.Map)
|
||
u := sortExpvarMap(urls, 10)
|
||
s := sortExpvarMap(status, 10)
|
||
g := expvar.Get("goroutine_num").(expvar.Func)
|
||
|
||
var stats runtime.MemStats
|
||
runtime.ReadMemStats(&stats)
|
||
|
||
return &getMetricsAPIOutput{
|
||
RealTimeRequests: req,
|
||
TotalRequests: reqs,
|
||
TotalResponses: resps,
|
||
RequestTop10: u,
|
||
StatusCodeTop10: s,
|
||
Goroutines: g(),
|
||
NumGC: stats.NumGC,
|
||
SysAlloc: stats.Sys,
|
||
StartAt: startRuntime.Format(time.DateTime),
|
||
}, nil
|
||
}
|
||
|
||
type KV struct {
|
||
Key string
|
||
Value int64
|
||
}
|
||
|
||
func sortExpvarMap(data *expvar.Map, top int) []KV {
|
||
kvs := make([]KV, 0, 8)
|
||
data.Do(func(kv expvar.KeyValue) {
|
||
kvs = append(kvs, KV{
|
||
Key: kv.Key,
|
||
Value: kv.Value.(*expvar.Int).Value(),
|
||
})
|
||
})
|
||
|
||
sort.Slice(kvs, func(i, j int) bool {
|
||
return kvs[i].Value > kvs[j].Value
|
||
})
|
||
|
||
idx := top
|
||
if l := len(kvs); l < top {
|
||
idx = len(kvs)
|
||
}
|
||
return kvs[:idx]
|
||
}
|