feat: add stop subscribe api, show reasons for subscriber closure

This commit is contained in:
langhuihui
2023-08-06 14:16:06 +08:00
parent 9a352bcbad
commit 1a347b5a0b
24 changed files with 238 additions and 188 deletions

View File

@@ -36,7 +36,8 @@
- 热更新配置信息 `/api/updateconfig?name=xxx` 热更新xxx插件的配置信息如果不带参数或参数为空则热更新全局配置 - 热更新配置信息 `/api/updateconfig?name=xxx` 热更新xxx插件的配置信息如果不带参数或参数为空则热更新全局配置
- 获取所有远端拉流信息 `/api/list/pull` 返回{RemoteURL:"",StreamPath:"",Type:"",StartTime:""} - 获取所有远端拉流信息 `/api/list/pull` 返回{RemoteURL:"",StreamPath:"",Type:"",StartTime:""}
- 获取所有向远端推流信息 `/api/list/push` 返回{RemoteURL:"",StreamPath:"",Type:"",StartTime:""} - 获取所有向远端推流信息 `/api/list/push` 返回{RemoteURL:"",StreamPath:"",Type:"",StartTime:""}
- 停止推流 `/api/stoppush?url=xxx` 停止向xxx推流 成功返回ok - 停止推流 `/api/stop/push?url=xxx` 停止向xxx推流 成功返回ok
- 停止某个订阅者 `/api/stop/subscribe?streamPath=xxx&id=xxx` 停止xxx流的xxx订阅者 成功返回ok
- 插入SEI帧 `/api/insertsei?streamPath=xxx&type=5` 向xxx流内插入SEI帧 成功返回ok。type为SEI类型可选默认是5 - 插入SEI帧 `/api/insertsei?streamPath=xxx&type=5` 向xxx流内插入SEI帧 成功返回ok。type为SEI类型可选默认是5
# 引擎默认配置 # 引擎默认配置
```yaml ```yaml

View File

@@ -177,6 +177,7 @@ func (av *AVFrame) Reset() {
av.ADTS = nil av.ADTS = nil
} }
av.Timestamp = 0 av.Timestamp = 0
av.IFrame = false
av.DataFrame.Reset() av.DataFrame.Reset()
} }

View File

@@ -135,7 +135,6 @@ type Engine struct {
LogLang string `default:"zh"` //日志语言 LogLang string `default:"zh"` //日志语言
LogLevel string `default:"info"` //日志级别 LogLevel string `default:"info"` //日志级别
RTPReorderBufferLen int `default:"50"` //RTP重排序缓冲长度 RTPReorderBufferLen int `default:"50"` //RTP重排序缓冲长度
SpeedLimit time.Duration `default:"500ms"` //速度限制最大等待时间
EventBusSize int `default:"10"` //事件总线大小 EventBusSize int `default:"10"` //事件总线大小
PulseInterval time.Duration `default:"5s"` //心跳事件间隔 PulseInterval time.Duration `default:"5s"` //心跳事件间隔
DisableAll bool `default:"false"` //禁用所有插件 DisableAll bool `default:"false"` //禁用所有插件

16
go.mod
View File

@@ -3,18 +3,18 @@ module m7s.live/engine/v4
go 1.19 go 1.19
require ( require (
github.com/aler9/gortsplib/v2 v2.2.2 github.com/bluenviron/mediacommon v0.7.0
github.com/cnotch/ipchub v1.1.0 github.com/cnotch/ipchub v1.1.0
github.com/google/uuid v1.3.0 github.com/google/uuid v1.3.0
github.com/logrusorgru/aurora v2.0.3+incompatible github.com/logrusorgru/aurora v2.0.3+incompatible
github.com/mcuadros/go-defaults v1.2.0 github.com/mcuadros/go-defaults v1.2.0
github.com/pion/rtp v1.7.13 github.com/pion/rtp v1.8.0
github.com/pion/webrtc/v3 v3.1.49 github.com/pion/webrtc/v3 v3.1.56
github.com/q191201771/naza v0.30.8 github.com/q191201771/naza v0.30.8
github.com/quic-go/quic-go v0.32.0 github.com/quic-go/quic-go v0.32.0
github.com/shirou/gopsutil/v3 v3.22.10 github.com/shirou/gopsutil/v3 v3.22.11
go.uber.org/zap v1.23.0 go.uber.org/zap v1.24.0
golang.org/x/net v0.8.0 golang.org/x/net v0.12.0
golang.org/x/sync v0.1.0 golang.org/x/sync v0.1.0
gopkg.in/yaml.v3 v3.0.1 gopkg.in/yaml.v3 v3.0.1
) )
@@ -43,9 +43,9 @@ require (
github.com/tklauser/numcpus v0.6.0 // indirect github.com/tklauser/numcpus v0.6.0 // indirect
github.com/yapingcat/gomedia v0.0.0-20230426092936-387031404274 github.com/yapingcat/gomedia v0.0.0-20230426092936-387031404274
github.com/yusufpapurcu/wmi v1.2.2 // indirect github.com/yusufpapurcu/wmi v1.2.2 // indirect
golang.org/x/crypto v0.4.0 // indirect golang.org/x/crypto v0.11.0 // indirect
golang.org/x/exp v0.0.0-20221205204356-47842c84f3db // indirect golang.org/x/exp v0.0.0-20221205204356-47842c84f3db // indirect
golang.org/x/mod v0.7.0 // indirect golang.org/x/mod v0.7.0 // indirect
golang.org/x/sys v0.6.0 // indirect golang.org/x/sys v0.10.0 // indirect
golang.org/x/tools v0.3.0 // indirect golang.org/x/tools v0.3.0 // indirect
) )

8
go.sum
View File

