mirror of
https://github.com/langhuihui/monibuca.git
synced 2025-10-04 02:56:23 +08:00
Compare commits
22 Commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
a1b40bd7b8 | ||
![]() |
827f6eac8d | ||
![]() |
ee056144a8 | ||
![]() |
20ec6c55cd | ||
![]() |
e478a1972e | ||
![]() |
94be02cd79 | ||
![]() |
bacda6f5a0 | ||
![]() |
61fae4cc97 | ||
![]() |
e0752242b2 | ||
![]() |
23f2ed39a1 | ||
![]() |
0b731e468b | ||
![]() |
4fe1472117 | ||
![]() |
a8b3a644c3 | ||
![]() |
4f0a097dac | ||
![]() |
4df3de00af | ||
![]() |
9c16905f28 | ||
![]() |
0470f78ed7 | ||
![]() |
7282f1f44d | ||
![]() |
67186cd669 | ||
![]() |
09e9761083 | ||
![]() |
4acdc19beb | ||
![]() |
80e19726d4 |
11
.github/workflows/go.yml
vendored
11
.github/workflows/go.yml
vendored
@@ -98,4 +98,13 @@ jobs:
|
||||
if: success() && !contains(env.version, 'beta')
|
||||
run: |
|
||||
docker tag langhuihui/monibuca:v5 langhuihui/monibuca:${{ env.version }}
|
||||
docker push langhuihui/monibuca:${{ env.version }}
|
||||
docker push langhuihui/monibuca:${{ env.version }}
|
||||
- name: docker build lite version
|
||||
if: success() && startsWith(github.ref, 'refs/tags/')
|
||||
run: |
|
||||
docker buildx build --platform linux/amd64,linux/arm64 -f DockerfileLite -t monibuca/v5:latest --push .
|
||||
- name: docker lite push version tag
|
||||
if: success() && !contains(env.version, 'beta')
|
||||
run: |
|
||||
docker tag monibuca/v5 monibuca/v5:${{ env.version }}
|
||||
docker push lmonibuca/v5:${{ env.version }}
|
31
DockerfileLite
Normal file
31
DockerfileLite
Normal file
@@ -0,0 +1,31 @@
|
||||
# Running Stage
|
||||
FROM alpine:latest
|
||||
|
||||
WORKDIR /monibuca
|
||||
|
||||
# Copy the pre-compiled binary from the build context
|
||||
# The GitHub Actions workflow prepares 'monibuca_linux' in the context root
|
||||
|
||||
COPY monibuca_amd64 ./monibuca_amd64
|
||||
COPY monibuca_arm64 ./monibuca_arm64
|
||||
|
||||
COPY admin.zip ./admin.zip
|
||||
|
||||
# Copy the configuration file from the build context
|
||||
COPY example/default/config.yaml /etc/monibuca/config.yaml
|
||||
|
||||
# Export necessary ports
|
||||
EXPOSE 6000 8080 8443 1935 554 5060 9000-20000
|
||||
EXPOSE 5060/udp 44944/udp
|
||||
|
||||
RUN if [ "$(uname -m)" = "aarch64" ]; then \
|
||||
mv ./monibuca_arm64 ./monibuca_linux; \
|
||||
rm ./monibuca_amd64; \
|
||||
else \
|
||||
mv ./monibuca_amd64 ./monibuca_linux; \
|
||||
rm ./monibuca_arm64; \
|
||||
fi
|
||||
|
||||
|
||||
ENTRYPOINT [ "./monibuca_linux"]
|
||||
CMD ["-c", "/etc/monibuca/config.yaml"]
|
111
RELEASE_NOTES_5.0.x_CN.md
Normal file
111
RELEASE_NOTES_5.0.x_CN.md
Normal file
@@ -0,0 +1,111 @@
|
||||
# Monibuca v5.0.x Release Notes
|
||||
|
||||
## v5.0.2 (2025-06-05)
|
||||
|
||||
### 🎉 新功能 (New Features)
|
||||
|
||||
#### 核心功能
|
||||
- **降低延迟** - 禁用了TCP WebRTC的重放保护功能,降低了延迟
|
||||
- **配置系统增强** - 支持更多配置格式(支持配置项中插入`-`、`_`和大写字母),提升配置灵活性
|
||||
- **原始数据检查** - 新增原始数据无帧检查功能,提升数据处理稳定性
|
||||
- **MP4循环读取** - 支持MP4文件循环读取功能(通过配置 pull 配置下的 `loop` 配置)
|
||||
- **S3插件** - 新增S3存储插件,支持云存储集成
|
||||
- **TCP读写缓冲配置** - 新增TCP连接读写缓冲区配置选项(针对高并发下的吞吐能力增强)
|
||||
- **拉流测试模式** - 新增拉流测试模式选项(可以选择拉流时不发布),便于调试和测试
|
||||
- **SEI API格式扩展** - 扩展SEI API支持更多数据格式
|
||||
- **Hook扩展** - 新增更多Hook回调点,增强扩展性
|
||||
- **定时任务插件** - 新增crontab定时任务插件
|
||||
- **服务器抓包** - 新增服务器抓包功能(调用`tcpdump`),支持TCP和UDP协议,API 说明见 [tcpdump](https://api.monibuca.com/api-301117332)
|
||||
|
||||
#### GB28181协议增强
|
||||
- **平台配置支持** - GB28181现在支持从config.yaml中添加平台和平台通道配置
|
||||
- **子码流播放** - 支持GB28181子码流播放功能
|
||||
- **SDP优化** - 优化invite SDP中的mediaip和sipip处理
|
||||
- **本地端口保存** - 修复GB28181本地端口保存到数据库的问题
|
||||
|
||||
#### MP4功能增强
|
||||
- **FLV格式下载** - 支持从MP4录制文件下载FLV格式
|
||||
- **下载功能修复** - 修复MP4下载功能的相关问题
|
||||
- **恢复功能修复** - 修复MP4恢复功能
|
||||
|
||||
### 🐛 问题修复 (Bug Fixes)
|
||||
|
||||
#### 网络通信
|
||||
- **TCP读取阻塞** - 修复TCP读取阻塞问题(增加了读取超时设置)
|
||||
- **RTSP内存泄漏** - 修复RTSP协议的内存泄漏问题
|
||||
- **RTSP音视频标识** - 修复RTSP无音频或视频标识的问题
|
||||
|
||||
#### GB28181协议
|
||||
- **任务管理** - 使用task.Manager解决注册处理器的问题
|
||||
- **计划长度** - 修复plan.length为168的问题
|
||||
- **注册频率** - 修复GB28181注册过快导致启动过多任务的问题
|
||||
- **联系信息** - 修复GB28181获取错误联系信息的问题
|
||||
|
||||
#### RTMP协议
|
||||
- **时间戳处理** - 修复RTMP时间戳开头跳跃问题
|
||||
|
||||
### 🛠️ 优化改进 (Improvements)
|
||||
|
||||
#### Docker支持
|
||||
- **tcpdump工具** - Docker镜像中新增tcpdump网络诊断工具
|
||||
|
||||
#### Linux平台优化
|
||||
- **SIP请求优化** - Linux平台移除SIP请求中的viaheader
|
||||
|
||||
### 👥 贡献者 (Contributors)
|
||||
- langhuihui
|
||||
- pggiroro
|
||||
- banshan
|
||||
|
||||
---
|
||||
|
||||
## v5.0.1 (2025-05-21)
|
||||
|
||||
### 🎉 新功能 (New Features)
|
||||
|
||||
#### WebRTC增强
|
||||
- **H265支持** - 新增WebRTC对H265编码的支持,提升视频质量和压缩效率
|
||||
|
||||
#### GB28181协议增强
|
||||
- **订阅功能扩展** - GB28181模块现在支持订阅报警、移动位置、目录信息
|
||||
- **通知请求** - 支持接收通知请求,增强与设备的交互能力
|
||||
|
||||
#### Docker优化
|
||||
- **FFmpeg集成** - Docker镜像中新增FFmpeg工具,支持更多音视频处理场景
|
||||
- **多架构支持** - 新增Docker多架构构建支持
|
||||
|
||||
### 🐛 问题修复 (Bug Fixes)
|
||||
|
||||
#### Docker相关
|
||||
- **构建问题** - 修复Docker构建过程中的多个问题
|
||||
- **构建优化** - 优化Docker构建流程,提升构建效率
|
||||
|
||||
#### RTMP协议
|
||||
- **时间戳处理** - 修复RTMP第一个chunk类型3需要添加时间戳的问题
|
||||
|
||||
#### GB28181协议
|
||||
- **路径匹配** - 修复GB28181模块中播放流路径的正则表达式匹配问题
|
||||
|
||||
#### MP4处理
|
||||
- **stsz box** - 修复stsz box采样大小的问题
|
||||
- **G711音频** - 修复拉取MP4文件时读取G711音频的问题
|
||||
- **H265解析** - 修复H265 MP4文件解析问题
|
||||
|
||||
### 🛠️ 优化改进 (Improvements)
|
||||
|
||||
#### 代码质量
|
||||
- **错误处理** - 新增maxcount错误处理机制
|
||||
- **文档更新** - 更新README文档和go.mod配置
|
||||
|
||||
#### 构建系统
|
||||
- **ARM架构** - 减少JavaScript代码,优化ARM架构Docker构建
|
||||
- **构建标签** - 移除Docker中不必要的构建标签
|
||||
|
||||
### 📦 其他更新 (Other Updates)
|
||||
- **MCP相关** - 更新Model Context Protocol相关功能
|
||||
- **依赖更新** - 更新项目依赖和模块配置
|
||||
|
||||
### 👥 贡献者 (Contributors)
|
||||
- langhuihui
|
||||
|
||||
---
|
139
api.go
139
api.go
@@ -180,19 +180,17 @@ func (s *Server) getStreamInfo(pub *Publisher) (res *pb.StreamInfoResponse, err
|
||||
|
||||
func (s *Server) StreamInfo(ctx context.Context, req *pb.StreamSnapRequest) (res *pb.StreamInfoResponse, err error) {
|
||||
var recordings []*pb.RecordingDetail
|
||||
s.Records.Call(func() error {
|
||||
for record := range s.Records.Range {
|
||||
if record.StreamPath == req.StreamPath {
|
||||
recordings = append(recordings, &pb.RecordingDetail{
|
||||
FilePath: record.RecConf.FilePath,
|
||||
Mode: record.Mode,
|
||||
Fragment: durationpb.New(record.RecConf.Fragment),
|
||||
Append: record.RecConf.Append,
|
||||
PluginName: record.Plugin.Meta.Name,
|
||||
})
|
||||
}
|
||||
s.Records.SafeRange(func(record *RecordJob) bool {
|
||||
if record.StreamPath == req.StreamPath {
|
||||
recordings = append(recordings, &pb.RecordingDetail{
|
||||
FilePath: record.RecConf.FilePath,
|
||||
Mode: record.RecConf.Mode,
|
||||
Fragment: durationpb.New(record.RecConf.Fragment),
|
||||
Append: record.RecConf.Append,
|
||||
PluginName: record.Plugin.Meta.Name,
|
||||
})
|
||||
}
|
||||
return nil
|
||||
return true
|
||||
})
|
||||
if pub, ok := s.Streams.SafeGet(req.StreamPath); ok {
|
||||
res, err = s.getStreamInfo(pub)
|
||||
@@ -260,17 +258,15 @@ func (s *Server) RestartTask(ctx context.Context, req *pb.RequestWithId64) (resp
|
||||
}
|
||||
|
||||
func (s *Server) GetRecording(ctx context.Context, req *emptypb.Empty) (resp *pb.RecordingListResponse, err error) {
|
||||
s.Records.Call(func() error {
|
||||
resp = &pb.RecordingListResponse{}
|
||||
for record := range s.Records.Range {
|
||||
resp.Data = append(resp.Data, &pb.Recording{
|
||||
StreamPath: record.StreamPath,
|
||||
StartTime: timestamppb.New(record.StartTime),
|
||||
Type: reflect.TypeOf(record.recorder).String(),
|
||||
Pointer: uint64(record.GetTaskPointer()),
|
||||
})
|
||||
}
|
||||
return nil
|
||||
resp = &pb.RecordingListResponse{}
|
||||
s.Records.SafeRange(func(record *RecordJob) bool {
|
||||
resp.Data = append(resp.Data, &pb.Recording{
|
||||
StreamPath: record.StreamPath,
|
||||
StartTime: timestamppb.New(record.StartTime),
|
||||
Type: reflect.TypeOf(record.recorder).String(),
|
||||
Pointer: uint64(record.GetTaskPointer()),
|
||||
})
|
||||
return true
|
||||
})
|
||||
return
|
||||
}
|
||||
@@ -490,7 +486,7 @@ func (s *Server) Shutdown(ctx context.Context, req *pb.RequestWithId) (res *pb.S
|
||||
func (s *Server) ChangeSubscribe(ctx context.Context, req *pb.ChangeSubscribeRequest) (res *pb.SuccessResponse, err error) {
|
||||
s.Streams.Call(func() error {
|
||||
if subscriber, ok := s.Subscribers.Get(req.Id); ok {
|
||||
if pub, ok := s.Streams.SafeGet(req.StreamPath); ok {
|
||||
if pub, ok := s.Streams.Get(req.StreamPath); ok {
|
||||
subscriber.Publisher.RemoveSubscriber(subscriber)
|
||||
subscriber.StreamPath = req.StreamPath
|
||||
pub.AddSubscriber(subscriber)
|
||||
@@ -516,86 +512,65 @@ func (s *Server) StopSubscribe(ctx context.Context, req *pb.RequestWithId) (res
|
||||
}
|
||||
|
||||
func (s *Server) PauseStream(ctx context.Context, req *pb.StreamSnapRequest) (res *pb.SuccessResponse, err error) {
|
||||
s.Streams.Call(func() error {
|
||||
if s, ok := s.Streams.SafeGet(req.StreamPath); ok {
|
||||
s.Pause()
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if s, ok := s.Streams.SafeGet(req.StreamPath); ok {
|
||||
s.Pause()
|
||||
}
|
||||
return &pb.SuccessResponse{}, err
|
||||
}
|
||||
|
||||
func (s *Server) ResumeStream(ctx context.Context, req *pb.StreamSnapRequest) (res *pb.SuccessResponse, err error) {
|
||||
s.Streams.Call(func() error {
|
||||
if s, ok := s.Streams.SafeGet(req.StreamPath); ok {
|
||||
s.Resume()
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if s, ok := s.Streams.SafeGet(req.StreamPath); ok {
|
||||
s.Resume()
|
||||
}
|
||||
return &pb.SuccessResponse{}, err
|
||||
}
|
||||
|
||||
func (s *Server) SetStreamSpeed(ctx context.Context, req *pb.SetStreamSpeedRequest) (res *pb.SuccessResponse, err error) {
|
||||
s.Streams.Call(func() error {
|
||||
if s, ok := s.Streams.SafeGet(req.StreamPath); ok {
|
||||
s.Speed = float64(req.Speed)
|
||||
s.Scale = float64(req.Speed)
|
||||
s.Info("set stream speed", "speed", req.Speed)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if s, ok := s.Streams.SafeGet(req.StreamPath); ok {
|
||||
s.Speed = float64(req.Speed)
|
||||
s.Scale = float64(req.Speed)
|
||||
s.Info("set stream speed", "speed", req.Speed)
|
||||
}
|
||||
return &pb.SuccessResponse{}, err
|
||||
}
|
||||
|
||||
func (s *Server) SeekStream(ctx context.Context, req *pb.SeekStreamRequest) (res *pb.SuccessResponse, err error) {
|
||||
s.Streams.Call(func() error {
|
||||
if s, ok := s.Streams.SafeGet(req.StreamPath); ok {
|
||||
s.Seek(time.Unix(int64(req.TimeStamp), 0))
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if s, ok := s.Streams.SafeGet(req.StreamPath); ok {
|
||||
s.Seek(time.Unix(int64(req.TimeStamp), 0))
|
||||
}
|
||||
return &pb.SuccessResponse{}, err
|
||||
}
|
||||
|
||||
func (s *Server) StopPublish(ctx context.Context, req *pb.StreamSnapRequest) (res *pb.SuccessResponse, err error) {
|
||||
s.Streams.Call(func() error {
|
||||
if s, ok := s.Streams.SafeGet(req.StreamPath); ok {
|
||||
s.Stop(task.ErrStopByUser)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if s, ok := s.Streams.SafeGet(req.StreamPath); ok {
|
||||
s.Stop(task.ErrStopByUser)
|
||||
}
|
||||
return &pb.SuccessResponse{}, err
|
||||
}
|
||||
|
||||
// /api/stream/list
|
||||
func (s *Server) StreamList(_ context.Context, req *pb.StreamListRequest) (res *pb.StreamListResponse, err error) {
|
||||
recordingMap := make(map[string][]*pb.RecordingDetail)
|
||||
s.Records.Call(func() error {
|
||||
for record := range s.Records.Range {
|
||||
recordingMap[record.StreamPath] = append(recordingMap[record.StreamPath], &pb.RecordingDetail{
|
||||
FilePath: record.RecConf.FilePath,
|
||||
Mode: record.Mode,
|
||||
Fragment: durationpb.New(record.RecConf.Fragment),
|
||||
Append: record.RecConf.Append,
|
||||
PluginName: record.Plugin.Meta.Name,
|
||||
Pointer: uint64(record.GetTaskPointer()),
|
||||
})
|
||||
for record := range s.Records.SafeRange {
|
||||
recordingMap[record.StreamPath] = append(recordingMap[record.StreamPath], &pb.RecordingDetail{
|
||||
FilePath: record.RecConf.FilePath,
|
||||
Mode: record.RecConf.Mode,
|
||||
Fragment: durationpb.New(record.RecConf.Fragment),
|
||||
Append: record.RecConf.Append,
|
||||
PluginName: record.Plugin.Meta.Name,
|
||||
Pointer: uint64(record.GetTaskPointer()),
|
||||
})
|
||||
}
|
||||
var streams []*pb.StreamInfo
|
||||
for publisher := range s.Streams.SafeRange {
|
||||
info, err := s.getStreamInfo(publisher)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
return nil
|
||||
})
|
||||
s.Streams.Call(func() error {
|
||||
var streams []*pb.StreamInfo
|
||||
for publisher := range s.Streams.Range {
|
||||
info, err := s.getStreamInfo(publisher)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
info.Data.Recording = recordingMap[info.Data.Path]
|
||||
streams = append(streams, info.Data)
|
||||
}
|
||||
res = &pb.StreamListResponse{Data: streams, Total: int32(s.Streams.Length), PageNum: req.PageNum, PageSize: req.PageSize}
|
||||
return nil
|
||||
})
|
||||
info.Data.Recording = recordingMap[info.Data.Path]
|
||||
streams = append(streams, info.Data)
|
||||
}
|
||||
res = &pb.StreamListResponse{Data: streams, Total: int32(s.Streams.Length), PageNum: req.PageNum, PageSize: req.PageSize}
|
||||
return
|
||||
}
|
||||
|
||||
@@ -775,7 +750,7 @@ func (s *Server) GetRecordList(ctx context.Context, req *pb.ReqRecordList) (resp
|
||||
offset := (req.PageNum - 1) * req.PageSize // 计算偏移量
|
||||
var totalCount int64 //总条数
|
||||
|
||||
var result []*RecordStream
|
||||
var result []*EventRecordStream
|
||||
query := s.DB.Model(&RecordStream{})
|
||||
if strings.Contains(req.StreamPath, "*") {
|
||||
query = query.Where("stream_path like ?", strings.ReplaceAll(req.StreamPath, "*", "%"))
|
||||
|
@@ -15,13 +15,33 @@ gb28181:
|
||||
sip:
|
||||
listenaddr:
|
||||
- udp::5060
|
||||
# pull:
|
||||
# live/test: dump/34020000001320000001
|
||||
onsub:
|
||||
pull:
|
||||
^\d{20}/\d{20}$: $0
|
||||
^gb_\d+/(.+)$: $1
|
||||
# .* : $0
|
||||
platforms:
|
||||
- enable: false #是否启用平台
|
||||
name: "测试平台" #平台名称
|
||||
servergbid: "34020000002000000002" #上级平台GBID
|
||||
servergbdomain: "3402000000" #上级平台GB域
|
||||
serverip: 192.168.1.106 #上级平台IP
|
||||
serverport: 5061 #上级平台端口
|
||||
devicegbid: "34020000002000000001" #本平台设备GBID
|
||||
deviceip: 192.168.1.106 #本平台设备IP
|
||||
deviceport: 5060 #本平台设备端口
|
||||
username: "34020000002000000001" #SIP账号
|
||||
password: "123456" #SIP密码
|
||||
expires: 3600 #注册有效期,单位秒
|
||||
keeptimeout: 60 #注册保持超时时间,单位秒
|
||||
civilCode: "340200" #行政区划代码
|
||||
manufacturer: "Monibuca" #设备制造商
|
||||
model: "GB28181" #设备型号
|
||||
address: "江苏南京" #设备地址
|
||||
register_way: 1
|
||||
platformchannels:
|
||||
- platformservergbid: "34020000002000000002" #上级平台GBID
|
||||
channeldbid: "34020000001110000003_34020000001320000005" #通道DBID,格式为设备ID_通道ID
|
||||
mp4:
|
||||
# enable: false
|
||||
# publish:
|
||||
|
2
go.mod
2
go.mod
@@ -53,7 +53,7 @@ require (
|
||||
google.golang.org/protobuf v1.34.2
|
||||
gorm.io/driver/mysql v1.5.7
|
||||
gorm.io/driver/postgres v1.5.9
|
||||
gorm.io/gorm v1.25.11
|
||||
gorm.io/gorm v1.30.0
|
||||
)
|
||||
|
||||
require (
|
||||
|
4
go.sum
4
go.sum
@@ -423,8 +423,8 @@ gorm.io/driver/mysql v1.5.7/go.mod h1:sEtPWMiqiN1N1cMXoXmBbd8C6/l+TESwriotuRRpkD
|
||||
gorm.io/driver/postgres v1.5.9 h1:DkegyItji119OlcaLjqN11kHoUgZ/j13E0jkJZgD6A8=
|
||||
gorm.io/driver/postgres v1.5.9/go.mod h1:DX3GReXH+3FPWGrrgffdvCk3DQ1dwDPdmbenSkweRGI=
|
||||
gorm.io/gorm v1.25.7/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8=
|
||||
gorm.io/gorm v1.25.11 h1:/Wfyg1B/je1hnDx3sMkX+gAlxrlZpn6X0BXRlwXlvHg=
|
||||
gorm.io/gorm v1.25.11/go.mod h1:xh7N7RHfYlNc5EmcI/El95gXusucDrQnHXe0+CgWcLQ=
|
||||
gorm.io/gorm v1.30.0 h1:qbT5aPv1UH8gI99OsRlvDToLxW5zR7FzS9acZDOZcgs=
|
||||
gorm.io/gorm v1.30.0/go.mod h1:8Z33v652h4//uMA76KjeDH8mJXPm1QNCYrMeatR0DOE=
|
||||
gotest.tools/v3 v3.0.2/go.mod h1:3SzNCllyD9/Y+b5r9JIKQ474KzkZyqLqEfYqMsX94Bk=
|
||||
gotest.tools/v3 v3.5.0 h1:Ljk6PdHdOhAb5aDMWXjDLMMhph+BpztA4v1QdqEW2eY=
|
||||
gotest.tools/v3 v3.5.0/go.mod h1:isy3WKz7GK6uNw/sbHzfKBLvlvXwUyV06n6brMxxopU=
|
||||
|
@@ -664,7 +664,7 @@ message ReqRecordList {
|
||||
string end = 4;
|
||||
uint32 pageNum = 5;
|
||||
uint32 pageSize = 6;
|
||||
string mode = 7;
|
||||
string eventId = 7;
|
||||
string type = 8;
|
||||
string eventLevel = 9;
|
||||
}
|
||||
|
@@ -16,6 +16,9 @@ const (
|
||||
RelayModeRelay = "relay"
|
||||
RelayModeMix = "mix"
|
||||
|
||||
RecordModeAuto RecordMode = "auto"
|
||||
RecordModeEvent RecordMode = "event"
|
||||
|
||||
HookOnServerKeepAlive HookType = "server_keep_alive"
|
||||
HookOnPublishStart HookType = "publish_start"
|
||||
HookOnPublishEnd HookType = "publish_end"
|
||||
@@ -29,11 +32,16 @@ const (
|
||||
HookOnRecordEnd HookType = "record_end"
|
||||
HookOnTransformStart HookType = "transform_start"
|
||||
HookOnTransformEnd HookType = "transform_end"
|
||||
|
||||
EventLevelLow EventLevel = "low"
|
||||
EventLevelHigh EventLevel = "high"
|
||||
)
|
||||
|
||||
type (
|
||||
HookType string
|
||||
Publish struct {
|
||||
EventLevel = string
|
||||
RecordMode = string
|
||||
HookType string
|
||||
Publish struct {
|
||||
MaxCount int `default:"0" desc:"最大发布者数量"` // 最大发布者数量
|
||||
PubAudio bool `default:"true" desc:"是否发布音频"`
|
||||
PubVideo bool `default:"true" desc:"是否发布视频"`
|
||||
@@ -84,11 +92,21 @@ type (
|
||||
Proxy string `desc:"代理地址"` // 代理地址
|
||||
Header HTTPValues
|
||||
}
|
||||
RecordEvent struct {
|
||||
EventId string
|
||||
BeforeDuration uint32 `json:"beforeDuration" desc:"事件前缓存时长" gorm:"comment:事件前缓存时长;default:30000"`
|
||||
AfterDuration uint32 `json:"afterDuration" desc:"事件后缓存时长" gorm:"comment:事件后缓存时长;default:30000"`
|
||||
EventDesc string `json:"eventDesc" desc:"事件描述" gorm:"type:varchar(255);comment:事件描述"`
|
||||
EventLevel EventLevel `json:"eventLevel" desc:"事件级别" gorm:"type:varchar(255);comment:事件级别,high表示重要事件,无法删除且表示无需自动删除,low表示非重要事件,达到自动删除时间后,自动删除;default:'low'"`
|
||||
EventName string `json:"eventName" desc:"事件名称" gorm:"type:varchar(255);comment:事件名称"`
|
||||
}
|
||||
Record struct {
|
||||
Type string `desc:"录制类型"` // 录制类型 mp4、flv、hls、hlsv7
|
||||
FilePath string `desc:"录制文件路径"` // 录制文件路径
|
||||
Fragment time.Duration `desc:"分片时长"` // 分片时长
|
||||
Append bool `desc:"是否追加录制"` // 是否追加录制
|
||||
Mode RecordMode `json:"mode" desc:"事件类型,auto=连续录像模式,event=事件录像模式" gorm:"type:varchar(255);comment:事件类型,auto=连续录像模式,event=事件录像模式;default:'auto'"`
|
||||
Type string `desc:"录制类型"` // 录制类型 mp4、flv、hls、hlsv7
|
||||
FilePath string `desc:"录制文件路径"` // 录制文件路径
|
||||
Fragment time.Duration `desc:"分片时长"` // 分片时长
|
||||
Append bool `desc:"是否追加录制"` // 是否追加录制
|
||||
Event *RecordEvent `json:"event" desc:"事件录像配置" gorm:"-"` // 事件录像配置
|
||||
}
|
||||
TransfromOutput struct {
|
||||
Target string `desc:"转码目标"` // 转码目标
|
||||
|
133
pkg/port.go
133
pkg/port.go
@@ -13,6 +13,7 @@ type (
|
||||
Port struct {
|
||||
Protocol string
|
||||
Ports [2]int
|
||||
Map [2]int // 映射端口范围,通常用于 NAT 或端口转发
|
||||
}
|
||||
IPort interface {
|
||||
IsTCP() bool
|
||||
@@ -22,10 +23,23 @@ type (
|
||||
)
|
||||
|
||||
func (p Port) String() string {
|
||||
var result string
|
||||
if p.Ports[0] == p.Ports[1] {
|
||||
return p.Protocol + ":" + strconv.Itoa(p.Ports[0])
|
||||
result = p.Protocol + ":" + strconv.Itoa(p.Ports[0])
|
||||
} else {
|
||||
result = p.Protocol + ":" + strconv.Itoa(p.Ports[0]) + "-" + strconv.Itoa(p.Ports[1])
|
||||
}
|
||||
return p.Protocol + ":" + strconv.Itoa(p.Ports[0]) + "-" + strconv.Itoa(p.Ports[1])
|
||||
|
||||
// 如果有端口映射,添加映射信息
|
||||
if p.HasMapping() {
|
||||
if p.Map[0] == p.Map[1] {
|
||||
result += ":" + strconv.Itoa(p.Map[0])
|
||||
} else {
|
||||
result += ":" + strconv.Itoa(p.Map[0]) + "-" + strconv.Itoa(p.Map[1])
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
func (p Port) IsTCP() bool {
|
||||
@@ -40,6 +54,36 @@ func (p Port) IsRange() bool {
|
||||
return p.Ports[0] != p.Ports[1]
|
||||
}
|
||||
|
||||
func (p Port) HasMapping() bool {
|
||||
return p.Map[0] > 0 || p.Map[1] > 0
|
||||
}
|
||||
|
||||
func (p Port) IsRangeMapping() bool {
|
||||
return p.HasMapping() && p.Map[0] != p.Map[1]
|
||||
}
|
||||
|
||||
// ParsePort2 解析端口配置字符串并返回对应的端口类型实例
|
||||
// 根据协议类型和端口范围返回不同的类型:
|
||||
// - TCP单端口:返回 TCPPort
|
||||
// - TCP端口范围:返回 TCPRangePort
|
||||
// - UDP单端口:返回 UDPPort
|
||||
// - UDP端口范围:返回 UDPRangePort
|
||||
//
|
||||
// 参数:
|
||||
//
|
||||
// conf - 端口配置字符串,格式:protocol:port 或 protocol:port1-port2
|
||||
//
|
||||
// 返回值:
|
||||
//
|
||||
// ret - 端口实例 (TCPPort/UDPPort/TCPRangePort/UDPRangePort)
|
||||
// err - 解析错误
|
||||
//
|
||||
// 示例:
|
||||
//
|
||||
// ParsePort2("tcp:8080") // 返回 TCPPort(8080)
|
||||
// ParsePort2("tcp:8080-8090") // 返回 TCPRangePort([2]int{8080, 8090})
|
||||
// ParsePort2("udp:5000") // 返回 UDPPort(5000)
|
||||
// ParsePort2("udp:5000-5010") // 返回 UDPRangePort([2]int{5000, 5010})
|
||||
func ParsePort2(conf string) (ret any, err error) {
|
||||
var port Port
|
||||
port, err = ParsePort(conf)
|
||||
@@ -58,10 +102,84 @@ func ParsePort2(conf string) (ret any, err error) {
|
||||
return UDPPort(port.Ports[0]), nil
|
||||
}
|
||||
|
||||
// ParsePort 解析端口配置字符串为 Port 结构体
|
||||
// 支持协议前缀、端口号/端口范围以及端口映射的解析
|
||||
//
|
||||
// 参数:
|
||||
//
|
||||
// conf - 端口配置字符串,格式:
|
||||
// - "protocol:port" 单端口,如 "tcp:8080"
|
||||
// - "protocol:port1-port2" 端口范围,如 "tcp:8080-8090"
|
||||
// - "protocol:port:mapPort" 单端口映射,如 "tcp:8080:9090"
|
||||
// - "protocol:port:mapPort1-mapPort2" 单端口映射到端口范围,如 "tcp:8080:9000-9010"
|
||||
// - "protocol:port1-port2:mapPort1-mapPort2" 端口范围映射,如 "tcp:8080-8090:9000-9010"
|
||||
//
|
||||
// 返回值:
|
||||
//
|
||||
// ret - Port 结构体,包含协议、端口和映射端口信息
|
||||
// err - 解析错误
|
||||
//
|
||||
// 注意:
|
||||
// - 如果端口范围中 min > max,会自动交换顺序
|
||||
// - 单端口时,Ports[0] 和 Ports[1] 值相同
|
||||
// - 端口映射时,Map[0] 和 Map[1] 存储映射的目标端口范围
|
||||
// - 单个映射端口时,Map[0] 和 Map[1] 值相同
|
||||
//
|
||||
// 示例:
|
||||
//
|
||||
// ParsePort("tcp:8080") // Port{Protocol:"tcp", Ports:[2]int{8080, 8080}, Map:[2]int{0, 0}}
|
||||
// ParsePort("tcp:8080-8090") // Port{Protocol:"tcp", Ports:[2]int{8080, 8090}, Map:[2]int{0, 0}}
|
||||
// ParsePort("tcp:8080:9090") // Port{Protocol:"tcp", Ports:[2]int{8080, 8080}, Map:[2]int{9090, 9090}}
|
||||
// ParsePort("tcp:8080:9000-9010") // Port{Protocol:"tcp", Ports:[2]int{8080, 8080}, Map:[2]int{9000, 9010}}
|
||||
// ParsePort("tcp:8080-8090:9000-9010") // Port{Protocol:"tcp", Ports:[2]int{8080, 8090}, Map:[2]int{9000, 9010}}
|
||||
// ParsePort("udp:5000") // Port{Protocol:"udp", Ports:[2]int{5000, 5000}, Map:[2]int{0, 0}}
|
||||
// ParsePort("udp:5010-5000") // Port{Protocol:"udp", Ports:[2]int{5000, 5010}, Map:[2]int{0, 0}}
|
||||
func ParsePort(conf string) (ret Port, err error) {
|
||||
var port string
|
||||
var port, mapPort string
|
||||
var min, max int
|
||||
ret.Protocol, port, _ = strings.Cut(conf, ":")
|
||||
|
||||
// 按冒号分割,支持端口映射
|
||||
parts := strings.Split(conf, ":")
|
||||
if len(parts) < 2 || len(parts) > 3 {
|
||||
err = strconv.ErrSyntax
|
||||
return
|
||||
}
|
||||
|
||||
ret.Protocol = parts[0]
|
||||
port = parts[1]
|
||||
|
||||
// 处理端口映射
|
||||
if len(parts) == 3 {
|
||||
mapPort = parts[2]
|
||||
// 解析映射端口,支持单端口和端口范围
|
||||
if mapRange := strings.Split(mapPort, "-"); len(mapRange) == 2 {
|
||||
// 映射端口范围
|
||||
var mapMin, mapMax int
|
||||
mapMin, err = strconv.Atoi(mapRange[0])
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
mapMax, err = strconv.Atoi(mapRange[1])
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if mapMin < mapMax {
|
||||
ret.Map[0], ret.Map[1] = mapMin, mapMax
|
||||
} else {
|
||||
ret.Map[0], ret.Map[1] = mapMax, mapMin
|
||||
}
|
||||
} else {
|
||||
// 单个映射端口
|
||||
var mapPortNum int
|
||||
mapPortNum, err = strconv.Atoi(mapPort)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
ret.Map[0], ret.Map[1] = mapPortNum, mapPortNum
|
||||
}
|
||||
}
|
||||
|
||||
// 处理端口范围
|
||||
if r := strings.Split(port, "-"); len(r) == 2 {
|
||||
min, err = strconv.Atoi(r[0])
|
||||
if err != nil {
|
||||
@@ -76,7 +194,12 @@ func ParsePort(conf string) (ret Port, err error) {
|
||||
} else {
|
||||
ret.Ports[0], ret.Ports[1] = max, min
|
||||
}
|
||||
} else if p, err := strconv.Atoi(port); err == nil {
|
||||
} else {
|
||||
var p int
|
||||
p, err = strconv.Atoi(port)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
ret.Ports[0], ret.Ports[1] = p, p
|
||||
}
|
||||
return
|
||||
|
370
pkg/port_test.go
Normal file
370
pkg/port_test.go
Normal file
@@ -0,0 +1,370 @@
|
||||
package pkg
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestParsePort(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
expected Port
|
||||
hasError bool
|
||||
}{
|
||||
{
|
||||
name: "TCP单端口",
|
||||
input: "tcp:8080",
|
||||
expected: Port{
|
||||
Protocol: "tcp",
|
||||
Ports: [2]int{8080, 8080},
|
||||
Map: [2]int{0, 0},
|
||||
},
|
||||
hasError: false,
|
||||
},
|
||||
{
|
||||
name: "TCP端口范围",
|
||||
input: "tcp:8080-8090",
|
||||
expected: Port{
|
||||
Protocol: "tcp",
|
||||
Ports: [2]int{8080, 8090},
|
||||
Map: [2]int{0, 0},
|
||||
},
|
||||
hasError: false,
|
||||
},
|
||||
{
|
||||
name: "TCP端口范围(反序)",
|
||||
input: "tcp:8090-8080",
|
||||
expected: Port{
|
||||
Protocol: "tcp",
|
||||
Ports: [2]int{8080, 8090},
|
||||
Map: [2]int{0, 0},
|
||||
},
|
||||
hasError: false,
|
||||
},
|
||||
{
|
||||
name: "TCP单端口映射到单端口",
|
||||
input: "tcp:8080:9090",
|
||||
expected: Port{
|
||||
Protocol: "tcp",
|
||||
Ports: [2]int{8080, 8080},
|
||||
Map: [2]int{9090, 9090},
|
||||
},
|
||||
hasError: false,
|
||||
},
|
||||
{
|
||||
name: "TCP单端口映射到端口范围",
|
||||
input: "tcp:8080:9000-9010",
|
||||
expected: Port{
|
||||
Protocol: "tcp",
|
||||
Ports: [2]int{8080, 8080},
|
||||
Map: [2]int{9000, 9010},
|
||||
},
|
||||
hasError: false,
|
||||
},
|
||||
{
|
||||
name: "TCP端口范围映射到端口范围",
|
||||
input: "tcp:8080-8090:9000-9010",
|
||||
expected: Port{
|
||||
Protocol: "tcp",
|
||||
Ports: [2]int{8080, 8090},
|
||||
Map: [2]int{9000, 9010},
|
||||
},
|
||||
hasError: false,
|
||||
},
|
||||
{
|
||||
name: "UDP单端口",
|
||||
input: "udp:5000",
|
||||
expected: Port{
|
||||
Protocol: "udp",
|
||||
Ports: [2]int{5000, 5000},
|
||||
Map: [2]int{0, 0},
|
||||
},
|
||||
hasError: false,
|
||||
},
|
||||
{
|
||||
name: "UDP端口范围",
|
||||
input: "udp:5000-5010",
|
||||
expected: Port{
|
||||
Protocol: "udp",
|
||||
Ports: [2]int{5000, 5010},
|
||||
Map: [2]int{0, 0},
|
||||
},
|
||||
hasError: false,
|
||||
},
|
||||
{
|
||||
name: "UDP端口映射",
|
||||
input: "udp:5000:6000",
|
||||
expected: Port{
|
||||
Protocol: "udp",
|
||||
Ports: [2]int{5000, 5000},
|
||||
Map: [2]int{6000, 6000},
|
||||
},
|
||||
hasError: false,
|
||||
},
|
||||
{
|
||||
name: "UDP端口范围映射(映射范围反序)",
|
||||
input: "udp:5000-5010:6010-6000",
|
||||
expected: Port{
|
||||
Protocol: "udp",
|
||||
Ports: [2]int{5000, 5010},
|
||||
Map: [2]int{6000, 6010},
|
||||
},
|
||||
hasError: false,
|
||||
},
|
||||
// 错误情况
|
||||
{
|
||||
name: "缺少协议",
|
||||
input: "8080",
|
||||
expected: Port{},
|
||||
hasError: true,
|
||||
},
|
||||
{
|
||||
name: "过多冒号",
|
||||
input: "tcp:8080:9090:extra",
|
||||
expected: Port{},
|
||||
hasError: true,
|
||||
},
|
||||
{
|
||||
name: "无效端口号",
|
||||
input: "tcp:abc",
|
||||
expected: Port{},
|
||||
hasError: true,
|
||||
},
|
||||
{
|
||||
name: "无效映射端口号",
|
||||
input: "tcp:8080:abc",
|
||||
expected: Port{},
|
||||
hasError: true,
|
||||
},
|
||||
{
|
||||
name: "无效端口范围",
|
||||
input: "tcp:8080-abc",
|
||||
expected: Port{},
|
||||
hasError: true,
|
||||
},
|
||||
{
|
||||
name: "无效映射端口范围",
|
||||
input: "tcp:8080:9000-abc",
|
||||
expected: Port{},
|
||||
hasError: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result, err := ParsePort(tt.input)
|
||||
|
||||
if tt.hasError {
|
||||
if err == nil {
|
||||
t.Errorf("期望有错误,但没有错误")
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
t.Errorf("意外的错误: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if result.Protocol != tt.expected.Protocol {
|
||||
t.Errorf("协议不匹配: 期望 %s, 得到 %s", tt.expected.Protocol, result.Protocol)
|
||||
}
|
||||
|
||||
if result.Ports != tt.expected.Ports {
|
||||
t.Errorf("端口不匹配: 期望 %v, 得到 %v", tt.expected.Ports, result.Ports)
|
||||
}
|
||||
|
||||
if result.Map != tt.expected.Map {
|
||||
t.Errorf("映射端口不匹配: 期望 %v, 得到 %v", tt.expected.Map, result.Map)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestPortMethods(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
port Port
|
||||
expectTCP bool
|
||||
expectUDP bool
|
||||
expectRange bool
|
||||
expectMapping bool
|
||||
expectRangeMap bool
|
||||
expectString string
|
||||
}{
|
||||
{
|
||||
name: "TCP单端口",
|
||||
port: Port{
|
||||
Protocol: "tcp",
|
||||
Ports: [2]int{8080, 8080},
|
||||
Map: [2]int{0, 0},
|
||||
},
|
||||
expectTCP: true,
|
||||
expectUDP: false,
|
||||
expectRange: false,
|
||||
expectMapping: false,
|
||||
expectRangeMap: false,
|
||||
expectString: "tcp:8080",
|
||||
},
|
||||
{
|
||||
name: "TCP端口范围",
|
||||
port: Port{
|
||||
Protocol: "tcp",
|
||||
Ports: [2]int{8080, 8090},
|
||||
Map: [2]int{0, 0},
|
||||
},
|
||||
expectTCP: true,
|
||||
expectUDP: false,
|
||||
expectRange: true,
|
||||
expectMapping: false,
|
||||
expectRangeMap: false,
|
||||
expectString: "tcp:8080-8090",
|
||||
},
|
||||
{
|
||||
name: "TCP单端口映射",
|
||||
port: Port{
|
||||
Protocol: "tcp",
|
||||
Ports: [2]int{8080, 8080},
|
||||
Map: [2]int{9090, 9090},
|
||||
},
|
||||
expectTCP: true,
|
||||
expectUDP: false,
|
||||
expectRange: false,
|
||||
expectMapping: true,
|
||||
expectRangeMap: false,
|
||||
expectString: "tcp:8080:9090",
|
||||
},
|
||||
{
|
||||
name: "TCP端口范围映射",
|
||||
port: Port{
|
||||
Protocol: "tcp",
|
||||
Ports: [2]int{8080, 8090},
|
||||
Map: [2]int{9000, 9010},
|
||||
},
|
||||
expectTCP: true,
|
||||
expectUDP: false,
|
||||
expectRange: true,
|
||||
expectMapping: true,
|
||||
expectRangeMap: true,
|
||||
expectString: "tcp:8080-8090:9000-9010",
|
||||
},
|
||||
{
|
||||
name: "UDP单端口映射到端口范围",
|
||||
port: Port{
|
||||
Protocol: "udp",
|
||||
Ports: [2]int{5000, 5000},
|
||||
Map: [2]int{6000, 6010},
|
||||
},
|
||||
expectTCP: false,
|
||||
expectUDP: true,
|
||||
expectRange: false,
|
||||
expectMapping: true,
|
||||
expectRangeMap: true,
|
||||
expectString: "udp:5000:6000-6010",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if tt.port.IsTCP() != tt.expectTCP {
|
||||
t.Errorf("IsTCP(): 期望 %v, 得到 %v", tt.expectTCP, tt.port.IsTCP())
|
||||
}
|
||||
|
||||
if tt.port.IsUDP() != tt.expectUDP {
|
||||
t.Errorf("IsUDP(): 期望 %v, 得到 %v", tt.expectUDP, tt.port.IsUDP())
|
||||
}
|
||||
|
||||
if tt.port.IsRange() != tt.expectRange {
|
||||
t.Errorf("IsRange(): 期望 %v, 得到 %v", tt.expectRange, tt.port.IsRange())
|
||||
}
|
||||
|
||||
if tt.port.HasMapping() != tt.expectMapping {
|
||||
t.Errorf("HasMapping(): 期望 %v, 得到 %v", tt.expectMapping, tt.port.HasMapping())
|
||||
}
|
||||
|
||||
if tt.port.IsRangeMapping() != tt.expectRangeMap {
|
||||
t.Errorf("IsRangeMapping(): 期望 %v, 得到 %v", tt.expectRangeMap, tt.port.IsRangeMapping())
|
||||
}
|
||||
|
||||
if tt.port.String() != tt.expectString {
|
||||
t.Errorf("String(): 期望 %s, 得到 %s", tt.expectString, tt.port.String())
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestParsePort2(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
expectedType string
|
||||
hasError bool
|
||||
}{
|
||||
{
|
||||
name: "TCP单端口",
|
||||
input: "tcp:8080",
|
||||
expectedType: "TCPPort",
|
||||
hasError: false,
|
||||
},
|
||||
{
|
||||
name: "TCP端口范围",
|
||||
input: "tcp:8080-8090",
|
||||
expectedType: "TCPRangePort",
|
||||
hasError: false,
|
||||
},
|
||||
{
|
||||
name: "UDP单端口",
|
||||
input: "udp:5000",
|
||||
expectedType: "UDPPort",
|
||||
hasError: false,
|
||||
},
|
||||
{
|
||||
name: "UDP端口范围",
|
||||
input: "udp:5000-5010",
|
||||
expectedType: "UDPRangePort",
|
||||
hasError: false,
|
||||
},
|
||||
{
|
||||
name: "无效输入",
|
||||
input: "invalid",
|
||||
hasError: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result, err := ParsePort2(tt.input)
|
||||
|
||||
if tt.hasError {
|
||||
if err == nil {
|
||||
t.Errorf("期望有错误,但没有错误")
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
t.Errorf("意外的错误: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
switch tt.expectedType {
|
||||
case "TCPPort":
|
||||
if _, ok := result.(TCPPort); !ok {
|
||||
t.Errorf("期望类型 TCPPort, 得到 %T", result)
|
||||
}
|
||||
case "TCPRangePort":
|
||||
if _, ok := result.(TCPRangePort); !ok {
|
||||
t.Errorf("期望类型 TCPRangePort, 得到 %T", result)
|
||||
}
|
||||
case "UDPPort":
|
||||
if _, ok := result.(UDPPort); !ok {
|
||||
t.Errorf("期望类型 UDPPort, 得到 %T", result)
|
||||
}
|
||||
case "UDPRangePort":
|
||||
if _, ok := result.(UDPRangePort); !ok {
|
||||
t.Errorf("期望类型 UDPRangePort, 得到 %T", result)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
@@ -2,33 +2,55 @@ package util
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"sync"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
type Buddy struct {
|
||||
size int
|
||||
longests []int
|
||||
size int
|
||||
longests [BuddySize>>(MinPowerOf2-1) - 1]int
|
||||
memoryPool [BuddySize]byte
|
||||
poolStart int64
|
||||
lock sync.Mutex // 保护 longests 数组的并发访问
|
||||
}
|
||||
|
||||
var (
|
||||
InValidParameterErr = errors.New("buddy: invalid parameter")
|
||||
NotFoundErr = errors.New("buddy: can't find block")
|
||||
buddyPool = sync.Pool{
|
||||
New: func() interface{} {
|
||||
return NewBuddy()
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
// GetBuddy 从池中获取一个 Buddy 实例
|
||||
func GetBuddy() *Buddy {
|
||||
buddy := buddyPool.Get().(*Buddy)
|
||||
return buddy
|
||||
}
|
||||
|
||||
// PutBuddy 将 Buddy 实例放回池中
|
||||
func PutBuddy(b *Buddy) {
|
||||
buddyPool.Put(b)
|
||||
}
|
||||
|
||||
// NewBuddy creates a buddy instance.
|
||||
// If the parameter isn't valid, return the nil and error as well
|
||||
func NewBuddy(size int) *Buddy {
|
||||
if !isPowerOf2(size) {
|
||||
size = fixSize(size)
|
||||
func NewBuddy() *Buddy {
|
||||
size := BuddySize >> MinPowerOf2
|
||||
ret := &Buddy{
|
||||
size: size,
|
||||
}
|
||||
nodeCount := 2*size - 1
|
||||
longests := make([]int, nodeCount)
|
||||
for nodeSize, i := 2*size, 0; i < nodeCount; i++ {
|
||||
for nodeSize, i := 2*size, 0; i < len(ret.longests); i++ {
|
||||
if isPowerOf2(i + 1) {
|
||||
nodeSize /= 2
|
||||
}
|
||||
longests[i] = nodeSize
|
||||
ret.longests[i] = nodeSize
|
||||
}
|
||||
return &Buddy{size, longests}
|
||||
ret.poolStart = int64(uintptr(unsafe.Pointer(&ret.memoryPool[0])))
|
||||
|
||||
return ret
|
||||
}
|
||||
|
||||
// Alloc find a unused block according to the size
|
||||
@@ -42,6 +64,8 @@ func (b *Buddy) Alloc(size int) (offset int, err error) {
|
||||
if !isPowerOf2(size) {
|
||||
size = fixSize(size)
|
||||
}
|
||||
b.lock.Lock()
|
||||
defer b.lock.Unlock()
|
||||
if size > b.longests[0] {
|
||||
err = NotFoundErr
|
||||
return
|
||||
@@ -70,6 +94,8 @@ func (b *Buddy) Free(offset int) error {
|
||||
if offset < 0 || offset >= b.size {
|
||||
return InValidParameterErr
|
||||
}
|
||||
b.lock.Lock()
|
||||
defer b.lock.Unlock()
|
||||
nodeSize := 1
|
||||
index := offset + b.size - 1
|
||||
for ; b.longests[index] != 0; index = parent(index) {
|
||||
|
@@ -3,11 +3,9 @@
|
||||
package util
|
||||
|
||||
import (
|
||||
"container/list"
|
||||
"fmt"
|
||||
"io"
|
||||
"slices"
|
||||
"sync"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
@@ -58,53 +56,59 @@ func (r *RecyclableMemory) Recycle() {
|
||||
}
|
||||
}
|
||||
|
||||
var (
|
||||
memoryPool [BuddySize]byte
|
||||
buddy = NewBuddy(BuddySize >> MinPowerOf2)
|
||||
lock sync.Mutex
|
||||
poolStart = int64(uintptr(unsafe.Pointer(&memoryPool[0])))
|
||||
blockPool = list.New()
|
||||
//EnableCheckSize bool = false
|
||||
)
|
||||
|
||||
type MemoryAllocator struct {
|
||||
allocator *Allocator
|
||||
start int64
|
||||
memory []byte
|
||||
Size int
|
||||
buddy *Buddy
|
||||
}
|
||||
|
||||
// createMemoryAllocator 创建并初始化 MemoryAllocator
|
||||
func createMemoryAllocator(size int, buddy *Buddy, offset int) *MemoryAllocator {
|
||||
ret := &MemoryAllocator{
|
||||
allocator: NewAllocator(size),
|
||||
buddy: buddy,
|
||||
Size: size,
|
||||
memory: buddy.memoryPool[offset : offset+size],
|
||||
start: buddy.poolStart + int64(offset),
|
||||
}
|
||||
ret.allocator.Init(size)
|
||||
return ret
|
||||
}
|
||||
|
||||
func GetMemoryAllocator(size int) (ret *MemoryAllocator) {
|
||||
lock.Lock()
|
||||
offset, err := buddy.Alloc(size >> MinPowerOf2)
|
||||
if blockPool.Len() > 0 {
|
||||
ret = blockPool.Remove(blockPool.Front()).(*MemoryAllocator)
|
||||
} else {
|
||||
ret = &MemoryAllocator{
|
||||
allocator: NewAllocator(size),
|
||||
if size < BuddySize {
|
||||
requiredSize := size >> MinPowerOf2
|
||||
// 循环尝试从池中获取可用的 buddy
|
||||
for {
|
||||
buddy := GetBuddy()
|
||||
offset, err := buddy.Alloc(requiredSize)
|
||||
PutBuddy(buddy)
|
||||
if err == nil {
|
||||
// 分配成功,使用这个 buddy
|
||||
return createMemoryAllocator(size, buddy, offset<<MinPowerOf2)
|
||||
}
|
||||
}
|
||||
}
|
||||
lock.Unlock()
|
||||
ret.Size = size
|
||||
ret.allocator.Init(size)
|
||||
if err != nil {
|
||||
ret.memory = make([]byte, size)
|
||||
ret.start = int64(uintptr(unsafe.Pointer(&ret.memory[0])))
|
||||
return
|
||||
// 池中的 buddy 都无法分配或大小不够,使用系统内存
|
||||
memory := make([]byte, size)
|
||||
start := int64(uintptr(unsafe.Pointer(&memory[0])))
|
||||
return &MemoryAllocator{
|
||||
allocator: NewAllocator(size),
|
||||
Size: size,
|
||||
memory: memory,
|
||||
start: start,
|
||||
}
|
||||
offset = offset << MinPowerOf2
|
||||
ret.memory = memoryPool[offset : offset+size]
|
||||
ret.start = poolStart + int64(offset)
|
||||
return
|
||||
}
|
||||
|
||||
func (ma *MemoryAllocator) Recycle() {
|
||||
ma.allocator.Recycle()
|
||||
lock.Lock()
|
||||
blockPool.PushBack(ma)
|
||||
_ = buddy.Free(int((poolStart - ma.start) >> MinPowerOf2))
|
||||
if ma.buddy != nil {
|
||||
_ = ma.buddy.Free(int((ma.buddy.poolStart - ma.start) >> MinPowerOf2))
|
||||
ma.buddy = nil
|
||||
}
|
||||
ma.memory = nil
|
||||
lock.Unlock()
|
||||
}
|
||||
|
||||
func (ma *MemoryAllocator) Find(size int) (memory []byte) {
|
||||
|
51
plugin.go
51
plugin.go
@@ -133,24 +133,9 @@ func (plugin *PluginMeta) Init(s *Server, userConfig map[string]any) (p *Plugin)
|
||||
finalConfig, _ := yaml.Marshal(p.Config.GetMap())
|
||||
p.Logger.Handler().(*MultiLogHandler).SetLevel(ParseLevel(p.config.LogLevel))
|
||||
p.Debug("config", "detail", string(finalConfig))
|
||||
if s.DisableAll {
|
||||
p.Disabled = true
|
||||
}
|
||||
if userConfig["enable"] == false {
|
||||
p.Disabled = true
|
||||
} else if userConfig["enable"] == true {
|
||||
p.Disabled = false
|
||||
}
|
||||
if p.Disabled {
|
||||
if userConfig["enable"] == false || (s.DisableAll && userConfig["enable"] != true) {
|
||||
p.disable("config")
|
||||
p.Warn("plugin disabled")
|
||||
return
|
||||
} else {
|
||||
var handlers map[string]http.HandlerFunc
|
||||
if v, ok := instance.(IRegisterHandler); ok {
|
||||
handlers = v.RegisterHandler()
|
||||
}
|
||||
p.registerHandler(handlers)
|
||||
}
|
||||
p.Info("init", "version", plugin.Version)
|
||||
var err error
|
||||
@@ -158,7 +143,7 @@ func (plugin *PluginMeta) Init(s *Server, userConfig map[string]any) (p *Plugin)
|
||||
p.DB = s.DB
|
||||
} else if p.config.DSN != "" {
|
||||
if factory, ok := db.Factory[p.config.DBType]; ok {
|
||||
s.DB, err = gorm.Open(factory(p.config.DSN), &gorm.Config{})
|
||||
p.DB, err = gorm.Open(factory(p.config.DSN), &gorm.Config{})
|
||||
if err != nil {
|
||||
s.Error("failed to connect database", "error", err, "dsn", s.config.DSN, "type", s.config.DBType)
|
||||
p.disable(fmt.Sprintf("database %v", err))
|
||||
@@ -171,8 +156,21 @@ func (plugin *PluginMeta) Init(s *Server, userConfig map[string]any) (p *Plugin)
|
||||
p.disable(fmt.Sprintf("auto migrate record stream failed %v", err))
|
||||
return
|
||||
}
|
||||
if err = p.DB.AutoMigrate(&EventRecordStream{}); err != nil {
|
||||
p.disable(fmt.Sprintf("auto migrate event record stream failed %v", err))
|
||||
return
|
||||
}
|
||||
}
|
||||
s.AddTask(instance)
|
||||
if err := s.AddTask(instance).WaitStarted(); err != nil {
|
||||
p.disable(instance.StopReason().Error())
|
||||
return
|
||||
}
|
||||
var handlers map[string]http.HandlerFunc
|
||||
if v, ok := instance.(IRegisterHandler); ok {
|
||||
handlers = v.RegisterHandler()
|
||||
}
|
||||
p.registerHandler(handlers)
|
||||
s.Plugins.Add(p)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -277,11 +275,19 @@ func (p *Plugin) GetPublicIP(netcardIP string) string {
|
||||
func (p *Plugin) disable(reason string) {
|
||||
p.Disabled = true
|
||||
p.SetDescription("disableReason", reason)
|
||||
p.Warn("plugin disabled")
|
||||
p.Server.disabledPlugins = append(p.Server.disabledPlugins, p)
|
||||
}
|
||||
|
||||
func (p *Plugin) Start() (err error) {
|
||||
s := p.Server
|
||||
|
||||
if err = p.listen(); err != nil {
|
||||
return
|
||||
}
|
||||
if err = p.handler.OnInit(); err != nil {
|
||||
return
|
||||
}
|
||||
if p.Meta.ServiceDesc != nil && s.grpcServer != nil {
|
||||
s.grpcServer.RegisterService(p.Meta.ServiceDesc, p.handler)
|
||||
if p.Meta.RegisterGRPCHandler != nil {
|
||||
@@ -293,15 +299,6 @@ func (p *Plugin) Start() (err error) {
|
||||
}
|
||||
}
|
||||
}
|
||||
s.Plugins.Add(p)
|
||||
if err = p.listen(); err != nil {
|
||||
p.disable(fmt.Sprintf("listen %v", err))
|
||||
return
|
||||
}
|
||||
if err = p.handler.OnInit(); err != nil {
|
||||
p.disable(fmt.Sprintf("init %v", err))
|
||||
return
|
||||
}
|
||||
if p.config.Hook != nil {
|
||||
if hook, ok := p.config.Hook[config.HookOnServerKeepAlive]; ok && hook.Interval > 0 {
|
||||
p.AddTask(&ServerKeepAliveTask{plugin: p})
|
||||
|
@@ -2,7 +2,10 @@ package plugin_crontab
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"google.golang.org/protobuf/types/known/timestamppb"
|
||||
cronpb "m7s.live/v5/plugin/crontab/pb"
|
||||
@@ -17,34 +20,29 @@ func (ct *CrontabPlugin) List(ctx context.Context, req *cronpb.ReqPlanList) (*cr
|
||||
req.PageSize = 10
|
||||
}
|
||||
|
||||
var total int64
|
||||
var plans []pkg.RecordPlan
|
||||
// 从内存中获取所有计划
|
||||
plans := ct.recordPlans.Items
|
||||
total := len(plans)
|
||||
|
||||
query := ct.DB.Model(&pkg.RecordPlan{})
|
||||
|
||||
result := query.Count(&total)
|
||||
if result.Error != nil {
|
||||
return &cronpb.PlanResponseList{
|
||||
Code: 500,
|
||||
Message: result.Error.Error(),
|
||||
}, nil
|
||||
// 计算分页
|
||||
start := int(req.PageNum-1) * int(req.PageSize)
|
||||
end := start + int(req.PageSize)
|
||||
if start >= total {
|
||||
start = total
|
||||
}
|
||||
if end > total {
|
||||
end = total
|
||||
}
|
||||
|
||||
offset := (req.PageNum - 1) * req.PageSize
|
||||
result = query.Order("id desc").Offset(int(offset)).Limit(int(req.PageSize)).Find(&plans)
|
||||
if result.Error != nil {
|
||||
return &cronpb.PlanResponseList{
|
||||
Code: 500,
|
||||
Message: result.Error.Error(),
|
||||
}, nil
|
||||
}
|
||||
// 获取当前页的数据
|
||||
pagePlans := plans[start:end]
|
||||
|
||||
data := make([]*cronpb.Plan, 0, len(plans))
|
||||
for _, plan := range plans {
|
||||
data := make([]*cronpb.Plan, 0, len(pagePlans))
|
||||
for _, plan := range pagePlans {
|
||||
data = append(data, &cronpb.Plan{
|
||||
Id: uint32(plan.ID),
|
||||
Name: plan.Name,
|
||||
Enable: plan.Enabled,
|
||||
Enable: plan.Enable,
|
||||
CreateTime: timestamppb.New(plan.CreatedAt),
|
||||
UpdateTime: timestamppb.New(plan.UpdatedAt),
|
||||
Plan: plan.Plan,
|
||||
@@ -94,9 +92,9 @@ func (ct *CrontabPlugin) Add(ctx context.Context, req *cronpb.Plan) (*cronpb.Res
|
||||
}
|
||||
|
||||
plan := &pkg.RecordPlan{
|
||||
Name: req.Name,
|
||||
Plan: req.Plan,
|
||||
Enabled: req.Enable,
|
||||
Name: req.Name,
|
||||
Plan: req.Plan,
|
||||
Enable: req.Enable,
|
||||
}
|
||||
|
||||
if err := ct.DB.Create(plan).Error; err != nil {
|
||||
@@ -106,6 +104,9 @@ func (ct *CrontabPlugin) Add(ctx context.Context, req *cronpb.Plan) (*cronpb.Res
|
||||
}, nil
|
||||
}
|
||||
|
||||
// 添加到内存中
|
||||
ct.recordPlans.Add(plan)
|
||||
|
||||
return &cronpb.Response{
|
||||
Code: 0,
|
||||
Message: "success",
|
||||
@@ -160,10 +161,14 @@ func (ct *CrontabPlugin) Update(ctx context.Context, req *cronpb.Plan) (*cronpb.
|
||||
}, nil
|
||||
}
|
||||
|
||||
// 处理 enable 状态变更
|
||||
enableChanged := existingPlan.Enable != req.Enable
|
||||
|
||||
// 更新记录
|
||||
updates := map[string]interface{}{
|
||||
"name": req.Name,
|
||||
"plan": req.Plan,
|
||||
"enabled": req.Enable,
|
||||
"name": req.Name,
|
||||
"plan": req.Plan,
|
||||
"enable": req.Enable,
|
||||
}
|
||||
|
||||
if err := ct.DB.Model(&existingPlan).Updates(updates).Error; err != nil {
|
||||
@@ -173,6 +178,45 @@ func (ct *CrontabPlugin) Update(ctx context.Context, req *cronpb.Plan) (*cronpb.
|
||||
}, nil
|
||||
}
|
||||
|
||||
// 更新内存中的记录
|
||||
existingPlan.Name = req.Name
|
||||
existingPlan.Plan = req.Plan
|
||||
existingPlan.Enable = req.Enable
|
||||
ct.recordPlans.Set(&existingPlan)
|
||||
|
||||
// 处理 enable 状态变更后的操作
|
||||
if enableChanged {
|
||||
if req.Enable {
|
||||
// 从 false 变为 true,需要创建并启动新的定时任务
|
||||
var streams []pkg.RecordPlanStream
|
||||
model := &pkg.RecordPlanStream{PlanID: existingPlan.ID}
|
||||
if err := ct.DB.Model(model).Where(model).Find(&streams).Error; err != nil {
|
||||
ct.Error("query record plan streams error: %v", err)
|
||||
} else {
|
||||
// 为每个流创建定时任务
|
||||
for _, stream := range streams {
|
||||
crontab := &Crontab{
|
||||
ctp: ct,
|
||||
RecordPlan: &existingPlan,
|
||||
RecordPlanStream: &stream,
|
||||
}
|
||||
crontab.OnStart(func() {
|
||||
ct.crontabs.Set(crontab)
|
||||
})
|
||||
ct.AddTask(crontab)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// 从 true 变为 false,需要停止相关的定时任务
|
||||
ct.crontabs.Range(func(crontab *Crontab) bool {
|
||||
if crontab.RecordPlan.ID == existingPlan.ID {
|
||||
crontab.Stop(nil)
|
||||
}
|
||||
return true
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return &cronpb.Response{
|
||||
Code: 0,
|
||||
Message: "success",
|
||||
@@ -196,6 +240,14 @@ func (ct *CrontabPlugin) Remove(ctx context.Context, req *cronpb.DeleteRequest)
|
||||
}, nil
|
||||
}
|
||||
|
||||
// 先停止所有相关的定时任务
|
||||
ct.crontabs.Range(func(crontab *Crontab) bool {
|
||||
if crontab.RecordPlan.ID == existingPlan.ID {
|
||||
crontab.Stop(nil)
|
||||
}
|
||||
return true
|
||||
})
|
||||
|
||||
// 执行软删除
|
||||
if err := ct.DB.Delete(&existingPlan).Error; err != nil {
|
||||
return &cronpb.Response{
|
||||
@@ -204,8 +256,735 @@ func (ct *CrontabPlugin) Remove(ctx context.Context, req *cronpb.DeleteRequest)
|
||||
}, nil
|
||||
}
|
||||
|
||||
// 从内存中移除
|
||||
ct.recordPlans.RemoveByKey(existingPlan.ID)
|
||||
|
||||
return &cronpb.Response{
|
||||
Code: 0,
|
||||
Message: "success",
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (ct *CrontabPlugin) ListRecordPlanStreams(ctx context.Context, req *cronpb.ReqPlanStreamList) (*cronpb.RecordPlanStreamResponseList, error) {
|
||||
if req.PageNum < 1 {
|
||||
req.PageNum = 1
|
||||
}
|
||||
if req.PageSize < 1 {
|
||||
req.PageSize = 10
|
||||
}
|
||||
|
||||
var total int64
|
||||
var streams []pkg.RecordPlanStream
|
||||
model := &pkg.RecordPlanStream{}
|
||||
|
||||
// 构建查询条件
|
||||
query := ct.DB.Model(model).
|
||||
Scopes(
|
||||
pkg.ScopeRecordPlanID(uint(req.PlanId)),
|
||||
pkg.ScopeStreamPathLike(req.StreamPath),
|
||||
pkg.ScopeOrderByCreatedAtDesc(),
|
||||
)
|
||||
|
||||
result := query.Count(&total)
|
||||
if result.Error != nil {
|
||||
return &cronpb.RecordPlanStreamResponseList{
|
||||
Code: 500,
|
||||
Message: result.Error.Error(),
|
||||
}, nil
|
||||
}
|
||||
|
||||
offset := (req.PageNum - 1) * req.PageSize
|
||||
result = query.Offset(int(offset)).Limit(int(req.PageSize)).Find(&streams)
|
||||
|
||||
if result.Error != nil {
|
||||
return &cronpb.RecordPlanStreamResponseList{
|
||||
Code: 500,
|
||||
Message: result.Error.Error(),
|
||||
}, nil
|
||||
}
|
||||
|
||||
data := make([]*cronpb.PlanStream, 0, len(streams))
|
||||
for _, stream := range streams {
|
||||
data = append(data, &cronpb.PlanStream{
|
||||
PlanId: uint32(stream.PlanID),
|
||||
StreamPath: stream.StreamPath,
|
||||
Fragment: stream.Fragment,
|
||||
FilePath: stream.FilePath,
|
||||
CreatedAt: timestamppb.New(stream.CreatedAt),
|
||||
UpdatedAt: timestamppb.New(stream.UpdatedAt),
|
||||
Enable: stream.Enable,
|
||||
})
|
||||
}
|
||||
|
||||
return &cronpb.RecordPlanStreamResponseList{
|
||||
Code: 0,
|
||||
Message: "success",
|
||||
TotalCount: uint32(total),
|
||||
PageNum: req.PageNum,
|
||||
PageSize: req.PageSize,
|
||||
Data: data,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (ct *CrontabPlugin) AddRecordPlanStream(ctx context.Context, req *cronpb.PlanStream) (*cronpb.Response, error) {
|
||||
if req.PlanId == 0 {
|
||||
return &cronpb.Response{
|
||||
Code: 400,
|
||||
Message: "record_plan_id is required",
|
||||
}, nil
|
||||
}
|
||||
|
||||
if strings.TrimSpace(req.StreamPath) == "" {
|
||||
return &cronpb.Response{
|
||||
Code: 400,
|
||||
Message: "stream_path is required",
|
||||
}, nil
|
||||
}
|
||||
|
||||
// 从内存中获取录制计划
|
||||
plan, ok := ct.recordPlans.Get(uint(req.PlanId))
|
||||
if !ok {
|
||||
return &cronpb.Response{
|
||||
Code: 404,
|
||||
Message: "record plan not found",
|
||||
}, nil
|
||||
}
|
||||
|
||||
// 检查是否已存在相同的记录
|
||||
var count int64
|
||||
searchModel := pkg.RecordPlanStream{
|
||||
PlanID: uint(req.PlanId),
|
||||
StreamPath: req.StreamPath,
|
||||
}
|
||||
if err := ct.DB.Model(&searchModel).Where(&searchModel).Count(&count).Error; err != nil {
|
||||
return &cronpb.Response{
|
||||
Code: 500,
|
||||
Message: err.Error(),
|
||||
}, nil
|
||||
}
|
||||
|
||||
if count > 0 {
|
||||
return &cronpb.Response{
|
||||
Code: 400,
|
||||
Message: "record already exists",
|
||||
}, nil
|
||||
}
|
||||
|
||||
stream := &pkg.RecordPlanStream{
|
||||
PlanID: uint(req.PlanId),
|
||||
StreamPath: req.StreamPath,
|
||||
Fragment: req.Fragment,
|
||||
FilePath: req.FilePath,
|
||||
Enable: req.Enable,
|
||||
RecordType: req.RecordType,
|
||||
}
|
||||
|
||||
if err := ct.DB.Create(stream).Error; err != nil {
|
||||
return &cronpb.Response{
|
||||
Code: 500,
|
||||
Message: err.Error(),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// 如果计划是启用状态,创建并启动定时任务
|
||||
if plan.Enable {
|
||||
crontab := &Crontab{
|
||||
ctp: ct,
|
||||
RecordPlan: plan,
|
||||
RecordPlanStream: stream,
|
||||
}
|
||||
crontab.OnStart(func() {
|
||||
ct.crontabs.Set(crontab)
|
||||
})
|
||||
ct.AddTask(crontab)
|
||||
}
|
||||
|
||||
return &cronpb.Response{
|
||||
Code: 0,
|
||||
Message: "success",
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (ct *CrontabPlugin) UpdateRecordPlanStream(ctx context.Context, req *cronpb.PlanStream) (*cronpb.Response, error) {
|
||||
if req.PlanId == 0 {
|
||||
return &cronpb.Response{
|
||||
Code: 400,
|
||||
Message: "record_plan_id is required",
|
||||
}, nil
|
||||
}
|
||||
|
||||
if strings.TrimSpace(req.StreamPath) == "" {
|
||||
return &cronpb.Response{
|
||||
Code: 400,
|
||||
Message: "stream_path is required",
|
||||
}, nil
|
||||
}
|
||||
|
||||
// 检查记录是否存在
|
||||
var existingStream pkg.RecordPlanStream
|
||||
searchModel := pkg.RecordPlanStream{
|
||||
PlanID: uint(req.PlanId),
|
||||
StreamPath: req.StreamPath,
|
||||
}
|
||||
if err := ct.DB.Where(&searchModel).First(&existingStream).Error; err != nil {
|
||||
return &cronpb.Response{
|
||||
Code: 404,
|
||||
Message: "record not found",
|
||||
}, nil
|
||||
}
|
||||
|
||||
// 更新记录
|
||||
existingStream.Fragment = req.Fragment
|
||||
existingStream.FilePath = req.FilePath
|
||||
existingStream.Enable = req.Enable
|
||||
existingStream.RecordType = req.RecordType
|
||||
|
||||
if err := ct.DB.Save(&existingStream).Error; err != nil {
|
||||
return &cronpb.Response{
|
||||
Code: 500,
|
||||
Message: err.Error(),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// 停止当前流相关的所有任务
|
||||
ct.crontabs.Range(func(crontab *Crontab) bool {
|
||||
if crontab.RecordPlanStream.StreamPath == req.StreamPath {
|
||||
crontab.Stop(nil)
|
||||
}
|
||||
return true
|
||||
})
|
||||
|
||||
// 查询所有关联此流的记录
|
||||
var streams []pkg.RecordPlanStream
|
||||
if err := ct.DB.Where("stream_path = ?", req.StreamPath).Find(&streams).Error; err != nil {
|
||||
ct.Error("query record plan streams error: %v", err)
|
||||
return &cronpb.Response{
|
||||
Code: 500,
|
||||
Message: err.Error(),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// 为每个启用的计划创建新的定时任务
|
||||
for _, stream := range streams {
|
||||
// 从内存中获取对应的计划
|
||||
plan, ok := ct.recordPlans.Get(stream.PlanID)
|
||||
if !ok {
|
||||
ct.Error("record plan not found in memory: %d", stream.PlanID)
|
||||
continue
|
||||
}
|
||||
|
||||
// 如果计划是启用状态,创建并启动定时任务
|
||||
if plan.Enable && stream.Enable {
|
||||
crontab := &Crontab{
|
||||
ctp: ct,
|
||||
RecordPlan: plan,
|
||||
RecordPlanStream: &stream,
|
||||
}
|
||||
crontab.OnStart(func() {
|
||||
ct.crontabs.Set(crontab)
|
||||
})
|
||||
ct.AddTask(crontab)
|
||||
}
|
||||
}
|
||||
|
||||
return &cronpb.Response{
|
||||
Code: 0,
|
||||
Message: "success",
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (ct *CrontabPlugin) RemoveRecordPlanStream(ctx context.Context, req *cronpb.DeletePlanStreamRequest) (*cronpb.Response, error) {
|
||||
if req.PlanId == 0 {
|
||||
return &cronpb.Response{
|
||||
Code: 400,
|
||||
Message: "record_plan_id is required",
|
||||
}, nil
|
||||
}
|
||||
|
||||
if strings.TrimSpace(req.StreamPath) == "" {
|
||||
return &cronpb.Response{
|
||||
Code: 400,
|
||||
Message: "stream_path is required",
|
||||
}, nil
|
||||
}
|
||||
|
||||
// 检查记录是否存在
|
||||
var existingStream pkg.RecordPlanStream
|
||||
searchModel := pkg.RecordPlanStream{
|
||||
PlanID: uint(req.PlanId),
|
||||
StreamPath: req.StreamPath,
|
||||
}
|
||||
if err := ct.DB.Where(&searchModel).First(&existingStream).Error; err != nil {
|
||||
return &cronpb.Response{
|
||||
Code: 404,
|
||||
Message: "record not found",
|
||||
}, nil
|
||||
}
|
||||
|
||||
// 停止所有相关的定时任务
|
||||
ct.crontabs.Range(func(crontab *Crontab) bool {
|
||||
if crontab.RecordPlanStream.StreamPath == req.StreamPath && crontab.RecordPlan.ID == uint(req.PlanId) {
|
||||
crontab.Stop(nil)
|
||||
}
|
||||
return true
|
||||
})
|
||||
|
||||
// 执行删除
|
||||
if err := ct.DB.Delete(&existingStream).Error; err != nil {
|
||||
return &cronpb.Response{
|
||||
Code: 500,
|
||||
Message: err.Error(),
|
||||
}, nil
|
||||
}
|
||||
|
||||
return &cronpb.Response{
|
||||
Code: 0,
|
||||
Message: "success",
|
||||
}, nil
|
||||
}
|
||||
|
||||
// 获取周几的名称(0=周日,1=周一,...,6=周六)
|
||||
func getWeekdayName(weekday int) string {
|
||||
weekdays := []string{"周日", "周一", "周二", "周三", "周四", "周五", "周六"}
|
||||
return weekdays[weekday]
|
||||
}
|
||||
|
||||
// 获取周几的索引(0=周日,1=周一,...,6=周六)
|
||||
func getWeekdayIndex(weekdayName string) int {
|
||||
weekdays := map[string]int{
|
||||
"周日": 0, "周一": 1, "周二": 2, "周三": 3, "周四": 4, "周五": 5, "周六": 6,
|
||||
}
|
||||
return weekdays[weekdayName]
|
||||
}
|
||||
|
||||
// 获取下一个指定周几的日期
|
||||
func getNextDateForWeekday(now time.Time, targetWeekday int, location *time.Location) time.Time {
|
||||
nowWeekday := int(now.Weekday())
|
||||
daysToAdd := 0
|
||||
|
||||
if targetWeekday >= nowWeekday {
|
||||
daysToAdd = targetWeekday - nowWeekday
|
||||
} else {
|
||||
daysToAdd = 7 - (nowWeekday - targetWeekday)
|
||||
}
|
||||
|
||||
// 如果是同一天但当前时间已经过了最后的时间段,则推到下一周
|
||||
if daysToAdd == 0 {
|
||||
// 这里简化处理,直接加7天到下周同一天
|
||||
daysToAdd = 7
|
||||
}
|
||||
|
||||
return now.AddDate(0, 0, daysToAdd)
|
||||
}
|
||||
|
||||
// 计算计划中的所有时间段
|
||||
func calculateTimeSlots(plan string, now time.Time, location *time.Location) ([]*cronpb.TimeSlotInfo, error) {
|
||||
if len(plan) != 168 {
|
||||
return nil, fmt.Errorf("invalid plan format: length should be 168")
|
||||
}
|
||||
|
||||
var slots []*cronpb.TimeSlotInfo
|
||||
|
||||
// 按周几遍历(0=周日,1=周一,...,6=周六)
|
||||
for weekday := 0; weekday < 7; weekday++ {
|
||||
dayOffset := weekday * 24
|
||||
var startHour int = -1
|
||||
|
||||
// 遍历这一天的每个小时
|
||||
for hour := 0; hour <= 24; hour++ {
|
||||
// 如果到了一天的结尾或者当前小时状态为0
|
||||
isEndOfDay := hour == 24
|
||||
isHourOff := !isEndOfDay && plan[dayOffset+hour] == '0'
|
||||
|
||||
if isEndOfDay || isHourOff {
|
||||
// 如果之前有开始的时间段,现在结束了
|
||||
if startHour != -1 {
|
||||
// 计算下一个该周几的日期
|
||||
targetDate := getNextDateForWeekday(now, weekday, location)
|
||||
|
||||
// 创建时间段
|
||||
startTime := time.Date(targetDate.Year(), targetDate.Month(), targetDate.Day(), startHour, 0, 0, 0, location)
|
||||
endTime := time.Date(targetDate.Year(), targetDate.Month(), targetDate.Day(), hour, 0, 0, 0, location)
|
||||
|
||||
// 转换为 UTC 时间
|
||||
startTs := timestamppb.New(startTime.UTC())
|
||||
endTs := timestamppb.New(endTime.UTC())
|
||||
|
||||
slots = append(slots, &cronpb.TimeSlotInfo{
|
||||
Start: startTs,
|
||||
End: endTs,
|
||||
Weekday: getWeekdayName(weekday),
|
||||
TimeRange: fmt.Sprintf("%02d:00-%02d:00", startHour, hour),
|
||||
})
|
||||
startHour = -1
|
||||
}
|
||||
} else if plan[dayOffset+hour] == '1' && startHour == -1 {
|
||||
// 找到新的开始时间
|
||||
startHour = hour
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 按时间排序
|
||||
sort.Slice(slots, func(i, j int) bool {
|
||||
// 先按周几排序
|
||||
weekdayI := getWeekdayIndex(slots[i].Weekday)
|
||||
weekdayJ := getWeekdayIndex(slots[j].Weekday)
|
||||
|
||||
if weekdayI != weekdayJ {
|
||||
return weekdayI < weekdayJ
|
||||
}
|
||||
|
||||
// 同一天按开始时间排序
|
||||
return slots[i].Start.AsTime().Hour() < slots[j].Start.AsTime().Hour()
|
||||
})
|
||||
|
||||
return slots, nil
|
||||
}
|
||||
|
||||
// 获取下一个时间段
|
||||
func getNextTimeSlotFromNow(plan string, now time.Time, location *time.Location) (*cronpb.TimeSlotInfo, error) {
|
||||
if len(plan) != 168 {
|
||||
return nil, fmt.Errorf("invalid plan format: length should be 168")
|
||||
}
|
||||
|
||||
// 将当前时间转换为本地时间
|
||||
localNow := now.In(location)
|
||||
currentWeekday := int(localNow.Weekday())
|
||||
currentHour := localNow.Hour()
|
||||
|
||||
// 检查是否在整点边界附近(前后30秒)
|
||||
isNearHourBoundary := localNow.Minute() == 59 && localNow.Second() >= 30 || localNow.Minute() == 0 && localNow.Second() <= 30
|
||||
|
||||
// 首先检查当前时间是否在某个时间段内
|
||||
dayOffset := currentWeekday * 24
|
||||
if currentHour < 24 && plan[dayOffset+currentHour] == '1' {
|
||||
// 找到当前小时所在的完整时间段
|
||||
startHour := currentHour
|
||||
// 向前查找时间段的开始
|
||||
for h := currentHour - 1; h >= 0; h-- {
|
||||
if plan[dayOffset+h] == '1' {
|
||||
startHour = h
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// 向后查找时间段的结束
|
||||
endHour := currentHour + 1
|
||||
for h := endHour; h < 24; h++ {
|
||||
if plan[dayOffset+h] == '1' {
|
||||
endHour = h + 1
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// 检查是否已经接近当前时间段的结束
|
||||
isNearEndOfTimeSlot := currentHour == endHour-1 && localNow.Minute() >= 59 && localNow.Second() >= 30
|
||||
|
||||
// 如果我们靠近时间段结束且在小时边界附近,我们跳过此时间段,找下一个
|
||||
if isNearEndOfTimeSlot && isNearHourBoundary {
|
||||
// 继续查找下一个时间段
|
||||
} else {
|
||||
// 创建时间段
|
||||
startTime := time.Date(localNow.Year(), localNow.Month(), localNow.Day(), startHour, 0, 0, 0, location)
|
||||
endTime := time.Date(localNow.Year(), localNow.Month(), localNow.Day(), endHour, 0, 0, 0, location)
|
||||
|
||||
// 如果当前时间已经接近或超过了结束时间,调整结束时间
|
||||
if localNow.After(endTime.Add(-30*time.Second)) || localNow.Equal(endTime) {
|
||||
// 继续查找下一个时间段
|
||||
} else {
|
||||
// 返回当前时间段
|
||||
return &cronpb.TimeSlotInfo{
|
||||
Start: timestamppb.New(startTime.UTC()),
|
||||
End: timestamppb.New(endTime.UTC()),
|
||||
Weekday: getWeekdayName(currentWeekday),
|
||||
TimeRange: fmt.Sprintf("%02d:00-%02d:00", startHour, endHour),
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 查找下一个时间段
|
||||
// 先查找当天剩余时间
|
||||
for h := currentHour + 1; h < 24; h++ {
|
||||
if plan[dayOffset+h] == '1' {
|
||||
// 找到开始小时
|
||||
startHour := h
|
||||
// 查找结束小时
|
||||
endHour := h + 1
|
||||
for j := h + 1; j < 24; j++ {
|
||||
if plan[dayOffset+j] == '1' {
|
||||
endHour = j + 1
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// 创建时间段
|
||||
startTime := time.Date(localNow.Year(), localNow.Month(), localNow.Day(), startHour, 0, 0, 0, location)
|
||||
endTime := time.Date(localNow.Year(), localNow.Month(), localNow.Day(), endHour, 0, 0, 0, location)
|
||||
|
||||
return &cronpb.TimeSlotInfo{
|
||||
Start: timestamppb.New(startTime.UTC()),
|
||||
End: timestamppb.New(endTime.UTC()),
|
||||
Weekday: getWeekdayName(currentWeekday),
|
||||
TimeRange: fmt.Sprintf("%02d:00-%02d:00", startHour, endHour),
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
|
||||
// 如果当天没有找到,则查找后续日期
|
||||
for d := 1; d <= 7; d++ {
|
||||
nextDay := (currentWeekday + d) % 7
|
||||
dayOffset := nextDay * 24
|
||||
|
||||
for h := 0; h < 24; h++ {
|
||||
if plan[dayOffset+h] == '1' {
|
||||
// 找到开始小时
|
||||
startHour := h
|
||||
// 查找结束小时
|
||||
endHour := h + 1
|
||||
for j := h + 1; j < 24; j++ {
|
||||
if plan[dayOffset+j] == '1' {
|
||||
endHour = j + 1
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// 计算日期
|
||||
nextDate := localNow.AddDate(0, 0, d)
|
||||
|
||||
// 创建时间段
|
||||
startTime := time.Date(nextDate.Year(), nextDate.Month(), nextDate.Day(), startHour, 0, 0, 0, location)
|
||||
endTime := time.Date(nextDate.Year(), nextDate.Month(), nextDate.Day(), endHour, 0, 0, 0, location)
|
||||
|
||||
return &cronpb.TimeSlotInfo{
|
||||
Start: timestamppb.New(startTime.UTC()),
|
||||
End: timestamppb.New(endTime.UTC()),
|
||||
Weekday: getWeekdayName(nextDay),
|
||||
TimeRange: fmt.Sprintf("%02d:00-%02d:00", startHour, endHour),
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (ct *CrontabPlugin) ParsePlanTime(ctx context.Context, req *cronpb.ParsePlanRequest) (*cronpb.ParsePlanResponse, error) {
|
||||
if len(req.Plan) != 168 {
|
||||
return &cronpb.ParsePlanResponse{
|
||||
Code: 400,
|
||||
Message: "invalid plan format: length should be 168",
|
||||
}, nil
|
||||
}
|
||||
|
||||
// 检查字符串格式是否正确(只包含0和1)
|
||||
for i, c := range req.Plan {
|
||||
if c != '0' && c != '1' {
|
||||
return &cronpb.ParsePlanResponse{
|
||||
Code: 400,
|
||||
Message: fmt.Sprintf("invalid character at position %d: %c (should be 0 or 1)", i, c),
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
|
||||
// 获取所有时间段
|
||||
slots, err := calculateTimeSlots(req.Plan, time.Now(), time.Local)
|
||||
if err != nil {
|
||||
return &cronpb.ParsePlanResponse{
|
||||
Code: 500,
|
||||
Message: err.Error(),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// 获取下一个时间段
|
||||
nextSlot, err := getNextTimeSlotFromNow(req.Plan, time.Now(), time.Local)
|
||||
if err != nil {
|
||||
return &cronpb.ParsePlanResponse{
|
||||
Code: 500,
|
||||
Message: err.Error(),
|
||||
}, nil
|
||||
}
|
||||
|
||||
return &cronpb.ParsePlanResponse{
|
||||
Code: 0,
|
||||
Message: "success",
|
||||
Slots: slots,
|
||||
NextSlot: nextSlot,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// 辅助函数:构建任务状态信息
|
||||
func buildCrontabTaskInfo(crontab *Crontab, now time.Time) *cronpb.CrontabTaskInfo {
|
||||
// 基础任务信息
|
||||
taskInfo := &cronpb.CrontabTaskInfo{
|
||||
PlanId: uint32(crontab.RecordPlan.ID),
|
||||
PlanName: crontab.RecordPlan.Name,
|
||||
StreamPath: crontab.StreamPath,
|
||||
FilePath: crontab.FilePath,
|
||||
Fragment: crontab.Fragment,
|
||||
}
|
||||
|
||||
// 获取完整计划时间段列表
|
||||
if crontab.RecordPlan != nil && crontab.RecordPlan.Plan != "" {
|
||||
planSlots, err := calculateTimeSlots(crontab.RecordPlan.Plan, now, time.Local)
|
||||
if err == nil && planSlots != nil && len(planSlots) > 0 {
|
||||
taskInfo.PlanSlots = planSlots
|
||||
}
|
||||
}
|
||||
|
||||
return taskInfo
|
||||
}
|
||||
|
||||
// GetCrontabStatus 获取当前Crontab任务状态
|
||||
func (ct *CrontabPlugin) GetCrontabStatus(ctx context.Context, req *cronpb.CrontabStatusRequest) (*cronpb.CrontabStatusResponse, error) {
|
||||
response := &cronpb.CrontabStatusResponse{
|
||||
Code: 0,
|
||||
Message: "success",
|
||||
RunningTasks: []*cronpb.CrontabTaskInfo{},
|
||||
NextTasks: []*cronpb.CrontabTaskInfo{},
|
||||
TotalRunning: 0,
|
||||
TotalPlanned: 0,
|
||||
}
|
||||
|
||||
// 获取当前正在运行的任务
|
||||
runningTasks := make([]*cronpb.CrontabTaskInfo, 0)
|
||||
nextTasks := make([]*cronpb.CrontabTaskInfo, 0)
|
||||
|
||||
// 如果只指定了流路径但未找到对应的任务,也返回该流的计划信息
|
||||
streamPathFound := false
|
||||
|
||||
// 遍历所有Crontab任务
|
||||
ct.crontabs.Range(func(crontab *Crontab) bool {
|
||||
// 如果指定了stream_path过滤条件,且不匹配,则跳过
|
||||
if req.StreamPath != "" && crontab.StreamPath != req.StreamPath {
|
||||
return true // 继续遍历
|
||||
}
|
||||
|
||||
// 标记已找到指定的流
|
||||
if req.StreamPath != "" {
|
||||
streamPathFound = true
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
|
||||
// 构建基本任务信息
|
||||
taskInfo := buildCrontabTaskInfo(crontab, now)
|
||||
|
||||
// 检查是否正在录制
|
||||
if crontab.recording && crontab.currentSlot != nil {
|
||||
// 当前正在录制
|
||||
taskInfo.IsRecording = true
|
||||
|
||||
// 设置时间信息
|
||||
taskInfo.StartTime = timestamppb.New(crontab.currentSlot.Start)
|
||||
taskInfo.EndTime = timestamppb.New(crontab.currentSlot.End)
|
||||
|
||||
// 计算已运行时间和剩余时间
|
||||
elapsedDuration := now.Sub(crontab.currentSlot.Start)
|
||||
remainingDuration := crontab.currentSlot.End.Sub(now)
|
||||
taskInfo.ElapsedSeconds = uint32(elapsedDuration.Seconds())
|
||||
taskInfo.RemainingSeconds = uint32(remainingDuration.Seconds())
|
||||
|
||||
// 设置时间范围和周几
|
||||
startHour := crontab.currentSlot.Start.Hour()
|
||||
endHour := crontab.currentSlot.End.Hour()
|
||||
taskInfo.TimeRange = fmt.Sprintf("%02d:00-%02d:00", startHour, endHour)
|
||||
taskInfo.Weekday = getWeekdayName(int(crontab.currentSlot.Start.Weekday()))
|
||||
|
||||
// 添加到正在运行的任务列表
|
||||
runningTasks = append(runningTasks, taskInfo)
|
||||
} else {
|
||||
// 获取下一个时间段
|
||||
nextSlot := crontab.getNextTimeSlot()
|
||||
if nextSlot != nil {
|
||||
// 设置下一个任务的信息
|
||||
taskInfo.IsRecording = false
|
||||
|
||||
// 设置时间信息
|
||||
taskInfo.StartTime = timestamppb.New(nextSlot.Start)
|
||||
taskInfo.EndTime = timestamppb.New(nextSlot.End)
|
||||
|
||||
// 计算等待时间
|
||||
waitingDuration := nextSlot.Start.Sub(now)
|
||||
taskInfo.RemainingSeconds = uint32(waitingDuration.Seconds())
|
||||
|
||||
// 设置时间范围和周几
|
||||
startHour := nextSlot.Start.Hour()
|
||||
endHour := nextSlot.End.Hour()
|
||||
taskInfo.TimeRange = fmt.Sprintf("%02d:00-%02d:00", startHour, endHour)
|
||||
taskInfo.Weekday = getWeekdayName(int(nextSlot.Start.Weekday()))
|
||||
|
||||
// 添加到计划任务列表
|
||||
nextTasks = append(nextTasks, taskInfo)
|
||||
}
|
||||
}
|
||||
|
||||
return true // 继续遍历
|
||||
})
|
||||
|
||||
// 如果指定了流路径但未找到对应的任务,查询数据库获取该流的计划信息
|
||||
if req.StreamPath != "" && !streamPathFound {
|
||||
// 查询与该流相关的所有计划
|
||||
var streams []pkg.RecordPlanStream
|
||||
if err := ct.DB.Where("stream_path = ?", req.StreamPath).Find(&streams).Error; err == nil && len(streams) > 0 {
|
||||
for _, stream := range streams {
|
||||
// 获取对应的计划
|
||||
var plan pkg.RecordPlan
|
||||
if err := ct.DB.First(&plan, stream.PlanID).Error; err == nil && plan.Enable && stream.Enable {
|
||||
now := time.Now()
|
||||
|
||||
// 构建任务信息
|
||||
taskInfo := &cronpb.CrontabTaskInfo{
|
||||
PlanId: uint32(plan.ID),
|
||||
PlanName: plan.Name,
|
||||
StreamPath: stream.StreamPath,
|
||||
FilePath: stream.FilePath,
|
||||
Fragment: stream.Fragment,
|
||||
IsRecording: false,
|
||||
}
|
||||
|
||||
// 获取完整计划时间段列表
|
||||
planSlots, err := calculateTimeSlots(plan.Plan, now, time.Local)
|
||||
if err == nil && planSlots != nil && len(planSlots) > 0 {
|
||||
taskInfo.PlanSlots = planSlots
|
||||
}
|
||||
|
||||
// 获取下一个时间段
|
||||
nextSlot, err := getNextTimeSlotFromNow(plan.Plan, now, time.Local)
|
||||
if err == nil && nextSlot != nil {
|
||||
// 设置时间信息
|
||||
taskInfo.StartTime = nextSlot.Start
|
||||
taskInfo.EndTime = nextSlot.End
|
||||
taskInfo.TimeRange = nextSlot.TimeRange
|
||||
taskInfo.Weekday = nextSlot.Weekday
|
||||
|
||||
// 计算等待时间
|
||||
waitingDuration := nextSlot.Start.AsTime().Sub(now)
|
||||
taskInfo.RemainingSeconds = uint32(waitingDuration.Seconds())
|
||||
|
||||
// 添加到计划任务列表
|
||||
nextTasks = append(nextTasks, taskInfo)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 按开始时间排序下一个任务列表
|
||||
sort.Slice(nextTasks, func(i, j int) bool {
|
||||
return nextTasks[i].StartTime.AsTime().Before(nextTasks[j].StartTime.AsTime())
|
||||
})
|
||||
|
||||
// 设置响应结果
|
||||
response.RunningTasks = runningTasks
|
||||
response.NextTasks = nextTasks
|
||||
response.TotalRunning = uint32(len(runningTasks))
|
||||
response.TotalPlanned = uint32(len(nextTasks))
|
||||
|
||||
return response, nil
|
||||
}
|
||||
|
244
plugin/crontab/api_test.go
Normal file
244
plugin/crontab/api_test.go
Normal file
@@ -0,0 +1,244 @@
|
||||
package plugin_crontab
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestCalculateTimeSlots(t *testing.T) {
|
||||
// 测试案例:周五的凌晨和上午有开启时间段
|
||||
// 字符串中1的索引是120(0点),122(2点),123(3点),125(5点),130(10点),135(15点)
|
||||
// 000000000000000000000000 - 周日(0-23小时) - 全0
|
||||
// 000000000000000000000000 - 周一(24-47小时) - 全0
|
||||
// 000000000000000000000000 - 周二(48-71小时) - 全0
|
||||
// 000000000000000000000000 - 周三(72-95小时) - 全0
|
||||
// 000000000000000000000000 - 周四(96-119小时) - 全0
|
||||
// 101101000010000100000000 - 周五(120-143小时) - 0,2,3,5,10,15点开启
|
||||
// 000000000000000000000000 - 周六(144-167小时) - 全0
|
||||
planStr := "000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000101101000010000100000000000000000000000000000000"
|
||||
|
||||
now := time.Date(2023, 5, 1, 12, 0, 0, 0, time.Local) // 周一中午
|
||||
|
||||
slots, err := calculateTimeSlots(planStr, now, time.Local)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 5, len(slots), "应该有5个时间段")
|
||||
|
||||
// 检查结果中的时间段(按实际解析结果排序)
|
||||
assert.Equal(t, "周五", slots[0].Weekday)
|
||||
assert.Equal(t, "10:00-11:00", slots[0].TimeRange)
|
||||
|
||||
assert.Equal(t, "周五", slots[1].Weekday)
|
||||
assert.Equal(t, "15:00-16:00", slots[1].TimeRange)
|
||||
|
||||
assert.Equal(t, "周五", slots[2].Weekday)
|
||||
assert.Equal(t, "00:00-01:00", slots[2].TimeRange)
|
||||
|
||||
assert.Equal(t, "周五", slots[3].Weekday)
|
||||
assert.Equal(t, "02:00-04:00", slots[3].TimeRange)
|
||||
|
||||
assert.Equal(t, "周五", slots[4].Weekday)
|
||||
assert.Equal(t, "05:00-06:00", slots[4].TimeRange)
|
||||
|
||||
// 打印出所有时间段,便于调试
|
||||
for i, slot := range slots {
|
||||
t.Logf("时间段 %d: %s %s", i, slot.Weekday, slot.TimeRange)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetNextTimeSlotFromNow(t *testing.T) {
|
||||
// 测试案例:周五的凌晨和上午有开启时间段
|
||||
planStr := "000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000101101000010000100000000000000000000000000000000"
|
||||
|
||||
// 测试1: 当前是周一,下一个时间段应该是周五凌晨0点
|
||||
now1 := time.Date(2023, 5, 1, 12, 0, 0, 0, time.Local) // 周一中午
|
||||
nextSlot1, err := getNextTimeSlotFromNow(planStr, now1, time.Local)
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, nextSlot1)
|
||||
assert.Equal(t, "周五", nextSlot1.Weekday)
|
||||
assert.Equal(t, "00:00-01:00", nextSlot1.TimeRange)
|
||||
|
||||
// 测试2: 当前是周五凌晨1点,下一个时间段应该是周五凌晨2点
|
||||
now2 := time.Date(2023, 5, 5, 1, 30, 0, 0, time.Local) // 周五凌晨1:30
|
||||
nextSlot2, err := getNextTimeSlotFromNow(planStr, now2, time.Local)
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, nextSlot2)
|
||||
assert.Equal(t, "周五", nextSlot2.Weekday)
|
||||
assert.Equal(t, "02:00-04:00", nextSlot2.TimeRange)
|
||||
|
||||
// 测试3: 当前是周五凌晨3点,此时正在一个时间段内
|
||||
now3 := time.Date(2023, 5, 5, 3, 0, 0, 0, time.Local) // 周五凌晨3:00
|
||||
nextSlot3, err := getNextTimeSlotFromNow(planStr, now3, time.Local)
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, nextSlot3)
|
||||
assert.Equal(t, "周五", nextSlot3.Weekday)
|
||||
assert.Equal(t, "02:00-04:00", nextSlot3.TimeRange)
|
||||
}
|
||||
|
||||
func TestParsePlanFromString(t *testing.T) {
|
||||
// 测试用户提供的案例:字符串的第36-41位表示周一的时间段
|
||||
// 这个案例中,对应周一的12点、14-15点、17点和22点开启
|
||||
planStr := "000000000000000000000000000000000000101101000010000100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"
|
||||
|
||||
now := time.Now()
|
||||
slots, err := calculateTimeSlots(planStr, now, time.Local)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// 验证解析结果
|
||||
var foundMondaySlots bool
|
||||
for _, slot := range slots {
|
||||
if slot.Weekday == "周一" {
|
||||
foundMondaySlots = true
|
||||
t.Logf("找到周一时间段: %s", slot.TimeRange)
|
||||
}
|
||||
}
|
||||
assert.True(t, foundMondaySlots, "应该找到周一的时间段")
|
||||
|
||||
// 预期的周一时间段
|
||||
var mondaySlots []string
|
||||
for _, slot := range slots {
|
||||
if slot.Weekday == "周一" {
|
||||
mondaySlots = append(mondaySlots, slot.TimeRange)
|
||||
}
|
||||
}
|
||||
|
||||
// 检查是否包含预期的时间段
|
||||
expectedSlots := []string{
|
||||
"12:00-13:00",
|
||||
"14:00-16:00",
|
||||
"17:00-18:00",
|
||||
"22:00-23:00",
|
||||
}
|
||||
|
||||
for _, expected := range expectedSlots {
|
||||
found := false
|
||||
for _, actual := range mondaySlots {
|
||||
if expected == actual {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
assert.True(t, found, "应该找到周一时间段:"+expected)
|
||||
}
|
||||
|
||||
// 获取下一个时间段
|
||||
nextSlot, err := getNextTimeSlotFromNow(planStr, now, time.Local)
|
||||
assert.NoError(t, err)
|
||||
if nextSlot != nil {
|
||||
t.Logf("下一个时间段: %s %s", nextSlot.Weekday, nextSlot.TimeRange)
|
||||
} else {
|
||||
t.Log("没有找到下一个时间段")
|
||||
}
|
||||
}
|
||||
|
||||
// 手动计算字符串长度的辅助函数
|
||||
func TestCountStringLength(t *testing.T) {
|
||||
str1 := "000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000101101000010000100000000000000000000000000000000"
|
||||
assert.Equal(t, 168, len(str1), "第一个测试字符串长度应为168")
|
||||
|
||||
str2 := "000000000000000000000000000000000000101101000010000100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"
|
||||
assert.Equal(t, 168, len(str2), "第二个测试字符串长度应为168")
|
||||
}
|
||||
|
||||
// 测试用户提供的具体字符串
|
||||
func TestUserProvidedPlanString(t *testing.T) {
|
||||
// 用户提供的测试字符串
|
||||
planStr := "000000000000000000000000000000000000101101000010000100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"
|
||||
|
||||
// 验证字符串长度
|
||||
assert.Equal(t, 168, len(planStr), "字符串长度应为168")
|
||||
|
||||
// 解析时间段
|
||||
now := time.Now()
|
||||
slots, err := calculateTimeSlots(planStr, now, time.Local)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// 打印所有时间段
|
||||
t.Log("所有时间段:")
|
||||
for i, slot := range slots {
|
||||
t.Logf("%d: %s %s", i, slot.Weekday, slot.TimeRange)
|
||||
}
|
||||
|
||||
// 获取下一个时间段
|
||||
nextSlot, err := getNextTimeSlotFromNow(planStr, now, time.Local)
|
||||
assert.NoError(t, err)
|
||||
|
||||
if nextSlot != nil {
|
||||
t.Logf("下一个执行时间段: %s %s", nextSlot.Weekday, nextSlot.TimeRange)
|
||||
t.Logf("开始时间: %s", nextSlot.Start.AsTime().In(time.Local).Format("2006-01-02 15:04:05"))
|
||||
t.Logf("结束时间: %s", nextSlot.End.AsTime().In(time.Local).Format("2006-01-02 15:04:05"))
|
||||
} else {
|
||||
t.Log("没有找到下一个时间段")
|
||||
}
|
||||
|
||||
// 验证周一的时间段
|
||||
var mondaySlots []string
|
||||
for _, slot := range slots {
|
||||
if slot.Weekday == "周一" {
|
||||
mondaySlots = append(mondaySlots, slot.TimeRange)
|
||||
}
|
||||
}
|
||||
|
||||
// 预期周一应该有这些时间段
|
||||
expectedMondaySlots := []string{
|
||||
"12:00-13:00",
|
||||
"14:00-16:00",
|
||||
"17:00-18:00",
|
||||
"22:00-23:00",
|
||||
}
|
||||
|
||||
assert.Equal(t, len(expectedMondaySlots), len(mondaySlots), "周一时间段数量不匹配")
|
||||
|
||||
for i, expected := range expectedMondaySlots {
|
||||
if i < len(mondaySlots) {
|
||||
t.Logf("期望周一时间段 %s, 实际是 %s", expected, mondaySlots[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 测试用户提供的第二个字符串
|
||||
func TestUserProvidedPlanString2(t *testing.T) {
|
||||
// 用户提供的第二个测试字符串
|
||||
planStr := "000000000000000000000000000000000000000000000000000000000000001011010100001000000000000000000000000100000000000000000000000010000000000000000000000001000000000000000000"
|
||||
|
||||
// 验证字符串长度
|
||||
assert.Equal(t, 168, len(planStr), "字符串长度应为168")
|
||||
|
||||
// 解析时间段
|
||||
now := time.Now()
|
||||
slots, err := calculateTimeSlots(planStr, now, time.Local)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// 打印所有时间段并按周几分组
|
||||
weekdaySlots := make(map[string][]string)
|
||||
for _, slot := range slots {
|
||||
weekdaySlots[slot.Weekday] = append(weekdaySlots[slot.Weekday], slot.TimeRange)
|
||||
}
|
||||
|
||||
t.Log("所有时间段(按周几分组):")
|
||||
weekdays := []string{"周日", "周一", "周二", "周三", "周四", "周五", "周六"}
|
||||
for _, weekday := range weekdays {
|
||||
if timeRanges, ok := weekdaySlots[weekday]; ok {
|
||||
t.Logf("%s: %v", weekday, timeRanges)
|
||||
}
|
||||
}
|
||||
|
||||
// 打印所有时间段的详细信息
|
||||
t.Log("\n所有时间段详细信息:")
|
||||
for i, slot := range slots {
|
||||
t.Logf("%d: %s %s", i, slot.Weekday, slot.TimeRange)
|
||||
}
|
||||
|
||||
// 获取下一个时间段
|
||||
nextSlot, err := getNextTimeSlotFromNow(planStr, now, time.Local)
|
||||
assert.NoError(t, err)
|
||||
|
||||
if nextSlot != nil {
|
||||
t.Logf("\n下一个执行时间段: %s %s", nextSlot.Weekday, nextSlot.TimeRange)
|
||||
t.Logf("开始时间: %s", nextSlot.Start.AsTime().In(time.Local).Format("2006-01-02 15:04:05"))
|
||||
t.Logf("结束时间: %s", nextSlot.End.AsTime().In(time.Local).Format("2006-01-02 15:04:05"))
|
||||
} else {
|
||||
t.Log("没有找到下一个时间段")
|
||||
}
|
||||
}
|
@@ -1,59 +1,422 @@
|
||||
package plugin_crontab
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"m7s.live/v5/pkg/task"
|
||||
"m7s.live/v5/plugin/crontab/pkg"
|
||||
)
|
||||
|
||||
// 计划时间段
|
||||
type TimeSlot struct {
|
||||
Start time.Time // 开始时间
|
||||
End time.Time // 结束时间
|
||||
}
|
||||
|
||||
// Crontab 定时任务调度器
|
||||
type Crontab struct {
|
||||
task.TickTask
|
||||
task.Job
|
||||
ctp *CrontabPlugin
|
||||
*pkg.RecordPlan
|
||||
*pkg.RecordPlanStream
|
||||
|
||||
stop chan struct{}
|
||||
running bool
|
||||
location *time.Location
|
||||
timer *time.Timer
|
||||
currentSlot *TimeSlot // 当前执行的时间段
|
||||
recording bool // 是否正在录制
|
||||
}
|
||||
|
||||
func (r *Crontab) GetTickInterval() time.Duration {
|
||||
return time.Minute
|
||||
func (cron *Crontab) GetKey() string {
|
||||
return strconv.Itoa(int(cron.PlanID)) + "_" + cron.StreamPath
|
||||
}
|
||||
|
||||
func (r *Crontab) Tick(any) {
|
||||
r.Info("开始检查录制计划")
|
||||
|
||||
// 获取当前时间
|
||||
now := time.Now()
|
||||
// 计算当前是一周中的第几天(0-6, 0是周日)和当前小时(0-23)
|
||||
weekday := int(now.Weekday())
|
||||
if weekday == 0 {
|
||||
weekday = 7 // 将周日从0改为7,以便计算
|
||||
}
|
||||
hour := now.Hour()
|
||||
|
||||
// 计算当前时间对应的位置索引
|
||||
// (weekday-1)*24 + hour 得到当前时间在144位字符串中的位置
|
||||
// weekday-1 是因为我们要从周一开始计算
|
||||
index := (weekday-1)*24 + hour
|
||||
|
||||
// 查询所有启用的录制计划
|
||||
var plans []pkg.RecordPlan
|
||||
model := pkg.RecordPlan{
|
||||
Enabled: true,
|
||||
}
|
||||
if err := r.ctp.DB.Where(&model).Find(&plans).Error; err != nil {
|
||||
r.Error("查询录制计划失败:", err)
|
||||
return
|
||||
// 初始化
|
||||
func (cron *Crontab) Start() (err error) {
|
||||
cron.Info("crontab plugin start")
|
||||
if cron.running {
|
||||
return // 已经运行中,不重复启动
|
||||
}
|
||||
|
||||
// 遍历所有计划
|
||||
for _, plan := range plans {
|
||||
if len(plan.Plan) != 168 {
|
||||
r.Error("录制计划格式错误,plan长度应为168位:", plan.Name)
|
||||
// 初始化必要字段
|
||||
if cron.stop == nil {
|
||||
cron.stop = make(chan struct{})
|
||||
}
|
||||
if cron.location == nil {
|
||||
cron.location = time.Local
|
||||
}
|
||||
|
||||
cron.running = true
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// 阻塞运行
|
||||
func (cron *Crontab) Run() (err error) {
|
||||
cron.Info("crontab plugin is running")
|
||||
// 初始化必要字段
|
||||
if cron.stop == nil {
|
||||
cron.stop = make(chan struct{})
|
||||
}
|
||||
if cron.location == nil {
|
||||
cron.location = time.Local
|
||||
}
|
||||
|
||||
cron.Info("调度器启动")
|
||||
|
||||
for {
|
||||
// 获取当前时间
|
||||
now := time.Now().In(cron.location)
|
||||
|
||||
// 首先检查是否需要立即执行操作(如停止录制)
|
||||
if cron.recording && cron.currentSlot != nil &&
|
||||
(now.Equal(cron.currentSlot.End) || now.After(cron.currentSlot.End)) {
|
||||
cron.stopRecording()
|
||||
continue
|
||||
}
|
||||
|
||||
// 检查当前时间对应的位置是否为1
|
||||
if plan.Plan[index] == '1' {
|
||||
r.Info("检测到需要开启录像的计划:", plan.Name)
|
||||
// TODO: 在这里添加开启录像的逻辑
|
||||
// 确定下一个事件
|
||||
var nextEvent time.Time
|
||||
var isStartEvent bool
|
||||
|
||||
if cron.recording {
|
||||
// 如果正在录制,下一个事件是结束时间
|
||||
nextEvent = cron.currentSlot.End
|
||||
isStartEvent = false
|
||||
} else {
|
||||
// 如果没有录制,计算下一个开始时间
|
||||
nextSlot := cron.getNextTimeSlot()
|
||||
if nextSlot == nil {
|
||||
// 无法确定下次执行时间,使用默认间隔
|
||||
cron.timer = time.NewTimer(1 * time.Hour)
|
||||
cron.Info("无有效计划,等待1小时后重试")
|
||||
|
||||
// 等待定时器或停止信号
|
||||
select {
|
||||
case <-cron.timer.C:
|
||||
continue // 继续循环
|
||||
case <-cron.stop:
|
||||
// 停止调度器
|
||||
if cron.timer != nil {
|
||||
cron.timer.Stop()
|
||||
}
|
||||
cron.Info("调度器停止")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
cron.currentSlot = nextSlot
|
||||
nextEvent = nextSlot.Start
|
||||
isStartEvent = true
|
||||
|
||||
// 如果已过开始时间,立即开始录制
|
||||
if now.Equal(nextEvent) || now.After(nextEvent) {
|
||||
cron.startRecording()
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
// 计算等待时间
|
||||
waitDuration := nextEvent.Sub(now)
|
||||
|
||||
// 如果等待时间为负,立即执行
|
||||
if waitDuration <= 0 {
|
||||
if isStartEvent {
|
||||
cron.startRecording()
|
||||
} else {
|
||||
cron.stopRecording()
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
// 设置定时器
|
||||
timer := time.NewTimer(waitDuration)
|
||||
|
||||
if isStartEvent {
|
||||
cron.Info("下次开始时间: ", nextEvent, "等待时间:", waitDuration)
|
||||
} else {
|
||||
cron.Info("下次结束时间: ", nextEvent, " 等待时间:", waitDuration)
|
||||
}
|
||||
|
||||
// 等待定时器或停止信号
|
||||
select {
|
||||
case now = <-timer.C:
|
||||
// 更新当前时间为定时器触发时间
|
||||
now = now.In(cron.location)
|
||||
|
||||
// 执行任务
|
||||
if isStartEvent {
|
||||
cron.startRecording()
|
||||
} else {
|
||||
cron.stopRecording()
|
||||
}
|
||||
|
||||
case <-cron.stop:
|
||||
// 停止调度器
|
||||
timer.Stop()
|
||||
cron.Info("调度器停止")
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 停止
|
||||
func (cron *Crontab) Dispose() (err error) {
|
||||
if cron.running {
|
||||
cron.stop <- struct{}{}
|
||||
cron.running = false
|
||||
if cron.timer != nil {
|
||||
cron.timer.Stop()
|
||||
}
|
||||
|
||||
// 如果还在录制,停止录制
|
||||
if cron.recording {
|
||||
cron.stopRecording()
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// 获取下一个时间段
|
||||
func (cron *Crontab) getNextTimeSlot() *TimeSlot {
|
||||
if cron.RecordPlan == nil || !cron.RecordPlan.Enable || cron.RecordPlan.Plan == "" {
|
||||
return nil // 无有效计划
|
||||
}
|
||||
|
||||
plan := cron.RecordPlan.Plan
|
||||
if len(plan) != 168 {
|
||||
cron.Error("无效的计划格式: %s, 长度应为168", plan)
|
||||
return nil
|
||||
}
|
||||
|
||||
// 使用当地时间
|
||||
now := time.Now().In(cron.location)
|
||||
cron.Debug("当前本地时间: %v, 星期%d, 小时%d", now.Format("2006-01-02 15:04:05"), now.Weekday(), now.Hour())
|
||||
|
||||
// 当前小时
|
||||
currentWeekday := int(now.Weekday())
|
||||
currentHour := now.Hour()
|
||||
|
||||
// 检查是否在整点边界附近(前后30秒)
|
||||
isNearHourBoundary := now.Minute() == 59 && now.Second() >= 30 || now.Minute() == 0 && now.Second() <= 30
|
||||
|
||||
// 首先检查当前时间是否在某个时间段内
|
||||
dayOffset := currentWeekday * 24
|
||||
if currentHour < 24 && plan[dayOffset+currentHour] == '1' {
|
||||
// 找到当前小时所在的完整时间段
|
||||
startHour := currentHour
|
||||
// 向前查找时间段的开始
|
||||
for h := currentHour - 1; h >= 0; h-- {
|
||||
if plan[dayOffset+h] == '1' {
|
||||
startHour = h
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// 向后查找时间段的结束
|
||||
endHour := currentHour + 1
|
||||
for h := endHour; h < 24; h++ {
|
||||
if plan[dayOffset+h] == '1' {
|
||||
endHour = h + 1
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// 检查我们是否已经接近当前时间段的结束
|
||||
isNearEndOfTimeSlot := currentHour == endHour-1 && now.Minute() == 59 && now.Second() >= 30
|
||||
|
||||
// 如果我们靠近时间段结束且在小时边界附近,我们跳过此时间段,找下一个
|
||||
if isNearEndOfTimeSlot && isNearHourBoundary {
|
||||
cron.Debug("接近当前时间段结束,准备查找下一个时间段")
|
||||
} else {
|
||||
// 创建时间段
|
||||
startTime := time.Date(now.Year(), now.Month(), now.Day(), startHour, 0, 0, 0, cron.location)
|
||||
endTime := time.Date(now.Year(), now.Month(), now.Day(), endHour, 0, 0, 0, cron.location)
|
||||
|
||||
// 如果当前时间已经接近或超过了结束时间,调整结束时间
|
||||
if now.After(endTime.Add(-30*time.Second)) || now.Equal(endTime) {
|
||||
cron.Debug("当前时间已接近或超过结束时间,尝试查找下一个时间段")
|
||||
} else {
|
||||
cron.Debug("当前已在有效时间段内: 开始=%v, 结束=%v",
|
||||
startTime.Format("2006-01-02 15:04:05"), endTime.Format("2006-01-02 15:04:05"))
|
||||
|
||||
return &TimeSlot{
|
||||
Start: startTime,
|
||||
End: endTime,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 查找下一个时间段
|
||||
// 先查找当天剩余时间
|
||||
for h := currentHour + 1; h < 24; h++ {
|
||||
if plan[dayOffset+h] == '1' {
|
||||
// 找到开始小时
|
||||
startHour := h
|
||||
// 查找结束小时
|
||||
endHour := h + 1
|
||||
for j := h + 1; j < 24; j++ {
|
||||
if plan[dayOffset+j] == '1' {
|
||||
endHour = j + 1
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// 创建时间段
|
||||
startTime := time.Date(now.Year(), now.Month(), now.Day(), startHour, 0, 0, 0, cron.location)
|
||||
endTime := time.Date(now.Year(), now.Month(), now.Day(), endHour, 0, 0, 0, cron.location)
|
||||
|
||||
cron.Debug("找到今天的有效时间段: 开始=%v, 结束=%v",
|
||||
startTime.Format("2006-01-02 15:04:05"), endTime.Format("2006-01-02 15:04:05"))
|
||||
|
||||
return &TimeSlot{
|
||||
Start: startTime,
|
||||
End: endTime,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 如果当天没有找到,则查找后续日期
|
||||
for d := 1; d <= 7; d++ {
|
||||
nextDay := (currentWeekday + d) % 7
|
||||
dayOffset := nextDay * 24
|
||||
|
||||
for h := 0; h < 24; h++ {
|
||||
if plan[dayOffset+h] == '1' {
|
||||
// 找到开始小时
|
||||
startHour := h
|
||||
// 查找结束小时
|
||||
endHour := h + 1
|
||||
for j := h + 1; j < 24; j++ {
|
||||
if plan[dayOffset+j] == '1' {
|
||||
endHour = j + 1
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// 计算日期
|
||||
nextDate := now.AddDate(0, 0, d)
|
||||
|
||||
// 创建时间段
|
||||
startTime := time.Date(nextDate.Year(), nextDate.Month(), nextDate.Day(), startHour, 0, 0, 0, cron.location)
|
||||
endTime := time.Date(nextDate.Year(), nextDate.Month(), nextDate.Day(), endHour, 0, 0, 0, cron.location)
|
||||
|
||||
cron.Debug("找到未来有效时间段: 开始=%v, 结束=%v",
|
||||
startTime.Format("2006-01-02 15:04:05"), endTime.Format("2006-01-02 15:04:05"))
|
||||
|
||||
return &TimeSlot{
|
||||
Start: startTime,
|
||||
End: endTime,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
cron.Debug("未找到有效的时间段")
|
||||
return nil
|
||||
}
|
||||
|
||||
// 开始录制
|
||||
func (cron *Crontab) startRecording() {
|
||||
if cron.recording {
|
||||
return // 已经在录制了
|
||||
}
|
||||
|
||||
now := time.Now().In(cron.location)
|
||||
cron.Info("开始录制任务: %s, 时间: %v, 计划结束时间: %v",
|
||||
cron.RecordPlan.Name, now, cron.currentSlot.End)
|
||||
|
||||
// 构造请求体
|
||||
reqBody := map[string]string{
|
||||
"fragment": cron.Fragment,
|
||||
"filePath": cron.FilePath,
|
||||
}
|
||||
jsonBody, err := json.Marshal(reqBody)
|
||||
if err != nil {
|
||||
cron.Error("构造请求体失败: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
// 获取 HTTP 地址
|
||||
addr := cron.ctp.Plugin.GetCommonConf().HTTP.ListenAddr
|
||||
if addr == "" {
|
||||
addr = ":8080" // 使用默认端口
|
||||
}
|
||||
if addr[0] == ':' {
|
||||
addr = "localhost" + addr
|
||||
}
|
||||
|
||||
// 发送开始录制请求
|
||||
resp, err := http.Post(fmt.Sprintf("http://%s/mp4/api/start/%s", addr, cron.StreamPath), "application/json", bytes.NewBuffer(jsonBody))
|
||||
if err != nil {
|
||||
cron.Error("开始录制失败: %v", err)
|
||||
return
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
cron.Error("开始录制失败,HTTP状态码: %d", resp.StatusCode)
|
||||
return
|
||||
}
|
||||
|
||||
cron.recording = true
|
||||
}
|
||||
|
||||
// 停止录制
|
||||
func (cron *Crontab) stopRecording() {
|
||||
if !cron.recording {
|
||||
return // 没有在录制
|
||||
}
|
||||
|
||||
// 立即记录当前时间并重置状态,避免重复调用
|
||||
now := time.Now().In(cron.location)
|
||||
cron.Info("停止录制任务: %s, 时间: %v", cron.RecordPlan.Name, now)
|
||||
|
||||
// 先重置状态,避免循环中重复检测到停止条件
|
||||
wasRecording := cron.recording
|
||||
cron.recording = false
|
||||
savedSlot := cron.currentSlot
|
||||
cron.currentSlot = nil
|
||||
|
||||
// 获取 HTTP 地址
|
||||
addr := cron.ctp.Plugin.GetCommonConf().HTTP.ListenAddr
|
||||
if addr == "" {
|
||||
addr = ":8080" // 使用默认端口
|
||||
}
|
||||
if addr[0] == ':' {
|
||||
addr = "localhost" + addr
|
||||
}
|
||||
|
||||
// 发送停止录制请求
|
||||
resp, err := http.Post(fmt.Sprintf("http://%s/mp4/api/stop/%s", addr, cron.StreamPath), "application/json", nil)
|
||||
if err != nil {
|
||||
cron.Error("停止录制失败: %v", err)
|
||||
// 如果请求失败,恢复状态以便下次重试
|
||||
if wasRecording {
|
||||
cron.recording = true
|
||||
cron.currentSlot = savedSlot
|
||||
}
|
||||
return
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
cron.Error("停止录制失败,HTTP状态码: %d", resp.StatusCode)
|
||||
// 如果请求失败,恢复状态以便下次重试
|
||||
if wasRecording {
|
||||
cron.recording = true
|
||||
cron.currentSlot = savedSlot
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -3,6 +3,8 @@ package plugin_crontab
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"m7s.live/v5/pkg/util"
|
||||
|
||||
"m7s.live/v5"
|
||||
"m7s.live/v5/plugin/crontab/pb"
|
||||
"m7s.live/v5/plugin/crontab/pkg"
|
||||
@@ -11,6 +13,8 @@ import (
|
||||
type CrontabPlugin struct {
|
||||
m7s.Plugin
|
||||
pb.UnimplementedApiServer
|
||||
crontabs util.Collection[string, *Crontab]
|
||||
recordPlans util.Collection[uint, *pkg.RecordPlan]
|
||||
}
|
||||
|
||||
var _ = m7s.InstallPlugin[CrontabPlugin](m7s.PluginMeta{
|
||||
@@ -22,14 +26,46 @@ func (ct *CrontabPlugin) OnInit() (err error) {
|
||||
if ct.DB == nil {
|
||||
ct.Error("DB is nil")
|
||||
} else {
|
||||
err = ct.DB.AutoMigrate(&pkg.RecordPlan{})
|
||||
err = ct.DB.AutoMigrate(&pkg.RecordPlan{}, &pkg.RecordPlanStream{})
|
||||
if err != nil {
|
||||
return fmt.Errorf("auto migrate tables error: %v", err)
|
||||
}
|
||||
ct.Info("init database success")
|
||||
|
||||
// 查询所有录制计划
|
||||
var plans []pkg.RecordPlan
|
||||
if err = ct.DB.Find(&plans).Error; err != nil {
|
||||
return fmt.Errorf("query record plans error: %v", err)
|
||||
}
|
||||
|
||||
// 遍历所有计划
|
||||
for _, plan := range plans {
|
||||
// 将计划存入 recordPlans 集合
|
||||
ct.recordPlans.Add(&plan)
|
||||
|
||||
// 如果计划已启用,查询对应的流信息并创建定时任务
|
||||
if plan.Enable {
|
||||
var streams []pkg.RecordPlanStream
|
||||
model := &pkg.RecordPlanStream{PlanID: plan.ID}
|
||||
if err = ct.DB.Model(model).Where(model).Find(&streams).Error; err != nil {
|
||||
ct.Error("query record plan streams error: %v", err)
|
||||
continue
|
||||
}
|
||||
|
||||
// 为每个流创建定时任务
|
||||
for _, stream := range streams {
|
||||
crontab := &Crontab{
|
||||
ctp: ct,
|
||||
RecordPlan: &plan,
|
||||
RecordPlanStream: &stream,
|
||||
}
|
||||
crontab.OnStart(func() {
|
||||
ct.crontabs.Set(crontab)
|
||||
})
|
||||
ct.AddTask(crontab)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
crontab := &Crontab{ctp: ct}
|
||||
ct.AddTask(crontab)
|
||||
crontab.Tick(nil)
|
||||
return
|
||||
}
|
||||
|
@@ -339,6 +339,767 @@ func (x *Response) GetMessage() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
// RecordPlanStream 相关消息定义
|
||||
type PlanStream struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
PlanId uint32 `protobuf:"varint,1,opt,name=planId,proto3" json:"planId,omitempty"`
|
||||
StreamPath string `protobuf:"bytes,2,opt,name=stream_path,json=streamPath,proto3" json:"stream_path,omitempty"`
|
||||
Fragment string `protobuf:"bytes,3,opt,name=fragment,proto3" json:"fragment,omitempty"`
|
||||
FilePath string `protobuf:"bytes,4,opt,name=filePath,proto3" json:"filePath,omitempty"`
|
||||
RecordType string `protobuf:"bytes,5,opt,name=record_type,json=recordType,proto3" json:"record_type,omitempty"` // 录制类型,例如 "mp4", "flv"
|
||||
CreatedAt *timestamppb.Timestamp `protobuf:"bytes,6,opt,name=created_at,json=createdAt,proto3" json:"created_at,omitempty"`
|
||||
UpdatedAt *timestamppb.Timestamp `protobuf:"bytes,7,opt,name=updated_at,json=updatedAt,proto3" json:"updated_at,omitempty"`
|
||||
Enable bool `protobuf:"varint,8,opt,name=enable,proto3" json:"enable,omitempty"` // 是否启用该录制流
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *PlanStream) Reset() {
|
||||
*x = PlanStream{}
|
||||
mi := &file_crontab_proto_msgTypes[5]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *PlanStream) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*PlanStream) ProtoMessage() {}
|
||||
|
||||
func (x *PlanStream) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_crontab_proto_msgTypes[5]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use PlanStream.ProtoReflect.Descriptor instead.
|
||||
func (*PlanStream) Descriptor() ([]byte, []int) {
|
||||
return file_crontab_proto_rawDescGZIP(), []int{5}
|
||||
}
|
||||
|
||||
func (x *PlanStream) GetPlanId() uint32 {
|
||||
if x != nil {
|
||||
return x.PlanId
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (x *PlanStream) GetStreamPath() string {
|
||||
if x != nil {
|
||||
return x.StreamPath
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *PlanStream) GetFragment() string {
|
||||
if x != nil {
|
||||
return x.Fragment
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *PlanStream) GetFilePath() string {
|
||||
if x != nil {
|
||||
return x.FilePath
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *PlanStream) GetRecordType() string {
|
||||
if x != nil {
|
||||
return x.RecordType
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *PlanStream) GetCreatedAt() *timestamppb.Timestamp {
|
||||
if x != nil {
|
||||
return x.CreatedAt
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *PlanStream) GetUpdatedAt() *timestamppb.Timestamp {
|
||||
if x != nil {
|
||||
return x.UpdatedAt
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *PlanStream) GetEnable() bool {
|
||||
if x != nil {
|
||||
return x.Enable
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
type ReqPlanStreamList struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
PageNum uint32 `protobuf:"varint,1,opt,name=pageNum,proto3" json:"pageNum,omitempty"`
|
||||
PageSize uint32 `protobuf:"varint,2,opt,name=pageSize,proto3" json:"pageSize,omitempty"`
|
||||
PlanId uint32 `protobuf:"varint,3,opt,name=planId,proto3" json:"planId,omitempty"` // 可选的按录制计划ID筛选
|
||||
StreamPath string `protobuf:"bytes,4,opt,name=stream_path,json=streamPath,proto3" json:"stream_path,omitempty"` // 可选的按流路径筛选
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *ReqPlanStreamList) Reset() {
|
||||
*x = ReqPlanStreamList{}
|
||||
mi := &file_crontab_proto_msgTypes[6]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *ReqPlanStreamList) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*ReqPlanStreamList) ProtoMessage() {}
|
||||
|
||||
func (x *ReqPlanStreamList) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_crontab_proto_msgTypes[6]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use ReqPlanStreamList.ProtoReflect.Descriptor instead.
|
||||
func (*ReqPlanStreamList) Descriptor() ([]byte, []int) {
|
||||
return file_crontab_proto_rawDescGZIP(), []int{6}
|
||||
}
|
||||
|
||||
func (x *ReqPlanStreamList) GetPageNum() uint32 {
|
||||
if x != nil {
|
||||
return x.PageNum
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (x *ReqPlanStreamList) GetPageSize() uint32 {
|
||||
if x != nil {
|
||||
return x.PageSize
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (x *ReqPlanStreamList) GetPlanId() uint32 {
|
||||
if x != nil {
|
||||
return x.PlanId
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (x *ReqPlanStreamList) GetStreamPath() string {
|
||||
if x != nil {
|
||||
return x.StreamPath
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
type RecordPlanStreamResponseList struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
Code int32 `protobuf:"varint,1,opt,name=code,proto3" json:"code,omitempty"`
|
||||
Message string `protobuf:"bytes,2,opt,name=message,proto3" json:"message,omitempty"`
|
||||
TotalCount uint32 `protobuf:"varint,3,opt,name=totalCount,proto3" json:"totalCount,omitempty"`
|
||||
PageNum uint32 `protobuf:"varint,4,opt,name=pageNum,proto3" json:"pageNum,omitempty"`
|
||||
PageSize uint32 `protobuf:"varint,5,opt,name=pageSize,proto3" json:"pageSize,omitempty"`
|
||||
Data []*PlanStream `protobuf:"bytes,6,rep,name=data,proto3" json:"data,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *RecordPlanStreamResponseList) Reset() {
|
||||
*x = RecordPlanStreamResponseList{}
|
||||
mi := &file_crontab_proto_msgTypes[7]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *RecordPlanStreamResponseList) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*RecordPlanStreamResponseList) ProtoMessage() {}
|
||||
|
||||
func (x *RecordPlanStreamResponseList) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_crontab_proto_msgTypes[7]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use RecordPlanStreamResponseList.ProtoReflect.Descriptor instead.
|
||||
func (*RecordPlanStreamResponseList) Descriptor() ([]byte, []int) {
|
||||
return file_crontab_proto_rawDescGZIP(), []int{7}
|
||||
}
|
||||
|
||||
func (x *RecordPlanStreamResponseList) GetCode() int32 {
|
||||
if x != nil {
|
||||
return x.Code
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (x *RecordPlanStreamResponseList) GetMessage() string {
|
||||
if x != nil {
|
||||
return x.Message
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *RecordPlanStreamResponseList) GetTotalCount() uint32 {
|
||||
if x != nil {
|
||||
return x.TotalCount
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (x *RecordPlanStreamResponseList) GetPageNum() uint32 {
|
||||
if x != nil {
|
||||
return x.PageNum
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (x *RecordPlanStreamResponseList) GetPageSize() uint32 {
|
||||
if x != nil {
|
||||
return x.PageSize
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (x *RecordPlanStreamResponseList) GetData() []*PlanStream {
|
||||
if x != nil {
|
||||
return x.Data
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type DeletePlanStreamRequest struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
PlanId uint32 `protobuf:"varint,1,opt,name=planId,proto3" json:"planId,omitempty"`
|
||||
StreamPath string `protobuf:"bytes,2,opt,name=streamPath,proto3" json:"streamPath,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *DeletePlanStreamRequest) Reset() {
|
||||
*x = DeletePlanStreamRequest{}
|
||||
mi := &file_crontab_proto_msgTypes[8]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *DeletePlanStreamRequest) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*DeletePlanStreamRequest) ProtoMessage() {}
|
||||
|
||||
func (x *DeletePlanStreamRequest) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_crontab_proto_msgTypes[8]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use DeletePlanStreamRequest.ProtoReflect.Descriptor instead.
|
||||
func (*DeletePlanStreamRequest) Descriptor() ([]byte, []int) {
|
||||
return file_crontab_proto_rawDescGZIP(), []int{8}
|
||||
}
|
||||
|
||||
func (x *DeletePlanStreamRequest) GetPlanId() uint32 {
|
||||
if x != nil {
|
||||
return x.PlanId
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (x *DeletePlanStreamRequest) GetStreamPath() string {
|
||||
if x != nil {
|
||||
return x.StreamPath
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// 解析计划请求
|
||||
type ParsePlanRequest struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
Plan string `protobuf:"bytes,1,opt,name=plan,proto3" json:"plan,omitempty"` // 168位的0/1字符串,表示一周的每个小时是否录制
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *ParsePlanRequest) Reset() {
|
||||
*x = ParsePlanRequest{}
|
||||
mi := &file_crontab_proto_msgTypes[9]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *ParsePlanRequest) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*ParsePlanRequest) ProtoMessage() {}
|
||||
|
||||
func (x *ParsePlanRequest) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_crontab_proto_msgTypes[9]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use ParsePlanRequest.ProtoReflect.Descriptor instead.
|
||||
func (*ParsePlanRequest) Descriptor() ([]byte, []int) {
|
||||
return file_crontab_proto_rawDescGZIP(), []int{9}
|
||||
}
|
||||
|
||||
func (x *ParsePlanRequest) GetPlan() string {
|
||||
if x != nil {
|
||||
return x.Plan
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// 时间段信息
|
||||
type TimeSlotInfo struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
Start *timestamppb.Timestamp `protobuf:"bytes,1,opt,name=start,proto3" json:"start,omitempty"` // 开始时间
|
||||
End *timestamppb.Timestamp `protobuf:"bytes,2,opt,name=end,proto3" json:"end,omitempty"` // 结束时间
|
||||
Weekday string `protobuf:"bytes,3,opt,name=weekday,proto3" json:"weekday,omitempty"` // 周几(例如:周一)
|
||||
TimeRange string `protobuf:"bytes,4,opt,name=time_range,json=timeRange,proto3" json:"time_range,omitempty"` // 时间范围(例如:09:00-10:00)
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *TimeSlotInfo) Reset() {
|
||||
*x = TimeSlotInfo{}
|
||||
mi := &file_crontab_proto_msgTypes[10]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *TimeSlotInfo) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*TimeSlotInfo) ProtoMessage() {}
|
||||
|
||||
func (x *TimeSlotInfo) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_crontab_proto_msgTypes[10]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use TimeSlotInfo.ProtoReflect.Descriptor instead.
|
||||
func (*TimeSlotInfo) Descriptor() ([]byte, []int) {
|
||||
return file_crontab_proto_rawDescGZIP(), []int{10}
|
||||
}
|
||||
|
||||
func (x *TimeSlotInfo) GetStart() *timestamppb.Timestamp {
|
||||
if x != nil {
|
||||
return x.Start
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *TimeSlotInfo) GetEnd() *timestamppb.Timestamp {
|
||||
if x != nil {
|
||||
return x.End
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *TimeSlotInfo) GetWeekday() string {
|
||||
if x != nil {
|
||||
return x.Weekday
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *TimeSlotInfo) GetTimeRange() string {
|
||||
if x != nil {
|
||||
return x.TimeRange
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// 解析计划响应
|
||||
type ParsePlanResponse struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
Code int32 `protobuf:"varint,1,opt,name=code,proto3" json:"code,omitempty"` // 响应码
|
||||
Message string `protobuf:"bytes,2,opt,name=message,proto3" json:"message,omitempty"` // 响应消息
|
||||
Slots []*TimeSlotInfo `protobuf:"bytes,3,rep,name=slots,proto3" json:"slots,omitempty"` // 所有计划的时间段
|
||||
NextSlot *TimeSlotInfo `protobuf:"bytes,4,opt,name=next_slot,json=nextSlot,proto3" json:"next_slot,omitempty"` // 从当前时间开始的下一个时间段
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *ParsePlanResponse) Reset() {
|
||||
*x = ParsePlanResponse{}
|
||||
mi := &file_crontab_proto_msgTypes[11]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *ParsePlanResponse) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*ParsePlanResponse) ProtoMessage() {}
|
||||
|
||||
func (x *ParsePlanResponse) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_crontab_proto_msgTypes[11]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use ParsePlanResponse.ProtoReflect.Descriptor instead.
|
||||
func (*ParsePlanResponse) Descriptor() ([]byte, []int) {
|
||||
return file_crontab_proto_rawDescGZIP(), []int{11}
|
||||
}
|
||||
|
||||
func (x *ParsePlanResponse) GetCode() int32 {
|
||||
if x != nil {
|
||||
return x.Code
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (x *ParsePlanResponse) GetMessage() string {
|
||||
if x != nil {
|
||||
return x.Message
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *ParsePlanResponse) GetSlots() []*TimeSlotInfo {
|
||||
if x != nil {
|
||||
return x.Slots
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *ParsePlanResponse) GetNextSlot() *TimeSlotInfo {
|
||||
if x != nil {
|
||||
return x.NextSlot
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// 新增的消息定义
|
||||
// 获取Crontab状态请求
|
||||
type CrontabStatusRequest struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
// 可以为空,表示获取所有任务
|
||||
StreamPath string `protobuf:"bytes,1,opt,name=stream_path,json=streamPath,proto3" json:"stream_path,omitempty"` // 可选,按流路径过滤
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *CrontabStatusRequest) Reset() {
|
||||
*x = CrontabStatusRequest{}
|
||||
mi := &file_crontab_proto_msgTypes[12]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *CrontabStatusRequest) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*CrontabStatusRequest) ProtoMessage() {}
|
||||
|
||||
func (x *CrontabStatusRequest) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_crontab_proto_msgTypes[12]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use CrontabStatusRequest.ProtoReflect.Descriptor instead.
|
||||
func (*CrontabStatusRequest) Descriptor() ([]byte, []int) {
|
||||
return file_crontab_proto_rawDescGZIP(), []int{12}
|
||||
}
|
||||
|
||||
func (x *CrontabStatusRequest) GetStreamPath() string {
|
||||
if x != nil {
|
||||
return x.StreamPath
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// 任务信息
|
||||
type CrontabTaskInfo struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
PlanId uint32 `protobuf:"varint,1,opt,name=plan_id,json=planId,proto3" json:"plan_id,omitempty"` // 计划ID
|
||||
PlanName string `protobuf:"bytes,2,opt,name=plan_name,json=planName,proto3" json:"plan_name,omitempty"` // 计划名称
|
||||
StreamPath string `protobuf:"bytes,3,opt,name=stream_path,json=streamPath,proto3" json:"stream_path,omitempty"` // 流路径
|
||||
IsRecording bool `protobuf:"varint,4,opt,name=is_recording,json=isRecording,proto3" json:"is_recording,omitempty"` // 是否正在录制
|
||||
StartTime *timestamppb.Timestamp `protobuf:"bytes,5,opt,name=start_time,json=startTime,proto3" json:"start_time,omitempty"` // 当前/下一个任务开始时间
|
||||
EndTime *timestamppb.Timestamp `protobuf:"bytes,6,opt,name=end_time,json=endTime,proto3" json:"end_time,omitempty"` // 当前/下一个任务结束时间
|
||||
TimeRange string `protobuf:"bytes,7,opt,name=time_range,json=timeRange,proto3" json:"time_range,omitempty"` // 时间范围(例如:09:00-10:00)
|
||||
Weekday string `protobuf:"bytes,8,opt,name=weekday,proto3" json:"weekday,omitempty"` // 周几(例如:周一)
|
||||
FilePath string `protobuf:"bytes,9,opt,name=file_path,json=filePath,proto3" json:"file_path,omitempty"` // 文件保存路径
|
||||
Fragment string `protobuf:"bytes,10,opt,name=fragment,proto3" json:"fragment,omitempty"` // 分片设置
|
||||
ElapsedSeconds uint32 `protobuf:"varint,11,opt,name=elapsed_seconds,json=elapsedSeconds,proto3" json:"elapsed_seconds,omitempty"` // 已运行时间(秒,仅对正在运行的任务有效)
|
||||
RemainingSeconds uint32 `protobuf:"varint,12,opt,name=remaining_seconds,json=remainingSeconds,proto3" json:"remaining_seconds,omitempty"` // 剩余时间(秒)
|
||||
PlanSlots []*TimeSlotInfo `protobuf:"bytes,13,rep,name=plan_slots,json=planSlots,proto3" json:"plan_slots,omitempty"` // 完整的计划时间段列表
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *CrontabTaskInfo) Reset() {
|
||||
*x = CrontabTaskInfo{}
|
||||
mi := &file_crontab_proto_msgTypes[13]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *CrontabTaskInfo) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*CrontabTaskInfo) ProtoMessage() {}
|
||||
|
||||
func (x *CrontabTaskInfo) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_crontab_proto_msgTypes[13]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use CrontabTaskInfo.ProtoReflect.Descriptor instead.
|
||||
func (*CrontabTaskInfo) Descriptor() ([]byte, []int) {
|
||||
return file_crontab_proto_rawDescGZIP(), []int{13}
|
||||
}
|
||||
|
||||
func (x *CrontabTaskInfo) GetPlanId() uint32 {
|
||||
if x != nil {
|
||||
return x.PlanId
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (x *CrontabTaskInfo) GetPlanName() string {
|
||||
if x != nil {
|
||||
return x.PlanName
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *CrontabTaskInfo) GetStreamPath() string {
|
||||
if x != nil {
|
||||
return x.StreamPath
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *CrontabTaskInfo) GetIsRecording() bool {
|
||||
if x != nil {
|
||||
return x.IsRecording
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (x *CrontabTaskInfo) GetStartTime() *timestamppb.Timestamp {
|
||||
if x != nil {
|
||||
return x.StartTime
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *CrontabTaskInfo) GetEndTime() *timestamppb.Timestamp {
|
||||
if x != nil {
|
||||
return x.EndTime
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *CrontabTaskInfo) GetTimeRange() string {
|
||||
if x != nil {
|
||||
return x.TimeRange
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *CrontabTaskInfo) GetWeekday() string {
|
||||
if x != nil {
|
||||
return x.Weekday
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *CrontabTaskInfo) GetFilePath() string {
|
||||
if x != nil {
|
||||
return x.FilePath
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *CrontabTaskInfo) GetFragment() string {
|
||||
if x != nil {
|
||||
return x.Fragment
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *CrontabTaskInfo) GetElapsedSeconds() uint32 {
|
||||
if x != nil {
|
||||
return x.ElapsedSeconds
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (x *CrontabTaskInfo) GetRemainingSeconds() uint32 {
|
||||
if x != nil {
|
||||
return x.RemainingSeconds
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (x *CrontabTaskInfo) GetPlanSlots() []*TimeSlotInfo {
|
||||
if x != nil {
|
||||
return x.PlanSlots
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// 获取Crontab状态响应
|
||||
type CrontabStatusResponse struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
Code int32 `protobuf:"varint,1,opt,name=code,proto3" json:"code,omitempty"` // 响应码
|
||||
Message string `protobuf:"bytes,2,opt,name=message,proto3" json:"message,omitempty"` // 响应消息
|
||||
RunningTasks []*CrontabTaskInfo `protobuf:"bytes,3,rep,name=running_tasks,json=runningTasks,proto3" json:"running_tasks,omitempty"` // 当前正在执行的任务列表
|
||||
NextTasks []*CrontabTaskInfo `protobuf:"bytes,4,rep,name=next_tasks,json=nextTasks,proto3" json:"next_tasks,omitempty"` // 下一个计划执行的任务列表
|
||||
TotalRunning uint32 `protobuf:"varint,5,opt,name=total_running,json=totalRunning,proto3" json:"total_running,omitempty"` // 正在运行的任务总数
|
||||
TotalPlanned uint32 `protobuf:"varint,6,opt,name=total_planned,json=totalPlanned,proto3" json:"total_planned,omitempty"` // 计划中的任务总数
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *CrontabStatusResponse) Reset() {
|
||||
*x = CrontabStatusResponse{}
|
||||
mi := &file_crontab_proto_msgTypes[14]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *CrontabStatusResponse) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*CrontabStatusResponse) ProtoMessage() {}
|
||||
|
||||
func (x *CrontabStatusResponse) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_crontab_proto_msgTypes[14]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use CrontabStatusResponse.ProtoReflect.Descriptor instead.
|
||||
func (*CrontabStatusResponse) Descriptor() ([]byte, []int) {
|
||||
return file_crontab_proto_rawDescGZIP(), []int{14}
|
||||
}
|
||||
|
||||
func (x *CrontabStatusResponse) GetCode() int32 {
|
||||
if x != nil {
|
||||
return x.Code
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (x *CrontabStatusResponse) GetMessage() string {
|
||||
if x != nil {
|
||||
return x.Message
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *CrontabStatusResponse) GetRunningTasks() []*CrontabTaskInfo {
|
||||
if x != nil {
|
||||
return x.RunningTasks
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *CrontabStatusResponse) GetNextTasks() []*CrontabTaskInfo {
|
||||
if x != nil {
|
||||
return x.NextTasks
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *CrontabStatusResponse) GetTotalRunning() uint32 {
|
||||
if x != nil {
|
||||
return x.TotalRunning
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (x *CrontabStatusResponse) GetTotalPlanned() uint32 {
|
||||
if x != nil {
|
||||
return x.TotalPlanned
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
var File_crontab_proto protoreflect.FileDescriptor
|
||||
|
||||
const file_crontab_proto_rawDesc = "" +
|
||||
@@ -371,12 +1132,95 @@ const file_crontab_proto_rawDesc = "" +
|
||||
"\x02id\x18\x01 \x01(\rR\x02id\"8\n" +
|
||||
"\bResponse\x12\x12\n" +
|
||||
"\x04code\x18\x01 \x01(\x05R\x04code\x12\x18\n" +
|
||||
"\amessage\x18\x02 \x01(\tR\amessage2\xbe\x02\n" +
|
||||
"\amessage\x18\x02 \x01(\tR\amessage\"\xac\x02\n" +
|
||||
"\n" +
|
||||
"PlanStream\x12\x16\n" +
|
||||
"\x06planId\x18\x01 \x01(\rR\x06planId\x12\x1f\n" +
|
||||
"\vstream_path\x18\x02 \x01(\tR\n" +
|
||||
"streamPath\x12\x1a\n" +
|
||||
"\bfragment\x18\x03 \x01(\tR\bfragment\x12\x1a\n" +
|
||||
"\bfilePath\x18\x04 \x01(\tR\bfilePath\x12\x1f\n" +
|
||||
"\vrecord_type\x18\x05 \x01(\tR\n" +
|
||||
"recordType\x129\n" +
|
||||
"\n" +
|
||||
"created_at\x18\x06 \x01(\v2\x1a.google.protobuf.TimestampR\tcreatedAt\x129\n" +
|
||||
"\n" +
|
||||
"updated_at\x18\a \x01(\v2\x1a.google.protobuf.TimestampR\tupdatedAt\x12\x16\n" +
|
||||
"\x06enable\x18\b \x01(\bR\x06enable\"\x82\x01\n" +
|
||||
"\x11ReqPlanStreamList\x12\x18\n" +
|
||||
"\apageNum\x18\x01 \x01(\rR\apageNum\x12\x1a\n" +
|
||||
"\bpageSize\x18\x02 \x01(\rR\bpageSize\x12\x16\n" +
|
||||
"\x06planId\x18\x03 \x01(\rR\x06planId\x12\x1f\n" +
|
||||
"\vstream_path\x18\x04 \x01(\tR\n" +
|
||||
"streamPath\"\xcb\x01\n" +
|
||||
"\x1cRecordPlanStreamResponseList\x12\x12\n" +
|
||||
"\x04code\x18\x01 \x01(\x05R\x04code\x12\x18\n" +
|
||||
"\amessage\x18\x02 \x01(\tR\amessage\x12\x1e\n" +
|
||||
"\n" +
|
||||
"totalCount\x18\x03 \x01(\rR\n" +
|
||||
"totalCount\x12\x18\n" +
|
||||
"\apageNum\x18\x04 \x01(\rR\apageNum\x12\x1a\n" +
|
||||
"\bpageSize\x18\x05 \x01(\rR\bpageSize\x12'\n" +
|
||||
"\x04data\x18\x06 \x03(\v2\x13.crontab.PlanStreamR\x04data\"Q\n" +
|
||||
"\x17DeletePlanStreamRequest\x12\x16\n" +
|
||||
"\x06planId\x18\x01 \x01(\rR\x06planId\x12\x1e\n" +
|
||||
"\n" +
|
||||
"streamPath\x18\x02 \x01(\tR\n" +
|
||||
"streamPath\"&\n" +
|
||||
"\x10ParsePlanRequest\x12\x12\n" +
|
||||
"\x04plan\x18\x01 \x01(\tR\x04plan\"\xa7\x01\n" +
|
||||
"\fTimeSlotInfo\x120\n" +
|
||||
"\x05start\x18\x01 \x01(\v2\x1a.google.protobuf.TimestampR\x05start\x12,\n" +
|
||||
"\x03end\x18\x02 \x01(\v2\x1a.google.protobuf.TimestampR\x03end\x12\x18\n" +
|
||||
"\aweekday\x18\x03 \x01(\tR\aweekday\x12\x1d\n" +
|
||||
"\n" +
|
||||
"time_range\x18\x04 \x01(\tR\ttimeRange\"\xa2\x01\n" +
|
||||
"\x11ParsePlanResponse\x12\x12\n" +
|
||||
"\x04code\x18\x01 \x01(\x05R\x04code\x12\x18\n" +
|
||||
"\amessage\x18\x02 \x01(\tR\amessage\x12+\n" +
|
||||
"\x05slots\x18\x03 \x03(\v2\x15.crontab.TimeSlotInfoR\x05slots\x122\n" +
|
||||
"\tnext_slot\x18\x04 \x01(\v2\x15.crontab.TimeSlotInfoR\bnextSlot\"7\n" +
|
||||
"\x14CrontabStatusRequest\x12\x1f\n" +
|
||||
"\vstream_path\x18\x01 \x01(\tR\n" +
|
||||
"streamPath\"\xfb\x03\n" +
|
||||
"\x0fCrontabTaskInfo\x12\x17\n" +
|
||||
"\aplan_id\x18\x01 \x01(\rR\x06planId\x12\x1b\n" +
|
||||
"\tplan_name\x18\x02 \x01(\tR\bplanName\x12\x1f\n" +
|
||||
"\vstream_path\x18\x03 \x01(\tR\n" +
|
||||
"streamPath\x12!\n" +
|
||||
"\fis_recording\x18\x04 \x01(\bR\visRecording\x129\n" +
|
||||
"\n" +
|
||||
"start_time\x18\x05 \x01(\v2\x1a.google.protobuf.TimestampR\tstartTime\x125\n" +
|
||||
"\bend_time\x18\x06 \x01(\v2\x1a.google.protobuf.TimestampR\aendTime\x12\x1d\n" +
|
||||
"\n" +
|
||||
"time_range\x18\a \x01(\tR\ttimeRange\x12\x18\n" +
|
||||
"\aweekday\x18\b \x01(\tR\aweekday\x12\x1b\n" +
|
||||
"\tfile_path\x18\t \x01(\tR\bfilePath\x12\x1a\n" +
|
||||
"\bfragment\x18\n" +
|
||||
" \x01(\tR\bfragment\x12'\n" +
|
||||
"\x0felapsed_seconds\x18\v \x01(\rR\x0eelapsedSeconds\x12+\n" +
|
||||
"\x11remaining_seconds\x18\f \x01(\rR\x10remainingSeconds\x124\n" +
|
||||
"\n" +
|
||||
"plan_slots\x18\r \x03(\v2\x15.crontab.TimeSlotInfoR\tplanSlots\"\x87\x02\n" +
|
||||
"\x15CrontabStatusResponse\x12\x12\n" +
|
||||
"\x04code\x18\x01 \x01(\x05R\x04code\x12\x18\n" +
|
||||
"\amessage\x18\x02 \x01(\tR\amessage\x12=\n" +
|
||||
"\rrunning_tasks\x18\x03 \x03(\v2\x18.crontab.CrontabTaskInfoR\frunningTasks\x127\n" +
|
||||
"\n" +
|
||||
"next_tasks\x18\x04 \x03(\v2\x18.crontab.CrontabTaskInfoR\tnextTasks\x12#\n" +
|
||||
"\rtotal_running\x18\x05 \x01(\rR\ftotalRunning\x12#\n" +
|
||||
"\rtotal_planned\x18\x06 \x01(\rR\ftotalPlanned2\xe0\a\n" +
|
||||
"\x03api\x12O\n" +
|
||||
"\x04List\x12\x14.crontab.ReqPlanList\x1a\x19.crontab.PlanResponseList\"\x16\x82\xd3\xe4\x93\x02\x10\x12\x0e/plan/api/list\x12A\n" +
|
||||
"\x03Add\x12\r.crontab.Plan\x1a\x11.crontab.Response\"\x18\x82\xd3\xe4\x93\x02\x12:\x01*\"\r/plan/api/add\x12L\n" +
|
||||
"\x06Update\x12\r.crontab.Plan\x1a\x11.crontab.Response\" \x82\xd3\xe4\x93\x02\x1a:\x01*\"\x15/plan/api/update/{id}\x12U\n" +
|
||||
"\x06Remove\x12\x16.crontab.DeleteRequest\x1a\x11.crontab.Response\" \x82\xd3\xe4\x93\x02\x1a:\x01*\"\x15/plan/api/remove/{id}B\x1fZ\x1dm7s.live/v5/plugin/crontab/pbb\x06proto3"
|
||||
"\x06Remove\x12\x16.crontab.DeleteRequest\x1a\x11.crontab.Response\" \x82\xd3\xe4\x93\x02\x1a:\x01*\"\x15/plan/api/remove/{id}\x12x\n" +
|
||||
"\x15ListRecordPlanStreams\x12\x1a.crontab.ReqPlanStreamList\x1a%.crontab.RecordPlanStreamResponseList\"\x1c\x82\xd3\xe4\x93\x02\x16\x12\x14/planstream/api/list\x12]\n" +
|
||||
"\x13AddRecordPlanStream\x12\x13.crontab.PlanStream\x1a\x11.crontab.Response\"\x1e\x82\xd3\xe4\x93\x02\x18:\x01*\"\x13/planstream/api/add\x12c\n" +
|
||||
"\x16UpdateRecordPlanStream\x12\x13.crontab.PlanStream\x1a\x11.crontab.Response\"!\x82\xd3\xe4\x93\x02\x1b:\x01*\"\x16/planstream/api/update\x12\x89\x01\n" +
|
||||
"\x16RemoveRecordPlanStream\x12 .crontab.DeletePlanStreamRequest\x1a\x11.crontab.Response\":\x82\xd3\xe4\x93\x024:\x01*\"//planstream/api/remove/{planId}/{streamPath=**}\x12f\n" +
|
||||
"\rParsePlanTime\x12\x19.crontab.ParsePlanRequest\x1a\x1a.crontab.ParsePlanResponse\"\x1e\x82\xd3\xe4\x93\x02\x18\x12\x16/plan/api/parse/{plan}\x12n\n" +
|
||||
"\x10GetCrontabStatus\x12\x1d.crontab.CrontabStatusRequest\x1a\x1e.crontab.CrontabStatusResponse\"\x1b\x82\xd3\xe4\x93\x02\x15\x12\x13/crontab/api/statusB\x1fZ\x1dm7s.live/v5/plugin/crontab/pbb\x06proto3"
|
||||
|
||||
var (
|
||||
file_crontab_proto_rawDescOnce sync.Once
|
||||
@@ -390,32 +1234,66 @@ func file_crontab_proto_rawDescGZIP() []byte {
|
||||
return file_crontab_proto_rawDescData
|
||||
}
|
||||
|
||||
var file_crontab_proto_msgTypes = make([]protoimpl.MessageInfo, 5)
|
||||
var file_crontab_proto_msgTypes = make([]protoimpl.MessageInfo, 15)
|
||||
var file_crontab_proto_goTypes = []any{
|
||||
(*PlanResponseList)(nil), // 0: crontab.PlanResponseList
|
||||
(*Plan)(nil), // 1: crontab.Plan
|
||||
(*ReqPlanList)(nil), // 2: crontab.ReqPlanList
|
||||
(*DeleteRequest)(nil), // 3: crontab.DeleteRequest
|
||||
(*Response)(nil), // 4: crontab.Response
|
||||
(*timestamppb.Timestamp)(nil), // 5: google.protobuf.Timestamp
|
||||
(*PlanResponseList)(nil), // 0: crontab.PlanResponseList
|
||||
(*Plan)(nil), // 1: crontab.Plan
|
||||
(*ReqPlanList)(nil), // 2: crontab.ReqPlanList
|
||||
(*DeleteRequest)(nil), // 3: crontab.DeleteRequest
|
||||
(*Response)(nil), // 4: crontab.Response
|
||||
(*PlanStream)(nil), // 5: crontab.PlanStream
|
||||
(*ReqPlanStreamList)(nil), // 6: crontab.ReqPlanStreamList
|
||||
(*RecordPlanStreamResponseList)(nil), // 7: crontab.RecordPlanStreamResponseList
|
||||
(*DeletePlanStreamRequest)(nil), // 8: crontab.DeletePlanStreamRequest
|
||||
(*ParsePlanRequest)(nil), // 9: crontab.ParsePlanRequest
|
||||
(*TimeSlotInfo)(nil), // 10: crontab.TimeSlotInfo
|
||||
(*ParsePlanResponse)(nil), // 11: crontab.ParsePlanResponse
|
||||
(*CrontabStatusRequest)(nil), // 12: crontab.CrontabStatusRequest
|
||||
(*CrontabTaskInfo)(nil), // 13: crontab.CrontabTaskInfo
|
||||
(*CrontabStatusResponse)(nil), // 14: crontab.CrontabStatusResponse
|
||||
(*timestamppb.Timestamp)(nil), // 15: google.protobuf.Timestamp
|
||||
}
|
||||
var file_crontab_proto_depIdxs = []int32{
|
||||
1, // 0: crontab.PlanResponseList.data:type_name -> crontab.Plan
|
||||
5, // 1: crontab.Plan.createTime:type_name -> google.protobuf.Timestamp
|
||||
5, // 2: crontab.Plan.updateTime:type_name -> google.protobuf.Timestamp
|
||||
2, // 3: crontab.api.List:input_type -> crontab.ReqPlanList
|
||||
1, // 4: crontab.api.Add:input_type -> crontab.Plan
|
||||
1, // 5: crontab.api.Update:input_type -> crontab.Plan
|
||||
3, // 6: crontab.api.Remove:input_type -> crontab.DeleteRequest
|
||||
0, // 7: crontab.api.List:output_type -> crontab.PlanResponseList
|
||||
4, // 8: crontab.api.Add:output_type -> crontab.Response
|
||||
4, // 9: crontab.api.Update:output_type -> crontab.Response
|
||||
4, // 10: crontab.api.Remove:output_type -> crontab.Response
|
||||
7, // [7:11] is the sub-list for method output_type
|
||||
3, // [3:7] is the sub-list for method input_type
|
||||
3, // [3:3] is the sub-list for extension type_name
|
||||
3, // [3:3] is the sub-list for extension extendee
|
||||
0, // [0:3] is the sub-list for field type_name
|
||||
1, // 0: crontab.PlanResponseList.data:type_name -> crontab.Plan
|
||||
15, // 1: crontab.Plan.createTime:type_name -> google.protobuf.Timestamp
|
||||
15, // 2: crontab.Plan.updateTime:type_name -> google.protobuf.Timestamp
|
||||
15, // 3: crontab.PlanStream.created_at:type_name -> google.protobuf.Timestamp
|
||||
15, // 4: crontab.PlanStream.updated_at:type_name -> google.protobuf.Timestamp
|
||||
5, // 5: crontab.RecordPlanStreamResponseList.data:type_name -> crontab.PlanStream
|
||||
15, // 6: crontab.TimeSlotInfo.start:type_name -> google.protobuf.Timestamp
|
||||
15, // 7: crontab.TimeSlotInfo.end:type_name -> google.protobuf.Timestamp
|
||||
10, // 8: crontab.ParsePlanResponse.slots:type_name -> crontab.TimeSlotInfo
|
||||
10, // 9: crontab.ParsePlanResponse.next_slot:type_name -> crontab.TimeSlotInfo
|
||||
15, // 10: crontab.CrontabTaskInfo.start_time:type_name -> google.protobuf.Timestamp
|
||||
15, // 11: crontab.CrontabTaskInfo.end_time:type_name -> google.protobuf.Timestamp
|
||||
10, // 12: crontab.CrontabTaskInfo.plan_slots:type_name -> crontab.TimeSlotInfo
|
||||
13, // 13: crontab.CrontabStatusResponse.running_tasks:type_name -> crontab.CrontabTaskInfo
|
||||
13, // 14: crontab.CrontabStatusResponse.next_tasks:type_name -> crontab.CrontabTaskInfo
|
||||
2, // 15: crontab.api.List:input_type -> crontab.ReqPlanList
|
||||
1, // 16: crontab.api.Add:input_type -> crontab.Plan
|
||||
1, // 17: crontab.api.Update:input_type -> crontab.Plan
|
||||
3, // 18: crontab.api.Remove:input_type -> crontab.DeleteRequest
|
||||
6, // 19: crontab.api.ListRecordPlanStreams:input_type -> crontab.ReqPlanStreamList
|
||||
5, // 20: crontab.api.AddRecordPlanStream:input_type -> crontab.PlanStream
|
||||
5, // 21: crontab.api.UpdateRecordPlanStream:input_type -> crontab.PlanStream
|
||||
8, // 22: crontab.api.RemoveRecordPlanStream:input_type -> crontab.DeletePlanStreamRequest
|
||||
9, // 23: crontab.api.ParsePlanTime:input_type -> crontab.ParsePlanRequest
|
||||
12, // 24: crontab.api.GetCrontabStatus:input_type -> crontab.CrontabStatusRequest
|
||||
0, // 25: crontab.api.List:output_type -> crontab.PlanResponseList
|
||||
4, // 26: crontab.api.Add:output_type -> crontab.Response
|
||||
4, // 27: crontab.api.Update:output_type -> crontab.Response
|
||||
4, // 28: crontab.api.Remove:output_type -> crontab.Response
|
||||
7, // 29: crontab.api.ListRecordPlanStreams:output_type -> crontab.RecordPlanStreamResponseList
|
||||
4, // 30: crontab.api.AddRecordPlanStream:output_type -> crontab.Response
|
||||
4, // 31: crontab.api.UpdateRecordPlanStream:output_type -> crontab.Response
|
||||
4, // 32: crontab.api.RemoveRecordPlanStream:output_type -> crontab.Response
|
||||
11, // 33: crontab.api.ParsePlanTime:output_type -> crontab.ParsePlanResponse
|
||||
14, // 34: crontab.api.GetCrontabStatus:output_type -> crontab.CrontabStatusResponse
|
||||
25, // [25:35] is the sub-list for method output_type
|
||||
15, // [15:25] is the sub-list for method input_type
|
||||
15, // [15:15] is the sub-list for extension type_name
|
||||
15, // [15:15] is the sub-list for extension extendee
|
||||
0, // [0:15] is the sub-list for field type_name
|
||||
}
|
||||
|
||||
func init() { file_crontab_proto_init() }
|
||||
@@ -429,7 +1307,7 @@ func file_crontab_proto_init() {
|
||||
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
|
||||
RawDescriptor: unsafe.Slice(unsafe.StringData(file_crontab_proto_rawDesc), len(file_crontab_proto_rawDesc)),
|
||||
NumEnums: 0,
|
||||
NumMessages: 5,
|
||||
NumMessages: 15,
|
||||
NumExtensions: 0,
|
||||
NumServices: 1,
|
||||
},
|
||||
|
@@ -176,6 +176,215 @@ func local_request_Api_Remove_0(ctx context.Context, marshaler runtime.Marshaler
|
||||
return msg, metadata, err
|
||||
}
|
||||
|
||||
var filter_Api_ListRecordPlanStreams_0 = &utilities.DoubleArray{Encoding: map[string]int{}, Base: []int(nil), Check: []int(nil)}
|
||||
|
||||
func request_Api_ListRecordPlanStreams_0(ctx context.Context, marshaler runtime.Marshaler, client ApiClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
|
||||
var (
|
||||
protoReq ReqPlanStreamList
|
||||
metadata runtime.ServerMetadata
|
||||
)
|
||||
io.Copy(io.Discard, req.Body)
|
||||
if err := req.ParseForm(); err != nil {
|
||||
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
|
||||
}
|
||||
if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_Api_ListRecordPlanStreams_0); err != nil {
|
||||
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
|
||||
}
|
||||
msg, err := client.ListRecordPlanStreams(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))
|
||||
return msg, metadata, err
|
||||
}
|
||||
|
||||
func local_request_Api_ListRecordPlanStreams_0(ctx context.Context, marshaler runtime.Marshaler, server ApiServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
|
||||
var (
|
||||
protoReq ReqPlanStreamList
|
||||
metadata runtime.ServerMetadata
|
||||
)
|
||||
if err := req.ParseForm(); err != nil {
|
||||
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
|
||||
}
|
||||
if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_Api_ListRecordPlanStreams_0); err != nil {
|
||||
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
|
||||
}
|
||||
msg, err := server.ListRecordPlanStreams(ctx, &protoReq)
|
||||
return msg, metadata, err
|
||||
}
|
||||
|
||||
func request_Api_AddRecordPlanStream_0(ctx context.Context, marshaler runtime.Marshaler, client ApiClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
|
||||
var (
|
||||
protoReq PlanStream
|
||||
metadata runtime.ServerMetadata
|
||||
)
|
||||
if err := marshaler.NewDecoder(req.Body).Decode(&protoReq); err != nil && !errors.Is(err, io.EOF) {
|
||||
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
|
||||
}
|
||||
msg, err := client.AddRecordPlanStream(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))
|
||||
return msg, metadata, err
|
||||
}
|
||||
|
||||
func local_request_Api_AddRecordPlanStream_0(ctx context.Context, marshaler runtime.Marshaler, server ApiServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
|
||||
var (
|
||||
protoReq PlanStream
|
||||
metadata runtime.ServerMetadata
|
||||
)
|
||||
if err := marshaler.NewDecoder(req.Body).Decode(&protoReq); err != nil && !errors.Is(err, io.EOF) {
|
||||
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
|
||||
}
|
||||
msg, err := server.AddRecordPlanStream(ctx, &protoReq)
|
||||
return msg, metadata, err
|
||||
}
|
||||
|
||||
func request_Api_UpdateRecordPlanStream_0(ctx context.Context, marshaler runtime.Marshaler, client ApiClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
|
||||
var (
|
||||
protoReq PlanStream
|
||||
metadata runtime.ServerMetadata
|
||||
)
|
||||
if err := marshaler.NewDecoder(req.Body).Decode(&protoReq); err != nil && !errors.Is(err, io.EOF) {
|
||||
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
|
||||
}
|
||||
msg, err := client.UpdateRecordPlanStream(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))
|
||||
return msg, metadata, err
|
||||
}
|
||||
|
||||
func local_request_Api_UpdateRecordPlanStream_0(ctx context.Context, marshaler runtime.Marshaler, server ApiServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
|
||||
var (
|
||||
protoReq PlanStream
|
||||
metadata runtime.ServerMetadata
|
||||
)
|
||||
if err := marshaler.NewDecoder(req.Body).Decode(&protoReq); err != nil && !errors.Is(err, io.EOF) {
|
||||
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
|
||||
}
|
||||
msg, err := server.UpdateRecordPlanStream(ctx, &protoReq)
|
||||
return msg, metadata, err
|
||||
}
|
||||
|
||||
func request_Api_RemoveRecordPlanStream_0(ctx context.Context, marshaler runtime.Marshaler, client ApiClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
|
||||
var (
|
||||
protoReq DeletePlanStreamRequest
|
||||
metadata runtime.ServerMetadata
|
||||
err error
|
||||
)
|
||||
if err := marshaler.NewDecoder(req.Body).Decode(&protoReq); err != nil && !errors.Is(err, io.EOF) {
|
||||
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
|
||||
}
|
||||
val, ok := pathParams["planId"]
|
||||
if !ok {
|
||||
return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "planId")
|
||||
}
|
||||
protoReq.PlanId, err = runtime.Uint32(val)
|
||||
if err != nil {
|
||||
return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "planId", err)
|
||||
}
|
||||
val, ok = pathParams["streamPath"]
|
||||
if !ok {
|
||||
return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "streamPath")
|
||||
}
|
||||
protoReq.StreamPath, err = runtime.String(val)
|
||||
if err != nil {
|
||||
return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "streamPath", err)
|
||||
}
|
||||
msg, err := client.RemoveRecordPlanStream(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))
|
||||
return msg, metadata, err
|
||||
}
|
||||
|
||||
func local_request_Api_RemoveRecordPlanStream_0(ctx context.Context, marshaler runtime.Marshaler, server ApiServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
|
||||
var (
|
||||
protoReq DeletePlanStreamRequest
|
||||
metadata runtime.ServerMetadata
|
||||
err error
|
||||
)
|
||||
if err := marshaler.NewDecoder(req.Body).Decode(&protoReq); err != nil && !errors.Is(err, io.EOF) {
|
||||
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
|
||||
}
|
||||
val, ok := pathParams["planId"]
|
||||
if !ok {
|
||||
return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "planId")
|
||||
}
|
||||
protoReq.PlanId, err = runtime.Uint32(val)
|
||||
if err != nil {
|
||||
return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "planId", err)
|
||||
}
|
||||
val, ok = pathParams["streamPath"]
|
||||
if !ok {
|
||||
return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "streamPath")
|
||||
}
|
||||
protoReq.StreamPath, err = runtime.String(val)
|
||||
if err != nil {
|
||||
return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "streamPath", err)
|
||||
}
|
||||
msg, err := server.RemoveRecordPlanStream(ctx, &protoReq)
|
||||
return msg, metadata, err
|
||||
}
|
||||
|
||||
func request_Api_ParsePlanTime_0(ctx context.Context, marshaler runtime.Marshaler, client ApiClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
|
||||
var (
|
||||
protoReq ParsePlanRequest
|
||||
metadata runtime.ServerMetadata
|
||||
err error
|
||||
)
|
||||
io.Copy(io.Discard, req.Body)
|
||||
val, ok := pathParams["plan"]
|
||||
if !ok {
|
||||
return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "plan")
|
||||
}
|
||||
protoReq.Plan, err = runtime.String(val)
|
||||
if err != nil {
|
||||
return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "plan", err)
|
||||
}
|
||||
msg, err := client.ParsePlanTime(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))
|
||||
return msg, metadata, err
|
||||
}
|
||||
|
||||
func local_request_Api_ParsePlanTime_0(ctx context.Context, marshaler runtime.Marshaler, server ApiServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
|
||||
var (
|
||||
protoReq ParsePlanRequest
|
||||
metadata runtime.ServerMetadata
|
||||
err error
|
||||
)
|
||||
val, ok := pathParams["plan"]
|
||||
if !ok {
|
||||
return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "plan")
|
||||
}
|
||||
protoReq.Plan, err = runtime.String(val)
|
||||
if err != nil {
|
||||
return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "plan", err)
|
||||
}
|
||||
msg, err := server.ParsePlanTime(ctx, &protoReq)
|
||||
return msg, metadata, err
|
||||
}
|
||||
|
||||
var filter_Api_GetCrontabStatus_0 = &utilities.DoubleArray{Encoding: map[string]int{}, Base: []int(nil), Check: []int(nil)}
|
||||
|
||||
func request_Api_GetCrontabStatus_0(ctx context.Context, marshaler runtime.Marshaler, client ApiClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
|
||||
var (
|
||||
protoReq CrontabStatusRequest
|
||||
metadata runtime.ServerMetadata
|
||||
)
|
||||
io.Copy(io.Discard, req.Body)
|
||||
if err := req.ParseForm(); err != nil {
|
||||
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
|
||||
}
|
||||
if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_Api_GetCrontabStatus_0); err != nil {
|
||||
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
|
||||
}
|
||||
msg, err := client.GetCrontabStatus(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))
|
||||
return msg, metadata, err
|
||||
}
|
||||
|
||||
func local_request_Api_GetCrontabStatus_0(ctx context.Context, marshaler runtime.Marshaler, server ApiServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
|
||||
var (
|
||||
protoReq CrontabStatusRequest
|
||||
metadata runtime.ServerMetadata
|
||||
)
|
||||
if err := req.ParseForm(); err != nil {
|
||||
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
|
||||
}
|
||||
if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_Api_GetCrontabStatus_0); err != nil {
|
||||
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
|
||||
}
|
||||
msg, err := server.GetCrontabStatus(ctx, &protoReq)
|
||||
return msg, metadata, err
|
||||
}
|
||||
|
||||
// RegisterApiHandlerServer registers the http handlers for service Api to "mux".
|
||||
// UnaryRPC :call ApiServer directly.
|
||||
// StreamingRPC :currently unsupported pending https://github.com/grpc/grpc-go/issues/906.
|
||||
@@ -262,6 +471,126 @@ func RegisterApiHandlerServer(ctx context.Context, mux *runtime.ServeMux, server
|
||||
}
|
||||
forward_Api_Remove_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
|
||||
})
|
||||
mux.Handle(http.MethodGet, pattern_Api_ListRecordPlanStreams_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
|
||||
ctx, cancel := context.WithCancel(req.Context())
|
||||
defer cancel()
|
||||
var stream runtime.ServerTransportStream
|
||||
ctx = grpc.NewContextWithServerTransportStream(ctx, &stream)
|
||||
inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
|
||||
annotatedContext, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/crontab.Api/ListRecordPlanStreams", runtime.WithHTTPPathPattern("/planstream/api/list"))
|
||||
if err != nil {
|
||||
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
|
||||
return
|
||||
}
|
||||
resp, md, err := local_request_Api_ListRecordPlanStreams_0(annotatedContext, inboundMarshaler, server, req, pathParams)
|
||||
md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer())
|
||||
annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md)
|
||||
if err != nil {
|
||||
runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err)
|
||||
return
|
||||
}
|
||||
forward_Api_ListRecordPlanStreams_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
|
||||
})
|
||||
mux.Handle(http.MethodPost, pattern_Api_AddRecordPlanStream_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
|
||||
ctx, cancel := context.WithCancel(req.Context())
|
||||
defer cancel()
|
||||
var stream runtime.ServerTransportStream
|
||||
ctx = grpc.NewContextWithServerTransportStream(ctx, &stream)
|
||||
inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
|
||||
annotatedContext, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/crontab.Api/AddRecordPlanStream", runtime.WithHTTPPathPattern("/planstream/api/add"))
|
||||
if err != nil {
|
||||
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
|
||||
return
|
||||
}
|
||||
resp, md, err := local_request_Api_AddRecordPlanStream_0(annotatedContext, inboundMarshaler, server, req, pathParams)
|
||||
md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer())
|
||||
annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md)
|
||||
if err != nil {
|
||||
runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err)
|
||||
return
|
||||
}
|
||||
forward_Api_AddRecordPlanStream_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
|
||||
})
|
||||
mux.Handle(http.MethodPost, pattern_Api_UpdateRecordPlanStream_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
|
||||
ctx, cancel := context.WithCancel(req.Context())
|
||||
defer cancel()
|
||||
var stream runtime.ServerTransportStream
|
||||
ctx = grpc.NewContextWithServerTransportStream(ctx, &stream)
|
||||
inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
|
||||
annotatedContext, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/crontab.Api/UpdateRecordPlanStream", runtime.WithHTTPPathPattern("/planstream/api/update"))
|
||||
if err != nil {
|
||||
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
|
||||
return
|
||||
}
|
||||
resp, md, err := local_request_Api_UpdateRecordPlanStream_0(annotatedContext, inboundMarshaler, server, req, pathParams)
|
||||
md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer())
|
||||
annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md)
|
||||
if err != nil {
|
||||
runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err)
|
||||
return
|
||||
}
|
||||
forward_Api_UpdateRecordPlanStream_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
|
||||
})
|
||||
mux.Handle(http.MethodPost, pattern_Api_RemoveRecordPlanStream_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
|
||||
ctx, cancel := context.WithCancel(req.Context())
|
||||
defer cancel()
|
||||
var stream runtime.ServerTransportStream
|
||||
ctx = grpc.NewContextWithServerTransportStream(ctx, &stream)
|
||||
inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
|
||||
annotatedContext, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/crontab.Api/RemoveRecordPlanStream", runtime.WithHTTPPathPattern("/planstream/api/remove/{planId}/{streamPath=**}"))
|
||||
if err != nil {
|
||||
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
|
||||
return
|
||||
}
|
||||
resp, md, err := local_request_Api_RemoveRecordPlanStream_0(annotatedContext, inboundMarshaler, server, req, pathParams)
|
||||
md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer())
|
||||
annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md)
|
||||
if err != nil {
|
||||
runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err)
|
||||
return
|
||||
}
|
||||
forward_Api_RemoveRecordPlanStream_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
|
||||
})
|
||||
mux.Handle(http.MethodGet, pattern_Api_ParsePlanTime_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
|
||||
ctx, cancel := context.WithCancel(req.Context())
|
||||
defer cancel()
|
||||
var stream runtime.ServerTransportStream
|
||||
ctx = grpc.NewContextWithServerTransportStream(ctx, &stream)
|
||||
inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
|
||||
annotatedContext, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/crontab.Api/ParsePlanTime", runtime.WithHTTPPathPattern("/plan/api/parse/{plan}"))
|
||||
if err != nil {
|
||||
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
|
||||
return
|
||||
}
|
||||
resp, md, err := local_request_Api_ParsePlanTime_0(annotatedContext, inboundMarshaler, server, req, pathParams)
|
||||
md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer())
|
||||
annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md)
|
||||
if err != nil {
|
||||
runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err)
|
||||
return
|
||||
}
|
||||
forward_Api_ParsePlanTime_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
|
||||
})
|
||||
mux.Handle(http.MethodGet, pattern_Api_GetCrontabStatus_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
|
||||
ctx, cancel := context.WithCancel(req.Context())
|
||||
defer cancel()
|
||||
var stream runtime.ServerTransportStream
|
||||
ctx = grpc.NewContextWithServerTransportStream(ctx, &stream)
|
||||
inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
|
||||
annotatedContext, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/crontab.Api/GetCrontabStatus", runtime.WithHTTPPathPattern("/crontab/api/status"))
|
||||
if err != nil {
|
||||
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
|
||||
return
|
||||
}
|
||||
resp, md, err := local_request_Api_GetCrontabStatus_0(annotatedContext, inboundMarshaler, server, req, pathParams)
|
||||
md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer())
|
||||
annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md)
|
||||
if err != nil {
|
||||
runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err)
|
||||
return
|
||||
}
|
||||
forward_Api_GetCrontabStatus_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -370,19 +699,133 @@ func RegisterApiHandlerClient(ctx context.Context, mux *runtime.ServeMux, client
|
||||
}
|
||||
forward_Api_Remove_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
|
||||
})
|
||||
mux.Handle(http.MethodGet, pattern_Api_ListRecordPlanStreams_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
|
||||
ctx, cancel := context.WithCancel(req.Context())
|
||||
defer cancel()
|
||||
inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
|
||||
annotatedContext, err := runtime.AnnotateContext(ctx, mux, req, "/crontab.Api/ListRecordPlanStreams", runtime.WithHTTPPathPattern("/planstream/api/list"))
|
||||
if err != nil {
|
||||
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
|
||||
return
|
||||
}
|
||||
resp, md, err := request_Api_ListRecordPlanStreams_0(annotatedContext, inboundMarshaler, client, req, pathParams)
|
||||
annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md)
|
||||
if err != nil {
|
||||
runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err)
|
||||
return
|
||||
}
|
||||
forward_Api_ListRecordPlanStreams_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
|
||||
})
|
||||
mux.Handle(http.MethodPost, pattern_Api_AddRecordPlanStream_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
|
||||
ctx, cancel := context.WithCancel(req.Context())
|
||||
defer cancel()
|
||||
inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
|
||||
annotatedContext, err := runtime.AnnotateContext(ctx, mux, req, "/crontab.Api/AddRecordPlanStream", runtime.WithHTTPPathPattern("/planstream/api/add"))
|
||||
if err != nil {
|
||||
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
|
||||
return
|
||||
}
|
||||
resp, md, err := request_Api_AddRecordPlanStream_0(annotatedContext, inboundMarshaler, client, req, pathParams)
|
||||
annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md)
|
||||
if err != nil {
|
||||
runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err)
|
||||
return
|
||||
}
|
||||
forward_Api_AddRecordPlanStream_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
|
||||
})
|
||||
mux.Handle(http.MethodPost, pattern_Api_UpdateRecordPlanStream_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
|
||||
ctx, cancel := context.WithCancel(req.Context())
|
||||
defer cancel()
|
||||
inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
|
||||
annotatedContext, err := runtime.AnnotateContext(ctx, mux, req, "/crontab.Api/UpdateRecordPlanStream", runtime.WithHTTPPathPattern("/planstream/api/update"))
|
||||
if err != nil {
|
||||
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
|
||||
return
|
||||
}
|
||||
resp, md, err := request_Api_UpdateRecordPlanStream_0(annotatedContext, inboundMarshaler, client, req, pathParams)
|
||||
annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md)
|
||||
if err != nil {
|
||||
runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err)
|
||||
return
|
||||
}
|
||||
forward_Api_UpdateRecordPlanStream_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
|
||||
})
|
||||
mux.Handle(http.MethodPost, pattern_Api_RemoveRecordPlanStream_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
|
||||
ctx, cancel := context.WithCancel(req.Context())
|
||||
defer cancel()
|
||||
inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
|
||||
annotatedContext, err := runtime.AnnotateContext(ctx, mux, req, "/crontab.Api/RemoveRecordPlanStream", runtime.WithHTTPPathPattern("/planstream/api/remove/{planId}/{streamPath=**}"))
|
||||
if err != nil {
|
||||
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
|
||||
return
|
||||
}
|
||||
resp, md, err := request_Api_RemoveRecordPlanStream_0(annotatedContext, inboundMarshaler, client, req, pathParams)
|
||||
annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md)
|
||||
if err != nil {
|
||||
runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err)
|
||||
return
|
||||
}
|
||||
forward_Api_RemoveRecordPlanStream_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
|
||||
})
|
||||
mux.Handle(http.MethodGet, pattern_Api_ParsePlanTime_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
|
||||
ctx, cancel := context.WithCancel(req.Context())
|
||||
defer cancel()
|
||||
inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
|
||||
annotatedContext, err := runtime.AnnotateContext(ctx, mux, req, "/crontab.Api/ParsePlanTime", runtime.WithHTTPPathPattern("/plan/api/parse/{plan}"))
|
||||
if err != nil {
|
||||
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
|
||||
return
|
||||
}
|
||||
resp, md, err := request_Api_ParsePlanTime_0(annotatedContext, inboundMarshaler, client, req, pathParams)
|
||||
annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md)
|
||||
if err != nil {
|
||||
runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err)
|
||||
return
|
||||
}
|
||||
forward_Api_ParsePlanTime_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
|
||||
})
|
||||
mux.Handle(http.MethodGet, pattern_Api_GetCrontabStatus_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
|
||||
ctx, cancel := context.WithCancel(req.Context())
|
||||
defer cancel()
|
||||
inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
|
||||
annotatedContext, err := runtime.AnnotateContext(ctx, mux, req, "/crontab.Api/GetCrontabStatus", runtime.WithHTTPPathPattern("/crontab/api/status"))
|
||||
if err != nil {
|
||||
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
|
||||
return
|
||||
}
|
||||
resp, md, err := request_Api_GetCrontabStatus_0(annotatedContext, inboundMarshaler, client, req, pathParams)
|
||||
annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md)
|
||||
if err != nil {
|
||||
runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err)
|
||||
return
|
||||
}
|
||||
forward_Api_GetCrontabStatus_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
|
||||
})
|
||||
return nil
|
||||
}
|
||||
|
||||
var (
|
||||
pattern_Api_List_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2}, []string{"plan", "api", "list"}, ""))
|
||||
pattern_Api_Add_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2}, []string{"plan", "api", "add"}, ""))
|
||||
pattern_Api_Update_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 1, 0, 4, 1, 5, 3}, []string{"plan", "api", "update", "id"}, ""))
|
||||
pattern_Api_Remove_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 1, 0, 4, 1, 5, 3}, []string{"plan", "api", "remove", "id"}, ""))
|
||||
pattern_Api_List_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2}, []string{"plan", "api", "list"}, ""))
|
||||
pattern_Api_Add_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2}, []string{"plan", "api", "add"}, ""))
|
||||
pattern_Api_Update_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 1, 0, 4, 1, 5, 3}, []string{"plan", "api", "update", "id"}, ""))
|
||||
pattern_Api_Remove_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 1, 0, 4, 1, 5, 3}, []string{"plan", "api", "remove", "id"}, ""))
|
||||
pattern_Api_ListRecordPlanStreams_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2}, []string{"planstream", "api", "list"}, ""))
|
||||
pattern_Api_AddRecordPlanStream_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2}, []string{"planstream", "api", "add"}, ""))
|
||||
pattern_Api_UpdateRecordPlanStream_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2}, []string{"planstream", "api", "update"}, ""))
|
||||
pattern_Api_RemoveRecordPlanStream_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 1, 0, 4, 1, 5, 3, 3, 0, 4, 1, 5, 4}, []string{"planstream", "api", "remove", "planId", "streamPath"}, ""))
|
||||
pattern_Api_ParsePlanTime_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 1, 0, 4, 1, 5, 0}, []string{"plan", "api", "parse"}, ""))
|
||||
pattern_Api_GetCrontabStatus_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2}, []string{"crontab", "api", "status"}, ""))
|
||||
)
|
||||
|
||||
var (
|
||||
forward_Api_List_0 = runtime.ForwardResponseMessage
|
||||
forward_Api_Add_0 = runtime.ForwardResponseMessage
|
||||
forward_Api_Update_0 = runtime.ForwardResponseMessage
|
||||
forward_Api_Remove_0 = runtime.ForwardResponseMessage
|
||||
forward_Api_List_0 = runtime.ForwardResponseMessage
|
||||
forward_Api_Add_0 = runtime.ForwardResponseMessage
|
||||
forward_Api_Update_0 = runtime.ForwardResponseMessage
|
||||
forward_Api_Remove_0 = runtime.ForwardResponseMessage
|
||||
forward_Api_ListRecordPlanStreams_0 = runtime.ForwardResponseMessage
|
||||
forward_Api_AddRecordPlanStream_0 = runtime.ForwardResponseMessage
|
||||
forward_Api_UpdateRecordPlanStream_0 = runtime.ForwardResponseMessage
|
||||
forward_Api_RemoveRecordPlanStream_0 = runtime.ForwardResponseMessage
|
||||
forward_Api_ParsePlanTime_0 = runtime.ForwardResponseMessage
|
||||
forward_Api_GetCrontabStatus_0 = runtime.ForwardResponseMessage
|
||||
)
|
||||
|
@@ -28,6 +28,45 @@ service api {
|
||||
body: "*"
|
||||
};
|
||||
}
|
||||
|
||||
// RecordPlanStream 相关接口
|
||||
rpc ListRecordPlanStreams (ReqPlanStreamList) returns (RecordPlanStreamResponseList) {
|
||||
option (google.api.http) = {
|
||||
get: "/planstream/api/list"
|
||||
};
|
||||
}
|
||||
rpc AddRecordPlanStream (PlanStream) returns (Response) {
|
||||
option (google.api.http) = {
|
||||
post: "/planstream/api/add"
|
||||
body: "*"
|
||||
};
|
||||
}
|
||||
rpc UpdateRecordPlanStream (PlanStream) returns (Response) {
|
||||
option (google.api.http) = {
|
||||
post: "/planstream/api/update"
|
||||
body: "*"
|
||||
};
|
||||
}
|
||||
rpc RemoveRecordPlanStream (DeletePlanStreamRequest) returns (Response) {
|
||||
option (google.api.http) = {
|
||||
post: "/planstream/api/remove/{planId}/{streamPath=**}"
|
||||
body: "*"
|
||||
};
|
||||
}
|
||||
|
||||
// 解析计划字符串,返回时间段信息
|
||||
rpc ParsePlanTime (ParsePlanRequest) returns (ParsePlanResponse) {
|
||||
option (google.api.http) = {
|
||||
get: "/plan/api/parse/{plan}"
|
||||
};
|
||||
}
|
||||
|
||||
// 获取当前Crontab任务状态
|
||||
rpc GetCrontabStatus (CrontabStatusRequest) returns (CrontabStatusResponse) {
|
||||
option (google.api.http) = {
|
||||
get: "/crontab/api/status"
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
message PlanResponseList {
|
||||
@@ -60,4 +99,92 @@ message DeleteRequest {
|
||||
message Response {
|
||||
int32 code = 1;
|
||||
string message = 2;
|
||||
}
|
||||
|
||||
// RecordPlanStream 相关消息定义
|
||||
message PlanStream {
|
||||
uint32 planId = 1;
|
||||
string stream_path = 2;
|
||||
string fragment = 3;
|
||||
string filePath = 4;
|
||||
string record_type = 5; // 录制类型,例如 "mp4", "flv"
|
||||
google.protobuf.Timestamp created_at = 6;
|
||||
google.protobuf.Timestamp updated_at = 7;
|
||||
bool enable = 8; // 是否启用该录制流
|
||||
}
|
||||
|
||||
message ReqPlanStreamList {
|
||||
uint32 pageNum = 1;
|
||||
uint32 pageSize = 2;
|
||||
uint32 planId = 3; // 可选的按录制计划ID筛选
|
||||
string stream_path = 4; // 可选的按流路径筛选
|
||||
}
|
||||
|
||||
message RecordPlanStreamResponseList {
|
||||
int32 code = 1;
|
||||
string message = 2;
|
||||
uint32 totalCount = 3;
|
||||
uint32 pageNum = 4;
|
||||
uint32 pageSize = 5;
|
||||
repeated PlanStream data = 6;
|
||||
}
|
||||
|
||||
message DeletePlanStreamRequest {
|
||||
uint32 planId = 1;
|
||||
string streamPath = 2;
|
||||
}
|
||||
|
||||
// 解析计划请求
|
||||
message ParsePlanRequest {
|
||||
string plan = 1; // 168位的0/1字符串,表示一周的每个小时是否录制
|
||||
}
|
||||
|
||||
// 时间段信息
|
||||
message TimeSlotInfo {
|
||||
google.protobuf.Timestamp start = 1; // 开始时间
|
||||
google.protobuf.Timestamp end = 2; // 结束时间
|
||||
string weekday = 3; // 周几(例如:周一)
|
||||
string time_range = 4; // 时间范围(例如:09:00-10:00)
|
||||
}
|
||||
|
||||
// 解析计划响应
|
||||
message ParsePlanResponse {
|
||||
int32 code = 1; // 响应码
|
||||
string message = 2; // 响应消息
|
||||
repeated TimeSlotInfo slots = 3; // 所有计划的时间段
|
||||
TimeSlotInfo next_slot = 4; // 从当前时间开始的下一个时间段
|
||||
}
|
||||
|
||||
// 新增的消息定义
|
||||
// 获取Crontab状态请求
|
||||
message CrontabStatusRequest {
|
||||
// 可以为空,表示获取所有任务
|
||||
string stream_path = 1; // 可选,按流路径过滤
|
||||
}
|
||||
|
||||
// 任务信息
|
||||
message CrontabTaskInfo {
|
||||
uint32 plan_id = 1; // 计划ID
|
||||
string plan_name = 2; // 计划名称
|
||||
string stream_path = 3; // 流路径
|
||||
bool is_recording = 4; // 是否正在录制
|
||||
google.protobuf.Timestamp start_time = 5; // 当前/下一个任务开始时间
|
||||
google.protobuf.Timestamp end_time = 6; // 当前/下一个任务结束时间
|
||||
string time_range = 7; // 时间范围(例如:09:00-10:00)
|
||||
string weekday = 8; // 周几(例如:周一)
|
||||
string file_path = 9; // 文件保存路径
|
||||
string fragment = 10; // 分片设置
|
||||
uint32 elapsed_seconds = 11; // 已运行时间(秒,仅对正在运行的任务有效)
|
||||
uint32 remaining_seconds = 12; // 剩余时间(秒)
|
||||
repeated TimeSlotInfo plan_slots = 13; // 完整的计划时间段列表
|
||||
}
|
||||
|
||||
// 获取Crontab状态响应
|
||||
message CrontabStatusResponse {
|
||||
int32 code = 1; // 响应码
|
||||
string message = 2; // 响应消息
|
||||
repeated CrontabTaskInfo running_tasks = 3; // 当前正在执行的任务列表
|
||||
repeated CrontabTaskInfo next_tasks = 4; // 下一个计划执行的任务列表
|
||||
uint32 total_running = 5; // 正在运行的任务总数
|
||||
uint32 total_planned = 6; // 计划中的任务总数
|
||||
}
|
@@ -19,10 +19,16 @@ import (
|
||||
const _ = grpc.SupportPackageIsVersion9
|
||||
|
||||
const (
|
||||
Api_List_FullMethodName = "/crontab.api/List"
|
||||
Api_Add_FullMethodName = "/crontab.api/Add"
|
||||
Api_Update_FullMethodName = "/crontab.api/Update"
|
||||
Api_Remove_FullMethodName = "/crontab.api/Remove"
|
||||
Api_List_FullMethodName = "/crontab.api/List"
|
||||
Api_Add_FullMethodName = "/crontab.api/Add"
|
||||
Api_Update_FullMethodName = "/crontab.api/Update"
|
||||
Api_Remove_FullMethodName = "/crontab.api/Remove"
|
||||
Api_ListRecordPlanStreams_FullMethodName = "/crontab.api/ListRecordPlanStreams"
|
||||
Api_AddRecordPlanStream_FullMethodName = "/crontab.api/AddRecordPlanStream"
|
||||
Api_UpdateRecordPlanStream_FullMethodName = "/crontab.api/UpdateRecordPlanStream"
|
||||
Api_RemoveRecordPlanStream_FullMethodName = "/crontab.api/RemoveRecordPlanStream"
|
||||
Api_ParsePlanTime_FullMethodName = "/crontab.api/ParsePlanTime"
|
||||
Api_GetCrontabStatus_FullMethodName = "/crontab.api/GetCrontabStatus"
|
||||
)
|
||||
|
||||
// ApiClient is the client API for Api service.
|
||||
@@ -33,6 +39,15 @@ type ApiClient interface {
|
||||
Add(ctx context.Context, in *Plan, opts ...grpc.CallOption) (*Response, error)
|
||||
Update(ctx context.Context, in *Plan, opts ...grpc.CallOption) (*Response, error)
|
||||
Remove(ctx context.Context, in *DeleteRequest, opts ...grpc.CallOption) (*Response, error)
|
||||
// RecordPlanStream 相关接口
|
||||
ListRecordPlanStreams(ctx context.Context, in *ReqPlanStreamList, opts ...grpc.CallOption) (*RecordPlanStreamResponseList, error)
|
||||
AddRecordPlanStream(ctx context.Context, in *PlanStream, opts ...grpc.CallOption) (*Response, error)
|
||||
UpdateRecordPlanStream(ctx context.Context, in *PlanStream, opts ...grpc.CallOption) (*Response, error)
|
||||
RemoveRecordPlanStream(ctx context.Context, in *DeletePlanStreamRequest, opts ...grpc.CallOption) (*Response, error)
|
||||
// 解析计划字符串,返回时间段信息
|
||||
ParsePlanTime(ctx context.Context, in *ParsePlanRequest, opts ...grpc.CallOption) (*ParsePlanResponse, error)
|
||||
// 获取当前Crontab任务状态
|
||||
GetCrontabStatus(ctx context.Context, in *CrontabStatusRequest, opts ...grpc.CallOption) (*CrontabStatusResponse, error)
|
||||
}
|
||||
|
||||
type apiClient struct {
|
||||
@@ -83,6 +98,66 @@ func (c *apiClient) Remove(ctx context.Context, in *DeleteRequest, opts ...grpc.
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *apiClient) ListRecordPlanStreams(ctx context.Context, in *ReqPlanStreamList, opts ...grpc.CallOption) (*RecordPlanStreamResponseList, error) {
|
||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||
out := new(RecordPlanStreamResponseList)
|
||||
err := c.cc.Invoke(ctx, Api_ListRecordPlanStreams_FullMethodName, in, out, cOpts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *apiClient) AddRecordPlanStream(ctx context.Context, in *PlanStream, opts ...grpc.CallOption) (*Response, error) {
|
||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||
out := new(Response)
|
||||
err := c.cc.Invoke(ctx, Api_AddRecordPlanStream_FullMethodName, in, out, cOpts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *apiClient) UpdateRecordPlanStream(ctx context.Context, in *PlanStream, opts ...grpc.CallOption) (*Response, error) {
|
||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||
out := new(Response)
|
||||
err := c.cc.Invoke(ctx, Api_UpdateRecordPlanStream_FullMethodName, in, out, cOpts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *apiClient) RemoveRecordPlanStream(ctx context.Context, in *DeletePlanStreamRequest, opts ...grpc.CallOption) (*Response, error) {
|
||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||
out := new(Response)
|
||||
err := c.cc.Invoke(ctx, Api_RemoveRecordPlanStream_FullMethodName, in, out, cOpts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *apiClient) ParsePlanTime(ctx context.Context, in *ParsePlanRequest, opts ...grpc.CallOption) (*ParsePlanResponse, error) {
|
||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||
out := new(ParsePlanResponse)
|
||||
err := c.cc.Invoke(ctx, Api_ParsePlanTime_FullMethodName, in, out, cOpts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *apiClient) GetCrontabStatus(ctx context.Context, in *CrontabStatusRequest, opts ...grpc.CallOption) (*CrontabStatusResponse, error) {
|
||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||
out := new(CrontabStatusResponse)
|
||||
err := c.cc.Invoke(ctx, Api_GetCrontabStatus_FullMethodName, in, out, cOpts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// ApiServer is the server API for Api service.
|
||||
// All implementations must embed UnimplementedApiServer
|
||||
// for forward compatibility.
|
||||
@@ -91,6 +166,15 @@ type ApiServer interface {
|
||||
Add(context.Context, *Plan) (*Response, error)
|
||||
Update(context.Context, *Plan) (*Response, error)
|
||||
Remove(context.Context, *DeleteRequest) (*Response, error)
|
||||
// RecordPlanStream 相关接口
|
||||
ListRecordPlanStreams(context.Context, *ReqPlanStreamList) (*RecordPlanStreamResponseList, error)
|
||||
AddRecordPlanStream(context.Context, *PlanStream) (*Response, error)
|
||||
UpdateRecordPlanStream(context.Context, *PlanStream) (*Response, error)
|
||||
RemoveRecordPlanStream(context.Context, *DeletePlanStreamRequest) (*Response, error)
|
||||
// 解析计划字符串,返回时间段信息
|
||||
ParsePlanTime(context.Context, *ParsePlanRequest) (*ParsePlanResponse, error)
|
||||
// 获取当前Crontab任务状态
|
||||
GetCrontabStatus(context.Context, *CrontabStatusRequest) (*CrontabStatusResponse, error)
|
||||
mustEmbedUnimplementedApiServer()
|
||||
}
|
||||
|
||||
@@ -113,6 +197,24 @@ func (UnimplementedApiServer) Update(context.Context, *Plan) (*Response, error)
|
||||
func (UnimplementedApiServer) Remove(context.Context, *DeleteRequest) (*Response, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method Remove not implemented")
|
||||
}
|
||||
func (UnimplementedApiServer) ListRecordPlanStreams(context.Context, *ReqPlanStreamList) (*RecordPlanStreamResponseList, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method ListRecordPlanStreams not implemented")
|
||||
}
|
||||
func (UnimplementedApiServer) AddRecordPlanStream(context.Context, *PlanStream) (*Response, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method AddRecordPlanStream not implemented")
|
||||
}
|
||||
func (UnimplementedApiServer) UpdateRecordPlanStream(context.Context, *PlanStream) (*Response, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method UpdateRecordPlanStream not implemented")
|
||||
}
|
||||
func (UnimplementedApiServer) RemoveRecordPlanStream(context.Context, *DeletePlanStreamRequest) (*Response, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method RemoveRecordPlanStream not implemented")
|
||||
}
|
||||
func (UnimplementedApiServer) ParsePlanTime(context.Context, *ParsePlanRequest) (*ParsePlanResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method ParsePlanTime not implemented")
|
||||
}
|
||||
func (UnimplementedApiServer) GetCrontabStatus(context.Context, *CrontabStatusRequest) (*CrontabStatusResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method GetCrontabStatus not implemented")
|
||||
}
|
||||
func (UnimplementedApiServer) mustEmbedUnimplementedApiServer() {}
|
||||
func (UnimplementedApiServer) testEmbeddedByValue() {}
|
||||
|
||||
@@ -206,6 +308,114 @@ func _Api_Remove_Handler(srv interface{}, ctx context.Context, dec func(interfac
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
func _Api_ListRecordPlanStreams_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(ReqPlanStreamList)
|
||||
if err := dec(in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if interceptor == nil {
|
||||
return srv.(ApiServer).ListRecordPlanStreams(ctx, in)
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: Api_ListRecordPlanStreams_FullMethodName,
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(ApiServer).ListRecordPlanStreams(ctx, req.(*ReqPlanStreamList))
|
||||
}
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
func _Api_AddRecordPlanStream_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(PlanStream)
|
||||
if err := dec(in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if interceptor == nil {
|
||||
return srv.(ApiServer).AddRecordPlanStream(ctx, in)
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: Api_AddRecordPlanStream_FullMethodName,
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(ApiServer).AddRecordPlanStream(ctx, req.(*PlanStream))
|
||||
}
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
func _Api_UpdateRecordPlanStream_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(PlanStream)
|
||||
if err := dec(in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if interceptor == nil {
|
||||
return srv.(ApiServer).UpdateRecordPlanStream(ctx, in)
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: Api_UpdateRecordPlanStream_FullMethodName,
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(ApiServer).UpdateRecordPlanStream(ctx, req.(*PlanStream))
|
||||
}
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
func _Api_RemoveRecordPlanStream_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(DeletePlanStreamRequest)
|
||||
if err := dec(in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if interceptor == nil {
|
||||
return srv.(ApiServer).RemoveRecordPlanStream(ctx, in)
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: Api_RemoveRecordPlanStream_FullMethodName,
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(ApiServer).RemoveRecordPlanStream(ctx, req.(*DeletePlanStreamRequest))
|
||||
}
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
func _Api_ParsePlanTime_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(ParsePlanRequest)
|
||||
if err := dec(in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if interceptor == nil {
|
||||
return srv.(ApiServer).ParsePlanTime(ctx, in)
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: Api_ParsePlanTime_FullMethodName,
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(ApiServer).ParsePlanTime(ctx, req.(*ParsePlanRequest))
|
||||
}
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
func _Api_GetCrontabStatus_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(CrontabStatusRequest)
|
||||
if err := dec(in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if interceptor == nil {
|
||||
return srv.(ApiServer).GetCrontabStatus(ctx, in)
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: Api_GetCrontabStatus_FullMethodName,
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(ApiServer).GetCrontabStatus(ctx, req.(*CrontabStatusRequest))
|
||||
}
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
// Api_ServiceDesc is the grpc.ServiceDesc for Api service.
|
||||
// It's only intended for direct use with grpc.RegisterService,
|
||||
// and not to be introspected or modified (even as a copy)
|
||||
@@ -229,6 +439,30 @@ var Api_ServiceDesc = grpc.ServiceDesc{
|
||||
MethodName: "Remove",
|
||||
Handler: _Api_Remove_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "ListRecordPlanStreams",
|
||||
Handler: _Api_ListRecordPlanStreams_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "AddRecordPlanStream",
|
||||
Handler: _Api_AddRecordPlanStream_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "UpdateRecordPlanStream",
|
||||
Handler: _Api_UpdateRecordPlanStream_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "RemoveRecordPlanStream",
|
||||
Handler: _Api_RemoveRecordPlanStream_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "ParsePlanTime",
|
||||
Handler: _Api_ParsePlanTime_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "GetCrontabStatus",
|
||||
Handler: _Api_GetCrontabStatus_Handler,
|
||||
},
|
||||
},
|
||||
Streams: []grpc.StreamDesc{},
|
||||
Metadata: "crontab.proto",
|
||||
|
@@ -1,13 +0,0 @@
|
||||
package pkg
|
||||
|
||||
import (
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// RecordPlan 录制计划模型
|
||||
type RecordPlan struct {
|
||||
gorm.Model
|
||||
Name string `json:"name" gorm:"default:''"`
|
||||
Plan string `json:"plan" gorm:"type:text"`
|
||||
Enabled bool `json:"enabled" gorm:"default:true"`
|
||||
}
|
17
plugin/crontab/pkg/recordplan.go
Normal file
17
plugin/crontab/pkg/recordplan.go
Normal file
@@ -0,0 +1,17 @@
|
||||
package pkg
|
||||
|
||||
import (
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// RecordPlan 录制计划模型
|
||||
type RecordPlan struct {
|
||||
gorm.Model
|
||||
Name string `json:"name" gorm:"default:''"`
|
||||
Plan string `json:"plan" gorm:"type:text"`
|
||||
Enable bool `json:"enable" gorm:"default:false"` // 是否启用
|
||||
}
|
||||
|
||||
func (r *RecordPlan) GetKey() uint {
|
||||
return r.ID
|
||||
}
|
51
plugin/crontab/pkg/recordplanstream.go
Normal file
51
plugin/crontab/pkg/recordplanstream.go
Normal file
@@ -0,0 +1,51 @@
|
||||
package pkg
|
||||
|
||||
import (
|
||||
"gorm.io/gorm"
|
||||
"time"
|
||||
)
|
||||
|
||||
// RecordPlanStream 录制计划流信息模型
|
||||
type RecordPlanStream struct {
|
||||
PlanID uint `json:"plan_id" gorm:"primaryKey;type:bigint;not null"` // 录制计划ID
|
||||
StreamPath string `json:"stream_path" gorm:"primaryKey;type:varchar(255)"`
|
||||
Fragment string `json:"fragment" gorm:"type:text"`
|
||||
FilePath string `json:"file_path" gorm:"type:varchar(255)"`
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
DeletedAt gorm.DeletedAt `gorm:"index"`
|
||||
Enable bool `json:"enable" gorm:"default:false"` // 是否启用
|
||||
RecordType string `json:"record_type" gorm:"type:varchar(255)"`
|
||||
}
|
||||
|
||||
// TableName 设置表名
|
||||
func (RecordPlanStream) TableName() string {
|
||||
return "record_plans_streams"
|
||||
}
|
||||
|
||||
// ScopeStreamPathLike 模糊查询 StreamPath
|
||||
func ScopeStreamPathLike(streamPath string) func(db *gorm.DB) *gorm.DB {
|
||||
return func(db *gorm.DB) *gorm.DB {
|
||||
if streamPath != "" {
|
||||
return db.Where("record_plans_streams.stream_path LIKE ?", "%"+streamPath+"%")
|
||||
}
|
||||
return db
|
||||
}
|
||||
}
|
||||
|
||||
// ScopeOrderByCreatedAtDesc 按创建时间倒序
|
||||
func ScopeOrderByCreatedAtDesc() func(db *gorm.DB) *gorm.DB {
|
||||
return func(db *gorm.DB) *gorm.DB {
|
||||
return db.Order("record_plans_streams.created_at DESC")
|
||||
}
|
||||
}
|
||||
|
||||
// ScopeRecordPlanID 按录制计划ID查询
|
||||
func ScopeRecordPlanID(recordPlanID uint) func(db *gorm.DB) *gorm.DB {
|
||||
return func(db *gorm.DB) *gorm.DB {
|
||||
if recordPlanID > 0 {
|
||||
return db.Where(&RecordPlanStream{PlanID: recordPlanID})
|
||||
}
|
||||
return db
|
||||
}
|
||||
}
|
@@ -15,6 +15,7 @@ import (
|
||||
"github.com/gorilla/websocket"
|
||||
"github.com/shirou/gopsutil/v4/cpu"
|
||||
"github.com/shirou/gopsutil/v4/process"
|
||||
"m7s.live/v5/pkg/task"
|
||||
)
|
||||
|
||||
//go:embed static/*
|
||||
@@ -40,8 +41,17 @@ type consumer struct {
|
||||
}
|
||||
|
||||
type server struct {
|
||||
task.TickTask
|
||||
consumers []consumer
|
||||
consumersMutex sync.RWMutex
|
||||
data DataStorage
|
||||
lastPause uint32
|
||||
dataMutex sync.RWMutex
|
||||
lastConsumerID uint
|
||||
upgrader websocket.Upgrader
|
||||
prevSysTime float64
|
||||
prevUserTime float64
|
||||
myProcess *process.Process
|
||||
}
|
||||
|
||||
type SimplePair struct {
|
||||
@@ -75,99 +85,91 @@ const (
|
||||
maxCount int = 86400
|
||||
)
|
||||
|
||||
var (
|
||||
data DataStorage
|
||||
lastPause uint32
|
||||
mutex sync.RWMutex
|
||||
lastConsumerID uint
|
||||
s server
|
||||
upgrader = websocket.Upgrader{
|
||||
func (s *server) Start() error {
|
||||
var err error
|
||||
s.myProcess, err = process.NewProcess(int32(os.Getpid()))
|
||||
if err != nil {
|
||||
log.Printf("Failed to get process: %v", err)
|
||||
}
|
||||
// 初始化 WebSocket upgrader
|
||||
s.upgrader = websocket.Upgrader{
|
||||
ReadBufferSize: 1024,
|
||||
WriteBufferSize: 1024,
|
||||
}
|
||||
prevSysTime float64
|
||||
prevUserTime float64
|
||||
myProcess *process.Process
|
||||
)
|
||||
|
||||
func init() {
|
||||
|
||||
myProcess, _ = process.NewProcess(int32(os.Getpid()))
|
||||
|
||||
// preallocate arrays in data, helps save on reallocations caused by append()
|
||||
// when maxCount is large
|
||||
data.BytesAllocated = make([]SimplePair, 0, maxCount)
|
||||
data.GcPauses = make([]SimplePair, 0, maxCount)
|
||||
data.CPUUsage = make([]CPUPair, 0, maxCount)
|
||||
data.Pprof = make([]PprofPair, 0, maxCount)
|
||||
|
||||
go s.gatherData()
|
||||
s.data.BytesAllocated = make([]SimplePair, 0, maxCount)
|
||||
s.data.GcPauses = make([]SimplePair, 0, maxCount)
|
||||
s.data.CPUUsage = make([]CPUPair, 0, maxCount)
|
||||
s.data.Pprof = make([]PprofPair, 0, maxCount)
|
||||
return s.TickTask.Start()
|
||||
}
|
||||
|
||||
func (s *server) gatherData() {
|
||||
timer := time.Tick(time.Second)
|
||||
func (s *server) GetTickInterval() time.Duration {
|
||||
return time.Second
|
||||
}
|
||||
|
||||
for now := range timer {
|
||||
nowUnix := now.Unix()
|
||||
func (s *server) Tick(any) {
|
||||
now := time.Now()
|
||||
nowUnix := now.Unix()
|
||||
|
||||
var ms runtime.MemStats
|
||||
runtime.ReadMemStats(&ms)
|
||||
var ms runtime.MemStats
|
||||
runtime.ReadMemStats(&ms)
|
||||
|
||||
u := update{
|
||||
Ts: nowUnix * 1000,
|
||||
Block: pprof.Lookup("block").Count(),
|
||||
Goroutine: pprof.Lookup("goroutine").Count(),
|
||||
Heap: pprof.Lookup("heap").Count(),
|
||||
Mutex: pprof.Lookup("mutex").Count(),
|
||||
Threadcreate: pprof.Lookup("threadcreate").Count(),
|
||||
}
|
||||
data.Pprof = append(data.Pprof, PprofPair{
|
||||
uint64(nowUnix) * 1000,
|
||||
u.Block,
|
||||
u.Goroutine,
|
||||
u.Heap,
|
||||
u.Mutex,
|
||||
u.Threadcreate,
|
||||
})
|
||||
|
||||
cpuTimes, err := myProcess.Times()
|
||||
if err != nil {
|
||||
cpuTimes = &cpu.TimesStat{}
|
||||
}
|
||||
|
||||
if prevUserTime != 0 {
|
||||
u.CPUUser = cpuTimes.User - prevUserTime
|
||||
u.CPUSys = cpuTimes.System - prevSysTime
|
||||
data.CPUUsage = append(data.CPUUsage, CPUPair{uint64(nowUnix) * 1000, u.CPUUser, u.CPUSys})
|
||||
}
|
||||
|
||||
prevUserTime = cpuTimes.User
|
||||
prevSysTime = cpuTimes.System
|
||||
|
||||
mutex.Lock()
|
||||
|
||||
bytesAllocated := ms.Alloc
|
||||
u.BytesAllocated = bytesAllocated
|
||||
data.BytesAllocated = append(data.BytesAllocated, SimplePair{uint64(nowUnix) * 1000, bytesAllocated})
|
||||
if lastPause == 0 || lastPause != ms.NumGC {
|
||||
gcPause := ms.PauseNs[(ms.NumGC+255)%256]
|
||||
u.GcPause = gcPause
|
||||
data.GcPauses = append(data.GcPauses, SimplePair{uint64(nowUnix) * 1000, gcPause})
|
||||
lastPause = ms.NumGC
|
||||
}
|
||||
|
||||
if len(data.BytesAllocated) > maxCount {
|
||||
data.BytesAllocated = data.BytesAllocated[len(data.BytesAllocated)-maxCount:]
|
||||
}
|
||||
|
||||
if len(data.GcPauses) > maxCount {
|
||||
data.GcPauses = data.GcPauses[len(data.GcPauses)-maxCount:]
|
||||
}
|
||||
|
||||
mutex.Unlock()
|
||||
|
||||
s.sendToConsumers(u)
|
||||
u := update{
|
||||
Ts: nowUnix * 1000,
|
||||
Block: pprof.Lookup("block").Count(),
|
||||
Goroutine: pprof.Lookup("goroutine").Count(),
|
||||
Heap: pprof.Lookup("heap").Count(),
|
||||
Mutex: pprof.Lookup("mutex").Count(),
|
||||
Threadcreate: pprof.Lookup("threadcreate").Count(),
|
||||
}
|
||||
s.data.Pprof = append(s.data.Pprof, PprofPair{
|
||||
uint64(nowUnix) * 1000,
|
||||
u.Block,
|
||||
u.Goroutine,
|
||||
u.Heap,
|
||||
u.Mutex,
|
||||
u.Threadcreate,
|
||||
})
|
||||
|
||||
cpuTimes, err := s.myProcess.Times()
|
||||
if err != nil {
|
||||
cpuTimes = &cpu.TimesStat{}
|
||||
}
|
||||
|
||||
if s.prevUserTime != 0 {
|
||||
u.CPUUser = cpuTimes.User - s.prevUserTime
|
||||
u.CPUSys = cpuTimes.System - s.prevSysTime
|
||||
s.data.CPUUsage = append(s.data.CPUUsage, CPUPair{uint64(nowUnix) * 1000, u.CPUUser, u.CPUSys})
|
||||
}
|
||||
|
||||
s.prevUserTime = cpuTimes.User
|
||||
s.prevSysTime = cpuTimes.System
|
||||
|
||||
s.dataMutex.Lock()
|
||||
|
||||
bytesAllocated := ms.Alloc
|
||||
u.BytesAllocated = bytesAllocated
|
||||
s.data.BytesAllocated = append(s.data.BytesAllocated, SimplePair{uint64(nowUnix) * 1000, bytesAllocated})
|
||||
if s.lastPause == 0 || s.lastPause != ms.NumGC {
|
||||
gcPause := ms.PauseNs[(ms.NumGC+255)%256]
|
||||
u.GcPause = gcPause
|
||||
s.data.GcPauses = append(s.data.GcPauses, SimplePair{uint64(nowUnix) * 1000, gcPause})
|
||||
s.lastPause = ms.NumGC
|
||||
}
|
||||
|
||||
if len(s.data.BytesAllocated) > maxCount {
|
||||
s.data.BytesAllocated = s.data.BytesAllocated[len(s.data.BytesAllocated)-maxCount:]
|
||||
}
|
||||
|
||||
if len(s.data.GcPauses) > maxCount {
|
||||
s.data.GcPauses = s.data.GcPauses[len(s.data.GcPauses)-maxCount:]
|
||||
}
|
||||
|
||||
s.dataMutex.Unlock()
|
||||
|
||||
s.sendToConsumers(u)
|
||||
}
|
||||
|
||||
func (s *server) sendToConsumers(u update) {
|
||||
@@ -203,10 +205,10 @@ func (s *server) addConsumer() consumer {
|
||||
s.consumersMutex.Lock()
|
||||
defer s.consumersMutex.Unlock()
|
||||
|
||||
lastConsumerID++
|
||||
s.lastConsumerID++
|
||||
|
||||
c := consumer{
|
||||
id: lastConsumerID,
|
||||
id: s.lastConsumerID,
|
||||
c: make(chan update),
|
||||
}
|
||||
|
||||
@@ -221,7 +223,7 @@ func (s *server) dataFeedHandler(w http.ResponseWriter, r *http.Request) {
|
||||
lastPong time.Time
|
||||
)
|
||||
|
||||
conn, err := upgrader.Upgrade(w, r, nil)
|
||||
conn, err := s.upgrader.Upgrade(w, r, nil)
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
return
|
||||
@@ -268,9 +270,9 @@ func (s *server) dataFeedHandler(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
}
|
||||
|
||||
func dataHandler(w http.ResponseWriter, r *http.Request) {
|
||||
mutex.RLock()
|
||||
defer mutex.RUnlock()
|
||||
func (s *server) dataHandler(w http.ResponseWriter, r *http.Request) {
|
||||
s.dataMutex.RLock()
|
||||
defer s.dataMutex.RUnlock()
|
||||
|
||||
if e := r.ParseForm(); e != nil {
|
||||
log.Print("error parsing form")
|
||||
@@ -284,7 +286,7 @@ func dataHandler(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
|
||||
encoder := json.NewEncoder(w)
|
||||
encoder.Encode(data)
|
||||
encoder.Encode(s.data)
|
||||
|
||||
fmt.Fprint(w, ")")
|
||||
}
|
||||
|
219
plugin/debug/envcheck.go
Normal file
219
plugin/debug/envcheck.go
Normal file
@@ -0,0 +1,219 @@
|
||||
package plugin_debug
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"time"
|
||||
|
||||
"google.golang.org/protobuf/types/known/timestamppb"
|
||||
"gopkg.in/yaml.v3"
|
||||
"m7s.live/v5/pb"
|
||||
"m7s.live/v5/pkg/util"
|
||||
)
|
||||
|
||||
type EnvCheckResult struct {
|
||||
Message string `json:"message"`
|
||||
Type string `json:"type"` // info, success, error, complete
|
||||
}
|
||||
|
||||
// 自定义系统信息响应结构体,用于 JSON 解析
|
||||
type SysInfoResponseJSON struct {
|
||||
Code int32 `json:"code"`
|
||||
Message string `json:"message"`
|
||||
Data struct {
|
||||
StartTime string `json:"startTime"`
|
||||
LocalIP string `json:"localIP"`
|
||||
PublicIP string `json:"publicIP"`
|
||||
Version string `json:"version"`
|
||||
GoVersion string `json:"goVersion"`
|
||||
OS string `json:"os"`
|
||||
Arch string `json:"arch"`
|
||||
CPUs int32 `json:"cpus"`
|
||||
Plugins []struct {
|
||||
Name string `json:"name"`
|
||||
PushAddr []string `json:"pushAddr"`
|
||||
PlayAddr []string `json:"playAddr"`
|
||||
Description map[string]string `json:"description"`
|
||||
} `json:"plugins"`
|
||||
} `json:"data"`
|
||||
}
|
||||
|
||||
// 插件配置响应结构体
|
||||
type PluginConfigResponse struct {
|
||||
Code int32 `json:"code"`
|
||||
Message string `json:"message"`
|
||||
Data struct {
|
||||
File string `json:"file"`
|
||||
Modified string `json:"modified"`
|
||||
Merged string `json:"merged"`
|
||||
} `json:"data"`
|
||||
}
|
||||
|
||||
// TCP 配置结构体
|
||||
type TCPConfig struct {
|
||||
ListenAddr string `yaml:"listenaddr"`
|
||||
ListenAddrTLS string `yaml:"listenaddrtls"`
|
||||
}
|
||||
|
||||
// 插件配置结构体
|
||||
type PluginConfig struct {
|
||||
TCP TCPConfig `yaml:"tcp"`
|
||||
}
|
||||
|
||||
func (p *DebugPlugin) EnvCheck(w http.ResponseWriter, r *http.Request) {
|
||||
// Get target URL from query parameter
|
||||
targetURL := r.URL.Query().Get("target")
|
||||
if targetURL == "" {
|
||||
r.URL.Path = "/static/envcheck.html"
|
||||
staticFSHandler.ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
// Create SSE connection
|
||||
util.NewSSE(w, r.Context(), func(sse *util.SSE) {
|
||||
// Function to send SSE messages
|
||||
sendMessage := func(message string, msgType string) {
|
||||
result := EnvCheckResult{
|
||||
Message: message,
|
||||
Type: msgType,
|
||||
}
|
||||
sse.WriteJSON(result)
|
||||
}
|
||||
|
||||
// Parse target URL
|
||||
_, err := url.Parse(targetURL)
|
||||
if err != nil {
|
||||
sendMessage(fmt.Sprintf("Invalid URL: %v", err), "error")
|
||||
return
|
||||
}
|
||||
|
||||
// Check if we can connect to the target server
|
||||
sendMessage(fmt.Sprintf("Checking connection to %s...", targetURL), "info")
|
||||
|
||||
// Get system info from target server
|
||||
resp, err := http.Get(fmt.Sprintf("%s/api/sysinfo", targetURL))
|
||||
if err != nil {
|
||||
sendMessage(fmt.Sprintf("Failed to connect to target server: %v", err), "error")
|
||||
return
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
sendMessage(fmt.Sprintf("Target server returned status code: %d", resp.StatusCode), "error")
|
||||
return
|
||||
}
|
||||
|
||||
// Read and parse system info
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
sendMessage(fmt.Sprintf("Failed to read response: %v", err), "error")
|
||||
return
|
||||
}
|
||||
|
||||
var sysInfoJSON SysInfoResponseJSON
|
||||
if err := json.Unmarshal(body, &sysInfoJSON); err != nil {
|
||||
sendMessage(fmt.Sprintf("Failed to parse system info: %v", err), "error")
|
||||
return
|
||||
}
|
||||
|
||||
// Convert JSON response to protobuf response
|
||||
sysInfo := &pb.SysInfoResponse{
|
||||
Code: sysInfoJSON.Code,
|
||||
Message: sysInfoJSON.Message,
|
||||
Data: &pb.SysInfoData{
|
||||
LocalIP: sysInfoJSON.Data.LocalIP,
|
||||
PublicIP: sysInfoJSON.Data.PublicIP,
|
||||
Version: sysInfoJSON.Data.Version,
|
||||
GoVersion: sysInfoJSON.Data.GoVersion,
|
||||
Os: sysInfoJSON.Data.OS,
|
||||
Arch: sysInfoJSON.Data.Arch,
|
||||
Cpus: sysInfoJSON.Data.CPUs,
|
||||
},
|
||||
}
|
||||
|
||||
// Parse start time
|
||||
if startTime, err := time.Parse(time.RFC3339, sysInfoJSON.Data.StartTime); err == nil {
|
||||
sysInfo.Data.StartTime = timestamppb.New(startTime)
|
||||
}
|
||||
|
||||
// Convert plugins
|
||||
for _, pluginJSON := range sysInfoJSON.Data.Plugins {
|
||||
plugin := &pb.PluginInfo{
|
||||
Name: pluginJSON.Name,
|
||||
PushAddr: pluginJSON.PushAddr,
|
||||
PlayAddr: pluginJSON.PlayAddr,
|
||||
Description: pluginJSON.Description,
|
||||
}
|
||||
sysInfo.Data.Plugins = append(sysInfo.Data.Plugins, plugin)
|
||||
}
|
||||
|
||||
// Check each plugin's configuration
|
||||
for _, plugin := range sysInfo.Data.Plugins {
|
||||
// Get plugin configuration
|
||||
configResp, err := http.Get(fmt.Sprintf("%s/api/config/get/%s", targetURL, plugin.Name))
|
||||
if err != nil {
|
||||
sendMessage(fmt.Sprintf("Failed to get configuration for plugin %s: %v", plugin.Name, err), "error")
|
||||
continue
|
||||
}
|
||||
defer configResp.Body.Close()
|
||||
|
||||
if configResp.StatusCode != http.StatusOK {
|
||||
sendMessage(fmt.Sprintf("Failed to get configuration for plugin %s: status code %d", plugin.Name, configResp.StatusCode), "error")
|
||||
continue
|
||||
}
|
||||
|
||||
var configRespJSON PluginConfigResponse
|
||||
if err := json.NewDecoder(configResp.Body).Decode(&configRespJSON); err != nil {
|
||||
sendMessage(fmt.Sprintf("Failed to parse configuration for plugin %s: %v", plugin.Name, err), "error")
|
||||
continue
|
||||
}
|
||||
|
||||
// Parse YAML configuration
|
||||
var config PluginConfig
|
||||
if err := yaml.Unmarshal([]byte(configRespJSON.Data.Merged), &config); err != nil {
|
||||
sendMessage(fmt.Sprintf("Failed to parse YAML configuration for plugin %s: %v", plugin.Name, err), "error")
|
||||
continue
|
||||
}
|
||||
// Check TCP configuration
|
||||
if config.TCP.ListenAddr != "" {
|
||||
host, port, err := net.SplitHostPort(config.TCP.ListenAddr)
|
||||
if err != nil {
|
||||
sendMessage(fmt.Sprintf("Invalid listenaddr format for plugin %s: %v", plugin.Name, err), "error")
|
||||
} else {
|
||||
sendMessage(fmt.Sprintf("Checking TCP listenaddr %s for plugin %s...", config.TCP.ListenAddr, plugin.Name), "info")
|
||||
// Try to establish TCP connection
|
||||
conn, err := net.DialTimeout("tcp", fmt.Sprintf("%s:%s", host, port), 5*time.Second)
|
||||
if err != nil {
|
||||
sendMessage(fmt.Sprintf("TCP listenaddr %s for plugin %s is not accessible: %v", config.TCP.ListenAddr, plugin.Name, err), "error")
|
||||
} else {
|
||||
conn.Close()
|
||||
sendMessage(fmt.Sprintf("TCP listenaddr %s for plugin %s is accessible", config.TCP.ListenAddr, plugin.Name), "success")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if config.TCP.ListenAddrTLS != "" {
|
||||
host, port, err := net.SplitHostPort(config.TCP.ListenAddrTLS)
|
||||
if err != nil {
|
||||
sendMessage(fmt.Sprintf("Invalid listenaddrtls format for plugin %s: %v", plugin.Name, err), "error")
|
||||
} else {
|
||||
sendMessage(fmt.Sprintf("Checking TCP TLS listenaddr %s for plugin %s...", config.TCP.ListenAddrTLS, plugin.Name), "info")
|
||||
// Try to establish TCP connection
|
||||
conn, err := net.DialTimeout("tcp", fmt.Sprintf("%s:%s", host, port), 5*time.Second)
|
||||
if err != nil {
|
||||
sendMessage(fmt.Sprintf("TCP TLS listenaddr %s for plugin %s is not accessible: %v", config.TCP.ListenAddrTLS, plugin.Name, err), "error")
|
||||
} else {
|
||||
conn.Close()
|
||||
sendMessage(fmt.Sprintf("TCP TLS listenaddr %s for plugin %s is accessible", config.TCP.ListenAddrTLS, plugin.Name), "success")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sendMessage("Environment check completed", "complete")
|
||||
})
|
||||
}
|
@@ -34,13 +34,13 @@ type DebugPlugin struct {
|
||||
m7s.Plugin
|
||||
ProfileDuration time.Duration `default:"10s" desc:"profile持续时间"`
|
||||
Profile string `desc:"采集profile存储文件"`
|
||||
ChartPeriod time.Duration `default:"1s" desc:"图表更新周期"`
|
||||
Grfout string `default:"grf.out" desc:"grf输出文件"`
|
||||
|
||||
EnableChart bool `default:"true" desc:"是否启用图表功能"`
|
||||
// 添加缓存字段
|
||||
cpuProfileData *profile.Profile // 缓存 CPU Profile 数据
|
||||
cpuProfileOnce sync.Once // 确保只采集一次
|
||||
cpuProfileLock sync.Mutex // 保护缓存数据
|
||||
chartServer server
|
||||
}
|
||||
|
||||
type WriteToFile struct {
|
||||
@@ -72,6 +72,10 @@ func (p *DebugPlugin) OnInit() error {
|
||||
p.Info("cpu profile done")
|
||||
}()
|
||||
}
|
||||
if p.EnableChart {
|
||||
p.AddTask(&p.chartServer)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -100,11 +104,11 @@ func (p *DebugPlugin) Charts_(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
func (p *DebugPlugin) Charts_data(w http.ResponseWriter, r *http.Request) {
|
||||
dataHandler(w, r)
|
||||
p.chartServer.dataHandler(w, r)
|
||||
}
|
||||
|
||||
func (p *DebugPlugin) Charts_datafeed(w http.ResponseWriter, r *http.Request) {
|
||||
s.dataFeedHandler(w, r)
|
||||
p.chartServer.dataFeedHandler(w, r)
|
||||
}
|
||||
|
||||
func (p *DebugPlugin) Grf(w http.ResponseWriter, r *http.Request) {
|
||||
|
122
plugin/debug/static/envcheck.html
Normal file
122
plugin/debug/static/envcheck.html
Normal file
@@ -0,0 +1,122 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Environment Check</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: Arial, sans-serif;
|
||||
margin: 20px;
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
background-color: white;
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.input-group {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
input[type="text"] {
|
||||
padding: 8px;
|
||||
width: 300px;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
button {
|
||||
padding: 8px 16px;
|
||||
background-color: #4CAF50;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
button:hover {
|
||||
background-color: #45a049;
|
||||
}
|
||||
|
||||
#log {
|
||||
background-color: #f8f9fa;
|
||||
border: 1px solid #ddd;
|
||||
padding: 10px;
|
||||
height: 400px;
|
||||
overflow-y: auto;
|
||||
font-family: monospace;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
.success {
|
||||
color: #28a745;
|
||||
}
|
||||
|
||||
.error {
|
||||
color: #dc3545;
|
||||
}
|
||||
|
||||
.info {
|
||||
color: #17a2b8;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>Environment Check</h1>
|
||||
<div class="input-group">
|
||||
<input type="text" id="targetUrl" placeholder="Enter target URL (e.g., http://192.168.1.100:8080)">
|
||||
<button onclick="startCheck()">Start Check</button>
|
||||
</div>
|
||||
<div id="log"></div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function appendLog(message, type = 'info') {
|
||||
const log = document.getElementById('log');
|
||||
const entry = document.createElement('div');
|
||||
entry.className = type;
|
||||
entry.textContent = message;
|
||||
log.appendChild(entry);
|
||||
log.scrollTop = log.scrollHeight;
|
||||
}
|
||||
|
||||
function startCheck() {
|
||||
const targetUrl = document.getElementById('targetUrl').value;
|
||||
if (!targetUrl) {
|
||||
appendLog('Please enter a target URL', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
// Clear previous log
|
||||
document.getElementById('log').innerHTML = '';
|
||||
appendLog('Starting environment check...');
|
||||
|
||||
// Create SSE connection
|
||||
const eventSource = new EventSource(`/debug/envcheck?target=${encodeURIComponent(targetUrl)}`);
|
||||
|
||||
eventSource.onmessage = function (event) {
|
||||
const data = JSON.parse(event.data);
|
||||
appendLog(data.message, data.type);
|
||||
|
||||
if (data.type === 'complete') {
|
||||
eventSource.close();
|
||||
}
|
||||
};
|
||||
|
||||
eventSource.onerror = function (error) {
|
||||
appendLog('Connection error occurred', 'error');
|
||||
eventSource.close();
|
||||
};
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
@@ -8,7 +8,6 @@ import (
|
||||
"slices"
|
||||
"time"
|
||||
|
||||
"gorm.io/gorm"
|
||||
"m7s.live/v5"
|
||||
"m7s.live/v5/pkg"
|
||||
"m7s.live/v5/pkg/config"
|
||||
@@ -144,7 +143,6 @@ func NewRecorder(conf config.Record) m7s.IRecorder {
|
||||
|
||||
type Recorder struct {
|
||||
m7s.DefaultRecorder
|
||||
stream m7s.RecordStream
|
||||
}
|
||||
|
||||
var CustomFileName = func(job *m7s.RecordJob) string {
|
||||
@@ -155,48 +153,21 @@ var CustomFileName = func(job *m7s.RecordJob) string {
|
||||
}
|
||||
|
||||
func (r *Recorder) createStream(start time.Time) (err error) {
|
||||
recordJob := &r.RecordJob
|
||||
sub := recordJob.Subscriber
|
||||
r.stream = m7s.RecordStream{
|
||||
StartTime: start,
|
||||
StreamPath: sub.StreamPath,
|
||||
FilePath: CustomFileName(&r.RecordJob),
|
||||
EventId: recordJob.EventId,
|
||||
EventDesc: recordJob.EventDesc,
|
||||
EventName: recordJob.EventName,
|
||||
EventLevel: recordJob.EventLevel,
|
||||
BeforeDuration: recordJob.BeforeDuration,
|
||||
AfterDuration: recordJob.AfterDuration,
|
||||
Mode: recordJob.Mode,
|
||||
Type: "flv",
|
||||
}
|
||||
dir := filepath.Dir(r.stream.FilePath)
|
||||
if err = os.MkdirAll(dir, 0755); err != nil {
|
||||
return
|
||||
}
|
||||
if sub.Publisher.HasAudioTrack() {
|
||||
r.stream.AudioCodec = sub.Publisher.AudioTrack.ICodecCtx.String()
|
||||
}
|
||||
if sub.Publisher.HasVideoTrack() {
|
||||
r.stream.VideoCodec = sub.Publisher.VideoTrack.ICodecCtx.String()
|
||||
}
|
||||
if recordJob.Plugin.DB != nil {
|
||||
recordJob.Plugin.DB.Save(&r.stream)
|
||||
}
|
||||
return
|
||||
return r.CreateStream(start, CustomFileName)
|
||||
}
|
||||
|
||||
func (r *Recorder) writeTailer(end time.Time) {
|
||||
if r.stream.EndTime.After(r.stream.StartTime) {
|
||||
if r.Event.EndTime.After(r.Event.StartTime) {
|
||||
return
|
||||
}
|
||||
r.stream.EndTime = end
|
||||
r.Event.EndTime = end
|
||||
if r.RecordJob.Plugin.DB != nil {
|
||||
r.RecordJob.Plugin.DB.Save(&r.stream)
|
||||
writeMetaTagQueueTask.AddTask(&eventRecordCheck{
|
||||
DB: r.RecordJob.Plugin.DB,
|
||||
streamPath: r.stream.StreamPath,
|
||||
})
|
||||
if r.RecordJob.Event != nil {
|
||||
r.RecordJob.Plugin.DB.Save(&r.Event)
|
||||
} else {
|
||||
r.RecordJob.Plugin.DB.Save(&r.Event.RecordStream)
|
||||
}
|
||||
writeMetaTagQueueTask.AddTask(m7s.NewEventRecordCheck(r.Event.Type, r.Event.StreamPath, r.RecordJob.Plugin.DB))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -204,40 +175,6 @@ func (r *Recorder) Dispose() {
|
||||
r.writeTailer(time.Now())
|
||||
}
|
||||
|
||||
type eventRecordCheck struct {
|
||||
task.Task
|
||||
DB *gorm.DB
|
||||
streamPath string
|
||||
}
|
||||
|
||||
func (t *eventRecordCheck) Run() (err error) {
|
||||
var eventRecordStreams []m7s.RecordStream
|
||||
queryRecord := m7s.RecordStream{
|
||||
EventLevel: m7s.EventLevelHigh,
|
||||
Mode: m7s.RecordModeEvent,
|
||||
Type: "flv",
|
||||
}
|
||||
t.DB.Where(&queryRecord).Find(&eventRecordStreams, "stream_path=?", t.streamPath) //搜索事件录像,且为重要事件(无法自动删除)
|
||||
if len(eventRecordStreams) > 0 {
|
||||
for _, recordStream := range eventRecordStreams {
|
||||
var unimportantEventRecordStreams []m7s.RecordStream
|
||||
queryRecord.EventLevel = m7s.EventLevelLow
|
||||
query := `(start_time BETWEEN ? AND ?)
|
||||
OR (end_time BETWEEN ? AND ?)
|
||||
OR (? BETWEEN start_time AND end_time)
|
||||
OR (? BETWEEN start_time AND end_time) AND stream_path=? `
|
||||
t.DB.Where(&queryRecord).Where(query, recordStream.StartTime, recordStream.EndTime, recordStream.StartTime, recordStream.EndTime, recordStream.StartTime, recordStream.EndTime, recordStream.StreamPath).Find(&unimportantEventRecordStreams)
|
||||
if len(unimportantEventRecordStreams) > 0 {
|
||||
for _, unimportantEventRecordStream := range unimportantEventRecordStreams {
|
||||
unimportantEventRecordStream.EventLevel = m7s.EventLevelHigh
|
||||
t.DB.Save(&unimportantEventRecordStream)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (r *Recorder) Run() (err error) {
|
||||
var file *os.File
|
||||
var filepositions []uint64
|
||||
@@ -248,14 +185,14 @@ func (r *Recorder) Run() (err error) {
|
||||
suber := ctx.Subscriber
|
||||
noFragment := ctx.RecConf.Fragment == 0 || ctx.RecConf.Append
|
||||
startTime := time.Now()
|
||||
if ctx.BeforeDuration > 0 {
|
||||
startTime = startTime.Add(-ctx.BeforeDuration)
|
||||
if ctx.Event.BeforeDuration > 0 {
|
||||
startTime = startTime.Add(-time.Duration(ctx.Event.BeforeDuration) * time.Millisecond)
|
||||
}
|
||||
if err = r.createStream(startTime); err != nil {
|
||||
return
|
||||
}
|
||||
if noFragment {
|
||||
file, err = os.OpenFile(r.stream.FilePath, os.O_CREATE|os.O_RDWR|util.Conditional(ctx.RecConf.Append, os.O_APPEND, os.O_TRUNC), 0666)
|
||||
file, err = os.OpenFile(r.Event.FilePath, os.O_CREATE|os.O_RDWR|util.Conditional(ctx.RecConf.Append, os.O_APPEND, os.O_TRUNC), 0666)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
@@ -291,7 +228,7 @@ func (r *Recorder) Run() (err error) {
|
||||
} else if ctx.RecConf.Fragment == 0 {
|
||||
_, err = file.Write(FLVHead)
|
||||
} else {
|
||||
if file, err = os.OpenFile(r.stream.FilePath, os.O_CREATE|os.O_RDWR, 0666); err != nil {
|
||||
if file, err = os.OpenFile(r.Event.FilePath, os.O_CREATE|os.O_RDWR, 0666); err != nil {
|
||||
return
|
||||
}
|
||||
_, err = file.Write(FLVHead)
|
||||
@@ -307,7 +244,7 @@ func (r *Recorder) Run() (err error) {
|
||||
if err = r.createStream(time.Now()); err != nil {
|
||||
return
|
||||
}
|
||||
if file, err = os.OpenFile(r.stream.FilePath, os.O_CREATE|os.O_RDWR, 0666); err != nil {
|
||||
if file, err = os.OpenFile(r.Event.FilePath, os.O_CREATE|os.O_RDWR, 0666); err != nil {
|
||||
return
|
||||
}
|
||||
_, err = file.Write(FLVHead)
|
||||
|
@@ -11,6 +11,8 @@ import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"gorm.io/gorm"
|
||||
|
||||
"github.com/emiago/sipgo"
|
||||
"github.com/emiago/sipgo/sip"
|
||||
"m7s.live/v5/pkg/util"
|
||||
@@ -612,35 +614,47 @@ func (gb *GB28181Plugin) UpdateDevice(ctx context.Context, req *pb.Device) (*pb.
|
||||
|
||||
// 如果需要订阅目录,创建并启动目录订阅任务
|
||||
if d.Online {
|
||||
if d.CatalogSubscribeTask != nil {
|
||||
if d.SubscribeCatalog > 0 {
|
||||
if d.SubscribeCatalog > 0 {
|
||||
if d.CatalogSubscribeTask != nil {
|
||||
d.CatalogSubscribeTask.Ticker.Reset(time.Second * time.Duration(d.SubscribeCatalog))
|
||||
d.CatalogSubscribeTask.Tick(nil)
|
||||
} else {
|
||||
catalogSubTask := NewCatalogSubscribeTask(d)
|
||||
d.AddTask(catalogSubTask)
|
||||
d.CatalogSubscribeTask.Tick(nil)
|
||||
}
|
||||
d.CatalogSubscribeTask.Tick(nil)
|
||||
} else {
|
||||
catalogSubTask := NewCatalogSubscribeTask(d)
|
||||
d.AddTask(catalogSubTask)
|
||||
d.CatalogSubscribeTask.Tick(nil)
|
||||
if d.CatalogSubscribeTask != nil {
|
||||
d.CatalogSubscribeTask.Stop(fmt.Errorf("catalog subscription disabled"))
|
||||
}
|
||||
}
|
||||
if d.PositionSubscribeTask != nil {
|
||||
if d.SubscribePosition > 0 {
|
||||
if d.SubscribePosition > 0 {
|
||||
if d.PositionSubscribeTask != nil {
|
||||
d.PositionSubscribeTask.Ticker.Reset(time.Second * time.Duration(d.SubscribePosition))
|
||||
d.PositionSubscribeTask.Tick(nil)
|
||||
} else {
|
||||
positionSubTask := NewPositionSubscribeTask(d)
|
||||
d.AddTask(positionSubTask)
|
||||
d.PositionSubscribeTask.Tick(nil)
|
||||
}
|
||||
d.PositionSubscribeTask.Tick(nil)
|
||||
} else {
|
||||
positionSubTask := NewPositionSubscribeTask(d)
|
||||
d.AddTask(positionSubTask)
|
||||
d.PositionSubscribeTask.Tick(nil)
|
||||
if d.PositionSubscribeTask != nil {
|
||||
d.PositionSubscribeTask.Stop(fmt.Errorf("position subscription disabled"))
|
||||
}
|
||||
}
|
||||
if d.AlarmSubscribeTask != nil {
|
||||
if d.SubscribeAlarm > 0 {
|
||||
if d.SubscribeAlarm > 0 {
|
||||
if d.AlarmSubscribeTask != nil {
|
||||
d.AlarmSubscribeTask.Ticker.Reset(time.Second * time.Duration(d.SubscribeAlarm))
|
||||
d.AlarmSubscribeTask.Tick(nil)
|
||||
} else {
|
||||
alarmSubTask := NewAlarmSubscribeTask(d)
|
||||
d.AddTask(alarmSubTask)
|
||||
d.AlarmSubscribeTask.Tick(nil)
|
||||
}
|
||||
d.AlarmSubscribeTask.Tick(nil)
|
||||
} else {
|
||||
alarmSubTask := NewAlarmSubscribeTask(d)
|
||||
d.AddTask(alarmSubTask)
|
||||
d.AlarmSubscribeTask.Tick(nil)
|
||||
if d.AlarmSubscribeTask != nil {
|
||||
d.AlarmSubscribeTask.Stop(fmt.Errorf("alarm subscription disabled"))
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@@ -1272,32 +1286,36 @@ func (gb *GB28181Plugin) TestSip(ctx context.Context, req *pb.TestSipRequest) (*
|
||||
// 创建一个临时设备用于测试
|
||||
device := &Device{
|
||||
DeviceId: "34020000002000000001",
|
||||
SipIp: "192.168.1.17",
|
||||
SipIp: "192.168.1.106",
|
||||
Port: 5060,
|
||||
IP: "192.168.1.102",
|
||||
StreamMode: "TCP-PASSIVE",
|
||||
}
|
||||
|
||||
//From: <sip:41010500002000000001@4101050000>;tag=4183af2ecc934758ad393dfe588f2dfd
|
||||
// 初始化设备的SIP相关字段
|
||||
device.fromHDR = sip.FromHeader{
|
||||
Address: sip.Uri{
|
||||
User: gb.Serial,
|
||||
Host: gb.Realm,
|
||||
User: "41010500002000000001",
|
||||
Host: "4101050000",
|
||||
},
|
||||
Params: sip.NewParams(),
|
||||
}
|
||||
device.fromHDR.Params.Add("tag", sip.GenerateTagN(16))
|
||||
|
||||
device.fromHDR.Params.Add("tag", "4183af2ecc934758ad393dfe588f2dfd")
|
||||
//Contact: <sip:41010500002000000001@192.168.1.106:5060>
|
||||
device.contactHDR = sip.ContactHeader{
|
||||
Address: sip.Uri{
|
||||
User: gb.Serial,
|
||||
Host: device.SipIp,
|
||||
Port: device.Port,
|
||||
User: "41010500002000000001",
|
||||
Host: "192.168.1.106",
|
||||
Port: 5060,
|
||||
},
|
||||
}
|
||||
|
||||
//Request-Line: INVITE sip:34020000001320000006@192.168.1.102:5060 SIP/2.0
|
||||
// Method: INVITE
|
||||
// Request-URI: sip:34020000001320000006@192.168.1.102:5060
|
||||
// [Resent Packet: False]
|
||||
// 初始化SIP客户端
|
||||
device.client, _ = sipgo.NewClient(gb.ua, sipgo.WithClientLogger(zerolog.New(os.Stdout)), sipgo.WithClientHostname(device.SipIp))
|
||||
device.client, _ = sipgo.NewClient(gb.ua, sipgo.WithClientLogger(zerolog.New(os.Stdout)), sipgo.WithClientHostname("192.168.1.106"))
|
||||
if device.client == nil {
|
||||
resp.Code = 500
|
||||
resp.Message = "failed to create sip client"
|
||||
@@ -1322,11 +1340,11 @@ func (gb *GB28181Plugin) TestSip(ctx context.Context, req *pb.TestSipRequest) (*
|
||||
// 构建SDP消息体
|
||||
sdpInfo := []string{
|
||||
"v=0",
|
||||
fmt.Sprintf("o=%s 0 0 IN IP4 %s", "34020000001320000004", device.SipIp),
|
||||
fmt.Sprintf("o=%s 0 0 IN IP4 %s", "34020000001320000102", "192.168.1.106"),
|
||||
"s=Play",
|
||||
"c=IN IP4 " + device.SipIp,
|
||||
"c=IN IP4 192.168.1.106",
|
||||
"t=0 0",
|
||||
"m=video 43970 TCP/RTP/AVP 96 97 98 99",
|
||||
"m=video 40940 TCP/RTP/AVP 96 97 98 99",
|
||||
"a=recvonly",
|
||||
"a=rtpmap:96 PS/90000",
|
||||
"a=rtpmap:98 H264/90000",
|
||||
@@ -1334,36 +1352,40 @@ func (gb *GB28181Plugin) TestSip(ctx context.Context, req *pb.TestSipRequest) (*
|
||||
"a=rtpmap:99 H265/90000",
|
||||
"a=setup:passive",
|
||||
"a=connection:new",
|
||||
"y=0200005507",
|
||||
"y=0105006213",
|
||||
}
|
||||
|
||||
// 设置必需的头部
|
||||
contentTypeHeader := sip.ContentTypeHeader("APPLICATION/SDP")
|
||||
subjectHeader := sip.NewHeader("Subject", "34020000001320000006:0200005507,34020000002000000001:0")
|
||||
//Subject: 34020000001320000006:0105006213,41010500002000000001:0
|
||||
subjectHeader := sip.NewHeader("Subject", "34020000001320000006:0105006213,41010500002000000001:0")
|
||||
|
||||
//To: <sip:34020000001320000006@192.168.1.102:5060>
|
||||
toHeader := sip.ToHeader{
|
||||
Address: sip.Uri{
|
||||
User: "34020000001320000006",
|
||||
Host: device.IP,
|
||||
Port: device.Port,
|
||||
Host: "192.168.1.102",
|
||||
Port: 5060,
|
||||
},
|
||||
}
|
||||
userAgentHeader := sip.NewHeader("User-Agent", "WVP-Pro v2.7.3.20241218")
|
||||
//Via: SIP/2.0/UDP 192.168.1.106:5060;branch=z9hG4bK9279674404;rport
|
||||
viaHeader := sip.ViaHeader{
|
||||
ProtocolName: "SIP",
|
||||
ProtocolVersion: "2.0",
|
||||
Transport: "UDP",
|
||||
Host: device.SipIp,
|
||||
Port: device.Port,
|
||||
Host: "192.168.1.106",
|
||||
Port: 5060,
|
||||
Params: sip.HeaderParams(sip.NewParams()),
|
||||
}
|
||||
viaHeader.Params.Add("branch", sip.GenerateBranchN(16)).Add("rport", "")
|
||||
viaHeader.Params.Add("branch", "z9hG4bK9279674404").Add("rport", "")
|
||||
|
||||
csqHeader := sip.CSeqHeader{
|
||||
SeqNo: 13,
|
||||
SeqNo: 3,
|
||||
MethodName: "INVITE",
|
||||
}
|
||||
maxforward := sip.MaxForwardsHeader(70)
|
||||
contentLengthHeader := sip.ContentLengthHeader(286)
|
||||
//contentLengthHeader := sip.ContentLengthHeader(288)
|
||||
request.AppendHeader(&contentTypeHeader)
|
||||
request.AppendHeader(subjectHeader)
|
||||
request.AppendHeader(&toHeader)
|
||||
@@ -1375,7 +1397,7 @@ func (gb *GB28181Plugin) TestSip(ctx context.Context, req *pb.TestSipRequest) (*
|
||||
|
||||
// 创建会话并发送请求
|
||||
dialogClientCache := sipgo.NewDialogClientCache(device.client, device.contactHDR)
|
||||
session, err := dialogClientCache.Invite(gb, recipient, request.Body(), &csqHeader, &device.fromHDR, &toHeader, &viaHeader, &maxforward, userAgentHeader, &device.contactHDR, subjectHeader, &contentTypeHeader, &contentLengthHeader)
|
||||
session, err := dialogClientCache.Invite(gb, recipient, request.Body(), &csqHeader, &device.fromHDR, &toHeader, &maxforward, userAgentHeader, &device.contactHDR, subjectHeader, &contentTypeHeader)
|
||||
if err != nil {
|
||||
resp.Code = 500
|
||||
resp.Message = fmt.Sprintf("发送INVITE请求失败: %v", err)
|
||||
@@ -2468,12 +2490,9 @@ func (gb *GB28181Plugin) PlaybackPause(ctx context.Context, req *pb.PlaybackPaus
|
||||
resp.Message = fmt.Sprintf("发送暂停请求失败: %v", err)
|
||||
return resp, nil
|
||||
}
|
||||
gb.Server.Streams.Call(func() error {
|
||||
if s, ok := gb.Server.Streams.Get(req.StreamPath); ok {
|
||||
s.Pause()
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if s, ok := gb.Server.Streams.SafeGet(req.StreamPath); ok {
|
||||
s.Pause()
|
||||
}
|
||||
gb.Info("暂停回放",
|
||||
"streampath", req.StreamPath)
|
||||
|
||||
@@ -2522,12 +2541,9 @@ func (gb *GB28181Plugin) PlaybackResume(ctx context.Context, req *pb.PlaybackRes
|
||||
resp.Message = fmt.Sprintf("发送恢复请求失败: %v", err)
|
||||
return resp, nil
|
||||
}
|
||||
gb.Server.Streams.Call(func() error {
|
||||
if s, ok := gb.Server.Streams.Get(req.StreamPath); ok {
|
||||
s.Resume()
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if s, ok := gb.Server.Streams.SafeGet(req.StreamPath); ok {
|
||||
s.Resume()
|
||||
}
|
||||
gb.Info("恢复回放",
|
||||
"streampath", req.StreamPath)
|
||||
|
||||
@@ -2595,14 +2611,11 @@ func (gb *GB28181Plugin) PlaybackSpeed(ctx context.Context, req *pb.PlaybackSpee
|
||||
// 发送请求
|
||||
_, err := dialog.session.TransactionRequest(ctx, request)
|
||||
|
||||
gb.Server.Streams.Call(func() error {
|
||||
if s, ok := gb.Server.Streams.Get(req.StreamPath); ok {
|
||||
s.Speed = float64(req.Speed)
|
||||
s.Scale = float64(req.Speed)
|
||||
s.Info("set stream speed", "speed", req.Speed)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if s, ok := gb.Server.Streams.SafeGet(req.StreamPath); ok {
|
||||
s.Speed = float64(req.Speed)
|
||||
s.Scale = float64(req.Speed)
|
||||
s.Info("set stream speed", "speed", req.Speed)
|
||||
}
|
||||
if err != nil {
|
||||
resp.Code = 500
|
||||
resp.Message = fmt.Sprintf("发送倍速请求失败: %v", err)
|
||||
@@ -2826,62 +2839,54 @@ func (gb *GB28181Plugin) RemoveDevice(ctx context.Context, req *pb.RemoveDeviceR
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
// 检查数据库连接
|
||||
if gb.DB == nil {
|
||||
resp.Code = 500
|
||||
resp.Message = "数据库未初始化"
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
// 开启事务
|
||||
tx := gb.DB.Begin()
|
||||
|
||||
// 先从数据库中查找设备
|
||||
var dbDevice Device
|
||||
if err := tx.Where(&Device{DeviceId: req.Id}).First(&dbDevice).Error; err != nil {
|
||||
tx.Rollback()
|
||||
resp.Code = 404
|
||||
resp.Message = fmt.Sprintf("设备不存在: %v", err)
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
// 使用数据库中的 DeviceId 从内存中查找设备
|
||||
if device, ok := gb.devices.Get(dbDevice.DeviceId); ok {
|
||||
if device, ok := gb.devices.Get(req.Id); ok {
|
||||
device.DeletedAt = gorm.DeletedAt{Time: time.Now(), Valid: true}
|
||||
device.channels.Range(func(channel *Channel) bool {
|
||||
channel.DeletedAt = gorm.DeletedAt{Time: time.Now(), Valid: true}
|
||||
return true
|
||||
})
|
||||
// 停止设备相关任务
|
||||
device.Stop(fmt.Errorf("device removed"))
|
||||
device.WaitStopped()
|
||||
// device.Stop() 会调用 Dispose(),其中已包含从 gb.devices 中移除设备的逻辑
|
||||
|
||||
// 开启数据库事务
|
||||
tx := gb.DB.Begin()
|
||||
if tx.Error != nil {
|
||||
resp.Code = 500
|
||||
resp.Message = "开启事务失败"
|
||||
return resp, tx.Error
|
||||
}
|
||||
|
||||
// 删除设备
|
||||
if err := tx.Delete(&Device{DeviceId: req.Id}).Error; err != nil {
|
||||
tx.Rollback()
|
||||
resp.Code = 500
|
||||
resp.Message = "删除设备失败"
|
||||
return resp, err
|
||||
}
|
||||
|
||||
// 删除设备关联的通道
|
||||
if err := tx.Delete(&gb28181.DeviceChannel{DeviceID: req.Id}).Error; err != nil {
|
||||
tx.Rollback()
|
||||
resp.Code = 500
|
||||
resp.Message = "删除设备通道失败"
|
||||
return resp, err
|
||||
}
|
||||
|
||||
// 提交事务
|
||||
if err := tx.Commit().Error; err != nil {
|
||||
tx.Rollback()
|
||||
resp.Code = 500
|
||||
resp.Message = "提交事务失败"
|
||||
return resp, err
|
||||
}
|
||||
|
||||
resp.Code = 200
|
||||
resp.Message = "设备删除成功"
|
||||
}
|
||||
|
||||
// 删除设备关联的所有通道
|
||||
if err := tx.Where(&gb28181.DeviceChannel{DeviceID: dbDevice.DeviceId}).Delete(&gb28181.DeviceChannel{}).Error; err != nil {
|
||||
tx.Rollback()
|
||||
resp.Code = 500
|
||||
resp.Message = fmt.Sprintf("删除设备通道失败: %v", err)
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
// 删除设备
|
||||
if err := tx.Delete(&dbDevice).Error; err != nil {
|
||||
tx.Rollback()
|
||||
resp.Code = 500
|
||||
resp.Message = fmt.Sprintf("删除设备失败: %v", err)
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
// 提交事务
|
||||
if err := tx.Commit().Error; err != nil {
|
||||
resp.Code = 500
|
||||
resp.Message = fmt.Sprintf("提交事务失败: %v", err)
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
gb.Info("删除设备成功",
|
||||
"deviceId", dbDevice.DeviceId,
|
||||
"deviceName", dbDevice.Name)
|
||||
|
||||
resp.Code = 0
|
||||
resp.Message = "success"
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
|
@@ -92,18 +92,16 @@ func (d *Device) TableName() string {
|
||||
|
||||
func (d *Device) Dispose() {
|
||||
if d.plugin.DB != nil {
|
||||
d.plugin.DB.Save(d)
|
||||
if d.channels.Length > 0 {
|
||||
d.channels.Range(func(channel *Channel) bool {
|
||||
d.plugin.DB.Save(channel.DeviceChannel)
|
||||
//d.plugin.DB.Model(&gb28181.DeviceChannel{}).Where("device_id = ? AND device_db_id = ?", channel.DeviceId, d.ID).Updates(channel.DeviceChannel)
|
||||
return true
|
||||
})
|
||||
} else {
|
||||
// 如果没有通道,则直接更新通道状态为 OFF
|
||||
d.plugin.DB.Model(&gb28181.DeviceChannel{}).Where("device_id = ?", d.ID).Update("status", "OFF")
|
||||
}
|
||||
d.plugin.DB.Save(d)
|
||||
}
|
||||
d.plugin.devices.RemoveByKey(d.DeviceId)
|
||||
}
|
||||
|
||||
func (d *Device) GetKey() string {
|
||||
@@ -140,7 +138,7 @@ func (r *CatalogRequest) IsComplete(channelsLength int) bool {
|
||||
}
|
||||
|
||||
func (d *Device) onMessage(req *sip.Request, tx sip.ServerTransaction, msg *gb28181.Message) (err error) {
|
||||
d.plugin.Debug("into onMessage,deviceid is ", d.DeviceId)
|
||||
d.plugin.Trace("into onMessage,deviceid is ", d.DeviceId)
|
||||
source := req.Source()
|
||||
hostname, portStr, _ := net.SplitHostPort(source)
|
||||
port, _ := strconv.Atoi(portStr)
|
||||
@@ -161,7 +159,7 @@ func (d *Device) onMessage(req *sip.Request, tx sip.ServerTransaction, msg *gb28
|
||||
case "Keepalive":
|
||||
d.KeepaliveInterval = int(time.Since(d.KeepaliveTime).Seconds())
|
||||
d.KeepaliveTime = time.Now()
|
||||
d.Debug("into keeplive,deviceid is ", d.DeviceId, "d.KeepaliveTime is", d.KeepaliveTime)
|
||||
d.Trace("into keeplive,deviceid is ", d.DeviceId, "d.KeepaliveTime is", d.KeepaliveTime)
|
||||
if d.plugin.DB != nil {
|
||||
if err := d.plugin.DB.Model(d).Updates(map[string]interface{}{
|
||||
"keepalive_interval": d.KeepaliveInterval,
|
||||
@@ -191,7 +189,7 @@ func (d *Device) onMessage(req *sip.Request, tx sip.ServerTransaction, msg *gb28
|
||||
if d.plugin.DB != nil {
|
||||
// 如果是第一个响应,先清空现有通道
|
||||
if isFirst {
|
||||
d.Debug("清空现有通道", "deviceId", d.DeviceId)
|
||||
d.Trace("清空现有通道", "deviceId", d.DeviceId)
|
||||
if err := d.plugin.DB.Where("device_id = ?", d.DeviceId).Delete(&gb28181.DeviceChannel{}).Error; err != nil {
|
||||
d.Error("删除通道失败", "error", err, "deviceId", d.DeviceId)
|
||||
}
|
||||
@@ -215,7 +213,7 @@ func (d *Device) onMessage(req *sip.Request, tx sip.ServerTransaction, msg *gb28
|
||||
// 更新当前设备的通道数
|
||||
d.ChannelCount = msg.SumNum
|
||||
d.UpdateTime = time.Now()
|
||||
d.Debug("save channel", "deviceid", d.DeviceId, "channels count", d.channels.Length)
|
||||
d.Trace("save channel", "deviceid", d.DeviceId, "channels count", d.channels.Length)
|
||||
if err := d.plugin.DB.Model(d).Updates(map[string]interface{}{
|
||||
"channel_count": d.ChannelCount,
|
||||
"update_time": d.UpdateTime,
|
||||
@@ -325,7 +323,10 @@ func (d *Device) onMessage(req *sip.Request, tx sip.ServerTransaction, msg *gb28
|
||||
}
|
||||
case "DeviceInfo":
|
||||
// 主设备信息
|
||||
d.Name = msg.DeviceName
|
||||
d.Info("DeviceInfo message", "body", req.Body(), "d.Name", d.Name, "d.DeviceId", d.DeviceId, "msg.DeviceName", msg.DeviceName)
|
||||
if d.Name == "" && msg.DeviceName != "" {
|
||||
d.Name = msg.DeviceName
|
||||
}
|
||||
d.Manufacturer = msg.Manufacturer
|
||||
d.Model = msg.Model
|
||||
d.Firmware = msg.Firmware
|
||||
@@ -400,12 +401,12 @@ func (d *Device) onMessage(req *sip.Request, tx sip.ServerTransaction, msg *gb28
|
||||
|
||||
func (d *Device) send(req *sip.Request) (*sip.Response, error) {
|
||||
d.SN++
|
||||
d.Debug("send", "req", req.String())
|
||||
d.Trace("send", "req", req.String())
|
||||
return d.client.Do(context.Background(), req)
|
||||
}
|
||||
|
||||
func (d *Device) Go() (err error) {
|
||||
d.Debug("into device.Go,deviceid is ", d.DeviceId)
|
||||
d.Trace("into device.Go,deviceid is ", d.DeviceId)
|
||||
var response *sip.Response
|
||||
|
||||
// 初始化catalogReqs
|
||||
@@ -423,7 +424,7 @@ func (d *Device) Go() (err error) {
|
||||
if err != nil {
|
||||
d.Error("catalog", "err", err)
|
||||
} else {
|
||||
d.Debug("catalog", "response", response.String())
|
||||
d.Trace("catalog", "response", response.String())
|
||||
}
|
||||
|
||||
// 创建并启动目录订阅任务
|
||||
@@ -450,7 +451,7 @@ func (d *Device) Go() (err error) {
|
||||
select {
|
||||
case <-d.Done():
|
||||
case <-keepLiveTick.C:
|
||||
d.Debug("keepLiveTick,deviceid is", d.DeviceId, "d.KeepaliveTime is ", d.KeepaliveTime)
|
||||
d.Trace("keepLiveTick,deviceid is", d.DeviceId, "d.KeepaliveTime is ", d.KeepaliveTime)
|
||||
if timeDiff := time.Since(d.KeepaliveTime); timeDiff > time.Duration(3*keepaliveSeconds)*time.Second {
|
||||
d.Online = false
|
||||
d.Status = DeviceOfflineStatus
|
||||
@@ -471,7 +472,7 @@ func (d *Device) Go() (err error) {
|
||||
if err != nil {
|
||||
d.Error("catalog", "err", err)
|
||||
} else {
|
||||
d.Debug("catalogTick", "response", response.String())
|
||||
d.Trace("catalogTick", "response", response.String())
|
||||
}
|
||||
//case event := <-d.eventChan:
|
||||
// d.Debug("eventChan", "event", event)
|
||||
|
@@ -44,7 +44,11 @@ type Dialog struct {
|
||||
}
|
||||
|
||||
func (d *Dialog) GetCallID() string {
|
||||
return d.session.InviteRequest.CallID().Value()
|
||||
if d.session != nil && d.session.InviteRequest != nil && d.session.InviteRequest.CallID() != nil {
|
||||
return d.session.InviteRequest.CallID().Value()
|
||||
} else {
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
func (d *Dialog) GetPullJob() *m7s.PullJob {
|
||||
@@ -115,9 +119,9 @@ func (d *Dialog) Start() (err error) {
|
||||
}
|
||||
|
||||
// 非直播模式下添加u行,保持在s=和c=之间
|
||||
//if !d.IsLive() {
|
||||
sdpInfo = append(sdpInfo, fmt.Sprintf("u=%s:0", channelId))
|
||||
//}
|
||||
if !d.IsLive() {
|
||||
sdpInfo = append(sdpInfo, fmt.Sprintf("u=%s:0", channelId))
|
||||
}
|
||||
|
||||
// 添加c行
|
||||
sdpInfo = append(sdpInfo, "c=IN IP4 "+device.MediaIp)
|
||||
@@ -126,7 +130,7 @@ func (d *Dialog) Start() (err error) {
|
||||
if !d.IsLive() {
|
||||
startTime, endTime, err := util.TimeRangeQueryParse(url.Values{"start": []string{d.start}, "end": []string{d.end}})
|
||||
if err != nil {
|
||||
d.Stop(errors.New("parse end time error"))
|
||||
return errors.New("parse end time error")
|
||||
}
|
||||
sdpInfo = append(sdpInfo, fmt.Sprintf("t=%d %d", startTime.Unix(), endTime.Unix()))
|
||||
} else {
|
||||
@@ -145,10 +149,12 @@ func (d *Dialog) Start() (err error) {
|
||||
}
|
||||
|
||||
sdpInfo = append(sdpInfo, mediaLine)
|
||||
|
||||
sdpInfo = append(sdpInfo, "a=recvonly")
|
||||
if d.stream != "" {
|
||||
sdpInfo = append(sdpInfo, "a="+d.stream)
|
||||
}
|
||||
sdpInfo = append(sdpInfo, "a=rtpmap:96 PS/90000")
|
||||
|
||||
//根据传输模式添加 setup 和 connection 属性
|
||||
switch strings.ToUpper(device.StreamMode) {
|
||||
@@ -163,14 +169,13 @@ func (d *Dialog) Start() (err error) {
|
||||
"a=connection:new",
|
||||
)
|
||||
case "UDP":
|
||||
d.Stop(errors.New("do not support udp mode"))
|
||||
return errors.New("do not support udp mode")
|
||||
default:
|
||||
sdpInfo = append(sdpInfo,
|
||||
"a=setup:passive",
|
||||
"a=connection:new",
|
||||
)
|
||||
}
|
||||
sdpInfo = append(sdpInfo, "a=rtpmap:96 PS/90000")
|
||||
|
||||
// 添加 SSRC
|
||||
sdpInfo = append(sdpInfo, fmt.Sprintf("y=%s", ssrc))
|
||||
@@ -238,21 +243,20 @@ func (d *Dialog) Start() (err error) {
|
||||
//}
|
||||
// 最后添加Content-Length头部
|
||||
if err != nil {
|
||||
d.gb.Error("invite error", err)
|
||||
return errors.New("dialog invite error" + err.Error())
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (d *Dialog) Run() (err error) {
|
||||
d.Channel.Info("before WaitAnswer")
|
||||
d.gb.Info("before WaitAnswer")
|
||||
err = d.session.WaitAnswer(d.gb, sipgo.AnswerOptions{})
|
||||
d.Channel.Info("after WaitAnswer")
|
||||
d.gb.Error(" WaitAnswer error", err)
|
||||
d.gb.Info("after WaitAnswer")
|
||||
if err != nil {
|
||||
return
|
||||
return errors.New("wait answer error" + err.Error())
|
||||
}
|
||||
inviteResponseBody := string(d.session.InviteResponse.Body())
|
||||
d.Channel.Info("inviteResponse", "body", inviteResponseBody)
|
||||
d.gb.Info("inviteResponse", "body", inviteResponseBody)
|
||||
ds := strings.Split(inviteResponseBody, "\r\n")
|
||||
for _, l := range ds {
|
||||
if ls := strings.Split(l, "="); len(ls) > 1 {
|
||||
@@ -299,6 +303,10 @@ func (d *Dialog) Run() (err error) {
|
||||
}
|
||||
pub.Receiver.StreamMode = d.StreamMode
|
||||
d.AddTask(&pub.Receiver)
|
||||
startResult := pub.Receiver.WaitStarted()
|
||||
if startResult != nil {
|
||||
return fmt.Errorf("pub.Receiver.WaitStarted %s", startResult)
|
||||
}
|
||||
pub.Demux()
|
||||
return
|
||||
}
|
||||
|
@@ -5,12 +5,15 @@ import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"regexp"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"m7s.live/v5/pkg"
|
||||
|
||||
"github.com/emiago/sipgo"
|
||||
"github.com/emiago/sipgo/sip"
|
||||
"github.com/rs/zerolog"
|
||||
@@ -129,6 +132,9 @@ func (gb *GB28181Plugin) initDatabase() error {
|
||||
}
|
||||
|
||||
func (gb *GB28181Plugin) OnInit() (err error) {
|
||||
if gb.DB == nil {
|
||||
return pkg.ErrNoDB
|
||||
}
|
||||
gb.Info("GB28181 initing", gb.Platforms)
|
||||
gb.AddTask(&gb.deviceManager)
|
||||
logger := zerolog.New(os.Stdout)
|
||||
@@ -377,45 +383,48 @@ func (gb *GB28181Plugin) checkPlatform() {
|
||||
|
||||
// 遍历所有平台进行初始化和注册
|
||||
for _, platformModel := range platformModels {
|
||||
// 创建Platform实例
|
||||
platform := NewPlatform(platformModel, gb, true)
|
||||
if platformModel.Enable {
|
||||
|
||||
if platformModel.PlatformChannels != nil && len(platformModel.PlatformChannels) > 0 {
|
||||
for i := range platformModel.PlatformChannels {
|
||||
channelDbId := platformModel.PlatformChannels[i].ChannelDBID
|
||||
if channelDbId != "" {
|
||||
if channel, ok := gb.channels.Get(channelDbId); ok {
|
||||
platform.channels.Set(channel)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// 查询通道列表
|
||||
var channels []gb28181.DeviceChannel
|
||||
if gb.DB != nil {
|
||||
if err := gb.DB.Table("gb28181_channel gc").
|
||||
Select(`gc.*`).
|
||||
Joins("left join gb28181_platform_channel gpc on gc.id=gpc.channel_db_id").
|
||||
Where("gpc.platform_server_gb_id = ? and gc.status='ON'", platformModel.ServerGBID).
|
||||
Find(&channels).Error; err != nil {
|
||||
gb.Error("<UNK>", "error", err.Error())
|
||||
}
|
||||
if channels != nil && len(channels) > 0 {
|
||||
for i := range channels {
|
||||
if channel, ok := gb.channels.Get(channels[i].ID); ok {
|
||||
// 创建Platform实例
|
||||
platform := NewPlatform(platformModel, gb, true)
|
||||
|
||||
if platformModel.PlatformChannels != nil && len(platformModel.PlatformChannels) > 0 {
|
||||
for i := range platformModel.PlatformChannels {
|
||||
channelDbId := platformModel.PlatformChannels[i].ChannelDBID
|
||||
if channelDbId != "" {
|
||||
if channel, ok := gb.channels.Get(channelDbId); ok {
|
||||
platform.channels.Set(channel)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// 查询通道列表
|
||||
var channels []gb28181.DeviceChannel
|
||||
if gb.DB != nil {
|
||||
if err := gb.DB.Table("gb28181_channel gc").
|
||||
Select(`gc.*`).
|
||||
Joins("left join gb28181_platform_channel gpc on gc.id=gpc.channel_db_id").
|
||||
Where("gpc.platform_server_gb_id = ? and gc.status='ON'", platformModel.ServerGBID).
|
||||
Find(&channels).Error; err != nil {
|
||||
gb.Error("<UNK>", "error", err.Error())
|
||||
}
|
||||
if channels != nil && len(channels) > 0 {
|
||||
for i := range channels {
|
||||
if channel, ok := gb.channels.Get(channels[i].ID); ok {
|
||||
platform.channels.Set(channel)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
//go platform.Unregister()
|
||||
//if err != nil {
|
||||
// gb.Error("unregister err ", err)
|
||||
//}
|
||||
// 添加到任务系统
|
||||
gb.AddTask(platform)
|
||||
gb.Info("平台初始化完成", "ID", platformModel.ServerGBID, "Name", platformModel.Name)
|
||||
}
|
||||
//go platform.Unregister()
|
||||
//if err != nil {
|
||||
// gb.Error("unregister err ", err)
|
||||
//}
|
||||
// 添加到任务系统
|
||||
gb.AddTask(platform)
|
||||
gb.Info("平台初始化完成", "ID", platformModel.ServerGBID, "Name", platformModel.Name)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -429,9 +438,22 @@ func (gb *GB28181Plugin) OnRegister(req *sip.Request, tx sip.ServerTransaction)
|
||||
from := req.From()
|
||||
if from == nil || from.Address.User == "" {
|
||||
gb.Error("OnRegister", "error", "no user")
|
||||
response := sip.NewResponseFromRequest(req, sip.StatusBadRequest, "Invalid sip from format", nil)
|
||||
if err := tx.Respond(response); err != nil {
|
||||
gb.Error("respond BadRequest", "error", err.Error())
|
||||
}
|
||||
return
|
||||
}
|
||||
deviceId := from.Address.User
|
||||
// 验证设备ID是否符合GB28181规范(20位数字)
|
||||
if match, _ := regexp.MatchString(`^\d{20}$`, deviceId); !match {
|
||||
gb.Error("OnRegister", "error", "invalid device id format, must be 20 digits", "deviceId", deviceId)
|
||||
response := sip.NewResponseFromRequest(req, sip.StatusBadRequest, "Invalid device ID format", nil)
|
||||
if err := tx.Respond(response); err != nil {
|
||||
gb.Error("respond BadRequest", "error", err.Error())
|
||||
}
|
||||
return
|
||||
}
|
||||
registerHandlerTask := registerHandlerTask{
|
||||
gb: gb,
|
||||
req: req,
|
||||
@@ -494,7 +516,6 @@ func (gb *GB28181Plugin) OnMessage(req *sip.Request, tx sip.ServerTransaction) {
|
||||
}
|
||||
}
|
||||
|
||||
gb.Debug("00000000000001,deviceid is ", id)
|
||||
// 如果设备和平台都存在,通过源地址判断真实来源
|
||||
if d != nil && d.Online && p != nil {
|
||||
source := req.Source()
|
||||
@@ -506,7 +527,6 @@ func (gb *GB28181Plugin) OnMessage(req *sip.Request, tx sip.ServerTransaction) {
|
||||
d = nil
|
||||
}
|
||||
}
|
||||
gb.Debug("00000000000002,deviceid is ", id)
|
||||
|
||||
// 如果既不是设备也不是平台,返回404
|
||||
if (d == nil && p == nil) || (d != nil && !d.Online) {
|
||||
@@ -519,7 +539,6 @@ func (gb *GB28181Plugin) OnMessage(req *sip.Request, tx sip.ServerTransaction) {
|
||||
gb.Debug("after on message respond")
|
||||
return
|
||||
}
|
||||
gb.Debug("00000000000003,deviceid is ", id)
|
||||
|
||||
// 根据来源调用不同的处理方法
|
||||
if d != nil && d.Online {
|
||||
|
@@ -335,6 +335,8 @@ func (p *Platform) Register(isUnregister bool) error {
|
||||
newReq := req.Clone()
|
||||
newReq.RemoveHeader("Via") // 必须由传输层重新生成
|
||||
newReq.AppendHeader(sip.NewHeader("Authorization", cred.String()))
|
||||
newReq.CSeq().SeqNo = uint32(p.SN) // 更新CSeq序号
|
||||
p.SN++
|
||||
|
||||
// 发送认证请求
|
||||
tx, err = p.Client.TransactionRequest(p.ctx, newReq, sipgo.ClientRequestAddVia)
|
||||
|
@@ -36,6 +36,14 @@ type registerHandlerTask struct {
|
||||
tx sip.ServerTransaction
|
||||
}
|
||||
|
||||
// getDevicePassword 获取设备密码
|
||||
func (task *registerHandlerTask) getDevicePassword(device *Device) string {
|
||||
if device != nil && device.Password != "" {
|
||||
return device.Password
|
||||
}
|
||||
return task.gb.Password
|
||||
}
|
||||
|
||||
func (task *registerHandlerTask) Run() (err error) {
|
||||
var password string
|
||||
var device *Device
|
||||
@@ -47,23 +55,25 @@ func (task *registerHandlerTask) Run() (err error) {
|
||||
}
|
||||
isUnregister := false
|
||||
deviceid := from.Address.User
|
||||
if devicetmp, ok := task.gb.devices.Get(deviceid); ok {
|
||||
device = devicetmp
|
||||
|
||||
if existingDevice, exists := task.gb.devices.Get(deviceid); exists && existingDevice != nil {
|
||||
device = existingDevice
|
||||
recover = true
|
||||
} else {
|
||||
if err := task.gb.DB.First(&device, Device{DeviceId: deviceid}).Error; err == nil {
|
||||
if device.Password != "" {
|
||||
password = device.Password
|
||||
} else if task.gb.Password != "" {
|
||||
password = task.gb.Password
|
||||
}
|
||||
} else {
|
||||
if !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
task.gb.Error("OnRegister", "error", err)
|
||||
// 尝试从数据库加载设备信息
|
||||
device = &Device{DeviceId: deviceid}
|
||||
if task.gb.DB != nil {
|
||||
if err := task.gb.DB.First(device, Device{DeviceId: deviceid}).Error; err != nil {
|
||||
if !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
task.gb.Error("OnRegister", "error", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 获取设备密码
|
||||
password = task.getDevicePassword(device)
|
||||
|
||||
exp := task.req.GetHeader("Expires")
|
||||
if exp == nil {
|
||||
task.gb.Error("OnRegister", "error", "no expires")
|
||||
@@ -136,7 +146,7 @@ func (task *registerHandlerTask) Run() (err error) {
|
||||
Method: "REGISTER",
|
||||
URI: cred.URI,
|
||||
Username: deviceid,
|
||||
Password: task.gb.Password,
|
||||
Password: password,
|
||||
Cnonce: cred.Cnonce,
|
||||
Count: int(cred.Nc),
|
||||
}
|
||||
@@ -222,11 +232,14 @@ func (task *registerHandlerTask) Run() (err error) {
|
||||
device.Status = DeviceOnlineStatus
|
||||
task.RecoverDevice(device, task.req)
|
||||
} else {
|
||||
device := &Device{
|
||||
DeviceId: deviceid,
|
||||
var newDevice *Device
|
||||
if device == nil {
|
||||
newDevice = &Device{DeviceId: deviceid}
|
||||
} else {
|
||||
newDevice = device
|
||||
}
|
||||
task.gb.Info("into StoreDevice", "deviceId", from)
|
||||
task.StoreDevice(deviceid, task.req, device)
|
||||
task.StoreDevice(deviceid, task.req, newDevice)
|
||||
}
|
||||
}
|
||||
task.gb.Info("registerHandlerTask start end", "deviceid", deviceid, "expires", expSec, "isUnregister", isUnregister)
|
||||
|
@@ -104,9 +104,8 @@ func (config *HLSPlugin) vod(w http.ResponseWriter, r *http.Request) {
|
||||
playlist.Init()
|
||||
|
||||
for _, record := range records {
|
||||
duration := record.EndTime.Sub(record.StartTime).Seconds()
|
||||
playlist.WriteInf(hls.PlaylistInf{
|
||||
Duration: duration,
|
||||
Duration: float64(record.Duration) / 1000,
|
||||
URL: fmt.Sprintf("/mp4/download/%s.fmp4?id=%d", streamPath, record.ID),
|
||||
Title: record.StartTime.Format(time.RFC3339),
|
||||
})
|
||||
@@ -128,9 +127,8 @@ func (config *HLSPlugin) vod(w http.ResponseWriter, r *http.Request) {
|
||||
playlist.Init()
|
||||
|
||||
for _, record := range records {
|
||||
duration := record.EndTime.Sub(record.StartTime).Seconds()
|
||||
playlist.WriteInf(hls.PlaylistInf{
|
||||
Duration: duration,
|
||||
Duration: float64(record.Duration) / 1000,
|
||||
URL: record.FilePath,
|
||||
})
|
||||
}
|
||||
|
@@ -2,16 +2,13 @@ package hls
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"gorm.io/gorm"
|
||||
"m7s.live/v5"
|
||||
"m7s.live/v5/pkg"
|
||||
"m7s.live/v5/pkg/codec"
|
||||
"m7s.live/v5/pkg/config"
|
||||
"m7s.live/v5/pkg/task"
|
||||
"m7s.live/v5/pkg/util"
|
||||
mpegts "m7s.live/v5/plugin/hls/pkg/ts"
|
||||
)
|
||||
@@ -22,7 +19,6 @@ func NewRecorder(conf config.Record) m7s.IRecorder {
|
||||
|
||||
type Recorder struct {
|
||||
m7s.DefaultRecorder
|
||||
stream m7s.RecordStream
|
||||
ts *TsInFile
|
||||
pesAudio *mpegts.MpegtsPESFrame
|
||||
pesVideo *mpegts.MpegtsPESFrame
|
||||
@@ -39,81 +35,11 @@ var CustomFileName = func(job *m7s.RecordJob) string {
|
||||
}
|
||||
|
||||
func (r *Recorder) createStream(start time.Time) (err error) {
|
||||
recordJob := &r.RecordJob
|
||||
sub := recordJob.Subscriber
|
||||
r.stream = m7s.RecordStream{
|
||||
StartTime: start,
|
||||
StreamPath: sub.StreamPath,
|
||||
FilePath: CustomFileName(&r.RecordJob),
|
||||
EventId: recordJob.EventId,
|
||||
EventDesc: recordJob.EventDesc,
|
||||
EventName: recordJob.EventName,
|
||||
EventLevel: recordJob.EventLevel,
|
||||
BeforeDuration: recordJob.BeforeDuration,
|
||||
AfterDuration: recordJob.AfterDuration,
|
||||
Mode: recordJob.Mode,
|
||||
Type: "hls",
|
||||
}
|
||||
dir := filepath.Dir(r.stream.FilePath)
|
||||
dir = filepath.Clean(dir)
|
||||
if err = os.MkdirAll(dir, 0755); err != nil {
|
||||
r.Error("create directory failed", "err", err, "dir", dir)
|
||||
return
|
||||
}
|
||||
if sub.Publisher.HasAudioTrack() {
|
||||
r.stream.AudioCodec = sub.Publisher.AudioTrack.ICodecCtx.String()
|
||||
}
|
||||
if sub.Publisher.HasVideoTrack() {
|
||||
r.stream.VideoCodec = sub.Publisher.VideoTrack.ICodecCtx.String()
|
||||
}
|
||||
if recordJob.Plugin.DB != nil {
|
||||
recordJob.Plugin.DB.Save(&r.stream)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
type eventRecordCheck struct {
|
||||
task.Task
|
||||
DB *gorm.DB
|
||||
streamPath string
|
||||
}
|
||||
|
||||
func (t *eventRecordCheck) Run() (err error) {
|
||||
var eventRecordStreams []m7s.RecordStream
|
||||
queryRecord := m7s.RecordStream{
|
||||
EventLevel: m7s.EventLevelHigh,
|
||||
Mode: m7s.RecordModeEvent,
|
||||
Type: "hls",
|
||||
}
|
||||
t.DB.Where(&queryRecord).Find(&eventRecordStreams, "stream_path=?", t.streamPath) //搜索事件录像,且为重要事件(无法自动删除)
|
||||
if len(eventRecordStreams) > 0 {
|
||||
for _, recordStream := range eventRecordStreams {
|
||||
var unimportantEventRecordStreams []m7s.RecordStream
|
||||
queryRecord.EventLevel = m7s.EventLevelLow
|
||||
query := `(start_time BETWEEN ? AND ?)
|
||||
OR (end_time BETWEEN ? AND ?)
|
||||
OR (? BETWEEN start_time AND end_time)
|
||||
OR (? BETWEEN start_time AND end_time) AND stream_path=? `
|
||||
t.DB.Where(&queryRecord).Where(query, recordStream.StartTime, recordStream.EndTime, recordStream.StartTime, recordStream.EndTime, recordStream.StartTime, recordStream.EndTime, recordStream.StreamPath).Find(&unimportantEventRecordStreams)
|
||||
if len(unimportantEventRecordStreams) > 0 {
|
||||
for _, unimportantEventRecordStream := range unimportantEventRecordStreams {
|
||||
unimportantEventRecordStream.EventLevel = m7s.EventLevelHigh
|
||||
t.DB.Save(&unimportantEventRecordStream)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return
|
||||
return r.CreateStream(start, CustomFileName)
|
||||
}
|
||||
|
||||
func (r *Recorder) writeTailer(end time.Time) {
|
||||
if r.stream.EndTime.After(r.stream.StartTime) {
|
||||
return
|
||||
}
|
||||
r.stream.EndTime = end
|
||||
if r.RecordJob.Plugin.DB != nil {
|
||||
r.RecordJob.Plugin.DB.Save(&r.stream)
|
||||
}
|
||||
r.WriteTail(end, nil)
|
||||
}
|
||||
|
||||
func (r *Recorder) Dispose() {
|
||||
@@ -131,9 +57,9 @@ func (r *Recorder) createNewTs() {
|
||||
r.ts.Close()
|
||||
}
|
||||
var err error
|
||||
r.ts, err = NewTsInFile(r.stream.FilePath)
|
||||
r.ts, err = NewTsInFile(r.Event.FilePath)
|
||||
if err != nil {
|
||||
r.Error("create ts file failed", "err", err, "path", r.stream.FilePath)
|
||||
r.Error("create ts file failed", "err", err, "path", r.Event.FilePath)
|
||||
return
|
||||
}
|
||||
if oldPMT.Len() > 0 {
|
||||
@@ -175,8 +101,8 @@ func (r *Recorder) Run() (err error) {
|
||||
ctx := &r.RecordJob
|
||||
suber := ctx.Subscriber
|
||||
startTime := time.Now()
|
||||
if ctx.BeforeDuration > 0 {
|
||||
startTime = startTime.Add(-ctx.BeforeDuration)
|
||||
if ctx.Event.BeforeDuration > 0 {
|
||||
startTime = startTime.Add(-time.Duration(ctx.Event.BeforeDuration) * time.Millisecond)
|
||||
}
|
||||
|
||||
// 创建第一个片段记录
|
||||
|
@@ -165,10 +165,9 @@ func (p *MP4Plugin) download(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
// 构建查询条件,查找指定时间范围内的录制记录
|
||||
queryRecord := m7s.RecordStream{
|
||||
Mode: m7s.RecordModeAuto,
|
||||
Type: "mp4",
|
||||
}
|
||||
p.DB.Where(&queryRecord).Find(&streams, "end_time>? AND start_time<? AND stream_path=?", startTime, endTime, streamPath)
|
||||
p.DB.Where(&queryRecord).Find(&streams, "event_id=0 AND end_time>? AND start_time<? AND stream_path=?", startTime, endTime, streamPath)
|
||||
|
||||
// 创建 MP4 混合器
|
||||
muxer := mp4.NewMuxer(flag)
|
||||
@@ -458,26 +457,34 @@ func (p *MP4Plugin) StartRecord(ctx context.Context, req *mp4pb.ReqStartRecord)
|
||||
filePath = req.FilePath
|
||||
}
|
||||
res = &mp4pb.ResponseStartRecord{}
|
||||
p.Server.Records.Call(func() error {
|
||||
_, recordExists = p.Server.Records.Find(func(job *m7s.RecordJob) bool {
|
||||
return job.StreamPath == req.StreamPath && job.RecConf.FilePath == req.FilePath
|
||||
})
|
||||
return nil
|
||||
_, recordExists = p.Server.Records.SafeFind(func(job *m7s.RecordJob) bool {
|
||||
return job.StreamPath == req.StreamPath && job.RecConf.FilePath == req.FilePath
|
||||
})
|
||||
if recordExists {
|
||||
err = pkg.ErrRecordExists
|
||||
return
|
||||
}
|
||||
|
||||
recordConf := config.Record{
|
||||
Append: false,
|
||||
Fragment: fragment,
|
||||
FilePath: filePath,
|
||||
}
|
||||
if stream, ok := p.Server.Streams.SafeGet(req.StreamPath); ok {
|
||||
recordConf := config.Record{
|
||||
Append: false,
|
||||
Fragment: fragment,
|
||||
FilePath: filePath,
|
||||
}
|
||||
job := p.Record(stream, recordConf, nil)
|
||||
res.Data = uint64(uintptr(unsafe.Pointer(job.GetTask())))
|
||||
} else {
|
||||
err = pkg.ErrNotFound
|
||||
sub, err := p.Subscribe(ctx, req.StreamPath)
|
||||
if err == nil && sub != nil {
|
||||
if stream, ok := p.Server.Streams.SafeGet(req.StreamPath); ok {
|
||||
job := p.Record(stream, recordConf, nil)
|
||||
res.Data = uint64(uintptr(unsafe.Pointer(job.GetTask())))
|
||||
} else {
|
||||
err = pkg.ErrNotFound
|
||||
}
|
||||
} else {
|
||||
err = pkg.ErrNotFound
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
@@ -485,19 +492,16 @@ func (p *MP4Plugin) StartRecord(ctx context.Context, req *mp4pb.ReqStartRecord)
|
||||
func (p *MP4Plugin) StopRecord(ctx context.Context, req *mp4pb.ReqStopRecord) (res *mp4pb.ResponseStopRecord, err error) {
|
||||
res = &mp4pb.ResponseStopRecord{}
|
||||
var recordJob *m7s.RecordJob
|
||||
p.Server.Records.Call(func() error {
|
||||
recordJob, _ = p.Server.Records.Find(func(job *m7s.RecordJob) bool {
|
||||
return job.StreamPath == req.StreamPath
|
||||
})
|
||||
if recordJob != nil {
|
||||
t := recordJob.GetTask()
|
||||
if t != nil {
|
||||
res.Data = uint64(uintptr(unsafe.Pointer(t)))
|
||||
t.Stop(task.ErrStopByUser)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
recordJob, _ = p.Server.Records.SafeFind(func(job *m7s.RecordJob) bool {
|
||||
return job.StreamPath == req.StreamPath
|
||||
})
|
||||
if recordJob != nil {
|
||||
t := recordJob.GetTask()
|
||||
if t != nil {
|
||||
res.Data = uint64(uintptr(unsafe.Pointer(t)))
|
||||
t.Stop(task.ErrStopByUser)
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
@@ -519,11 +523,8 @@ func (p *MP4Plugin) EventStart(ctx context.Context, req *mp4pb.ReqEventRecord) (
|
||||
}
|
||||
//recorder := p.Meta.Recorder(config.Record{})
|
||||
var tmpJob *m7s.RecordJob
|
||||
p.Server.Records.Call(func() error {
|
||||
tmpJob, _ = p.Server.Records.Find(func(job *m7s.RecordJob) bool {
|
||||
return job.StreamPath == req.StreamPath
|
||||
})
|
||||
return nil
|
||||
tmpJob, _ = p.Server.Records.SafeFind(func(job *m7s.RecordJob) bool {
|
||||
return job.StreamPath == req.StreamPath
|
||||
})
|
||||
if tmpJob == nil { //为空表示没有正在进行的录制,也就是没有自动录像,则进行正常的事件录像
|
||||
if stream, ok := p.Server.Streams.SafeGet(req.StreamPath); ok {
|
||||
@@ -531,42 +532,44 @@ func (p *MP4Plugin) EventStart(ctx context.Context, req *mp4pb.ReqEventRecord) (
|
||||
Append: false,
|
||||
Fragment: 0,
|
||||
FilePath: filepath.Join(p.EventRecordFilePath, stream.StreamPath, time.Now().Local().Format("2006-01-02-15-04-05")),
|
||||
Mode: config.RecordModeEvent,
|
||||
Event: &config.RecordEvent{
|
||||
EventId: req.EventId,
|
||||
EventLevel: req.EventLevel,
|
||||
EventName: req.EventName,
|
||||
EventDesc: req.EventDesc,
|
||||
BeforeDuration: uint32(beforeDuration / time.Millisecond),
|
||||
AfterDuration: uint32(afterDuration / time.Millisecond),
|
||||
},
|
||||
}
|
||||
//recordJob := recorder.GetRecordJob()
|
||||
var subconfig config.Subscribe
|
||||
defaults.SetDefaults(&subconfig)
|
||||
subconfig.BufferTime = beforeDuration
|
||||
recordJob := p.Record(stream, recordConf, &subconfig)
|
||||
recordJob.EventId = req.EventId
|
||||
recordJob.EventLevel = req.EventLevel
|
||||
recordJob.EventName = req.EventName
|
||||
recordJob.EventDesc = req.EventDesc
|
||||
recordJob.AfterDuration = afterDuration
|
||||
recordJob.BeforeDuration = beforeDuration
|
||||
recordJob.Mode = m7s.RecordModeEvent
|
||||
p.Record(stream, recordConf, &subconfig)
|
||||
}
|
||||
} else {
|
||||
if tmpJob.AfterDuration != 0 { //当前有事件录像正在录制,则更新该录像的结束时间
|
||||
tmpJob.AfterDuration = time.Duration(tmpJob.Subscriber.VideoReader.AbsTime)*time.Millisecond + afterDuration
|
||||
if tmpJob.Event != nil { //当前有事件录像正在录制,则更新该录像的结束时间
|
||||
tmpJob.Event.AfterDuration = tmpJob.Subscriber.VideoReader.AbsTime + uint32(afterDuration/time.Millisecond)
|
||||
if p.DB != nil {
|
||||
p.DB.Save(&tmpJob.Event)
|
||||
}
|
||||
} else { //当前有自动录像正在录制,则生成事件录像的记录,而不去生成事件录像的文件
|
||||
recordStream := &m7s.RecordStream{
|
||||
StreamPath: req.StreamPath,
|
||||
newEvent := &config.RecordEvent{
|
||||
EventId: req.EventId,
|
||||
EventLevel: req.EventLevel,
|
||||
EventDesc: req.EventDesc,
|
||||
EventName: req.EventName,
|
||||
Mode: m7s.RecordModeEvent,
|
||||
BeforeDuration: beforeDuration,
|
||||
AfterDuration: afterDuration,
|
||||
Type: "mp4",
|
||||
EventDesc: req.EventDesc,
|
||||
BeforeDuration: uint32(beforeDuration / time.Millisecond),
|
||||
AfterDuration: uint32(afterDuration / time.Millisecond),
|
||||
}
|
||||
now := time.Now()
|
||||
startTime := now.Add(-beforeDuration)
|
||||
endTime := now.Add(afterDuration)
|
||||
recordStream.StartTime = startTime
|
||||
recordStream.EndTime = endTime
|
||||
if p.DB != nil {
|
||||
p.DB.Save(&recordStream)
|
||||
p.DB.Save(&m7s.EventRecordStream{
|
||||
RecordEvent: newEvent,
|
||||
RecordStream: m7s.RecordStream{
|
||||
StreamPath: req.StreamPath,
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -37,9 +37,27 @@
|
||||
| | | | | | sbgp | | sample-to-group |
|
||||
| | | | | | sgpd | | sample group description |
|
||||
| | | | | | subs | | sub-sample information |
|
||||
| | | udta | | | | | user-data (track level)<br>轨道级别的用户数据容器 |
|
||||
| | | | cprt | | | | copyright etc.<br>版权信息 |
|
||||
| | | | titl | | | | title<br>标题 |
|
||||
| | | | auth | | | | author<br>作者 |
|
||||
| | mvex | | | | | | movie extends box |
|
||||
| | | mehd | | | | | movie extends header box |
|
||||
| | | trex | | | | ✓ | track extends defaults |
|
||||
| | udta | | | | | | user-data (movie level)<br>电影级别的用户数据容器 |
|
||||
| | | cprt | | | | | copyright etc.<br>版权信息 |
|
||||
| | | titl | | | | | title<br>标题 |
|
||||
| | | auth | | | | | author<br>作者 |
|
||||
| | | albm | | | | | album<br>专辑 |
|
||||
| | | yrrc | | | | | year<br>年份 |
|
||||
| | | rtng | | | | | rating<br>评级 |
|
||||
| | | clsf | | | | | classification<br>分类 |
|
||||
| | | kywd | | | | | keywords<br>关键词 |
|
||||
| | | loci | | | | | location information<br>位置信息 |
|
||||
| | | dscp | | | | | description<br>描述 |
|
||||
| | | perf | | | | | performer<br>表演者 |
|
||||
| | | gnre | | | | | genre<br>类型 |
|
||||
| | | meta | | | | | metadata atom<br>元数据原子 |
|
||||
| | ipmc | | | | | | IPMP Control Box |
|
||||
| moof | | | | | | | movie fragment |
|
||||
| | mfhd | | | | | ✓ | movie fragment header |
|
||||
@@ -54,8 +72,10 @@
|
||||
| mdat | | | | | | | media data container |
|
||||
| free | | | | | | | free space |
|
||||
| skip | | | | | | | free space |
|
||||
| | udta | | | | | | user-data |
|
||||
| | | cprt | | | | | copyright etc. |
|
||||
| udta | | | | | | | user-data (file level)<br>文件级别的用户数据容器 |
|
||||
| | cprt | | | | | | copyright etc.<br>版权信息 |
|
||||
| | titl | | | | | | title<br>标题 |
|
||||
| | auth | | | | | | author<br>作者 |
|
||||
| meta | | | | | | | metadata |
|
||||
| | hdlr | | | | | ✓ | handler, declares the metadata (handler) type |
|
||||
| | dinf | | | | | | data information box, container |
|
||||
|
@@ -91,9 +91,6 @@ func (p *DeleteRecordTask) deleteOldestFile() {
|
||||
}
|
||||
for _, filePath := range filePaths {
|
||||
for p.getDiskOutOfSpace(filePath) {
|
||||
queryRecord := m7s.RecordStream{
|
||||
EventLevel: m7s.EventLevelLow, // 查询条件:event_level = 1,非重要事件
|
||||
}
|
||||
var eventRecords []m7s.RecordStream
|
||||
// 使用不同的方法进行路径匹配,避免ESCAPE语法问题
|
||||
// 解决方案:用MySQL能理解的简单方式匹配路径前缀
|
||||
@@ -103,7 +100,7 @@ func (p *DeleteRecordTask) deleteOldestFile() {
|
||||
searchPattern := basePath + "%"
|
||||
p.Info("deleteOldestFile", "searching with path pattern", searchPattern)
|
||||
|
||||
err := p.DB.Where(&queryRecord).Where("end_time IS NOT NULL").
|
||||
err := p.DB.Where("event_id=0 AND end_time IS NOT NULL").
|
||||
Where("file_path LIKE ?", searchPattern).
|
||||
Order("end_time ASC").Find(&eventRecords).Error
|
||||
if err == nil {
|
||||
@@ -149,14 +146,11 @@ func (t *DeleteRecordTask) Tick(any) {
|
||||
if t.RecordFileExpireDays <= 0 {
|
||||
return
|
||||
}
|
||||
//搜索event_records表中event_level值为1的(非重要)数据,并将其create_time与当前时间比对,大于RecordFileExpireDays则进行删除,数据库标记is_delete为1,磁盘上删除录像文件
|
||||
//搜索event_records表中event_id值为0的(非事件)录像,并将其create_time与当前时间比对,大于RecordFileExpireDays则进行删除,数据库标记is_delete为1,磁盘上删除录像文件
|
||||
var eventRecords []m7s.RecordStream
|
||||
expireTime := time.Now().AddDate(0, 0, -t.RecordFileExpireDays)
|
||||
t.Debug("RecordFileExpireDays is set to auto delete oldestfile", "expireTime", expireTime.Format("2006-01-02 15:04:05"))
|
||||
queryRecord := m7s.RecordStream{
|
||||
EventLevel: m7s.EventLevelLow, // 查询条件:event_level = low,非重要事件
|
||||
}
|
||||
err := t.DB.Where(&queryRecord).Find(&eventRecords, "end_time < ? AND end_time IS NOT NULL", expireTime).Error
|
||||
err := t.DB.Find(&eventRecords, "event_id=0 AND end_time < ? AND end_time IS NOT NULL", expireTime).Error
|
||||
if err == nil {
|
||||
for _, record := range eventRecords {
|
||||
t.Info("RecordFileExpireDays is set to auto delete oldestfile", "ID", record.ID, "create time", record.EndTime, "filepath", record.FilePath)
|
||||
|
@@ -343,7 +343,25 @@ var (
|
||||
TypeAUXV = f("auxv")
|
||||
TypeHINT = f("hint")
|
||||
TypeUDTA = f("udta")
|
||||
TypeM7SP = f("m7sp") // Custom box type for M7S StreamPath
|
||||
|
||||
// Common metadata box types
|
||||
TypeTITL = f("©nam") // Title
|
||||
TypeART = f("©ART") // Artist/Author
|
||||
TypeALB = f("©alb") // Album
|
||||
TypeDAY = f("©day") // Date/Year
|
||||
TypeCMT = f("©cmt") // Comment/Description
|
||||
TypeGEN = f("©gen") // Genre
|
||||
TypeCPRT = f("cprt") // Copyright
|
||||
TypeENCO = f("©too") // Encoder/Tool
|
||||
TypeWRT = f("©wrt") // Writer/Composer
|
||||
TypePRD = f("©prd") // Producer
|
||||
TypePRF = f("©prf") // Performer
|
||||
TypeGRP = f("©grp") // Grouping
|
||||
TypeLYR = f("©lyr") // Lyrics
|
||||
TypeKEYW = f("keyw") // Keywords
|
||||
TypeLOCI = f("loci") // Location Information
|
||||
TypeRTNG = f("rtng") // Rating
|
||||
TypeMETA_CUST = f("----") // Custom metadata (iTunes-style)
|
||||
)
|
||||
|
||||
// aligned(8) class Box (unsigned int(32) boxtype, optional unsigned int(8)[16] extended_type) {
|
||||
|
334
plugin/mp4/pkg/box/metadata.go
Normal file
334
plugin/mp4/pkg/box/metadata.go
Normal file
@@ -0,0 +1,334 @@
|
||||
package box
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"io"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Metadata holds various metadata information for MP4
|
||||
type Metadata struct {
|
||||
Title string // 标题
|
||||
Artist string // 艺术家/作者
|
||||
Album string // 专辑
|
||||
Date string // 日期
|
||||
Comment string // 注释/描述
|
||||
Genre string // 类型
|
||||
Copyright string // 版权信息
|
||||
Encoder string // 编码器
|
||||
Writer string // 作词者
|
||||
Producer string // 制作人
|
||||
Performer string // 表演者
|
||||
Grouping string // 分组
|
||||
Lyrics string // 歌词
|
||||
Keywords string // 关键词
|
||||
Location string // 位置信息
|
||||
Rating uint8 // 评级 (0-5)
|
||||
Custom map[string]string // 自定义键值对
|
||||
}
|
||||
|
||||
// Text Data Box - for storing text metadata
|
||||
type TextDataBox struct {
|
||||
FullBox
|
||||
Text string
|
||||
}
|
||||
|
||||
// Metadata Data Box - for storing binary metadata with type indicator
|
||||
type MetadataDataBox struct {
|
||||
FullBox
|
||||
DataType uint32 // Data type indicator
|
||||
Country uint32 // Country code
|
||||
Language uint32 // Language code
|
||||
Data []byte // Actual data
|
||||
}
|
||||
|
||||
// Copyright Box
|
||||
type CopyrightBox struct {
|
||||
FullBox
|
||||
Language [3]byte
|
||||
Notice string
|
||||
}
|
||||
|
||||
// Custom Metadata Box (iTunes-style ---- box)
|
||||
type CustomMetadataBox struct {
|
||||
BaseBox
|
||||
Mean string // Mean (namespace)
|
||||
Name string // Name (key)
|
||||
Data []byte // Data
|
||||
}
|
||||
|
||||
// Create functions
|
||||
|
||||
func CreateTextDataBox(boxType BoxType, text string) *TextDataBox {
|
||||
return &TextDataBox{
|
||||
FullBox: FullBox{
|
||||
BaseBox: BaseBox{
|
||||
typ: boxType,
|
||||
size: uint32(FullBoxLen + len(text)),
|
||||
},
|
||||
Version: 0,
|
||||
Flags: [3]byte{0, 0, 0},
|
||||
},
|
||||
Text: text,
|
||||
}
|
||||
}
|
||||
|
||||
func CreateMetadataDataBox(dataType uint32, data []byte) *MetadataDataBox {
|
||||
return &MetadataDataBox{
|
||||
FullBox: FullBox{
|
||||
BaseBox: BaseBox{
|
||||
typ: f("data"),
|
||||
size: uint32(FullBoxLen + 8 + len(data)), // 8 bytes for type+country+language
|
||||
},
|
||||
Version: 0,
|
||||
Flags: [3]byte{0, 0, 0},
|
||||
},
|
||||
DataType: dataType,
|
||||
Country: 0,
|
||||
Language: 0,
|
||||
Data: data,
|
||||
}
|
||||
}
|
||||
|
||||
func CreateCopyrightBox(language [3]byte, notice string) *CopyrightBox {
|
||||
return &CopyrightBox{
|
||||
FullBox: FullBox{
|
||||
BaseBox: BaseBox{
|
||||
typ: TypeCPRT,
|
||||
size: uint32(FullBoxLen + 3 + 1 + len(notice)), // 3 for language, 1 for null terminator
|
||||
},
|
||||
Version: 0,
|
||||
Flags: [3]byte{0, 0, 0},
|
||||
},
|
||||
Language: language,
|
||||
Notice: notice,
|
||||
}
|
||||
}
|
||||
|
||||
func CreateCustomMetadataBox(mean, name string, data []byte) *CustomMetadataBox {
|
||||
size := uint32(BasicBoxLen + 4 + len(mean) + 4 + len(name) + len(data))
|
||||
return &CustomMetadataBox{
|
||||
BaseBox: BaseBox{
|
||||
typ: TypeMETA_CUST,
|
||||
size: size,
|
||||
},
|
||||
Mean: mean,
|
||||
Name: name,
|
||||
Data: data,
|
||||
}
|
||||
}
|
||||
|
||||
// WriteTo methods
|
||||
|
||||
func (box *TextDataBox) WriteTo(w io.Writer) (n int64, err error) {
|
||||
nn, err := w.Write([]byte(box.Text))
|
||||
return int64(nn), err
|
||||
}
|
||||
|
||||
func (box *MetadataDataBox) WriteTo(w io.Writer) (n int64, err error) {
|
||||
var tmp [8]byte
|
||||
binary.BigEndian.PutUint32(tmp[0:4], box.DataType)
|
||||
binary.BigEndian.PutUint32(tmp[4:8], box.Country)
|
||||
// Language field is implicit zero
|
||||
|
||||
nn, err := w.Write(tmp[:8])
|
||||
if err != nil {
|
||||
return int64(nn), err
|
||||
}
|
||||
n = int64(nn)
|
||||
|
||||
nn, err = w.Write(box.Data)
|
||||
return n + int64(nn), err
|
||||
}
|
||||
|
||||
func (box *CopyrightBox) WriteTo(w io.Writer) (n int64, err error) {
|
||||
// Write language code
|
||||
nn, err := w.Write(box.Language[:])
|
||||
if err != nil {
|
||||
return int64(nn), err
|
||||
}
|
||||
n = int64(nn)
|
||||
|
||||
// Write notice + null terminator
|
||||
nn, err = w.Write([]byte(box.Notice + "\x00"))
|
||||
return n + int64(nn), err
|
||||
}
|
||||
|
||||
func (box *CustomMetadataBox) WriteTo(w io.Writer) (n int64, err error) {
|
||||
var tmp [4]byte
|
||||
|
||||
// Write mean length + mean
|
||||
binary.BigEndian.PutUint32(tmp[:], uint32(len(box.Mean)))
|
||||
nn, err := w.Write(tmp[:])
|
||||
if err != nil {
|
||||
return int64(nn), err
|
||||
}
|
||||
n = int64(nn)
|
||||
|
||||
nn, err = w.Write([]byte(box.Mean))
|
||||
if err != nil {
|
||||
return n + int64(nn), err
|
||||
}
|
||||
n += int64(nn)
|
||||
|
||||
// Write name length + name
|
||||
binary.BigEndian.PutUint32(tmp[:], uint32(len(box.Name)))
|
||||
nn, err = w.Write(tmp[:])
|
||||
if err != nil {
|
||||
return n + int64(nn), err
|
||||
}
|
||||
n += int64(nn)
|
||||
|
||||
nn, err = w.Write([]byte(box.Name))
|
||||
if err != nil {
|
||||
return n + int64(nn), err
|
||||
}
|
||||
n += int64(nn)
|
||||
|
||||
// Write data
|
||||
nn, err = w.Write(box.Data)
|
||||
return n + int64(nn), err
|
||||
}
|
||||
|
||||
// Unmarshal methods
|
||||
|
||||
func (box *TextDataBox) Unmarshal(buf []byte) (IBox, error) {
|
||||
box.Text = string(buf)
|
||||
return box, nil
|
||||
}
|
||||
|
||||
func (box *MetadataDataBox) Unmarshal(buf []byte) (IBox, error) {
|
||||
if len(buf) < 8 {
|
||||
return nil, io.ErrShortBuffer
|
||||
}
|
||||
|
||||
box.DataType = binary.BigEndian.Uint32(buf[0:4])
|
||||
box.Country = binary.BigEndian.Uint32(buf[4:8])
|
||||
box.Data = buf[8:]
|
||||
return box, nil
|
||||
}
|
||||
|
||||
func (box *CopyrightBox) Unmarshal(buf []byte) (IBox, error) {
|
||||
if len(buf) < 4 {
|
||||
return nil, io.ErrShortBuffer
|
||||
}
|
||||
|
||||
copy(box.Language[:], buf[0:3])
|
||||
// Find null terminator
|
||||
for i := 3; i < len(buf); i++ {
|
||||
if buf[i] == 0 {
|
||||
box.Notice = string(buf[3:i])
|
||||
break
|
||||
}
|
||||
}
|
||||
if box.Notice == "" && len(buf) > 3 {
|
||||
box.Notice = string(buf[3:])
|
||||
}
|
||||
return box, nil
|
||||
}
|
||||
|
||||
func (box *CustomMetadataBox) Unmarshal(buf []byte) (IBox, error) {
|
||||
if len(buf) < 8 {
|
||||
return nil, io.ErrShortBuffer
|
||||
}
|
||||
|
||||
offset := 0
|
||||
|
||||
// Read mean length + mean
|
||||
meanLen := binary.BigEndian.Uint32(buf[offset:])
|
||||
offset += 4
|
||||
if offset+int(meanLen) > len(buf) {
|
||||
return nil, io.ErrShortBuffer
|
||||
}
|
||||
box.Mean = string(buf[offset : offset+int(meanLen)])
|
||||
offset += int(meanLen)
|
||||
|
||||
// Read name length + name
|
||||
if offset+4 > len(buf) {
|
||||
return nil, io.ErrShortBuffer
|
||||
}
|
||||
nameLen := binary.BigEndian.Uint32(buf[offset:])
|
||||
offset += 4
|
||||
if offset+int(nameLen) > len(buf) {
|
||||
return nil, io.ErrShortBuffer
|
||||
}
|
||||
box.Name = string(buf[offset : offset+int(nameLen)])
|
||||
offset += int(nameLen)
|
||||
|
||||
// Read remaining data
|
||||
box.Data = buf[offset:]
|
||||
return box, nil
|
||||
}
|
||||
|
||||
// Create metadata entries from Metadata struct
|
||||
func CreateMetadataEntries(metadata *Metadata) []IBox {
|
||||
var entries []IBox
|
||||
|
||||
// Standard text metadata
|
||||
if metadata.Title != "" {
|
||||
entries = append(entries, CreateTextDataBox(TypeTITL, metadata.Title))
|
||||
}
|
||||
if metadata.Artist != "" {
|
||||
entries = append(entries, CreateTextDataBox(TypeART, metadata.Artist))
|
||||
}
|
||||
if metadata.Album != "" {
|
||||
entries = append(entries, CreateTextDataBox(TypeALB, metadata.Album))
|
||||
}
|
||||
if metadata.Date != "" {
|
||||
entries = append(entries, CreateTextDataBox(TypeDAY, metadata.Date))
|
||||
}
|
||||
if metadata.Comment != "" {
|
||||
entries = append(entries, CreateTextDataBox(TypeCMT, metadata.Comment))
|
||||
}
|
||||
if metadata.Genre != "" {
|
||||
entries = append(entries, CreateTextDataBox(TypeGEN, metadata.Genre))
|
||||
}
|
||||
if metadata.Encoder != "" {
|
||||
entries = append(entries, CreateTextDataBox(TypeENCO, metadata.Encoder))
|
||||
}
|
||||
if metadata.Writer != "" {
|
||||
entries = append(entries, CreateTextDataBox(TypeWRT, metadata.Writer))
|
||||
}
|
||||
if metadata.Producer != "" {
|
||||
entries = append(entries, CreateTextDataBox(TypePRD, metadata.Producer))
|
||||
}
|
||||
if metadata.Performer != "" {
|
||||
entries = append(entries, CreateTextDataBox(TypePRF, metadata.Performer))
|
||||
}
|
||||
if metadata.Grouping != "" {
|
||||
entries = append(entries, CreateTextDataBox(TypeGRP, metadata.Grouping))
|
||||
}
|
||||
if metadata.Lyrics != "" {
|
||||
entries = append(entries, CreateTextDataBox(TypeLYR, metadata.Lyrics))
|
||||
}
|
||||
if metadata.Keywords != "" {
|
||||
entries = append(entries, CreateTextDataBox(TypeKEYW, metadata.Keywords))
|
||||
}
|
||||
if metadata.Location != "" {
|
||||
entries = append(entries, CreateTextDataBox(TypeLOCI, metadata.Location))
|
||||
}
|
||||
|
||||
// Copyright (special format)
|
||||
if metadata.Copyright != "" {
|
||||
entries = append(entries, CreateCopyrightBox([3]byte{'u', 'n', 'd'}, metadata.Copyright))
|
||||
}
|
||||
|
||||
// Custom metadata
|
||||
for key, value := range metadata.Custom {
|
||||
entries = append(entries, CreateCustomMetadataBox("live.m7s.custom", key, []byte(value)))
|
||||
}
|
||||
|
||||
return entries
|
||||
}
|
||||
|
||||
// Helper function to create current date string
|
||||
func GetCurrentDateString() string {
|
||||
return time.Now().Format("2006-01-02")
|
||||
}
|
||||
|
||||
func init() {
|
||||
RegisterBox[*TextDataBox](TypeTITL, TypeART, TypeALB, TypeDAY, TypeCMT, TypeGEN, TypeENCO, TypeWRT, TypePRD, TypePRF, TypeGRP, TypeLYR, TypeKEYW, TypeLOCI, TypeRTNG)
|
||||
RegisterBox[*MetadataDataBox](f("data"))
|
||||
RegisterBox[*CopyrightBox](TypeCPRT)
|
||||
RegisterBox[*CustomMetadataBox](TypeMETA_CUST)
|
||||
}
|
@@ -12,12 +12,6 @@ type UserDataBox struct {
|
||||
Entries []IBox
|
||||
}
|
||||
|
||||
// Custom metadata box for storing stream path
|
||||
type StreamPathBox struct {
|
||||
FullBox
|
||||
StreamPath string
|
||||
}
|
||||
|
||||
// Create a new User Data Box
|
||||
func CreateUserDataBox(entries ...IBox) *UserDataBox {
|
||||
size := uint32(BasicBoxLen)
|
||||
@@ -33,21 +27,6 @@ func CreateUserDataBox(entries ...IBox) *UserDataBox {
|
||||
}
|
||||
}
|
||||
|
||||
// Create a new StreamPath Box
|
||||
func CreateStreamPathBox(streamPath string) *StreamPathBox {
|
||||
return &StreamPathBox{
|
||||
FullBox: FullBox{
|
||||
BaseBox: BaseBox{
|
||||
typ: TypeM7SP, // Custom box type for M7S StreamPath
|
||||
size: uint32(FullBoxLen + len(streamPath)),
|
||||
},
|
||||
Version: 0,
|
||||
Flags: [3]byte{0, 0, 0},
|
||||
},
|
||||
StreamPath: streamPath,
|
||||
}
|
||||
}
|
||||
|
||||
// WriteTo writes the UserDataBox to the given writer
|
||||
func (box *UserDataBox) WriteTo(w io.Writer) (n int64, err error) {
|
||||
return WriteTo(w, box.Entries...)
|
||||
@@ -69,19 +48,6 @@ func (box *UserDataBox) Unmarshal(buf []byte) (IBox, error) {
|
||||
return box, nil
|
||||
}
|
||||
|
||||
// WriteTo writes the StreamPathBox to the given writer
|
||||
func (box *StreamPathBox) WriteTo(w io.Writer) (n int64, err error) {
|
||||
nn, err := w.Write([]byte(box.StreamPath))
|
||||
return int64(nn), err
|
||||
}
|
||||
|
||||
// Unmarshal parses the given buffer into a StreamPathBox
|
||||
func (box *StreamPathBox) Unmarshal(buf []byte) (IBox, error) {
|
||||
box.StreamPath = string(buf)
|
||||
return box, nil
|
||||
}
|
||||
|
||||
func init() {
|
||||
RegisterBox[*UserDataBox](TypeUDTA)
|
||||
RegisterBox[*StreamPathBox](TypeM7SP)
|
||||
}
|
||||
|
@@ -29,7 +29,8 @@ type (
|
||||
moov IBox
|
||||
mdatOffset uint64
|
||||
mdatSize uint64
|
||||
StreamPath string // Added to store the stream path
|
||||
StreamPath string // Added to store the stream path
|
||||
Metadata *Metadata // 添加元数据支持
|
||||
}
|
||||
)
|
||||
|
||||
@@ -52,6 +53,7 @@ func NewMuxer(flag Flag) *Muxer {
|
||||
Tracks: make(map[uint32]*Track),
|
||||
Flag: flag,
|
||||
fragDuration: 2000,
|
||||
Metadata: &Metadata{Custom: make(map[string]string)},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -59,6 +61,8 @@ func NewMuxer(flag Flag) *Muxer {
|
||||
func NewMuxerWithStreamPath(flag Flag, streamPath string) *Muxer {
|
||||
muxer := NewMuxer(flag)
|
||||
muxer.StreamPath = streamPath
|
||||
muxer.Metadata.Producer = "M7S Live"
|
||||
muxer.Metadata.Album = streamPath
|
||||
return muxer
|
||||
}
|
||||
|
||||
@@ -232,10 +236,10 @@ func (m *Muxer) MakeMoov() IBox {
|
||||
children = append(children, m.makeMvex())
|
||||
}
|
||||
|
||||
// Add user data box with stream path if available
|
||||
if m.StreamPath != "" {
|
||||
streamPathBox := CreateStreamPathBox(m.StreamPath)
|
||||
udta := CreateUserDataBox(streamPathBox)
|
||||
// Add user data box with metadata if available
|
||||
metadataEntries := CreateMetadataEntries(m.Metadata)
|
||||
if len(metadataEntries) > 0 {
|
||||
udta := CreateUserDataBox(metadataEntries...)
|
||||
children = append(children, udta)
|
||||
}
|
||||
|
||||
@@ -365,3 +369,82 @@ func (m *Muxer) WriteTrailer(file *os.File) (err error) {
|
||||
func (m *Muxer) SetFragmentDuration(duration uint32) {
|
||||
m.fragDuration = duration
|
||||
}
|
||||
|
||||
// SetMetadata sets the metadata for the MP4 file
|
||||
func (m *Muxer) SetMetadata(metadata *Metadata) {
|
||||
m.Metadata = metadata
|
||||
if metadata.Custom == nil {
|
||||
metadata.Custom = make(map[string]string)
|
||||
}
|
||||
}
|
||||
|
||||
// SetTitle sets the title metadata
|
||||
func (m *Muxer) SetTitle(title string) {
|
||||
m.Metadata.Title = title
|
||||
}
|
||||
|
||||
// SetArtist sets the artist/author metadata
|
||||
func (m *Muxer) SetArtist(artist string) {
|
||||
m.Metadata.Artist = artist
|
||||
}
|
||||
|
||||
// SetAlbum sets the album metadata
|
||||
func (m *Muxer) SetAlbum(album string) {
|
||||
m.Metadata.Album = album
|
||||
}
|
||||
|
||||
// SetComment sets the comment/description metadata
|
||||
func (m *Muxer) SetComment(comment string) {
|
||||
m.Metadata.Comment = comment
|
||||
}
|
||||
|
||||
// SetGenre sets the genre metadata
|
||||
func (m *Muxer) SetGenre(genre string) {
|
||||
m.Metadata.Genre = genre
|
||||
}
|
||||
|
||||
// SetCopyright sets the copyright metadata
|
||||
func (m *Muxer) SetCopyright(copyright string) {
|
||||
m.Metadata.Copyright = copyright
|
||||
}
|
||||
|
||||
// SetEncoder sets the encoder metadata
|
||||
func (m *Muxer) SetEncoder(encoder string) {
|
||||
m.Metadata.Encoder = encoder
|
||||
}
|
||||
|
||||
// SetDate sets the date metadata (format: YYYY-MM-DD)
|
||||
func (m *Muxer) SetDate(date string) {
|
||||
m.Metadata.Date = date
|
||||
}
|
||||
|
||||
// SetCurrentDate sets the date metadata to current date
|
||||
func (m *Muxer) SetCurrentDate() {
|
||||
m.Metadata.Date = GetCurrentDateString()
|
||||
}
|
||||
|
||||
// AddCustomMetadata adds custom key-value metadata
|
||||
func (m *Muxer) AddCustomMetadata(key, value string) {
|
||||
if m.Metadata.Custom == nil {
|
||||
m.Metadata.Custom = make(map[string]string)
|
||||
}
|
||||
m.Metadata.Custom[key] = value
|
||||
}
|
||||
|
||||
// SetKeywords sets the keywords metadata
|
||||
func (m *Muxer) SetKeywords(keywords string) {
|
||||
m.Metadata.Keywords = keywords
|
||||
}
|
||||
|
||||
// SetLocation sets the location metadata
|
||||
func (m *Muxer) SetLocation(location string) {
|
||||
m.Metadata.Location = location
|
||||
}
|
||||
|
||||
// SetRating sets the rating metadata (0-5)
|
||||
func (m *Muxer) SetRating(rating uint8) {
|
||||
if rating > 5 {
|
||||
rating = 5
|
||||
}
|
||||
m.Metadata.Rating = rating
|
||||
}
|
||||
|
@@ -7,7 +7,6 @@ import (
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"gorm.io/gorm"
|
||||
m7s "m7s.live/v5"
|
||||
"m7s.live/v5/pkg"
|
||||
"m7s.live/v5/pkg/codec"
|
||||
@@ -107,39 +106,6 @@ func (t *writeTrailerTask) Run() (err error) {
|
||||
return
|
||||
}
|
||||
|
||||
type eventRecordCheck struct {
|
||||
task.Task
|
||||
DB *gorm.DB
|
||||
streamPath string
|
||||
}
|
||||
|
||||
func (t *eventRecordCheck) Run() (err error) {
|
||||
var eventRecordStreams []m7s.RecordStream
|
||||
queryRecord := m7s.RecordStream{
|
||||
EventLevel: m7s.EventLevelHigh,
|
||||
Mode: m7s.RecordModeEvent,
|
||||
Type: "mp4",
|
||||
StreamPath: t.streamPath,
|
||||
}
|
||||
t.DB.Where(&queryRecord).Find(&eventRecordStreams) //搜索事件录像,且为重要事件(无法自动删除)
|
||||
if len(eventRecordStreams) > 0 {
|
||||
for _, recordStream := range eventRecordStreams {
|
||||
var unimportantEventRecordStreams []m7s.RecordStream
|
||||
queryRecord.EventLevel = m7s.EventLevelLow
|
||||
queryRecord.Mode = m7s.RecordModeAuto
|
||||
query := `start_time <= ? and end_time >= ?`
|
||||
t.DB.Where(&queryRecord).Where(query, recordStream.EndTime, recordStream.StartTime).Find(&unimportantEventRecordStreams)
|
||||
if len(unimportantEventRecordStreams) > 0 {
|
||||
for _, unimportantEventRecordStream := range unimportantEventRecordStreams {
|
||||
unimportantEventRecordStream.EventLevel = m7s.EventLevelHigh
|
||||
t.DB.Save(&unimportantEventRecordStream)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func init() {
|
||||
m7s.Servers.AddTask(&writeTrailerQueueTask)
|
||||
}
|
||||
@@ -150,20 +116,12 @@ func NewRecorder(conf config.Record) m7s.IRecorder {
|
||||
|
||||
type Recorder struct {
|
||||
m7s.DefaultRecorder
|
||||
muxer *Muxer
|
||||
file *os.File
|
||||
stream m7s.RecordStream
|
||||
muxer *Muxer
|
||||
file *os.File
|
||||
}
|
||||
|
||||
func (r *Recorder) writeTailer(end time.Time) {
|
||||
r.stream.EndTime = end
|
||||
if r.RecordJob.Plugin.DB != nil {
|
||||
r.RecordJob.Plugin.DB.Save(&r.stream)
|
||||
writeTrailerQueueTask.AddTask(&eventRecordCheck{
|
||||
DB: r.RecordJob.Plugin.DB,
|
||||
streamPath: r.stream.StreamPath,
|
||||
})
|
||||
}
|
||||
r.WriteTail(end, &writeTrailerQueueTask)
|
||||
writeTrailerQueueTask.AddTask(&writeTrailerTask{
|
||||
muxer: r.muxer,
|
||||
file: r.file,
|
||||
@@ -178,46 +136,7 @@ var CustomFileName = func(job *m7s.RecordJob) string {
|
||||
}
|
||||
|
||||
func (r *Recorder) createStream(start time.Time) (err error) {
|
||||
recordJob := &r.RecordJob
|
||||
sub := recordJob.Subscriber
|
||||
r.stream = m7s.RecordStream{
|
||||
StartTime: start,
|
||||
StreamPath: sub.StreamPath,
|
||||
FilePath: CustomFileName(&r.RecordJob),
|
||||
EventId: recordJob.EventId,
|
||||
EventDesc: recordJob.EventDesc,
|
||||
EventName: recordJob.EventName,
|
||||
EventLevel: recordJob.EventLevel,
|
||||
BeforeDuration: recordJob.BeforeDuration,
|
||||
AfterDuration: recordJob.AfterDuration,
|
||||
Mode: recordJob.Mode,
|
||||
Type: "mp4",
|
||||
}
|
||||
dir := filepath.Dir(r.stream.FilePath)
|
||||
if err = os.MkdirAll(dir, 0755); err != nil {
|
||||
return
|
||||
}
|
||||
r.file, err = os.Create(r.stream.FilePath)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if recordJob.RecConf.Type == "fmp4" {
|
||||
r.stream.Type = "fmp4"
|
||||
r.muxer = NewMuxerWithStreamPath(FLAG_FRAGMENT, r.stream.StreamPath)
|
||||
} else {
|
||||
r.muxer = NewMuxerWithStreamPath(0, r.stream.StreamPath)
|
||||
}
|
||||
r.muxer.WriteInitSegment(r.file)
|
||||
if sub.Publisher.HasAudioTrack() {
|
||||
r.stream.AudioCodec = sub.Publisher.AudioTrack.ICodecCtx.String()
|
||||
}
|
||||
if sub.Publisher.HasVideoTrack() {
|
||||
r.stream.VideoCodec = sub.Publisher.VideoTrack.ICodecCtx.String()
|
||||
}
|
||||
if recordJob.Plugin.DB != nil {
|
||||
recordJob.Plugin.DB.Save(&r.stream)
|
||||
}
|
||||
return
|
||||
return r.CreateStream(start, CustomFileName)
|
||||
}
|
||||
|
||||
func (r *Recorder) Dispose() {
|
||||
@@ -231,17 +150,28 @@ func (r *Recorder) Run() (err error) {
|
||||
sub := recordJob.Subscriber
|
||||
var audioTrack, videoTrack *Track
|
||||
startTime := time.Now()
|
||||
if recordJob.BeforeDuration > 0 {
|
||||
startTime = startTime.Add(-recordJob.BeforeDuration)
|
||||
if recordJob.Event != nil {
|
||||
startTime = startTime.Add(-time.Duration(recordJob.Event.BeforeDuration) * time.Millisecond)
|
||||
}
|
||||
err = r.createStream(startTime)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
r.file, err = os.Create(r.Event.FilePath)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if recordJob.RecConf.Type == "fmp4" {
|
||||
r.Event.Type = "fmp4"
|
||||
r.muxer = NewMuxerWithStreamPath(FLAG_FRAGMENT, r.Event.StreamPath)
|
||||
} else {
|
||||
r.muxer = NewMuxerWithStreamPath(0, r.Event.StreamPath)
|
||||
}
|
||||
r.muxer.WriteInitSegment(r.file)
|
||||
var at, vt *pkg.AVTrack
|
||||
|
||||
checkEventRecordStop := func(absTime uint32) (err error) {
|
||||
if duration := int64(absTime); time.Duration(duration)*time.Millisecond >= recordJob.AfterDuration+recordJob.BeforeDuration {
|
||||
if absTime >= recordJob.Event.AfterDuration+recordJob.Event.BeforeDuration {
|
||||
r.RecordJob.Stop(task.ErrStopByUser)
|
||||
}
|
||||
return
|
||||
@@ -269,8 +199,9 @@ func (r *Recorder) Run() (err error) {
|
||||
}
|
||||
|
||||
return m7s.PlayBlock(sub, func(audio *pkg.RawAudio) error {
|
||||
r.Event.Duration = sub.AudioReader.AbsTime
|
||||
if sub.VideoReader == nil {
|
||||
if recordJob.AfterDuration != 0 {
|
||||
if recordJob.Event != nil {
|
||||
err := checkEventRecordStop(sub.VideoReader.AbsTime)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -313,8 +244,9 @@ func (r *Recorder) Run() (err error) {
|
||||
Timestamp: uint32(dts),
|
||||
})
|
||||
}, func(video *rtmp.RTMPVideo) error {
|
||||
r.Event.Duration = sub.VideoReader.AbsTime
|
||||
if sub.VideoReader.Value.IDR {
|
||||
if recordJob.AfterDuration != 0 {
|
||||
if recordJob.Event != nil {
|
||||
err := checkEventRecordStop(sub.VideoReader.AbsTime)
|
||||
if err != nil {
|
||||
return err
|
||||
|
@@ -185,8 +185,6 @@ func (t *RecordRecoveryTask) recoverRecordFromFile(filePath string) error {
|
||||
FilePath: filePath,
|
||||
StreamPath: streamPath,
|
||||
Type: "mp4",
|
||||
Mode: m7s.RecordModeAuto, // 默认为自动录制模式
|
||||
EventLevel: m7s.EventLevelLow, // 默认为低级别事件
|
||||
}
|
||||
|
||||
// 设置开始和结束时间
|
||||
@@ -220,8 +218,8 @@ func extractStreamPathFromMP4(demuxer *mp4.Demuxer) string {
|
||||
moov := demuxer.GetMoovBox()
|
||||
if moov != nil && moov.UDTA != nil {
|
||||
for _, entry := range moov.UDTA.Entries {
|
||||
if streamPathBox, ok := entry.(*box.StreamPathBox); ok {
|
||||
return streamPathBox.StreamPath
|
||||
if entry.Type() == box.TypeALB {
|
||||
return entry.(*box.TextDataBox).Text
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -79,10 +79,10 @@ func (nc *NetConnection) Handshake(checkC2 bool) (err error) {
|
||||
if len(C1) != C1S1_SIZE {
|
||||
return errors.New("C1 Error")
|
||||
}
|
||||
var ts int
|
||||
util.GetBE(C1[4:8], &ts)
|
||||
var zero int
|
||||
util.GetBE(C1[4:8], &zero)
|
||||
|
||||
if ts == 0 {
|
||||
if zero == 0 {
|
||||
return nc.simple_handshake(C1, checkC2)
|
||||
}
|
||||
|
||||
@@ -92,12 +92,26 @@ func (nc *NetConnection) Handshake(checkC2 bool) (err error) {
|
||||
func (nc *NetConnection) ClientHandshake() (err error) {
|
||||
C0C1 := nc.mediaDataPool.NextN(C1S1_SIZE + 1)
|
||||
defer nc.mediaDataPool.Recycle()
|
||||
|
||||
// 构造 C0
|
||||
C0C1[0] = RTMP_HANDSHAKE_VERSION
|
||||
|
||||
// 构造 C1 使用简单握手格式
|
||||
C1 := C0C1[1:]
|
||||
// Time (4 bytes): 当前时间戳
|
||||
util.PutBE(C1[0:4], time.Now().Unix()&0xFFFFFFFF)
|
||||
// Zero (4 bytes): 必须为 0,确保使用简单握手
|
||||
util.PutBE(C1[4:8], 0)
|
||||
// Random data (1528 bytes): 填充随机数据
|
||||
for i := 8; i < C1S1_SIZE; i++ {
|
||||
C1[i] = byte(rand.Int() % 256)
|
||||
}
|
||||
|
||||
if _, err = nc.Write(C0C1); err == nil {
|
||||
// read S0 S1
|
||||
if _, err = io.ReadFull(nc.Conn, C0C1); err == nil {
|
||||
if C0C1[0] != RTMP_HANDSHAKE_VERSION {
|
||||
err = errors.New("S1 C1 Error")
|
||||
err = errors.New("S0 Error")
|
||||
// C2
|
||||
} else if _, err = nc.Write(C0C1[1:]); err == nil {
|
||||
_, err = io.ReadFull(nc.Conn, C0C1[1:]) // S2
|
||||
@@ -222,13 +236,7 @@ func clientScheme(C1 []byte, schem int) (scheme int, challenge []byte, digest []
|
||||
return 0, nil, nil, false, err
|
||||
}
|
||||
|
||||
// ok
|
||||
if bytes.Compare(digest, tmp_Hash) == 0 {
|
||||
ok = true
|
||||
} else {
|
||||
ok = false
|
||||
}
|
||||
|
||||
ok = bytes.Equal(digest, tmp_Hash)
|
||||
// challenge scheme
|
||||
challenge = C1[key_offset : key_offset+C1S1_KEY_DATA_SIZE]
|
||||
scheme = schem
|
||||
|
@@ -40,7 +40,11 @@ func (conf *WebRTCPlugin) servePush(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
conn.SDP = string(bytes)
|
||||
conn.Logger = conf.Logger
|
||||
if conn.PeerConnection, err = conf.api.NewPeerConnection(Configuration{
|
||||
|
||||
if conn.PeerConnection, err = conf.CreatePC(SessionDescription{
|
||||
Type: SDPTypeOffer,
|
||||
SDP: conn.SDP,
|
||||
}, Configuration{
|
||||
ICEServers: conf.ICEServers,
|
||||
}); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
@@ -56,10 +60,7 @@ func (conf *WebRTCPlugin) servePush(w http.ResponseWriter, r *http.Request) {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
if err := conn.SetRemoteDescription(SessionDescription{Type: SDPTypeOffer, SDP: conn.SDP}); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
if answer, err := conn.GetAnswer(); err == nil {
|
||||
w.WriteHeader(http.StatusCreated)
|
||||
fmt.Fprint(w, answer.SDP)
|
||||
@@ -89,9 +90,15 @@ func (conf *WebRTCPlugin) servePlay(w http.ResponseWriter, r *http.Request) {
|
||||
if strings.Contains(strings.ToLower(conn.SDP), "h265") {
|
||||
conn.SupportsH265 = true
|
||||
}
|
||||
if conn.PeerConnection, err = conf.api.NewPeerConnection(Configuration{
|
||||
|
||||
conn.PeerConnection, err = conf.CreatePC(SessionDescription{
|
||||
Type: SDPTypeOffer,
|
||||
SDP: conn.SDP,
|
||||
}, Configuration{
|
||||
ICEServers: conf.ICEServers,
|
||||
}); err != nil {
|
||||
})
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
if rawQuery != "" {
|
||||
@@ -106,10 +113,7 @@ func (conf *WebRTCPlugin) servePlay(w http.ResponseWriter, r *http.Request) {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
if err = conn.SetRemoteDescription(SessionDescription{Type: SDPTypeOffer, SDP: conn.SDP}); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
if sdp, err := conn.GetAnswer(); err == nil {
|
||||
w.Write([]byte(sdp.SDP))
|
||||
} else {
|
||||
|
@@ -2,6 +2,7 @@ package plugin_webrtc
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"net"
|
||||
"net/http"
|
||||
"strings"
|
||||
@@ -35,17 +36,7 @@ func (conf *WebRTCPlugin) BatchV2(w http.ResponseWriter, r *http.Request) {
|
||||
conn: wsConn,
|
||||
config: conf,
|
||||
}
|
||||
// 创建PeerConnection并设置高级配置
|
||||
if wsHandler.PeerConnection, err = conf.api.NewPeerConnection(Configuration{
|
||||
// 本地测试不需要配置 ICE 服务器
|
||||
ICETransportPolicy: ICETransportPolicyAll,
|
||||
BundlePolicy: BundlePolicyMaxBundle,
|
||||
RTCPMuxPolicy: RTCPMuxPolicyRequire,
|
||||
ICECandidatePoolSize: 1,
|
||||
}); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// 添加任务
|
||||
conf.AddTask(wsHandler).WaitStopped()
|
||||
}
|
||||
@@ -89,15 +80,43 @@ func (wsh *WebSocketHandler) Go() (err error) {
|
||||
if strings.Contains(strings.ToLower(wsh.SDP), "h265") {
|
||||
wsh.SupportsH265 = true
|
||||
}
|
||||
// 设置远程描述
|
||||
if err = wsh.SetRemoteDescription(SessionDescription{
|
||||
|
||||
if wsh.PeerConnection, err = wsh.config.CreatePC(SessionDescription{
|
||||
Type: SDPTypeOffer,
|
||||
SDP: initialSignal.SDP,
|
||||
}, Configuration{
|
||||
// 本地测试不需要配置 ICE 服务器
|
||||
ICETransportPolicy: ICETransportPolicyAll,
|
||||
BundlePolicy: BundlePolicyMaxBundle,
|
||||
RTCPMuxPolicy: RTCPMuxPolicyRequire,
|
||||
ICECandidatePoolSize: 1,
|
||||
}); err != nil {
|
||||
wsh.Error("Failed to set remote description", "error", err)
|
||||
return err
|
||||
return
|
||||
}
|
||||
|
||||
wsh.OnICECandidate(func(ice *ICECandidate) {
|
||||
if ice != nil {
|
||||
wsh.Info(ice.ToJSON().Candidate)
|
||||
}
|
||||
})
|
||||
// 监听ICE连接状态变化
|
||||
wsh.OnICEConnectionStateChange(func(state ICEConnectionState) {
|
||||
wsh.Debug("ICE connection state changed", "state", state.String())
|
||||
if state == ICEConnectionStateFailed {
|
||||
wsh.Error("ICE connection failed")
|
||||
}
|
||||
})
|
||||
|
||||
wsh.OnConnectionStateChange(func(state PeerConnectionState) {
|
||||
wsh.Info("Connection State has changed:" + state.String())
|
||||
switch state {
|
||||
case PeerConnectionStateConnected:
|
||||
|
||||
case PeerConnectionStateDisconnected, PeerConnectionStateFailed, PeerConnectionStateClosed:
|
||||
wsh.Stop(errors.New("connection state:" + state.String()))
|
||||
}
|
||||
})
|
||||
|
||||
// 创建并发送应答
|
||||
if answer, err := wsh.GetAnswer(); err == nil {
|
||||
wsh.sendAnswer(answer.SDP)
|
||||
|
5
plugin/webrtc/default.yaml
Normal file
5
plugin/webrtc/default.yaml
Normal file
@@ -0,0 +1,5 @@
|
||||
mimetype:
|
||||
- video/H264
|
||||
- video/H265
|
||||
- audio/PCMA
|
||||
- audio/PCMU
|
@@ -2,16 +2,17 @@ package plugin_webrtc
|
||||
|
||||
import (
|
||||
"embed"
|
||||
_ "embed"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/pion/logging"
|
||||
"github.com/pion/sdp/v3"
|
||||
"m7s.live/v5/pkg/config"
|
||||
|
||||
"github.com/pion/interceptor"
|
||||
@@ -23,23 +24,26 @@ import (
|
||||
|
||||
var (
|
||||
//go:embed web
|
||||
web embed.FS
|
||||
reg_level = regexp.MustCompile("profile-level-id=(4.+f)")
|
||||
_ = m7s.InstallPlugin[WebRTCPlugin](NewPuller, NewPusher)
|
||||
web embed.FS
|
||||
//go:embed default.yaml
|
||||
defaultYaml m7s.DefaultYaml
|
||||
reg_level = regexp.MustCompile("profile-level-id=(4.+f)")
|
||||
_ = m7s.InstallPlugin[WebRTCPlugin](m7s.PluginMeta{
|
||||
DefaultYaml: defaultYaml,
|
||||
NewPuller: NewPuller,
|
||||
NewPusher: NewPusher,
|
||||
})
|
||||
)
|
||||
|
||||
type WebRTCPlugin struct {
|
||||
m7s.Plugin
|
||||
ICEServers []ICEServer `desc:"ice服务器配置"`
|
||||
Port string `default:"tcp:9000" desc:"监听端口"`
|
||||
PLI time.Duration `default:"2s" desc:"发送PLI请求间隔"` // 视频流丢包后,发送PLI请求
|
||||
EnableOpus bool `default:"true" desc:"是否启用opus编码"` // 是否启用opus编码
|
||||
EnableVP9 bool `default:"false" desc:"是否启用vp9编码"` // 是否启用vp9编码
|
||||
EnableAv1 bool `default:"false" desc:"是否启用av1编码"` // 是否启用av1编码
|
||||
EnableDC bool `default:"true" desc:"是否启用DataChannel"` // 在不支持编码格式的情况下是否启用DataChannel传输
|
||||
m MediaEngine
|
||||
s SettingEngine
|
||||
api *API
|
||||
ICEServers []ICEServer `desc:"ice服务器配置"`
|
||||
Port string `default:"tcp:9000" desc:"监听端口"`
|
||||
PLI time.Duration `default:"2s" desc:"发送PLI请求间隔"` // 视频流丢包后,发送PLI请求
|
||||
EnableDC bool `default:"true" desc:"是否启用DataChannel"` // 在不支持编码格式的情况下是否启用DataChannel传输
|
||||
MimeType []string `desc:"MimeType过滤列表,为空则不过滤"` // MimeType过滤列表,支持的格式如:video/H264, audio/opus
|
||||
s SettingEngine
|
||||
portMapping map[int]int // 内部端口到外部端口的映射
|
||||
}
|
||||
|
||||
func (p *WebRTCPlugin) RegisterHandler() map[string]http.HandlerFunc {
|
||||
@@ -54,6 +58,387 @@ func (p *WebRTCPlugin) NewLogger(scope string) logging.LeveledLogger {
|
||||
return &LoggerTransform{Logger: p.Logger.With("scope", scope)}
|
||||
}
|
||||
|
||||
// createMediaEngine 创建新的MediaEngine实例
|
||||
func (p *WebRTCPlugin) createMediaEngine(ssd *sdp.SessionDescription) *MediaEngine {
|
||||
m := &MediaEngine{}
|
||||
|
||||
// 如果没有提供SDP,则使用传统方式注册编解码器
|
||||
if ssd == nil {
|
||||
p.registerLegacyCodecs(m)
|
||||
return m
|
||||
}
|
||||
|
||||
// 从SDP中解析MediaDescription并注册编解码器
|
||||
p.registerCodecsFromSDP(m, ssd)
|
||||
|
||||
return m
|
||||
}
|
||||
|
||||
// registerLegacyCodecs 注册传统方式的编解码器(向后兼容)
|
||||
func (p *WebRTCPlugin) registerLegacyCodecs(m *MediaEngine) {
|
||||
// 注册基础编解码器
|
||||
if defaultCodecs, err := GetDefaultCodecs(); err != nil {
|
||||
p.Error("Failed to get default codecs", "error", err)
|
||||
} else {
|
||||
for _, codecWithType := range defaultCodecs {
|
||||
// 检查MimeType过滤
|
||||
if p.isMimeTypeAllowed(codecWithType.Codec.MimeType) {
|
||||
if err := m.RegisterCodec(codecWithType.Codec, codecWithType.Type); err != nil {
|
||||
p.Warn("Failed to register default codec", "error", err, "mimeType", codecWithType.Codec.MimeType)
|
||||
} else {
|
||||
p.Debug("Registered default codec", "mimeType", codecWithType.Codec.MimeType, "payloadType", codecWithType.Codec.PayloadType)
|
||||
}
|
||||
} else {
|
||||
p.Debug("Default codec filtered", "mimeType", codecWithType.Codec.MimeType)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// registerCodecsFromSDP 从SDP的MediaDescription中注册编解码器
|
||||
func (p *WebRTCPlugin) registerCodecsFromSDP(m *MediaEngine, ssd *sdp.SessionDescription) {
|
||||
for _, md := range ssd.MediaDescriptions {
|
||||
// 跳过非音视频媒体类型
|
||||
if md.MediaName.Media != "audio" && md.MediaName.Media != "video" {
|
||||
continue
|
||||
}
|
||||
|
||||
// 解析每个format(编解码器)
|
||||
for _, format := range md.MediaName.Formats {
|
||||
codec := p.parseCodecFromSDP(md, format)
|
||||
if codec == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
// 检查MimeType过滤
|
||||
if !p.isMimeTypeAllowed(codec.MimeType) {
|
||||
p.Debug("MimeType filtered", "mimeType", codec.MimeType)
|
||||
continue
|
||||
}
|
||||
|
||||
// 确定编解码器类型
|
||||
var codecType RTPCodecType
|
||||
if md.MediaName.Media == "audio" {
|
||||
codecType = RTPCodecTypeAudio
|
||||
} else {
|
||||
codecType = RTPCodecTypeVideo
|
||||
}
|
||||
|
||||
// 注册编解码器
|
||||
if err := m.RegisterCodec(*codec, codecType); err != nil {
|
||||
p.Warn("Failed to register codec from SDP", "error", err, "mimeType", codec.MimeType)
|
||||
} else {
|
||||
p.Debug("Registered codec from SDP", "mimeType", codec.MimeType, "payloadType", codec.PayloadType)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// parseCodecFromSDP 从SDP的MediaDescription中解析单个编解码器
|
||||
func (p *WebRTCPlugin) parseCodecFromSDP(md *sdp.MediaDescription, format string) *RTPCodecParameters {
|
||||
var codecName string
|
||||
var clockRate uint32
|
||||
var channels uint16
|
||||
var fmtpLine string
|
||||
|
||||
// 从rtpmap属性中解析编解码器名称、时钟频率和通道数
|
||||
for _, attr := range md.Attributes {
|
||||
if attr.Key == "rtpmap" && strings.HasPrefix(attr.Value, format+" ") {
|
||||
// 格式:payloadType codecName/clockRate[/channels]
|
||||
parts := strings.Split(attr.Value, " ")
|
||||
if len(parts) >= 2 {
|
||||
codecParts := strings.Split(parts[1], "/")
|
||||
if len(codecParts) >= 2 {
|
||||
codecName = strings.ToUpper(codecParts[0])
|
||||
if rate, err := strconv.ParseUint(codecParts[1], 10, 32); err == nil {
|
||||
clockRate = uint32(rate)
|
||||
}
|
||||
if len(codecParts) >= 3 {
|
||||
if ch, err := strconv.ParseUint(codecParts[2], 10, 16); err == nil {
|
||||
channels = uint16(ch)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// 从fmtp属性中解析格式参数
|
||||
for _, attr := range md.Attributes {
|
||||
if attr.Key == "fmtp" && strings.HasPrefix(attr.Value, format+" ") {
|
||||
if spaceIdx := strings.Index(attr.Value, " "); spaceIdx >= 0 {
|
||||
fmtpLine = attr.Value[spaceIdx+1:]
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// 如果没有找到rtpmap,尝试静态payload类型
|
||||
if codecName == "" {
|
||||
codecName, clockRate, channels = p.getStaticPayloadInfo(format)
|
||||
}
|
||||
|
||||
if codecName == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
// 构造MimeType
|
||||
var mimeType string
|
||||
if md.MediaName.Media == "audio" {
|
||||
mimeType = "audio/" + codecName
|
||||
} else {
|
||||
mimeType = "video/" + codecName
|
||||
}
|
||||
|
||||
// 解析PayloadType
|
||||
payloadType, err := strconv.ParseUint(format, 10, 8)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return &RTPCodecParameters{
|
||||
RTPCodecCapability: RTPCodecCapability{
|
||||
MimeType: mimeType,
|
||||
ClockRate: clockRate,
|
||||
Channels: channels,
|
||||
SDPFmtpLine: fmtpLine,
|
||||
RTCPFeedback: p.getDefaultRTCPFeedback(mimeType),
|
||||
},
|
||||
PayloadType: PayloadType(payloadType),
|
||||
}
|
||||
}
|
||||
|
||||
// getStaticPayloadInfo 获取静态payload类型的编解码器信息
|
||||
func (p *WebRTCPlugin) getStaticPayloadInfo(format string) (string, uint32, uint16) {
|
||||
switch format {
|
||||
case "0":
|
||||
return "PCMU", 8000, 1
|
||||
case "8":
|
||||
return "PCMA", 8000, 1
|
||||
case "96":
|
||||
return "H264", 90000, 0
|
||||
case "97":
|
||||
return "H264", 90000, 0
|
||||
case "98":
|
||||
return "H264", 90000, 0
|
||||
case "111":
|
||||
return "OPUS", 48000, 2
|
||||
default:
|
||||
return "", 0, 0
|
||||
}
|
||||
}
|
||||
|
||||
// getDefaultRTCPFeedback 获取默认的RTCP反馈
|
||||
func (p *WebRTCPlugin) getDefaultRTCPFeedback(mimeType string) []RTCPFeedback {
|
||||
if strings.HasPrefix(mimeType, "video/") {
|
||||
return []RTCPFeedback{
|
||||
{Type: "goog-remb", Parameter: ""},
|
||||
{Type: "ccm", Parameter: "fir"},
|
||||
{Type: "nack", Parameter: ""},
|
||||
{Type: "nack", Parameter: "pli"},
|
||||
{Type: "transport-cc", Parameter: ""},
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// isMimeTypeAllowed 检查MimeType是否在允许列表中
|
||||
func (p *WebRTCPlugin) isMimeTypeAllowed(mimeType string) bool {
|
||||
// 如果过滤列表为空,则允许所有类型
|
||||
if len(p.MimeType) == 0 {
|
||||
return true
|
||||
}
|
||||
|
||||
// 检查精确匹配
|
||||
for _, filter := range p.MimeType {
|
||||
if strings.EqualFold(filter, mimeType) {
|
||||
return true
|
||||
}
|
||||
// 支持通配符匹配,如 "video/*" 或 "audio/*"
|
||||
if strings.HasSuffix(filter, "/*") {
|
||||
prefix := strings.TrimSuffix(filter, "/*")
|
||||
if strings.HasPrefix(strings.ToLower(mimeType), strings.ToLower(prefix)+"/") {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// createAPI 创建新的API实例
|
||||
func (p *WebRTCPlugin) createAPI(ssd *sdp.SessionDescription) (api *API, err error) {
|
||||
m := p.createMediaEngine(ssd)
|
||||
i := &interceptor.Registry{}
|
||||
|
||||
// 注册默认拦截器
|
||||
if err := RegisterDefaultInterceptors(m, i); err != nil {
|
||||
p.Error("register default interceptors error", "error", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 创建API
|
||||
api = NewAPI(WithMediaEngine(m), WithInterceptorRegistry(i), WithSettingEngine(p.s))
|
||||
return
|
||||
}
|
||||
|
||||
// initSettingEngine 初始化SettingEngine
|
||||
func (p *WebRTCPlugin) initSettingEngine() error {
|
||||
// 设置LoggerFactory
|
||||
p.s.LoggerFactory = p
|
||||
|
||||
// 配置NAT 1:1 IP映射
|
||||
if p.GetCommonConf().PublicIP != "" {
|
||||
ips := []string{p.GetCommonConf().PublicIP}
|
||||
if p.GetCommonConf().PublicIPv6 != "" {
|
||||
ips = append(ips, p.GetCommonConf().PublicIPv6)
|
||||
}
|
||||
p.s.SetNAT1To1IPs(ips, ICECandidateTypeHost)
|
||||
}
|
||||
|
||||
// 配置端口
|
||||
if err := p.configurePort(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// configurePort 配置端口设置
|
||||
func (p *WebRTCPlugin) configurePort() error {
|
||||
// 使用 ParsePort 而不是 ParsePort2 来获取端口映射信息
|
||||
portInfo, err := ParsePort(p.Port)
|
||||
if err != nil {
|
||||
p.Error("webrtc port config error", "error", err, "port", p.Port)
|
||||
return err
|
||||
}
|
||||
|
||||
// 初始化端口映射
|
||||
p.portMapping = make(map[int]int)
|
||||
|
||||
// 如果有端口映射,存储映射关系
|
||||
if portInfo.HasMapping() {
|
||||
if portInfo.IsRange() {
|
||||
// 端口范围映射
|
||||
for i := 0; i <= portInfo.Ports[1]-portInfo.Ports[0]; i++ {
|
||||
internalPort := portInfo.Ports[0] + i
|
||||
var externalPort int
|
||||
if portInfo.IsRangeMapping() {
|
||||
// 映射端口也是范围
|
||||
externalPort = portInfo.Map[0] + i
|
||||
} else {
|
||||
// 映射端口是单个端口
|
||||
externalPort = portInfo.Map[0]
|
||||
}
|
||||
p.portMapping[internalPort] = externalPort
|
||||
}
|
||||
} else {
|
||||
// 单端口映射
|
||||
p.portMapping[portInfo.Ports[0]] = portInfo.Map[0]
|
||||
}
|
||||
p.Info("Port mapping configured", "mapping", p.portMapping)
|
||||
}
|
||||
|
||||
// 根据协议类型进行配置
|
||||
if portInfo.IsTCP() {
|
||||
if portInfo.IsRange() {
|
||||
// TCP端口范围,这里可能需要特殊处理
|
||||
p.Error("TCP port range not supported in current implementation")
|
||||
return fmt.Errorf("TCP port range not supported")
|
||||
} else {
|
||||
// TCP单端口
|
||||
tcpport := portInfo.Ports[0]
|
||||
tcpl, err := net.ListenTCP("tcp", &net.TCPAddr{
|
||||
IP: net.IP{0, 0, 0, 0},
|
||||
Port: tcpport,
|
||||
})
|
||||
p.OnDispose(func() {
|
||||
_ = tcpl.Close()
|
||||
})
|
||||
if err != nil {
|
||||
p.Error("webrtc listener tcp", "error", err)
|
||||
return err
|
||||
}
|
||||
p.SetDescription("tcp", fmt.Sprintf("%d", tcpport))
|
||||
p.Info("webrtc start listen", "port", tcpport)
|
||||
p.s.SetICETCPMux(NewICETCPMux(nil, tcpl, 4096))
|
||||
p.s.SetNetworkTypes([]NetworkType{NetworkTypeTCP4, NetworkTypeTCP6})
|
||||
p.s.DisableSRTPReplayProtection(true)
|
||||
}
|
||||
} else {
|
||||
// UDP配置
|
||||
if portInfo.IsRange() {
|
||||
// UDP端口范围
|
||||
p.s.SetEphemeralUDPPortRange(uint16(portInfo.Ports[0]), uint16(portInfo.Ports[1]))
|
||||
p.SetDescription("udp", fmt.Sprintf("%d-%d", portInfo.Ports[0], portInfo.Ports[1]))
|
||||
} else {
|
||||
// UDP单端口
|
||||
udpport := portInfo.Ports[0]
|
||||
udpListener, err := net.ListenUDP("udp", &net.UDPAddr{
|
||||
IP: net.IP{0, 0, 0, 0},
|
||||
Port: udpport,
|
||||
})
|
||||
p.OnDispose(func() {
|
||||
_ = udpListener.Close()
|
||||
})
|
||||
if err != nil {
|
||||
p.Error("webrtc listener udp", "error", err)
|
||||
return err
|
||||
}
|
||||
p.SetDescription("udp", fmt.Sprintf("%d", udpport))
|
||||
p.Info("webrtc start listen", "port", udpport)
|
||||
p.s.SetICEUDPMux(NewICEUDPMux(nil, udpListener))
|
||||
p.s.SetNetworkTypes([]NetworkType{NetworkTypeUDP4, NetworkTypeUDP6})
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *WebRTCPlugin) CreatePC(sd SessionDescription, conf Configuration) (pc *PeerConnection, err error) {
|
||||
var ssd *sdp.SessionDescription
|
||||
ssd, err = sd.Unmarshal()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
var api *API
|
||||
// 创建PeerConnection并设置高级配置
|
||||
api, err = p.createAPI(ssd)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
pc, err = api.NewPeerConnection(conf)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// 如果有端口映射配置,记录 ICE 候选者信息以供调试
|
||||
if len(p.portMapping) > 0 {
|
||||
pc.OnICECandidate(func(candidate *ICECandidate) {
|
||||
if candidate != nil {
|
||||
// 记录端口映射信息(用于调试和监控)
|
||||
if mappedPort, exists := p.portMapping[int(candidate.Port)]; exists {
|
||||
p.Debug("ICE candidate with port mapping detected",
|
||||
"original_port", candidate.Port,
|
||||
"mapped_port", mappedPort,
|
||||
"candidate_address", candidate.Address,
|
||||
"candidate_type", candidate.Typ)
|
||||
candidate.Port = uint16(mappedPort) // 更新候选者端口为映射后的端口
|
||||
} else {
|
||||
p.Debug("ICE candidate generated",
|
||||
"port", candidate.Port,
|
||||
"address", candidate.Address,
|
||||
"type", candidate.Typ)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
err = pc.SetRemoteDescription(sd)
|
||||
return
|
||||
}
|
||||
|
||||
func (p *WebRTCPlugin) OnInit() (err error) {
|
||||
if len(p.ICEServers) > 0 {
|
||||
for i := range p.ICEServers {
|
||||
@@ -62,84 +447,10 @@ func (p *WebRTCPlugin) OnInit() (err error) {
|
||||
}
|
||||
}
|
||||
|
||||
p.s.LoggerFactory = p
|
||||
RegisterCodecs(&p.m)
|
||||
if p.EnableOpus {
|
||||
p.m.RegisterCodec(RTPCodecParameters{
|
||||
RTPCodecCapability: RTPCodecCapability{MimeTypeOpus, 48000, 2, "minptime=10;useinbandfec=1", nil},
|
||||
PayloadType: 111,
|
||||
}, RTPCodecTypeAudio)
|
||||
}
|
||||
if p.EnableVP9 {
|
||||
p.m.RegisterCodec(RTPCodecParameters{
|
||||
RTPCodecCapability: RTPCodecCapability{MimeTypeVP9, 90000, 0, "", nil},
|
||||
PayloadType: 100,
|
||||
}, RTPCodecTypeVideo)
|
||||
}
|
||||
if p.EnableAv1 {
|
||||
p.m.RegisterCodec(RTPCodecParameters{
|
||||
RTPCodecCapability: RTPCodecCapability{MimeTypeAV1, 90000, 0, "profile=2;level-idx=8;tier=1", nil},
|
||||
PayloadType: 45,
|
||||
}, RTPCodecTypeVideo)
|
||||
}
|
||||
i := &interceptor.Registry{}
|
||||
if p.GetCommonConf().PublicIP != "" {
|
||||
ips := []string{p.GetCommonConf().PublicIP}
|
||||
if p.GetCommonConf().PublicIPv6 != "" {
|
||||
ips = append(ips, p.GetCommonConf().PublicIPv6)
|
||||
}
|
||||
p.s.SetNAT1To1IPs(ips, ICECandidateTypeHost)
|
||||
}
|
||||
ports, err := ParsePort2(p.Port)
|
||||
if err != nil {
|
||||
p.Error("webrtc port config error", "error", err, "port", p.Port)
|
||||
if err = p.initSettingEngine(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
switch v := ports.(type) {
|
||||
case TCPPort:
|
||||
tcpport := int(v)
|
||||
tcpl, err := net.ListenTCP("tcp", &net.TCPAddr{
|
||||
IP: net.IP{0, 0, 0, 0},
|
||||
Port: tcpport,
|
||||
})
|
||||
p.OnDispose(func() {
|
||||
_ = tcpl.Close()
|
||||
})
|
||||
if err != nil {
|
||||
p.Error("webrtc listener tcp", "error", err)
|
||||
}
|
||||
p.SetDescription("tcp", fmt.Sprintf("%d", tcpport))
|
||||
p.Info("webrtc start listen", "port", tcpport)
|
||||
p.s.SetICETCPMux(NewICETCPMux(nil, tcpl, 4096))
|
||||
p.s.SetNetworkTypes([]NetworkType{NetworkTypeTCP4, NetworkTypeTCP6})
|
||||
p.s.DisableSRTPReplayProtection(true)
|
||||
case UDPRangePort:
|
||||
p.s.SetEphemeralUDPPortRange(uint16(v[0]), uint16(v[1]))
|
||||
p.SetDescription("udp", fmt.Sprintf("%d-%d", v[0], v[1]))
|
||||
case UDPPort:
|
||||
// 创建共享WEBRTC端口 默认9000
|
||||
udpListener, err := net.ListenUDP("udp", &net.UDPAddr{
|
||||
IP: net.IP{0, 0, 0, 0},
|
||||
Port: int(v),
|
||||
})
|
||||
p.OnDispose(func() {
|
||||
_ = udpListener.Close()
|
||||
})
|
||||
if err != nil {
|
||||
p.Error("webrtc listener udp", "error", err)
|
||||
return err
|
||||
}
|
||||
p.SetDescription("udp", fmt.Sprintf("%d", v))
|
||||
p.Info("webrtc start listen", "port", v)
|
||||
p.s.SetICEUDPMux(NewICEUDPMux(nil, udpListener))
|
||||
p.s.SetNetworkTypes([]NetworkType{NetworkTypeUDP4, NetworkTypeUDP6})
|
||||
}
|
||||
if err = RegisterDefaultInterceptors(&p.m, i); err != nil {
|
||||
return err
|
||||
}
|
||||
p.api = NewAPI(WithMediaEngine(&p.m),
|
||||
WithInterceptorRegistry(i), WithSettingEngine(p.s))
|
||||
_, port, _ := strings.Cut(p.GetCommonConf().HTTP.ListenAddr, ":")
|
||||
if port == "80" {
|
||||
p.PushAddr = append(p.PushAddr, "http://{hostName}/webrtc/push")
|
||||
@@ -203,8 +514,12 @@ func (p *WebRTCPlugin) testPage(w http.ResponseWriter, r *http.Request) {
|
||||
func (p *WebRTCPlugin) Pull(streamPath string, conf config.Pull, pubConf *config.Publish) {
|
||||
if strings.HasPrefix(conf.URL, "https://rtc.live.cloudflare.com") {
|
||||
cfClient := NewCFClient(DIRECTION_PULL)
|
||||
var err error
|
||||
cfClient.PeerConnection, err = p.api.NewPeerConnection(Configuration{
|
||||
api, err := p.createAPI(nil)
|
||||
if err != nil {
|
||||
p.Error("create API failed", "error", err)
|
||||
return
|
||||
}
|
||||
cfClient.PeerConnection, err = api.NewPeerConnection(Configuration{
|
||||
ICEServers: p.ICEServers,
|
||||
BundlePolicy: BundlePolicyMaxBundle,
|
||||
})
|
||||
|
@@ -2,6 +2,7 @@ package webrtc
|
||||
|
||||
import (
|
||||
"m7s.live/v5"
|
||||
"m7s.live/v5/pkg/config"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -31,7 +32,7 @@ func (c *Client) GetPushJob() *m7s.PushJob {
|
||||
return &c.pushCtx
|
||||
}
|
||||
|
||||
func NewPuller() m7s.IPuller {
|
||||
func NewPuller(config.Pull) m7s.IPuller {
|
||||
return &Client{
|
||||
direction: DIRECTION_PULL,
|
||||
}
|
||||
|
@@ -4,7 +4,17 @@ import (
|
||||
. "github.com/pion/webrtc/v4"
|
||||
)
|
||||
|
||||
func RegisterCodecs(m *MediaEngine) error {
|
||||
var videoRTCPFeedback = []RTCPFeedback{{"goog-remb", ""}, {"ccm", "fir"}, {"nack", ""}, {"nack", "pli"}, {"transport-cc", ""}}
|
||||
|
||||
type CodecWithType struct {
|
||||
Codec RTPCodecParameters
|
||||
Type RTPCodecType
|
||||
}
|
||||
|
||||
func GetDefaultCodecs() ([]CodecWithType, error) {
|
||||
var codecs []CodecWithType
|
||||
|
||||
// 音频编解码器
|
||||
for _, codec := range []RTPCodecParameters{
|
||||
{
|
||||
RTPCodecCapability: RTPCodecCapability{MimeTypePCMU, 8000, 0, "", nil},
|
||||
@@ -14,12 +24,15 @@ func RegisterCodecs(m *MediaEngine) error {
|
||||
RTPCodecCapability: RTPCodecCapability{MimeTypePCMA, 8000, 0, "", nil},
|
||||
PayloadType: 8,
|
||||
},
|
||||
{
|
||||
RTPCodecCapability: RTPCodecCapability{MimeTypeOpus, 48000, 2, "minptime=10;useinbandfec=1", nil},
|
||||
PayloadType: 111,
|
||||
},
|
||||
} {
|
||||
if err := m.RegisterCodec(codec, RTPCodecTypeAudio); err != nil {
|
||||
return err
|
||||
}
|
||||
codecs = append(codecs, CodecWithType{Codec: codec, Type: RTPCodecTypeAudio})
|
||||
}
|
||||
videoRTCPFeedback := []RTCPFeedback{{"goog-remb", ""}, {"ccm", "fir"}, {"nack", ""}, {"nack", "pli"}}
|
||||
|
||||
// 视频编解码器
|
||||
for _, codec := range []RTPCodecParameters{
|
||||
// {
|
||||
// RTPCodecCapability: RTPCodecCapability{"video/rtx", 90000, 0, "apt=96", nil},
|
||||
@@ -95,10 +108,28 @@ func RegisterCodecs(m *MediaEngine) error {
|
||||
RTPCodecCapability: RTPCodecCapability{MimeTypeH265, 90000, 0, "level-id=180;profile-id=1;tier-flag=0;tx-mode=SRST", videoRTCPFeedback},
|
||||
PayloadType: 49,
|
||||
},
|
||||
{
|
||||
RTPCodecCapability: RTPCodecCapability{MimeTypeH265, 90000, 0, "level-id=186;profile-id=1;tier-flag=0;tx-mode=SRST", videoRTCPFeedback},
|
||||
PayloadType: 50,
|
||||
},
|
||||
{
|
||||
RTPCodecCapability: RTPCodecCapability{MimeTypeH265, 90000, 0, "level-id=180;profile-id=2;tier-flag=0;tx-mode=SRST", videoRTCPFeedback},
|
||||
PayloadType: 51,
|
||||
},
|
||||
{
|
||||
RTPCodecCapability: RTPCodecCapability{MimeTypeH265, 90000, 0, "level-id=186;profile-id=2;tier-flag=0;tx-mode=SRST", videoRTCPFeedback},
|
||||
PayloadType: 52,
|
||||
},
|
||||
{
|
||||
RTPCodecCapability: RTPCodecCapability{MimeTypeVP9, 90000, 0, "", nil},
|
||||
PayloadType: 100,
|
||||
},
|
||||
{
|
||||
RTPCodecCapability: RTPCodecCapability{MimeTypeAV1, 90000, 0, "profile=2;level-idx=8;tier=1", nil},
|
||||
PayloadType: 45,
|
||||
},
|
||||
} {
|
||||
if err := m.RegisterCodec(codec, RTPCodecTypeVideo); err != nil {
|
||||
return err
|
||||
}
|
||||
codecs = append(codecs, CodecWithType{Codec: codec, Type: RTPCodecTypeVideo})
|
||||
}
|
||||
return nil
|
||||
return codecs, nil
|
||||
}
|
||||
|
@@ -241,7 +241,7 @@ func (IO *MultipleConnection) SendSubscriber(subscriber *m7s.Subscriber) (audioS
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
rcc.RTCPFeedback = videoRTCPFeedback
|
||||
videoTLSRTP, err = NewTrackLocalStaticRTP(rcc.RTPCodecCapability, videoCodec.String(), subscriber.StreamPath)
|
||||
if err != nil {
|
||||
return
|
||||
@@ -452,32 +452,6 @@ type SingleConnection struct {
|
||||
Connection
|
||||
}
|
||||
|
||||
func (c *SingleConnection) Start() (err error) {
|
||||
c.OnICECandidate(func(ice *ICECandidate) {
|
||||
if ice != nil {
|
||||
c.Info(ice.ToJSON().Candidate)
|
||||
}
|
||||
})
|
||||
// 监听ICE连接状态变化
|
||||
c.OnICEConnectionStateChange(func(state ICEConnectionState) {
|
||||
c.Debug("ICE connection state changed", "state", state.String())
|
||||
if state == ICEConnectionStateFailed {
|
||||
c.Error("ICE connection failed")
|
||||
}
|
||||
})
|
||||
|
||||
c.OnConnectionStateChange(func(state PeerConnectionState) {
|
||||
c.Info("Connection State has changed:" + state.String())
|
||||
switch state {
|
||||
case PeerConnectionStateConnected:
|
||||
|
||||
case PeerConnectionStateDisconnected, PeerConnectionStateFailed, PeerConnectionStateClosed:
|
||||
c.Stop(errors.New("connection state:" + state.String()))
|
||||
}
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
func (c *SingleConnection) Receive() {
|
||||
c.OnTrack(func(track *TrackRemote, receiver *RTPReceiver) {
|
||||
c.Info("OnTrack", "kind", track.Kind().String(), "payloadType", uint8(track.Codec().PayloadType))
|
||||
|
@@ -76,13 +76,10 @@ func (s *Server) Collect(ch chan<- prometheus.Metric) {
|
||||
ch <- prometheus.MustNewConstMetric(s.prometheusDesc.Net.ReceiveSpeed, prometheus.GaugeValue, float64(net.ReceiveSpeed), net.Name)
|
||||
}
|
||||
}
|
||||
s.Call(func() error {
|
||||
for stream := range s.Streams.Range {
|
||||
ch <- prometheus.MustNewConstMetric(s.prometheusDesc.BPS, prometheus.GaugeValue, float64(stream.VideoTrack.AVTrack.BPS), stream.StreamPath, stream.Plugin.Meta.Name, "video")
|
||||
ch <- prometheus.MustNewConstMetric(s.prometheusDesc.FPS, prometheus.GaugeValue, float64(stream.VideoTrack.AVTrack.FPS), stream.StreamPath, stream.Plugin.Meta.Name, "video")
|
||||
ch <- prometheus.MustNewConstMetric(s.prometheusDesc.BPS, prometheus.GaugeValue, float64(stream.AudioTrack.AVTrack.BPS), stream.StreamPath, stream.Plugin.Meta.Name, "audio")
|
||||
ch <- prometheus.MustNewConstMetric(s.prometheusDesc.FPS, prometheus.GaugeValue, float64(stream.AudioTrack.AVTrack.FPS), stream.StreamPath, stream.Plugin.Meta.Name, "audio")
|
||||
}
|
||||
return nil
|
||||
})
|
||||
for stream := range s.Streams.SafeRange {
|
||||
ch <- prometheus.MustNewConstMetric(s.prometheusDesc.BPS, prometheus.GaugeValue, float64(stream.VideoTrack.AVTrack.BPS), stream.StreamPath, stream.Plugin.Meta.Name, "video")
|
||||
ch <- prometheus.MustNewConstMetric(s.prometheusDesc.FPS, prometheus.GaugeValue, float64(stream.VideoTrack.AVTrack.FPS), stream.StreamPath, stream.Plugin.Meta.Name, "video")
|
||||
ch <- prometheus.MustNewConstMetric(s.prometheusDesc.BPS, prometheus.GaugeValue, float64(stream.AudioTrack.AVTrack.BPS), stream.StreamPath, stream.Plugin.Meta.Name, "audio")
|
||||
ch <- prometheus.MustNewConstMetric(s.prometheusDesc.FPS, prometheus.GaugeValue, float64(stream.AudioTrack.AVTrack.FPS), stream.StreamPath, stream.Plugin.Meta.Name, "audio")
|
||||
}
|
||||
}
|
||||
|
@@ -260,7 +260,7 @@ func (s *Server) GetPullProxyList(ctx context.Context, req *emptypb.Empty) (res
|
||||
}
|
||||
|
||||
func (s *Server) AddPullProxy(ctx context.Context, req *pb.PullProxyInfo) (res *pb.SuccessResponse, err error) {
|
||||
device := &PullProxyConfig{
|
||||
pullProxyConfig := &PullProxyConfig{
|
||||
Name: req.Name,
|
||||
Type: req.Type,
|
||||
ParentID: uint(req.ParentID),
|
||||
@@ -268,7 +268,7 @@ func (s *Server) AddPullProxy(ctx context.Context, req *pb.PullProxyInfo) (res *
|
||||
Description: req.Description,
|
||||
StreamPath: req.StreamPath,
|
||||
}
|
||||
if device.Type == "" {
|
||||
if pullProxyConfig.Type == "" {
|
||||
var u *url.URL
|
||||
u, err = url.Parse(req.PullURL)
|
||||
if err != nil {
|
||||
@@ -277,35 +277,49 @@ func (s *Server) AddPullProxy(ctx context.Context, req *pb.PullProxyInfo) (res *
|
||||
}
|
||||
switch u.Scheme {
|
||||
case "srt", "rtsp", "rtmp":
|
||||
device.Type = u.Scheme
|
||||
pullProxyConfig.Type = u.Scheme
|
||||
default:
|
||||
ext := filepath.Ext(u.Path)
|
||||
switch ext {
|
||||
case ".m3u8":
|
||||
device.Type = "hls"
|
||||
pullProxyConfig.Type = "hls"
|
||||
case ".flv":
|
||||
device.Type = "flv"
|
||||
pullProxyConfig.Type = "flv"
|
||||
case ".mp4":
|
||||
device.Type = "mp4"
|
||||
pullProxyConfig.Type = "mp4"
|
||||
}
|
||||
}
|
||||
}
|
||||
defaults.SetDefaults(&device.Pull)
|
||||
defaults.SetDefaults(&device.Record)
|
||||
device.URL = req.PullURL
|
||||
device.Audio = req.Audio
|
||||
device.StopOnIdle = req.StopOnIdle
|
||||
device.Record.FilePath = req.RecordPath
|
||||
device.Record.Fragment = req.RecordFragment.AsDuration()
|
||||
defaults.SetDefaults(&pullProxyConfig.Pull)
|
||||
defaults.SetDefaults(&pullProxyConfig.Record)
|
||||
pullProxyConfig.URL = req.PullURL
|
||||
pullProxyConfig.Audio = req.Audio
|
||||
pullProxyConfig.StopOnIdle = req.StopOnIdle
|
||||
pullProxyConfig.Record.FilePath = req.RecordPath
|
||||
pullProxyConfig.Record.Fragment = req.RecordFragment.AsDuration()
|
||||
if s.DB == nil {
|
||||
err = pkg.ErrNoDB
|
||||
return
|
||||
}
|
||||
s.DB.Create(device)
|
||||
if req.StreamPath == "" {
|
||||
device.StreamPath = device.GetStreamPath()
|
||||
|
||||
// 检查数据库中是否有相同的 streamPath 且状态不是 disabled 的记录
|
||||
var existingCount int64
|
||||
streamPath := pullProxyConfig.StreamPath
|
||||
if streamPath == "" {
|
||||
streamPath = pullProxyConfig.GetStreamPath()
|
||||
}
|
||||
_, err = s.createPullProxy(device)
|
||||
s.DB.Model(&PullProxyConfig{}).Where("stream_path = ? AND status != ?", streamPath, PullProxyStatusDisabled).Count(&existingCount)
|
||||
|
||||
// 如果存在相同 streamPath 且状态不是 disabled 的记录,将当前记录状态设置为 disabled
|
||||
if existingCount > 0 {
|
||||
pullProxyConfig.Status = PullProxyStatusDisabled
|
||||
}
|
||||
|
||||
s.DB.Create(pullProxyConfig)
|
||||
if req.StreamPath == "" {
|
||||
pullProxyConfig.StreamPath = pullProxyConfig.GetStreamPath()
|
||||
}
|
||||
_, err = s.createPullProxy(pullProxyConfig)
|
||||
|
||||
res = &pb.SuccessResponse{}
|
||||
return
|
||||
@@ -321,6 +335,10 @@ func (s *Server) UpdatePullProxy(ctx context.Context, req *pb.PullProxyInfo) (re
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// 记录原始状态,用于后续判断状态变化
|
||||
originalStatus := target.Status
|
||||
|
||||
target.Name = req.Name
|
||||
target.URL = req.PullURL
|
||||
target.ParentID = uint(req.ParentID)
|
||||
@@ -355,19 +373,50 @@ func (s *Server) UpdatePullProxy(ctx context.Context, req *pb.PullProxyInfo) (re
|
||||
target.Record.Fragment = req.RecordFragment.AsDuration()
|
||||
target.RTT = time.Duration(int(req.Rtt)) * time.Millisecond
|
||||
target.StreamPath = req.StreamPath
|
||||
|
||||
// 如果设置状态为非 disable,需要检查是否有相同 streamPath 的其他非 disable 代理
|
||||
if req.Status != uint32(PullProxyStatusDisabled) {
|
||||
var existingCount int64
|
||||
streamPath := target.StreamPath
|
||||
if streamPath == "" {
|
||||
streamPath = target.GetStreamPath()
|
||||
}
|
||||
s.DB.Model(&PullProxyConfig{}).Where("stream_path = ? AND id != ? AND status != ?", streamPath, req.ID, PullProxyStatusDisabled).Count(&existingCount)
|
||||
|
||||
// 如果存在相同 streamPath 且状态不是 disabled 的其他记录,更新失败
|
||||
if existingCount > 0 {
|
||||
err = fmt.Errorf("已存在相同 streamPath [%s] 的非禁用代理,更新失败", streamPath)
|
||||
return
|
||||
}
|
||||
target.Status = byte(req.Status)
|
||||
} else {
|
||||
target.Status = PullProxyStatusDisabled
|
||||
}
|
||||
|
||||
s.DB.Save(target)
|
||||
|
||||
// 检查是否从 disable 状态变为非 disable 状态
|
||||
wasDisabled := originalStatus == PullProxyStatusDisabled
|
||||
isNowEnabled := target.Status != PullProxyStatusDisabled
|
||||
isNowDisabled := target.Status == PullProxyStatusDisabled
|
||||
wasEnabled := originalStatus != PullProxyStatusDisabled
|
||||
|
||||
if device, ok := s.PullProxies.SafeGet(uint(req.ID)); ok {
|
||||
// 如果现在变为 disable 状态,需要停止并移除代理
|
||||
if wasEnabled && isNowDisabled {
|
||||
device.Stop(task.ErrStopByUser)
|
||||
return
|
||||
}
|
||||
|
||||
conf := device.GetConfig()
|
||||
if target.URL != conf.URL || conf.Audio != target.Audio || conf.StreamPath != target.StreamPath || conf.Record.FilePath != target.Record.FilePath || conf.Record.Fragment != target.Record.Fragment {
|
||||
device.Stop(task.ErrStopByUser)
|
||||
device.WaitStopped()
|
||||
_, err = s.createPullProxy(target)
|
||||
device, err = s.createPullProxy(target)
|
||||
if target.Status == PullProxyStatusPulling {
|
||||
if pullJob, ok := s.Pulls.SafeGet(device.GetStreamPath()); ok {
|
||||
pullJob.OnDispose(device.Pull)
|
||||
pullJob.Stop(task.ErrStopByUser)
|
||||
pullJob.WaitStopped()
|
||||
}
|
||||
device.Pull()
|
||||
}
|
||||
} else {
|
||||
conf.Name = target.Name
|
||||
@@ -382,6 +431,12 @@ func (s *Server) UpdatePullProxy(ctx context.Context, req *pb.PullProxyInfo) (re
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if wasDisabled && isNowEnabled {
|
||||
// 如果原来是 disable 现在不是了,需要创建 PullProxy 并添加到集合中
|
||||
_, err = s.createPullProxy(target)
|
||||
if err != nil {
|
||||
s.Error("create pull proxy failed", "error", err)
|
||||
}
|
||||
}
|
||||
res = &pb.SuccessResponse{}
|
||||
return
|
||||
|
@@ -238,10 +238,9 @@ func (p *RecordFilePuller) queryRecordStreams(startTime, endTime time.Time) (err
|
||||
return pkg.ErrNoDB
|
||||
}
|
||||
queryRecord := RecordStream{
|
||||
Mode: RecordModeAuto,
|
||||
Type: p.Type,
|
||||
}
|
||||
tx := p.PullJob.Plugin.DB.Where(&queryRecord).Find(&p.Streams, "end_time>=? AND start_time<=? AND stream_path=?", startTime, endTime, p.PullJob.RemoteURL)
|
||||
tx := p.PullJob.Plugin.DB.Where(&queryRecord).Find(&p.Streams, "event_id=0 AND end_time>=? AND start_time<=? AND stream_path=?", startTime, endTime, p.PullJob.RemoteURL)
|
||||
if tx.Error != nil {
|
||||
return tx.Error
|
||||
}
|
||||
|
139
recoder.go
139
recoder.go
@@ -1,6 +1,8 @@
|
||||
package m7s
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"gorm.io/gorm"
|
||||
@@ -12,57 +14,46 @@ import (
|
||||
"m7s.live/v5/pkg"
|
||||
)
|
||||
|
||||
const (
|
||||
RecordModeAuto RecordMode = "auto"
|
||||
RecordModeEvent RecordMode = "event"
|
||||
EventLevelLow EventLevel = "low"
|
||||
EventLevelHigh EventLevel = "high"
|
||||
)
|
||||
|
||||
type (
|
||||
EventLevel = string
|
||||
RecordMode = string
|
||||
IRecorder interface {
|
||||
IRecorder interface {
|
||||
task.ITask
|
||||
GetRecordJob() *RecordJob
|
||||
}
|
||||
RecorderFactory = func(config.Record) IRecorder
|
||||
RecordJob struct {
|
||||
// RecordEvent 包含录像事件的公共字段
|
||||
|
||||
EventRecordStream struct {
|
||||
CreatedAt time.Time
|
||||
*config.RecordEvent
|
||||
RecordStream
|
||||
}
|
||||
RecordJob struct {
|
||||
task.Job
|
||||
StreamPath string // 对应本地流
|
||||
Plugin *Plugin
|
||||
Subscriber *Subscriber
|
||||
SubConf *config.Subscribe
|
||||
RecConf *config.Record
|
||||
recorder IRecorder
|
||||
EventId string `json:"eventId" desc:"事件编号"`
|
||||
Mode RecordMode `json:"mode" desc:"事件类型,auto=连续录像模式,event=事件录像模式"`
|
||||
BeforeDuration time.Duration `json:"beforeDuration" desc:"事件前缓存时长"`
|
||||
AfterDuration time.Duration `json:"afterDuration" desc:"事件后缓存时长"`
|
||||
EventDesc string `json:"eventDesc" desc:"事件描述"`
|
||||
EventLevel EventLevel `json:"eventLevel" desc:"事件级别"`
|
||||
EventName string `json:"eventName" desc:"事件名称"`
|
||||
Event *config.RecordEvent
|
||||
StreamPath string // 对应本地流
|
||||
Plugin *Plugin
|
||||
Subscriber *Subscriber
|
||||
SubConf *config.Subscribe
|
||||
RecConf *config.Record
|
||||
recorder IRecorder
|
||||
}
|
||||
DefaultRecorder struct {
|
||||
task.Task
|
||||
RecordJob RecordJob
|
||||
Event EventRecordStream
|
||||
}
|
||||
RecordStream struct {
|
||||
ID uint `gorm:"primarykey"`
|
||||
StartTime, EndTime time.Time `gorm:"type:datetime;default:NULL"`
|
||||
EventId string `json:"eventId" desc:"事件编号" gorm:"type:varchar(255);comment:事件编号"`
|
||||
Mode RecordMode `json:"mode" desc:"事件类型,auto=连续录像模式,event=事件录像模式" gorm:"type:varchar(255);comment:事件类型,auto=连续录像模式,event=事件录像模式;default:'auto'"`
|
||||
EventName string `json:"eventName" desc:"事件名称" gorm:"type:varchar(255);comment:事件名称"`
|
||||
BeforeDuration time.Duration `json:"beforeDuration" desc:"事件前缓存时长" gorm:"type:BIGINT;comment:事件前缓存时长;default:30000000000"`
|
||||
AfterDuration time.Duration `json:"afterDuration" desc:"事件后缓存时长" gorm:"type:BIGINT;comment:事件后缓存时长;default:30000000000"`
|
||||
Filename string `json:"fileName" desc:"文件名" gorm:"type:varchar(255);comment:文件名"`
|
||||
EventDesc string `json:"eventDesc" desc:"事件描述" gorm:"type:varchar(255);comment:事件描述"`
|
||||
Type string `json:"type" desc:"录像文件类型" gorm:"type:varchar(255);comment:录像文件类型,flv,mp4,raw,fmp4,hls"`
|
||||
EventLevel EventLevel `json:"eventLevel" desc:"事件级别" gorm:"type:varchar(255);comment:事件级别,high表示重要事件,无法删除且表示无需自动删除,low表示非重要事件,达到自动删除时间后,自动删除;default:'low'"`
|
||||
FilePath string
|
||||
StreamPath string
|
||||
AudioCodec, VideoCodec string
|
||||
DeletedAt gorm.DeletedAt `gorm:"index" yaml:"-"`
|
||||
ID uint `gorm:"primarykey"`
|
||||
StartTime time.Time `gorm:"type:datetime;default:NULL"`
|
||||
EndTime time.Time `gorm:"type:datetime;default:NULL"`
|
||||
Duration uint32 `gorm:"comment:录像时长;default:0"`
|
||||
Filename string `json:"fileName" desc:"文件名" gorm:"type:varchar(255);comment:文件名"`
|
||||
Type string `json:"type" desc:"录像文件类型" gorm:"type:varchar(255);comment:录像文件类型,flv,mp4,raw,fmp4,hls"`
|
||||
FilePath string
|
||||
StreamPath string
|
||||
AudioCodec string
|
||||
VideoCodec string
|
||||
DeletedAt gorm.DeletedAt `gorm:"index" yaml:"-"`
|
||||
}
|
||||
)
|
||||
|
||||
@@ -74,6 +65,52 @@ func (r *DefaultRecorder) Start() (err error) {
|
||||
return r.RecordJob.Subscribe()
|
||||
}
|
||||
|
||||
func (r *DefaultRecorder) CreateStream(start time.Time, customFileName func(*RecordJob) string) (err error) {
|
||||
recordJob := &r.RecordJob
|
||||
sub := recordJob.Subscriber
|
||||
r.Event.RecordStream = RecordStream{
|
||||
StartTime: start,
|
||||
StreamPath: sub.StreamPath,
|
||||
FilePath: customFileName(recordJob),
|
||||
Type: recordJob.RecConf.Type,
|
||||
}
|
||||
dir := filepath.Dir(r.Event.FilePath)
|
||||
if err = os.MkdirAll(dir, 0755); err != nil {
|
||||
return
|
||||
}
|
||||
if sub.Publisher.HasAudioTrack() {
|
||||
r.Event.AudioCodec = sub.Publisher.AudioTrack.ICodecCtx.String()
|
||||
}
|
||||
if sub.Publisher.HasVideoTrack() {
|
||||
r.Event.VideoCodec = sub.Publisher.VideoTrack.ICodecCtx.String()
|
||||
}
|
||||
if recordJob.Plugin.DB != nil {
|
||||
if recordJob.Event != nil {
|
||||
r.Event.RecordEvent = recordJob.Event
|
||||
recordJob.Plugin.DB.Save(&r.Event)
|
||||
} else {
|
||||
recordJob.Plugin.DB.Save(&r.Event.RecordStream)
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (r *DefaultRecorder) WriteTail(end time.Time, tailJob task.IJob) {
|
||||
r.Event.EndTime = end
|
||||
if r.RecordJob.Plugin.DB != nil {
|
||||
// 将事件和录像记录关联
|
||||
if r.RecordJob.Event != nil {
|
||||
r.RecordJob.Plugin.DB.Save(&r.Event)
|
||||
} else {
|
||||
r.RecordJob.Plugin.DB.Save(&r.Event.RecordStream)
|
||||
}
|
||||
}
|
||||
if tailJob == nil {
|
||||
return
|
||||
}
|
||||
tailJob.AddTask(NewEventRecordCheck(r.Event.Type, r.Event.StreamPath, r.RecordJob.Plugin.DB))
|
||||
}
|
||||
|
||||
func (p *RecordJob) GetKey() string {
|
||||
return p.RecConf.FilePath
|
||||
}
|
||||
@@ -149,3 +186,27 @@ func (p *RecordJob) Start() (err error) {
|
||||
p.AddTask(p.recorder, p.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
func NewEventRecordCheck(t string, streamPath string, db *gorm.DB) *eventRecordCheck {
|
||||
return &eventRecordCheck{
|
||||
DB: db,
|
||||
streamPath: streamPath,
|
||||
Type: t,
|
||||
}
|
||||
}
|
||||
|
||||
type eventRecordCheck struct {
|
||||
task.Task
|
||||
DB *gorm.DB
|
||||
streamPath string
|
||||
Type string
|
||||
}
|
||||
|
||||
func (t *eventRecordCheck) Run() (err error) {
|
||||
var eventRecordStreams []EventRecordStream
|
||||
t.DB.Find(&eventRecordStreams, "type=? AND level=high AND stream_path=?", t.Type, t.streamPath) //搜索事件录像,且为重要事件(无法自动删除)
|
||||
for _, recordStream := range eventRecordStreams {
|
||||
t.DB.Model(&EventRecordStream{}).Where(`level=low AND start_time <= ? and end_time >= ?`, recordStream.EndTime, recordStream.StartTime).Update("level", config.EventLevelHigh)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
38
server.go
38
server.go
@@ -427,16 +427,23 @@ func (s *Server) Start() (err error) {
|
||||
}
|
||||
}
|
||||
}
|
||||
s.initDB()
|
||||
if s.DB != nil {
|
||||
s.initPullProxies()
|
||||
s.initPushProxies()
|
||||
s.initStreamAlias()
|
||||
} else {
|
||||
s.initPullProxiesWithoutDB()
|
||||
s.initPushProxiesWithoutDB()
|
||||
}
|
||||
return nil
|
||||
}, "serverStart")
|
||||
return
|
||||
}
|
||||
|
||||
func (s *Server) initPullProxies() {
|
||||
// 1. First read all pull proxies from database
|
||||
// 1. First read all pull proxies from database, excluding disabled ones
|
||||
var pullProxies []*PullProxyConfig
|
||||
s.DB.Find(&pullProxies)
|
||||
s.DB.Where("status != ?", PullProxyStatusDisabled).Find(&pullProxies)
|
||||
|
||||
// Create a map for quick lookup of existing proxies
|
||||
existingPullProxies := make(map[uint]*PullProxyConfig)
|
||||
@@ -466,9 +473,11 @@ func (s *Server) initPullProxies() {
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Finally add all proxies to collections
|
||||
// 3. Finally add all proxies to collections, excluding disabled ones
|
||||
for _, proxy := range pullProxies {
|
||||
s.createPullProxy(proxy)
|
||||
if proxy.Status != PullProxyStatusDisabled {
|
||||
s.createPullProxy(proxy)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -532,18 +541,6 @@ func (s *Server) initPushProxiesWithoutDB() {
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Server) initDB() {
|
||||
if s.DB != nil {
|
||||
|
||||
s.initPullProxies()
|
||||
s.initPushProxies()
|
||||
s.initStreamAlias()
|
||||
} else {
|
||||
s.initPullProxiesWithoutDB()
|
||||
s.initPushProxiesWithoutDB()
|
||||
}
|
||||
}
|
||||
|
||||
func (c *CheckSubWaitTimeout) GetTickInterval() time.Duration {
|
||||
return c.s.PulseInterval
|
||||
}
|
||||
@@ -582,12 +579,9 @@ func (s *Server) Dispose() {
|
||||
|
||||
func (s *Server) GetPublisher(streamPath string) (publisher *Publisher, err error) {
|
||||
var ok bool
|
||||
s.Streams.Call(func() error {
|
||||
publisher, ok = s.Streams.Get(streamPath)
|
||||
return nil
|
||||
})
|
||||
publisher, ok = s.Streams.SafeGet(streamPath)
|
||||
if !ok {
|
||||
err = fmt.Errorf("src stream not found")
|
||||
err = pkg.ErrNotFound
|
||||
return
|
||||
}
|
||||
return
|
||||
|
Reference in New Issue
Block a user