You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
RTSPtoWeb/apiHTTPRouter.go

410 lines
13 KiB
Go

package main
import (
"io/ioutil"
"net/http"
"os"
"path/filepath"
"strings"
"time"
"github.com/gin-gonic/autotls"
"github.com/gin-gonic/gin"
"github.com/gomarkdown/markdown"
"github.com/gomarkdown/markdown/html"
"github.com/gomarkdown/markdown/parser"
"github.com/sirupsen/logrus"
)
// Message resp struct
type Message struct {
Status int `json:"status"`
Payload interface{} `json:"payload"`
}
// HTTPAPIServer start http server routes
func HTTPAPIServer() {
//Set HTTP API mode
log.WithFields(logrus.Fields{
"module": "http_server",
"func": "RTSPServer",
"call": "Start",
}).Infoln("Server HTTP start")
var public *gin.Engine
if !Storage.ServerHTTPDebug() {
gin.SetMode(gin.ReleaseMode)
public = gin.New()
} else {
gin.SetMode(gin.DebugMode)
public = gin.Default()
}
public.Use(CrossOrigin())
//Add private login password protect methods
privat := public.Group("/")
if Storage.ServerHTTPLogin() != "" && Storage.ServerHTTPPassword() != "" {
privat.Use(gin.BasicAuth(gin.Accounts{Storage.ServerHTTPLogin(): Storage.ServerHTTPPassword()}))
}
/*
Static HTML Files Demo Mode
*/
if Storage.ServerHTTPDemo() {
public.LoadHTMLGlob(Storage.ServerHTTPDir() + "/templates/*")
public.GET("/", HTTPAPIServerIndex)
public.GET("/pages/stream/list", HTTPAPIStreamList)
public.GET("/pages/stream/add", HTTPAPIAddStream)
public.GET("/pages/stream/edit/:uuid", HTTPAPIEditStream)
public.GET("/pages/player/hls/:uuid/:channel", HTTPAPIPlayHls)
public.GET("/pages/player/mse/:uuid/:channel", HTTPAPIPlayMse)
public.GET("/pages/player/webrtc/:uuid/:channel", HTTPAPIPlayWebrtc)
public.GET("/pages/multiview", HTTPAPIMultiview)
public.Any("/pages/multiview/full", HTTPAPIFullScreenMultiView)
public.GET("/pages/documentation", HTTPAPIServerDocumentation)
public.GET("/pages/player/all/:uuid/:channel", HTTPAPIPlayAll)
public.GET("/pages/cameras", HTTPAPICameraManagement)
public.StaticFS("/static", http.Dir(Storage.ServerHTTPDir()+"/static"))
public.GET("/docs/*filepath", HTTPAPIMarkdownRenderer)
}
/*
Stream Control elements
*/
privat.GET("/streams", HTTPAPIServerStreams)
privat.POST("/stream/:uuid/add", HTTPAPIServerStreamAdd)
privat.POST("/stream/:uuid/edit", HTTPAPIServerStreamEdit)
privat.GET("/stream/:uuid/delete", HTTPAPIServerStreamDelete)
privat.GET("/stream/:uuid/reload", HTTPAPIServerStreamReload)
privat.GET("/stream/:uuid/info", HTTPAPIServerStreamInfo)
/*
Streams Multi Control elements
*/
privat.POST("/streams/multi/control/add", HTTPAPIServerStreamsMultiControlAdd)
privat.POST("/streams/multi/control/delete", HTTPAPIServerStreamsMultiControlDelete)
/*
Camera Management elements
*/
privat.GET("/cameras", HTTPAPIServerCameras)
privat.GET("/cameras/unit/:unitcode", HTTPAPIServerCamerasByUnitCode)
privat.POST("/camera/add", HTTPAPIServerCameraAdd)
privat.GET("/camera/:uuid", HTTPAPIServerCameraGet)
privat.PUT("/camera/:uuid", HTTPAPIServerCameraUpdate)
privat.DELETE("/camera/:uuid", HTTPAPIServerCameraDelete)
privat.POST("/cameras/refresh", HTTPAPIServerCameraRefresh)
privat.GET("/database/status", HTTPAPIServerDatabaseStatus)
/*
Java Integration API elements
*/
public.GET("/api/java/cameras", HTTPJavaAPICameras)
public.GET("/api/java/streams", HTTPJavaAPIStreams)
public.GET("/api/java/camera/:id", HTTPJavaAPICameraDetail)
public.GET("/api/java/system/info", HTTPJavaAPISystemInfo)
/*
Stream Channel elements
*/
privat.POST("/stream/:uuid/channel/:channel/add", HTTPAPIServerStreamChannelAdd)
privat.POST("/stream/:uuid/channel/:channel/edit", HTTPAPIServerStreamChannelEdit)
privat.GET("/stream/:uuid/channel/:channel/delete", HTTPAPIServerStreamChannelDelete)
privat.GET("/stream/:uuid/channel/:channel/codec", HTTPAPIServerStreamChannelCodec)
privat.GET("/stream/:uuid/channel/:channel/reload", HTTPAPIServerStreamChannelReload)
privat.GET("/stream/:uuid/channel/:channel/info", HTTPAPIServerStreamChannelInfo)
/*
Stream video elements
*/
//HLS
public.GET("/stream/:uuid/channel/:channel/hls/live/index.m3u8", HTTPAPIServerStreamHLSM3U8)
public.GET("/stream/:uuid/channel/:channel/hls/live/segment/:seq/file.ts", HTTPAPIServerStreamHLSTS)
//HLS remote record
//public.GET("/stream/:uuid/channel/:channel/hls/rr/:s/:e/index.m3u8", HTTPAPIServerStreamRRM3U8)
//public.GET("/stream/:uuid/channel/:channel/hls/rr/:s/:e/:seq/file.ts", HTTPAPIServerStreamRRTS)
//HLS LL
public.GET("/stream/:uuid/channel/:channel/hlsll/live/index.m3u8", HTTPAPIServerStreamHLSLLM3U8)
public.GET("/stream/:uuid/channel/:channel/hlsll/live/init.mp4", HTTPAPIServerStreamHLSLLInit)
public.GET("/stream/:uuid/channel/:channel/hlsll/live/segment/:segment/:any", HTTPAPIServerStreamHLSLLM4Segment)
public.GET("/stream/:uuid/channel/:channel/hlsll/live/fragment/:segment/:fragment/:any", HTTPAPIServerStreamHLSLLM4Fragment)
//MSE
public.GET("/stream/:uuid/channel/:channel/mse", HTTPAPIServerStreamMSE)
public.POST("/stream/:uuid/channel/:channel/webrtc", HTTPAPIServerStreamWebRTC)
//Save fragment to mp4
public.GET("/stream/:uuid/channel/:channel/save/mp4/fragment/:duration", HTTPAPIServerStreamSaveToMP4)
/*
HTTPS Mode Cert
# Key considerations for algorithm "RSA" ≥ 2048-bit
openssl genrsa -out server.key 2048
# Key considerations for algorithm "ECDSA" ≥ secp384r1
# List ECDSA the supported curves (openssl ecparam -list_curves)
#openssl ecparam -genkey -name secp384r1 -out server.key
#Generation of self-signed(x509) public key (PEM-encodings .pem|.crt) based on the private (.key)
openssl req -new -x509 -sha256 -key server.key -out server.crt -days 3650
*/
if Storage.ServerHTTPS() {
if Storage.ServerHTTPSAutoTLSEnable() {
go func() {
err := autotls.Run(public, Storage.ServerHTTPSAutoTLSName()+Storage.ServerHTTPSPort())
if err != nil {
log.Println("Start HTTPS Server Error", err)
}
}()
} else {
go func() {
err := public.RunTLS(Storage.ServerHTTPSPort(), Storage.ServerHTTPSCert(), Storage.ServerHTTPSKey())
if err != nil {
log.WithFields(logrus.Fields{
"module": "http_router",
"func": "HTTPSAPIServer",
"call": "ServerHTTPSPort",
}).Fatalln(err.Error())
os.Exit(1)
}
}()
}
}
err := public.Run(Storage.ServerHTTPPort())
if err != nil {
log.WithFields(logrus.Fields{
"module": "http_router",
"func": "HTTPAPIServer",
"call": "ServerHTTPPort",
}).Fatalln(err.Error())
os.Exit(1)
}
}
// HTTPAPIServerIndex index file
func HTTPAPIServerIndex(c *gin.Context) {
c.HTML(http.StatusOK, "index.tmpl", gin.H{
"port": Storage.ServerHTTPPort(),
"streams": Storage.Streams,
"version": time.Now().String(),
"page": "index",
})
}
func HTTPAPIServerDocumentation(c *gin.Context) {
c.HTML(http.StatusOK, "documentation.tmpl", gin.H{
"port": Storage.ServerHTTPPort(),
"streams": Storage.Streams,
"version": time.Now().String(),
"page": "documentation",
})
}
func HTTPAPIStreamList(c *gin.Context) {
c.HTML(http.StatusOK, "stream_list.tmpl", gin.H{
"port": Storage.ServerHTTPPort(),
"streams": Storage.Streams,
"version": time.Now().String(),
"page": "stream_list",
})
}
func HTTPAPIPlayHls(c *gin.Context) {
c.HTML(http.StatusOK, "play_hls.tmpl", gin.H{
"port": Storage.ServerHTTPPort(),
"streams": Storage.Streams,
"version": time.Now().String(),
"page": "play_hls",
"uuid": c.Param("uuid"),
"channel": c.Param("channel"),
})
}
func HTTPAPIPlayMse(c *gin.Context) {
c.HTML(http.StatusOK, "play_mse.tmpl", gin.H{
"port": Storage.ServerHTTPPort(),
"streams": Storage.Streams,
"version": time.Now().String(),
"page": "play_mse",
"uuid": c.Param("uuid"),
"channel": c.Param("channel"),
})
}
func HTTPAPIPlayWebrtc(c *gin.Context) {
c.HTML(http.StatusOK, "play_webrtc.tmpl", gin.H{
"port": Storage.ServerHTTPPort(),
"streams": Storage.Streams,
"version": time.Now().String(),
"page": "play_webrtc",
"uuid": c.Param("uuid"),
"channel": c.Param("channel"),
})
}
func HTTPAPIAddStream(c *gin.Context) {
c.HTML(http.StatusOK, "add_stream.tmpl", gin.H{
"port": Storage.ServerHTTPPort(),
"streams": Storage.Streams,
"version": time.Now().String(),
"page": "add_stream",
})
}
func HTTPAPIEditStream(c *gin.Context) {
c.HTML(http.StatusOK, "edit_stream.tmpl", gin.H{
"port": Storage.ServerHTTPPort(),
"streams": Storage.Streams,
"version": time.Now().String(),
"page": "edit_stream",
"uuid": c.Param("uuid"),
})
}
func HTTPAPIMultiview(c *gin.Context) {
c.HTML(http.StatusOK, "multiview.tmpl", gin.H{
"port": Storage.ServerHTTPPort(),
"streams": Storage.Streams,
"version": time.Now().String(),
"page": "multiview",
})
}
func HTTPAPIPlayAll(c *gin.Context) {
c.HTML(http.StatusOK, "play_all.tmpl", gin.H{
"port": Storage.ServerHTTPPort(),
"streams": Storage.Streams,
"version": time.Now().String(),
"page": "play_all",
"uuid": c.Param("uuid"),
"channel": c.Param("channel"),
})
}
type MultiViewOptions struct {
Grid int `json:"grid"`
Player map[string]MultiViewOptionsGrid `json:"player"`
}
type MultiViewOptionsGrid struct {
UUID string `json:"uuid"`
Channel int `json:"channel"`
PlayerType string `json:"playerType"`
}
func HTTPAPIFullScreenMultiView(c *gin.Context) {
var createParams MultiViewOptions
err := c.ShouldBindJSON(&createParams)
if err != nil {
log.WithFields(logrus.Fields{
"module": "http_page",
"func": "HTTPAPIFullScreenMultiView",
"call": "BindJSON",
}).Errorln(err.Error())
}
log.WithFields(logrus.Fields{
"module": "http_page",
"func": "HTTPAPIFullScreenMultiView",
"call": "Options",
}).Debugln(createParams)
c.HTML(http.StatusOK, "fullscreenmulti.tmpl", gin.H{
"port": Storage.ServerHTTPPort(),
"streams": Storage.Streams,
"version": time.Now().String(),
"options": createParams,
"page": "fullscreenmulti",
"query": c.Request.URL.Query(),
})
}
// HTTPAPIMarkdownRenderer renders markdown files as HTML
func HTTPAPIMarkdownRenderer(c *gin.Context) {
filePath := c.Param("filepath")
// Remove leading slash
if strings.HasPrefix(filePath, "/") {
filePath = filePath[1:]
}
// Construct full file path
fullPath := filepath.Join("./docs", filePath)
// Check if file exists and has .md extension
if !strings.HasSuffix(filePath, ".md") {
c.String(http.StatusNotFound, "只支持 .md 文件")
return
}
// Read markdown file
markdownBytes, err := ioutil.ReadFile(fullPath)
if err != nil {
c.String(http.StatusNotFound, "文件未找到")
return
}
// Create markdown parser with extensions
extensions := parser.CommonExtensions | parser.AutoHeadingIDs | parser.NoEmptyLineBeforeBlock
p := parser.NewWithExtensions(extensions)
doc := p.Parse(markdownBytes)
// Create HTML renderer with extensions
htmlFlags := html.CommonFlags | html.HrefTargetBlank
opts := html.RendererOptions{Flags: htmlFlags}
renderer := html.NewRenderer(opts)
// Render markdown to HTML
htmlContent := markdown.Render(doc, renderer)
// Create HTML content by concatenating strings
htmlStart := `<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>API 文档</title>
<link rel="stylesheet" href="/static/plugins/bootstrap/css/bootstrap.min.css">
<style>
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; }
.container { max-width: 1200px; margin: 0 auto; padding: 20px; }
.markdown-body { line-height: 1.6; }
.markdown-body h1, .markdown-body h2, .markdown-body h3 { color: #333; margin-top: 24px; margin-bottom: 16px; }
.markdown-body code { background-color: #f6f8fa; padding: 2px 4px; border-radius: 3px; }
.markdown-body pre { background-color: #f6f8fa; padding: 16px; border-radius: 6px; overflow: auto; }
.markdown-body table { border-collapse: collapse; width: 100%; }
.markdown-body th, .markdown-body td { border: 1px solid #dfe2e5; padding: 6px 13px; }
.markdown-body th { background-color: #f6f8fa; font-weight: 600; }
</style>
</head>
<body>
<div class="container">
<nav class="mb-4">
<a href="/" class="btn btn-secondary">首页</a>
<a href="/pages/documentation" class="btn btn-secondary ml-2">返回文档</a>
</nav>
<div class="markdown-body">`
htmlEnd := ` </div>
</div>
</body>
</html>`
// Combine HTML parts with markdown content
finalHTML := htmlStart + string(htmlContent) + htmlEnd
c.Header("Content-Type", "text/html; charset=utf-8")
c.String(http.StatusOK, finalHTML)
}
// CrossOrigin Access-Control-Allow-Origin any methods
func CrossOrigin() gin.HandlerFunc {
return func(c *gin.Context) {
c.Writer.Header().Set("Access-Control-Allow-Origin", "*")
c.Writer.Header().Set("Access-Control-Allow-Credentials", "true")
c.Writer.Header().Set("Access-Control-Allow-Headers", "Content-Type, Content-Length, Accept-Encoding, X-CSRF-Token, Authorization, accept, origin, Cache-Control, X-Requested-With")
c.Writer.Header().Set("Access-Control-Allow-Methods", "POST, OPTIONS, GET, PUT, DELETE")
if c.Request.Method == "OPTIONS" {
c.AbortWithStatus(204)
return
}
c.Next()
}
}