@@ -1,7 +1,7 @@
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/aler9/gortsplib/v2 v2.2.2 h1:tTw8pdKSOEjlZjjE1S4ftXPHJkYOqjNNv3hjQ0Nto9M=
github.com/aler9/gortsplib/v2 v2.2.2/go.mod h1:k6uBVHGwsIc/0L5SLLqWwi6bSJUb4VR0HfvncyHlKQI=
github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8= github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8=
github.com/bluenviron/mediacommon v0.7.0 h1:dJWLLL9oDbAqfK8KuNfnDUQwNbeMAtGeRjZc9Vo95js=
github.com/bluenviron/mediacommon v0.7.0/go.mod h1:wuLJdxcITiSPgY1MvQqrX+qPlKmNfeV9wNvXth5M98I=
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
@@ -141,15 +141,13 @@ github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8= github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
github.com/tklauser/go-sysconf v0.3.10/go.mod h1:C8XykCvCb+Gn0oNCWPIlcb0RuglQTYaQ2hGm7jmxEFk= github.com/tklauser/go-sysconf v0.3.10/go.mod h1:C8XykCvCb+Gn0oNCWPIlcb0RuglQTYaQ2hGm7jmxEFk=
github.com/tklauser/go-sysconf v0.3.11 h1:89WgdJhk5SNwJfu+GKyYveZ4IaJ7xAkecBo+KdJV0CM= github.com/tklauser/go-sysconf v0.3.11 h1:89WgdJhk5SNwJfu+GKyYveZ4IaJ7xAkecBo+KdJV0CM=
github.com/tklauser/go-sysconf v0.3.11/go.mod h1:GqXfhXY3kiPa0nAXPDIQIWzJbMCB7AmcWpGR8lSZfqI= github.com/tklauser/go-sysconf v0.3.11/go.mod h1:GqXfhXY3kiPa0nAXPDIQIWzJbMCB7AmcWpGR8lSZfqI=
github.com/tklauser/numcpus v0.4.0/go.mod h1:1+UI3pD8NW14VMwdgJNJ1ESk2UnwhAnz5hMwiKKqXCQ= github.com/tklauser/numcpus v0.4.0/go.mod h1:1+UI3pD8NW14VMwdgJNJ1ESk2UnwhAnz5hMwiKKqXCQ=
github.com/tklauser/numcpus v0.6.0 h1:kebhY2Qt+3U6RNK7UqpYNA+tJ23IBEGKkB7JQBfDYms= github.com/tklauser/numcpus v0.6.0 h1:kebhY2Qt+3U6RNK7UqpYNA+tJ23IBEGKkB7JQBfDYms=
github.com/tklauser/numcpus v0.6.0/go.mod h1:FEZLMke0lhOUG6w2JadTzp0a+Nl8PF/GFkQ5UVIcaL4= github.com/tklauser/numcpus v0.6.0/go.mod h1:FEZLMke0lhOUG6w2JadTzp0a+Nl8PF/GFkQ5UVIcaL4=
github.com/yapingcat/gomedia v0.0.0-20230222121919-c67df405bf33 h1:uyZY++dluUg7iTSsNzuOVln/mC2U2KXwgKLfKLCJ74Y=
github.com/yapingcat/gomedia v0.0.0-20230222121919-c67df405bf33/go.mod h1:WSZ59bidJOO40JSJmLqlkBJrjZCtjbKKkygEMfzY/kc=
github.com/yapingcat/gomedia v0.0.0-20230426092936-387031404274 h1:cj4I+bvWX9I+Hg6tnZ7DAiOVxzhyLhdvYVKp+WpM/2c= github.com/yapingcat/gomedia v0.0.0-20230426092936-387031404274 h1:cj4I+bvWX9I+Hg6tnZ7DAiOVxzhyLhdvYVKp+WpM/2c=
github.com/yapingcat/gomedia v0.0.0-20230426092936-387031404274/go.mod h1:WSZ59bidJOO40JSJmLqlkBJrjZCtjbKKkygEMfzY/kc= github.com/yapingcat/gomedia v0.0.0-20230426092936-387031404274/go.mod h1:WSZ59bidJOO40JSJmLqlkBJrjZCtjbKKkygEMfzY/kc=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=

57
http.go
View File

@@ -2,6 +2,7 @@ package engine
import ( import (
"encoding/json" "encoding/json"
"fmt"
"io" "io"
"net/http" "net/http"
"os" "os"
@@ -9,6 +10,7 @@ import (
"strings" "strings"
"time" "time"
"go.uber.org/zap"
"gopkg.in/yaml.v3" "gopkg.in/yaml.v3"
"m7s.live/engine/v4/codec" "m7s.live/engine/v4/codec"
"m7s.live/engine/v4/config" "m7s.live/engine/v4/config"
@@ -40,33 +42,12 @@ func (conf *GlobalConfig) ServeHTTP(rw http.ResponseWriter, r *http.Request) {
} }
} }
func fetchSummary() *Summary {
return &summary
}
func (conf *GlobalConfig) API_summary(rw http.ResponseWriter, r *http.Request) { func (conf *GlobalConfig) API_summary(rw http.ResponseWriter, r *http.Request) {
y := ShouldYaml(r) y := ShouldYaml(r)
if r.Header.Get("Accept") == "text/event-stream" { if y {
summary.Add() util.ReturnYaml(util.FetchValue(&summary), time.Second, rw, r)
defer summary.Done()
if y {
util.ReturnYaml(fetchSummary, time.Second, rw, r)
} else {
util.ReturnJson(fetchSummary, time.Second, rw, r)
}
} else { } else {
if !summary.Running() { util.ReturnJson(util.FetchValue(&summary), time.Second, rw, r)
summary.collect()
}
summary.rw.RLock()
defer summary.rw.RUnlock()
if y {
if err := yaml.NewEncoder(rw).Encode(&summary); err != nil {
http.Error(rw, err.Error(), http.StatusInternalServerError)
}
} else if err := json.NewEncoder(rw).Encode(&summary); err != nil {
http.Error(rw, err.Error(), http.StatusInternalServerError)
}
} }
} }
@@ -84,9 +65,9 @@ func (conf *GlobalConfig) API_stream(rw http.ResponseWriter, r *http.Request) {
if streamPath := r.URL.Query().Get("streamPath"); streamPath != "" { if streamPath := r.URL.Query().Get("streamPath"); streamPath != "" {
if s := Streams.Get(streamPath); s != nil { if s := Streams.Get(streamPath); s != nil {
if ShouldYaml(r) { if ShouldYaml(r) {
util.ReturnYaml(func() *Stream { return s }, time.Second, rw, r) util.ReturnYaml(util.FetchValue(s), time.Second, rw, r)
} else { } else {
util.ReturnJson(func() *Stream { return s }, time.Second, rw, r) util.ReturnJson(util.FetchValue(s), time.Second, rw, r)
} }
} else { } else {
http.Error(rw, NO_SUCH_STREAM, http.StatusNotFound) http.Error(rw, NO_SUCH_STREAM, http.StatusNotFound)
@@ -218,17 +199,35 @@ func (conf *GlobalConfig) API_list_push(w http.ResponseWriter, r *http.Request)
}, time.Second, w, r) }, time.Second, w, r)
} }
func (conf *GlobalConfig) API_stopPush(w http.ResponseWriter, r *http.Request) { func (conf *GlobalConfig) API_stop_push(w http.ResponseWriter, r *http.Request) {
q := r.URL.Query() q := r.URL.Query()
pusher, ok := Pushers.Load(q.Get("url")) pusher, ok := Pushers.Load(q.Get("url"))
if ok { if ok {
pusher.(IPusher).Stop() pusher.(IPusher).Stop()
w.Write([]byte("ok")) fmt.Fprintln(w, "ok")
} else { } else {
http.Error(w, "no such pusher", http.StatusNotFound) http.Error(w, "no such pusher", http.StatusNotFound)
} }
} }
func (conf *GlobalConfig) API_stop_subscribe(w http.ResponseWriter, r *http.Request) {
q := r.URL.Query()
streamPath := q.Get("streamPath")
id := q.Get("id")
s := Streams.Get(streamPath)
if s == nil {
http.Error(w, NO_SUCH_STREAM, http.StatusNotFound)
return
}
suber := s.Subscribers.Find(id)
if suber == nil {
http.Error(w, "no such subscriber", http.StatusNotFound)
return
}
suber.Stop(zap.String("reason", "stop by api"))
fmt.Fprintln(w, "ok")
}
func (conf *GlobalConfig) API_replay_rtpdump(w http.ResponseWriter, r *http.Request) { func (conf *GlobalConfig) API_replay_rtpdump(w http.ResponseWriter, r *http.Request) {
q := r.URL.Query() q := r.URL.Query()
streamPath := q.Get("streamPath") streamPath := q.Get("streamPath")
@@ -314,7 +313,7 @@ func (conf *GlobalConfig) API_replay_ts(w http.ResponseWriter, r *http.Request)
} else { } else {
tsReader := NewTSReader(&pub) tsReader := NewTSReader(&pub)
pub.SetIO(f) pub.SetIO(f)
go func(){ go func() {
tsReader.Feed(f) tsReader.Feed(f)
tsReader.Close() tsReader.Close()
}() }()

11
io.go
View File

@@ -12,6 +12,7 @@ import (
"time" "time"
"go.uber.org/zap" "go.uber.org/zap"
"go.uber.org/zap/zapcore"
"m7s.live/engine/v4/config" "m7s.live/engine/v4/config"
"m7s.live/engine/v4/log" "m7s.live/engine/v4/log"
"m7s.live/engine/v4/util" "m7s.live/engine/v4/util"
@@ -36,6 +37,7 @@ type AuthPub interface {
type IO struct { type IO struct {
ID string ID string
Type string Type string
RemoteAddr string
context.Context `json:"-" yaml:"-"` //不要直接设置应当通过OnEvent传入父级Context context.Context `json:"-" yaml:"-"` //不要直接设置应当通过OnEvent传入父级Context
context.CancelFunc `json:"-" yaml:"-"` //流关闭是关闭发布者或者订阅者 context.CancelFunc `json:"-" yaml:"-"` //流关闭是关闭发布者或者订阅者
*log.Logger `json:"-" yaml:"-"` *log.Logger `json:"-" yaml:"-"`
@@ -92,7 +94,7 @@ type IIO interface {
receive(string, IIO) error receive(string, IIO) error
IsClosed() bool IsClosed() bool
OnEvent(any) OnEvent(any)
Stop() Stop(reason ...zapcore.Field)
SetIO(any) SetIO(any)
SetParentCtx(context.Context) SetParentCtx(context.Context)
SetLogger(*log.Logger) SetLogger(*log.Logger)
@@ -113,9 +115,11 @@ func (i *IO) close() bool {
} }
// Stop 停止订阅或者发布,由订阅者或者发布者调用 // Stop 停止订阅或者发布,由订阅者或者发布者调用
func (io *IO) Stop() { func (io *IO) Stop(reason ...zapcore.Field) {
if io.close() { if io.close() {
io.Debug("stop", zap.Stack("stack")) io.Info("stop", reason...)
} else {
io.Warn("already stopped", reason...)
} }
} }
@@ -194,6 +198,7 @@ func (io *IO) receive(streamPath string, specific IIO) error {
} else if oldPublisher == specific { } else if oldPublisher == specific {
//断线重连 //断线重连
} else { } else {
s.Warn("duplicate publish", zap.String("type", oldPublisher.GetPublisher().Type))
return ErrDuplicatePublish return ErrDuplicatePublish
} }
} }

View File

@@ -19,6 +19,7 @@ state: 状态
initialize: 初始化 initialize: 初始化
"start read": 开始读取 "start read": 开始读取
"start pull": 开始从远端拉流 "start pull": 开始从远端拉流
"restart pull": 重新拉流
"pull failed": 拉取失败 "pull failed": 拉取失败
"wait publisher": 等待发布者发布 "wait publisher": 等待发布者发布
"wait timeout": 等待超时 "wait timeout": 等待超时
@@ -45,6 +46,7 @@ reamins: 剩余
"video track attached": 视频轨道已附加 "video track attached": 视频轨道已附加
"audio track attached": 音频轨道已附加 "audio track attached": 音频轨道已附加
"data track attached": 数据轨道已附加 "data track attached": 数据轨道已附加
"track back online": 轨道已恢复
"first frame read": 第一帧已读取 "first frame read": 第一帧已读取
"fu have no start": rtp的FU起始包丢了 "fu have no start": rtp的FU起始包丢了
"disabled by env": 被环境变量禁用 "disabled by env": 被环境变量禁用
@@ -54,4 +56,6 @@ reamins: 剩余
firstTs: 第一帧时间戳 firstTs: 第一帧时间戳
firstSeq: 第一帧序列号 firstSeq: 第一帧序列号
skipSeq: 跳过序列号 skipSeq: 跳过序列号
skipTs: 跳过时间戳 skipTs: 跳过时间戳
"nalu type not supported": nalu类型不支持
"create file": 创建文件

View File

@@ -50,6 +50,9 @@ func (ts *MemoryTs) WritePESPacket(frame *mpegts.MpegtsPESFrame, packet mpegts.M
var tsHeaderLength int var tsHeaderLength int
for i := 0; len(pesBuffers) > 0; i++ { for i := 0; len(pesBuffers) > 0; i++ {
if bigLen < mpegts.TS_PACKET_SIZE { if bigLen < mpegts.TS_PACKET_SIZE {
if i == 0 {
ts.Recycle()
}
headerItem := ts.Get(mpegts.TS_PACKET_SIZE) headerItem := ts.Get(mpegts.TS_PACKET_SIZE)
ts.BLL.Push(headerItem) ts.BLL.Push(headerItem)
bwTsHeader = &headerItem.Value bwTsHeader = &headerItem.Value

104
plugin.go
View File

@@ -294,7 +294,9 @@ func (opt *Plugin) Subscribe(streamPath string, sub ISubscriber) error {
copyConfig := *conf.GetSubscribeConfig() copyConfig := *conf.GetSubscribeConfig()
suber.Config = &copyConfig suber.Config = &copyConfig
} }
suber.ID = fmt.Sprintf("%s_%d", suber.ID, uintptr(unsafe.Pointer(suber))) if suber.ID == "" {
suber.ID = fmt.Sprintf("%d", uintptr(unsafe.Pointer(suber)))
}
return sub.Subscribe(streamPath, sub) return sub.Subscribe(streamPath, sub)
} }
@@ -310,60 +312,66 @@ var ErrNoPullConfig = errors.New("no pull config")
var Pullers sync.Map var Pullers sync.Map
func (opt *Plugin) Pull(streamPath string, url string, puller IPuller, save int) (err error) { func (opt *Plugin) Pull(streamPath string, url string, puller IPuller, save int) (err error) {
zurl := zap.String("url", url)
zpath := zap.String("path", streamPath)
opt.Info("pull", zpath, zurl)
defer func() {
if err != nil {
opt.Error("pull failed", zurl, zap.Error(err))
}
}()
conf, ok := opt.Config.(config.PullConfig) conf, ok := opt.Config.(config.PullConfig)
if !ok { if !ok {
return ErrNoPullConfig return ErrNoPullConfig
} }
pullConf := conf.GetPullConfig() pullConf := conf.GetPullConfig()
if save < 2 {
puller.init(streamPath, url, pullConf) zurl := zap.String("url", url)
puller.SetLogger(opt.Logger.With(zpath, zurl)) zpath := zap.String("path", streamPath)
go func() { opt.Info("pull", zpath, zurl)
Pullers.Store(puller, url) defer func() {
defer Pullers.Delete(puller) if err != nil {
for opt.Info("start pull", zurl); puller.Reconnect(); opt.Warn("restart pull", zurl) { opt.Error("pull failed", zurl, zap.Error(err))
if err = puller.Connect(); err != nil { }
if err == io.EOF { }()
puller.GetPublisher().Stream.Close() puller.init(streamPath, url, pullConf)
opt.Info("pull complete", zurl) puller.SetLogger(opt.Logger.With(zpath, zurl))
return badPuller := true
} go func() {
opt.Error("pull connect", zurl, zap.Error(err)) Pullers.Store(puller, url)
time.Sleep(time.Second * 5) defer Pullers.Delete(puller)
} else { for opt.Info("start pull", zurl); puller.Reconnect(); opt.Warn("restart pull", zurl) {
if err = opt.Publish(streamPath, puller); err != nil { if err = puller.Connect(); err != nil {
if stream := Streams.Get(streamPath); stream != nil { if err == io.EOF {
if stream.Publisher != puller && stream.Publisher != nil { puller.GetPublisher().Stream.Close()
io := stream.Publisher.GetPublisher() opt.Info("pull complete", zurl)
opt.Error("puller is not publisher", zap.String("ID", io.ID), zap.String("Type", io.Type), zap.Error(err))
return
} else {
opt.Warn("pull publish", zurl, zap.Error(err))
}
} else {
opt.Error("pull publish", zurl, zap.Error(err))
return return
} }
opt.Error("pull connect", zurl, zap.Error(err))
if badPuller {
return
}
time.Sleep(time.Second * 5)
} else {
if err = opt.Publish(streamPath, puller); err != nil {
if stream := Streams.Get(streamPath); stream != nil {
if stream.Publisher != puller && stream.Publisher != nil {
io := stream.Publisher.GetPublisher()
opt.Error("puller is not publisher", zap.String("ID", io.ID), zap.String("Type", io.Type), zap.Error(err))
return
} else {
opt.Warn("pull publish", zurl, zap.Error(err))
}
} else {
opt.Error("pull publish", zurl, zap.Error(err))
return
}
}
badPuller = false
if err = puller.Pull(); err != nil && !puller.IsShutdown() {
opt.Error("pull", zurl, zap.Error(err))
}
} }
if err = puller.Pull(); err != nil && !puller.IsShutdown() { if puller.IsShutdown() {
opt.Error("pull", zurl, zap.Error(err)) opt.Info("stop pull shutdown", zurl)
return
} }
} }
if puller.IsShutdown() { opt.Warn("stop pull stop reconnect", zurl)
opt.Info("stop pull shutdown", zurl) }()
return }
}
}
opt.Warn("stop pull stop reconnect", zurl)
}()
switch save { switch save {
case 1: case 1:
pullConf.AddPullOnStart(streamPath, url) pullConf.AddPullOnStart(streamPath, url)
@@ -400,7 +408,7 @@ func (opt *Plugin) Push(streamPath string, url string, pusher IPusher, save bool
pushConfig := conf.GetPushConfig() pushConfig := conf.GetPushConfig()
pusher.init(streamPath, url, pushConfig) pusher.init(streamPath, url, pushConfig)
badPusher := true
go func() { go func() {
Pushers.Store(url, pusher) Pushers.Store(url, pusher)
defer Pushers.Delete(url) defer Pushers.Delete(url)
@@ -418,10 +426,14 @@ func (opt *Plugin) Push(streamPath string, url string, pusher IPusher, save bool
opt.Error("push connect", zp, zu, zap.Error(err)) opt.Error("push connect", zp, zu, zap.Error(err))
time.Sleep(time.Second * 5) time.Sleep(time.Second * 5)
stream.Receive(pusher) // 通知stream移除订阅者 stream.Receive(pusher) // 通知stream移除订阅者
if badPusher {
return
}
} else if err = pusher.Push(); err != nil && !stream.IsClosed() { } else if err = pusher.Push(); err != nil && !stream.IsClosed() {
opt.Error("push", zp, zu, zap.Error(err)) opt.Error("push", zp, zu, zap.Error(err))
pusher.Stop() pusher.Stop()
} }
badPusher = false
if stream.IsClosed() { if stream.IsClosed() {
opt.Info("stop push closed", zp, zu) opt.Info("stop push closed", zp, zu)
return return

View File

@@ -5,7 +5,7 @@ import (
"sync" "sync"
"time" "time"
"github.com/aler9/gortsplib/v2/pkg/codecs/mpeg4audio" "github.com/bluenviron/mediacommon/pkg/codecs/mpeg4audio"
"github.com/pion/webrtc/v3/pkg/media/rtpdump" "github.com/pion/webrtc/v3/pkg/media/rtpdump"
"go.uber.org/zap" "go.uber.org/zap"
"m7s.live/engine/v4/codec" "m7s.live/engine/v4/codec"
@@ -20,7 +20,7 @@ type RTPDumpPublisher struct {
ACodec codec.AudioCodecID ACodec codec.AudioCodecID
VPayloadType uint8 VPayloadType uint8
APayloadType uint8 APayloadType uint8
other *rtpdump.Packet other rtpdump.Packet
sync.Mutex sync.Mutex
} }
@@ -77,15 +77,15 @@ func (t *RTPDumpPublisher) Feed(file *os.File) {
if needLock { if needLock {
t.Lock() t.Lock()
} }
if t.other == nil { if t.other.Payload == nil {
t.other = &packet t.other = packet
t.Unlock() t.Unlock()
needLock = true needLock = true
continue continue
} }
if packet.Offset > t.other.Offset { if packet.Offset >= t.other.Offset {
t.WriteRTP(t.other.Payload) t.WriteRTP(t.other.Payload)
t.other = &packet t.other = packet
t.Unlock() t.Unlock()
needLock = true needLock = true
continue continue

View File

@@ -2,6 +2,7 @@ package engine
import ( import (
"go.uber.org/zap" "go.uber.org/zap"
"go.uber.org/zap/zapcore"
"m7s.live/engine/v4/codec" "m7s.live/engine/v4/codec"
"m7s.live/engine/v4/common" "m7s.live/engine/v4/common"
"m7s.live/engine/v4/config" "m7s.live/engine/v4/config"
@@ -34,10 +35,11 @@ func (p *Publisher) GetPublisher() *Publisher {
return p return p
} }
func (p *Publisher) Stop() { func (p *Publisher) Stop(reason ...zapcore.Field) {
p.IO.Stop() p.IO.Stop(reason...)
p.Stream.Receive(ACTION_PUBLISHLOST) p.Stream.Receive(ACTION_PUBLISHLOST)
} }
func (p *Publisher) getAudioTrack() common.AudioTrack { func (p *Publisher) getAudioTrack() common.AudioTrack {
return p.AudioTrack return p.AudioTrack
} }

View File

@@ -105,7 +105,7 @@ type ISubscriber interface {
PlayRaw() PlayRaw()
PlayBlock(byte) PlayBlock(byte)
PlayFLV() PlayFLV()
Stop() Stop(reason ...zapcore.Field)
Subscribe(streamPath string, sub ISubscriber) error Subscribe(streamPath string, sub ISubscriber) error
} }
@@ -316,6 +316,9 @@ func (s *Subscriber) PlayBlock(subType byte) {
if hasVideo { if hasVideo {
for ctx.Err() == nil { for ctx.Err() == nil {
err := s.VideoReader.ReadFrame(subMode) err := s.VideoReader.ReadFrame(subMode)
if err == nil {
err = ctx.Err()
}
if err != nil { if err != nil {
stopReason = zap.Error(err) stopReason = zap.Error(err)
return return
@@ -361,6 +364,9 @@ func (s *Subscriber) PlayBlock(subType byte) {
} }
} }
err := s.AudioReader.ReadFrame(subMode) err := s.AudioReader.ReadFrame(subMode)
if err == nil {
err = ctx.Err()
}
if err != nil { if err != nil {
stopReason = zap.Error(err) stopReason = zap.Error(err)
return return

View File

@@ -94,6 +94,15 @@ func (s *Subscribers) AbortWait() {
} }
} }
func (s *Subscribers) Find(id string) ISubscriber {
for sub := range s.public {
if sub.GetSubscriber().ID == id {
return sub
}
}
return nil
}
func (s *Subscribers) Delete(suber ISubscriber) { func (s *Subscribers) Delete(suber ISubscriber) {
delete(s.public, suber) delete(s.public, suber)
io := suber.GetSubscriber() io := suber.GetSubscriber()

View File

@@ -1,25 +1,23 @@
package engine package engine
import ( import (
"encoding/json"
"sync" "sync"
"sync/atomic"
"time" "time"
"github.com/shirou/gopsutil/v3/cpu" "github.com/shirou/gopsutil/v3/cpu"
"github.com/shirou/gopsutil/v3/disk" "github.com/shirou/gopsutil/v3/disk"
"github.com/shirou/gopsutil/v3/mem" "github.com/shirou/gopsutil/v3/mem"
"github.com/shirou/gopsutil/v3/net" "github.com/shirou/gopsutil/v3/net"
"m7s.live/engine/v4/log"
"m7s.live/engine/v4/util" "m7s.live/engine/v4/util"
) )
var summary Summary var (
var children util.Map[string, *Summary] summary SummaryUtil
lastSummary Summary
func init() { children util.Map[string, *Summary]
go summary.Start() collectLock sync.Mutex
} )
// ServerSummary 系统摘要定义 // ServerSummary 系统摘要定义
type Summary struct { type Summary struct {
Address string Address string
@@ -36,11 +34,9 @@ type Summary struct {
Used uint64 Used uint64
Usage float64 Usage float64
} }
NetWork []NetWorkInfo NetWork []NetWorkInfo
Streams []StreamSummay Streams []StreamSummay
lastNetWork []net.IOCountersStat ts time.Time //上次更新时间
ref atomic.Int32
rw sync.RWMutex
} }
// NetWorkInfo 网速信息 // NetWorkInfo 网速信息
@@ -51,51 +47,28 @@ type NetWorkInfo struct {
ReceiveSpeed uint64 ReceiveSpeed uint64
SentSpeed uint64 SentSpeed uint64
} }
type SummaryUtil Summary
// StartSummary 开始定时采集数据,每秒一次
func (s *Summary) Start() {
for range time.Tick(time.Second) {
if s.Running() {
summary.collect()
}
}
}
func (s *Summary) Point() *Summary {
return s
}
// Running 是否正在采集数据
func (s *Summary) Running() bool {
return s.ref.Load() > 0
}
// Add 增加订阅者
func (s *Summary) Add() {
if count := s.ref.Add(1); count == 1 {
log.Info("start report summary")
} else {
log.Info("summary count", count)
}
}
// Done 删除订阅者
func (s *Summary) Done() {
if count := s.ref.Add(-1); count == 0 {
log.Info("stop report summary")
s.lastNetWork = nil
} else {
log.Info("summary count", count)
}
}
// Report 上报数据 // Report 上报数据
func (s *Summary) Report(slave *Summary) { func (s *Summary) Report(slave *Summary) {
children.Set(slave.Address, slave) children.Set(slave.Address, slave)
} }
func (s *Summary) collect() *Summary { func (s *SummaryUtil) MarshalJSON() ([]byte, error) {
s.rw.Lock() return json.Marshal(s.collect())
defer s.rw.Unlock() }
func (s *SummaryUtil) MarshalYAML() (any, error) {
return s.collect(), nil
}
func (s *SummaryUtil) collect() *Summary {
collectLock.Lock()
defer collectLock.Unlock()
dur := time.Since(s.ts)
if dur < time.Second {
return &lastSummary
}
s.ts = time.Now()
v, _ := mem.VirtualMemory() v, _ := mem.VirtualMemory()
d, _ := disk.Usage("/") d, _ := disk.Usage("/")
nv, _ := net.IOCounters(true) nv, _ := net.IOCounters(true)
@@ -119,16 +92,16 @@ func (s *Summary) collect() *Summary {
Receive: n.BytesRecv, Receive: n.BytesRecv,
Sent: n.BytesSent, Sent: n.BytesSent,
} }
if s.lastNetWork != nil && len(s.lastNetWork) > i { if len(lastSummary.NetWork) > i {
info.ReceiveSpeed = n.BytesRecv - s.lastNetWork[i].BytesRecv info.ReceiveSpeed = (n.BytesRecv - lastSummary.NetWork[i].Receive) / uint64(dur.Seconds())
info.SentSpeed = n.BytesSent - s.lastNetWork[i].BytesSent info.SentSpeed = (n.BytesSent - lastSummary.NetWork[i].Sent) / uint64(dur.Seconds())
} }
netWorks = append(netWorks, info) netWorks = append(netWorks, info)
} }
s.NetWork = netWorks s.NetWork = netWorks
s.lastNetWork = nv
s.Streams = util.MapList(&Streams, func(name string, ss *Stream) StreamSummay { s.Streams = util.MapList(&Streams, func(name string, ss *Stream) StreamSummay {
return ss.Summary() return ss.Summary()
}) })
return s lastSummary = Summary(*s)
return &lastSummary
} }

View File

@@ -5,7 +5,7 @@ import (
"io" "io"
"net" "net"
"github.com/aler9/gortsplib/v2/pkg/bits" "github.com/bluenviron/mediacommon/pkg/bits"
"go.uber.org/zap" "go.uber.org/zap"
"m7s.live/engine/v4/codec" "m7s.live/engine/v4/codec"
. "m7s.live/engine/v4/common" . "m7s.live/engine/v4/common"
@@ -171,7 +171,7 @@ func (aac *AAC) WriteSequenceHead(sh []byte) {
aac.Channels = ((config2 >> 3) & 0x0F) //声道 aac.Channels = ((config2 >> 3) & 0x0F) //声道
aac.SampleRate = uint32(codec.SamplingFrequencies[((config1&0x7)<<1)|(config2>>7)]) aac.SampleRate = uint32(codec.SamplingFrequencies[((config1&0x7)<<1)|(config2>>7)])
aac.Parse(aac.SequenceHead[2:]) aac.Parse(aac.SequenceHead[2:])
aac.Attach() go aac.Attach()
} }
func (aac *AAC) WriteAVCC(ts uint32, frame *util.BLL) error { func (aac *AAC) WriteAVCC(ts uint32, frame *util.BLL) error {

View File

@@ -32,7 +32,7 @@ func (p *流速控制) 根据起始DTS计算绝对时间戳(dts time.Duration) t
return ((dts-p.起始dts)*time.Millisecond + p.起始时间戳*90) / 90 return ((dts-p.起始dts)*time.Millisecond + p.起始时间戳*90) / 90
} }
func (p *流速控制) 控制流速(绝对时间戳 time.Duration, dts time.Duration) { func (p *流速控制) 控制流速(绝对时间戳 time.Duration, dts time.Duration) (等待了 time.Duration) {
数据时间差, 实际时间差 := 绝对时间戳-p.起始时间戳, time.Since(p.起始时间) 数据时间差, 实际时间差 := 绝对时间戳-p.起始时间戳, time.Since(p.起始时间)
// println("数据时间差", 数据时间差, "实际时间差", 实际时间差, "绝对时间戳", 绝对时间戳, "起始时间戳", p.起始时间戳, "起始时间", p.起始时间.Format("2006-01-02 15:04:05")) // println("数据时间差", 数据时间差, "实际时间差", 实际时间差, "绝对时间戳", 绝对时间戳, "起始时间戳", p.起始时间戳, "起始时间", p.起始时间.Format("2006-01-02 15:04:05"))
// if 实际时间差 > 数据时间差 { // if 实际时间差 > 数据时间差 {
@@ -43,19 +43,18 @@ func (p *流速控制) 控制流速(绝对时间戳 time.Duration, dts time.Dura
if 过快 := (数据时间差 - 实际时间差); 过快 > 100*time.Millisecond { if 过快 := (数据时间差 - 实际时间差); 过快 > 100*time.Millisecond {
// fmt.Println("过快毫秒", 过快.Milliseconds()) // fmt.Println("过快毫秒", 过快.Milliseconds())
// println("过快毫秒", p.name, 过快.Milliseconds()) // println("过快毫秒", p.name, 过快.Milliseconds())
// if log.Trace {
// log.Trace("sleep", zap.Duration("sleep", 过快))
// }
if 过快 > p.等待上限 { if 过快 > p.等待上限 {
time.Sleep(p.等待上限) 等待了 = p.等待上限
} else { } else {
time.Sleep(过快) 等待了 = 过快
} }
time.Sleep(等待了)
} else if 过快 < -100*time.Millisecond { } else if 过快 < -100*time.Millisecond {
// fmt.Println("过慢毫秒", 过快.Milliseconds()) // fmt.Println("过慢毫秒", 过快.Milliseconds())
// p.重置(绝对时间戳, dts) // p.重置(绝对时间戳, dts)
// println("过慢毫秒", p.name, 过快.Milliseconds()) // println("过慢毫秒", p.name, 过快.Milliseconds())
} }
return
} }
type SpesificTrack interface { type SpesificTrack interface {
@@ -305,7 +304,10 @@ func (av *Media) Flush() {
av.ComputeBPS(curValue.BytesIn) av.ComputeBPS(curValue.BytesIn)
av.Step() av.Step()
if av.等待上限 > 0 { if av.等待上限 > 0 {
av.控制流速(curValue.Timestamp, curValue.DTS) 等待了 := av.控制流速(curValue.Timestamp, curValue.DTS)
if log.Trace && 等待了 > 0 {
av.Trace("speed control", zap.Duration("sleep", 等待了))
}
} }
} }

View File

@@ -66,3 +66,16 @@ func (g711 *G711) WriteRTPFrame(frame *RTPFrame) {
g711.AppendAuBytes(frame.Payload) g711.AppendAuBytes(frame.Payload)
g711.Flush() g711.Flush()
} }
func (g711 *G711) CompleteRTP(value *AVFrame) {
if value.AUList.ByteLength > RTPMTU {
var packets [][][]byte
r := value.AUList.Next.Value.NewReader()
for bufs := r.ReadN(RTPMTU); len(bufs) > 0; bufs = r.ReadN(RTPMTU) {
packets = append(packets, bufs)
}
g711.PacketizeRTP(packets...)
} else {
g711.Audio.CompleteRTP(value)
}
}

View File

@@ -35,6 +35,10 @@ func (vt *H264) WriteSliceBytes(slice []byte) {
if log.Trace { if log.Trace {
vt.Trace("naluType", zap.Uint8("naluType", naluType.Byte())) vt.Trace("naluType", zap.Uint8("naluType", naluType.Byte()))
} }
if vt.Value.IFrame {
vt.AppendAuBytes(slice)
return
}
switch naluType { switch naluType {
case codec.NALU_SPS: case codec.NALU_SPS:
spsInfo, _ := codec.ParseSPS(slice) spsInfo, _ := codec.ParseSPS(slice)
@@ -78,7 +82,7 @@ func (vt *H264) WriteSliceBytes(slice []byte) {
case codec.NALU_Access_Unit_Delimiter: case codec.NALU_Access_Unit_Delimiter:
case codec.NALU_Filler_Data: case codec.NALU_Filler_Data:
default: default:
vt.Error("WriteSliceBytes naluType not support", zap.Int("naluType", int(naluType))) vt.Error("nalu type not support", zap.Int("type", int(naluType)))
} }
} }
@@ -155,6 +159,7 @@ func (vt *H264) CompleteRTP(value *AVFrame) {
if value.IFrame { if value.IFrame {
out = append(out, [][]byte{vt.SPS}, [][]byte{vt.PPS}) out = append(out, [][]byte{vt.SPS}, [][]byte{vt.PPS})
} }
startIndex := len(out)
vt.Value.AUList.Range(func(au *util.BLL) bool { vt.Value.AUList.Range(func(au *util.BLL) bool {
if au.ByteLength < RTPMTU { if au.ByteLength < RTPMTU {
out = append(out, au.ToBuffers()) out = append(out, au.ToBuffers())
@@ -164,14 +169,11 @@ func (vt *H264) CompleteRTP(value *AVFrame) {
b0, _ := r.ReadByte() b0, _ := r.ReadByte()
naluType = naluType.Parse(b0) naluType = naluType.Parse(b0)
b0 = codec.NALU_FUA.Or(b0 & 0x60) b0 = codec.NALU_FUA.Or(b0 & 0x60)
buf := [][]byte{{b0, naluType.Or(1 << 7)}}
buf = append(buf, r.ReadN(RTPMTU-2)...)
out = append(out, buf)
for bufs := r.ReadN(RTPMTU); len(bufs) > 0; bufs = r.ReadN(RTPMTU) { for bufs := r.ReadN(RTPMTU); len(bufs) > 0; bufs = r.ReadN(RTPMTU) {
buf = append([][]byte{{b0, naluType.Byte()}}, bufs...) out = append(out, append([][]byte{{b0, naluType.Byte()}}, bufs...))
out = append(out, buf)
} }
buf[0][1] |= 1 << 6 // set end bit out[startIndex][0][1] |= 1 << 7 // set start bit
out[len(out)-1][0][1] |= 1 << 6 // set end bit
} }
return true return true
}) })

View File

@@ -69,10 +69,10 @@ func (vt *H265) WriteSliceBytes(slice []byte) {
case 0, 1, 2, 3, 4, 5, 6, 7, 8, 9: case 0, 1, 2, 3, 4, 5, 6, 7, 8, 9:
vt.Value.IFrame = false vt.Value.IFrame = false
vt.AppendAuBytes(slice) vt.AppendAuBytes(slice)
case codec.NAL_UNIT_SEI: case codec.NAL_UNIT_SEI, codec.NAL_UNIT_SEI_SUFFIX:
vt.AppendAuBytes(slice) vt.AppendAuBytes(slice)
default: default:
vt.Warn("h265 slice type not supported", zap.Uint("type", uint(t))) vt.Warn("nalu type not supported", zap.Uint("type", uint(t)))
} }
} }
func (vt *H265) writeSequenceHead(head []byte) (err error) { func (vt *H265) writeSequenceHead(head []byte) (err error) {
@@ -189,6 +189,7 @@ func (vt *H265) CompleteRTP(value *AVFrame) {
if value.IFrame { if value.IFrame {
out = append(out, [][]byte{vt.VPS}, [][]byte{vt.SPS}, [][]byte{vt.PPS}) out = append(out, [][]byte{vt.VPS}, [][]byte{vt.SPS}, [][]byte{vt.PPS})
} }
startIndex := len(out)
vt.Value.AUList.Range(func(au *util.BLL) bool { vt.Value.AUList.Range(func(au *util.BLL) bool {
if au.ByteLength < RTPMTU { if au.ByteLength < RTPMTU {
out = append(out, au.ToBuffers()) out = append(out, au.ToBuffers())
@@ -199,14 +200,11 @@ func (vt *H265) CompleteRTP(value *AVFrame) {
b1, _ := r.ReadByte() b1, _ := r.ReadByte()
naluType = naluType.Parse(b0) naluType = naluType.Parse(b0)
b0 = (byte(codec.NAL_UNIT_RTP_FU) << 1) | (b0 & 0b10000001) b0 = (byte(codec.NAL_UNIT_RTP_FU) << 1) | (b0 & 0b10000001)
buf := [][]byte{{b0, b1, (1 << 7) | byte(naluType)}}
buf = append(buf, r.ReadN(RTPMTU-3)...)
out = append(out, buf)
for bufs := r.ReadN(RTPMTU); len(bufs) > 0; bufs = r.ReadN(RTPMTU) { for bufs := r.ReadN(RTPMTU); len(bufs) > 0; bufs = r.ReadN(RTPMTU) {
buf = append([][]byte{{b0, b1, byte(naluType)}}, bufs...) out = append(out, append([][]byte{{b0, b1, byte(naluType)}}, bufs...))
out = append(out, buf)
} }
buf[0][2] |= 1 << 6 // set end bit out[startIndex][0][2] |= 1 << 7 // set start bit
out[len(out)-1][0][2] |= 1 << 6 // set end bit
} }
return true return true
}) })

View File

@@ -60,7 +60,15 @@ func (av *Media) PacketizeRTP(payloads ...[][]byte) {
} }
packet.Marker = false packet.Marker = false
for _, p := range pp { for _, p := range pp {
br.Write(p) if _, err := br.Write(p); err != nil {
av.Error("rtp payload write error", zap.Error(err))
for i, pp := range payloads {
for j, p := range pp {
av.Error("rtp payload", zap.Int("i", i), zap.Int("j", j), zap.Int("len", len(p)))
}
}
return
}
} }
packet.Payload = br.Bytes() packet.Payload = br.Bytes()
av.Value.RTP.Push(rtpItem) av.Value.RTP.Push(rtpItem)

View File

@@ -55,7 +55,8 @@ func (b *LimitBuffer) Write(a []byte) (n int, err error) {
l := b.Len() l := b.Len()
newL := l + len(a) newL := l + len(a)
if c := b.Cap(); newL > c { if c := b.Cap(); newL > c {
panic(fmt.Sprintf("LimitBuffer Write %d > %d", newL, c)) return 0, fmt.Errorf("LimitBuffer Write %d > %d", newL, c)
// panic(fmt.Sprintf("LimitBuffer Write %d > %d", newL, c))
} else { } else {
b.Buffer = b.Buffer.SubBuf(0, newL) b.Buffer = b.Buffer.SubBuf(0, newL)
copy(b.Buffer[l:], a) copy(b.Buffer[l:], a)

View File

@@ -56,3 +56,11 @@ func IsSubdir(baseDir, joinedDir string) bool {
} }
return !strings.HasPrefix(rel, "..") && !strings.HasPrefix(rel, "/") return !strings.HasPrefix(rel, "..") && !strings.HasPrefix(rel, "/")
} }
func Conditoinal[T any](cond bool, t, f T) T {
if cond {
return t
} else {
return f
}
}

View File

@@ -11,6 +11,12 @@ import (
"gopkg.in/yaml.v3" "gopkg.in/yaml.v3"
) )
func FetchValue[T any](t T) func() T {
return func() T {
return t
}
}
func ReturnJson[T any](fetch func() T, tickDur time.Duration, rw http.ResponseWriter, r *http.Request) { func ReturnJson[T any](fetch func() T, tickDur time.Duration, rw http.ResponseWriter, r *http.Request) {
if r.Header.Get("Accept") == "text/event-stream" { if r.Header.Get("Accept") == "text/event-stream" {
sse := NewSSE(rw, r.Context()) sse := NewSSE(rw, r.Context())