Compare commits

..

136 Commits

Author SHA1 Message Date
langhuihui
e021131e06 fix: mp4 audio mux codecId miss 2025-05-06 16:33:31 +08:00
pggiroro
2c8e1a7f6e fix: index.go GetPullableList support generatePathFromRegex 2025-05-06 14:04:38 +08:00
langhuihui
d9ef7e46f9 fix: webtransport 2025-05-06 10:43:46 +08:00
pggiroro
987cd4fc4f fix: deviceId to deviceID 2025-05-01 09:41:33 +08:00
liuchao
aa611c3a0d feat: gb28181 save the latitude and longitude in DeviceInfo and DeviceStatus. 2025-04-30 17:45:39 +08:00
langhuihui
57f3d150e4 fix: cors 2025-04-30 14:42:11 +08:00
pggiroro
3c5becd569 fix: Standardize field naming in lowerCamelCase 2025-04-30 14:14:44 +08:00
langhuihui
25d10473f3 feat: add fasthttp 2025-04-30 10:36:25 +08:00
pggiroro
ee3a94c0ba feat: api update deivce 2025-04-29 23:38:05 +08:00
pggiroro
aef28a4c60 feat: getGroups return channels 2025-04-29 21:16:00 +08:00
langhuihui
a5678668a3 fix: rtsp receive 302 2025-04-29 16:04:21 +08:00
pggiroro
8d5daba63b fix: api GetGroupChannels return 0 when success 2025-04-29 14:44:48 +08:00
pggiroro
1e83a96c40 fix: api GetGroupChannels add inGroup 2025-04-29 10:38:10 +08:00
langhuihui
742f8938c3 fix: filter bad h264 nalus 2025-04-29 10:26:28 +08:00
pggiroro
bef04a41ef fix: update api.go,when req.Channels is null,clear channels in the group 2025-04-29 10:04:07 +08:00
langhuihui
3e6e8de20b fix: save pull and push prorxy error 2025-04-29 09:06:04 +08:00
pggiroro
21d5728607 feat: catalog and position subscribe 2025-04-28 22:57:49 +08:00
pggiroro
7bcd619bf5 fix: GetGroupChannels api 2025-04-28 20:31:23 +08:00
pggiroro
4c2a8ed7f4 feat: gb28181 delete channel group api 2025-04-28 16:43:36 +08:00
langhuihui
e29a22a875 chore: commit for test git action 2025-04-27 17:16:28 +08:00
langhuihui
cf604cadc6 fix: udta write 2025-04-27 11:07:58 +08:00
pg
8ca001e74c fix: gb28181 change channel.deviceid to channel.channelid 2025-04-26 23:47:02 +08:00
pg
aff69206d3 feat: gb28181 getgroups interface returns "children" and places them into sub-organizations 2025-04-25 14:55:47 +08:00
pg
4de4a832b7 fix: gb28181 update channels when device modify channelid or add,delete channels 2025-04-24 22:47:35 +08:00
langhuihui
c8fa0bd087 fix: After the streaming proxy is changed from automatic streaming to on-demand streaming, the streaming that was originally being pulled will not stop after the set time of delayclossetimeout after no one subscribes 2025-04-24 19:13:41 +08:00
pg
192a8460ce feat: Modify the deviceid column in the device table to be the primary key, and remove the original auto-incrementing id column to prevent duplicate registration of the same deviceid 2025-04-24 16:07:33 +08:00
langhuihui
16dcafba9d feat: add auto recovery to mp4 record 2025-04-23 17:17:03 +08:00
langhuihui
42b29bccec chore: update stress plugin 2025-04-23 13:23:02 +08:00
pg
dbd1a3697c fix:gb28181 optimize save device to db 2025-04-21 22:30:59 +08:00
pg
c9954149aa fix: Improve the method of utilizing UDP ports for RTSP 2025-04-21 09:48:21 +08:00
pg
324193d30e fix: 1.data.Parse before p.fixTimestamp
2.Solution to RTSP Memory Overflow Issues
2025-04-20 20:50:59 +08:00
pg
8c6cb12d48 feature: RTSP server supports UDP streaming push. 2025-04-18 21:44:31 +08:00
langhuihui
d25f85f9a3 feat: add batch v2 to webrtc 2025-04-18 09:43:14 +08:00
pg
5f4815c7ed feature: gb28181 group api support get all group when pid is -1 2025-04-17 14:37:42 +08:00
langhuihui
66a3e93f4b feat: add webtransport plugin 2025-04-16 13:58:41 +08:00
pg
ea2c61cf69 feature: gb28181 add api to support remove device 2025-04-15 23:39:35 +08:00
pg
34397235d8 fix: gb28181 check device expire when program init 2025-04-14 23:12:54 +08:00
pg
6069ddf2c2 feature: gb28181 support playback speed 2025-04-14 23:12:54 +08:00
langhuihui
3d6d618a79 fix: speed control 2025-04-14 16:17:28 +08:00
langhuihui
45479b41b5 refactor: pull and push proxy 2025-04-14 09:46:58 +08:00
pg
122a91a9b8 feature:gb18181 playback support pause and resume 2025-04-13 22:53:12 +08:00
langhuihui
7af711fbf4 refactor: pull proxy 2025-04-11 17:44:37 +08:00
pg
ed6e4b48fe feature: gb28181 add playback api include pause resume speed seek 2025-04-11 17:17:57 +08:00
langhuihui
546ca02eb6 refactor: pull proxy 2025-04-11 17:10:48 +08:00
langhuihui
74dd4d7235 feat: add safe get in manager 2025-04-11 13:46:22 +08:00
langhuihui
da338c05c1 fix: update pull proxy restart pull 2025-04-11 13:12:16 +08:00
langhuihui
88c35d22d2 fix: rtsp pull proxy panic 2025-04-11 10:03:47 +08:00
langhuihui
f5abb1b436 fix: job 2025-04-10 22:45:10 +08:00
langhuihui
032855f2cc fix: rtmp h265 ctx 2025-04-10 16:51:30 +08:00
langhuihui
254bd2d98e feat: add AsyncTickTask 2025-04-10 15:07:15 +08:00
pg
851ba4329a feature: gb28181 support add group and add channels to group 2025-04-09 09:08:52 +08:00
pg
d1d6b28e0a feature: gb28181 support add group and add channels to group 2025-04-08 21:43:29 +08:00
langhuihui
c2f49795fd fix: speed bigger then 8x 2025-04-07 16:16:46 +08:00
pg
a1a455306b fix: Optimize the GB28181 superior platform registration function 2025-04-06 16:48:34 +08:00
langhuihui
6c898cb487 fix: rtmp parse hevc 2025-04-06 10:44:34 +08:00
langhuihui
79365b7315 fix: InsecureSkipVerify in tls client 2025-04-04 10:56:56 +08:00
pg
940a220c11 feature: gb28181 supports unregistration with password authentication to up platform 2025-04-03 22:29:50 +08:00
langhuihui
5f77f2f5f9 refactor: snap plugin 2025-04-03 17:22:39 +08:00
langhuihui
4e46ecc8cd fix: snap plugin 2025-04-03 17:22:39 +08:00
pg
0914fb8da7 feature: gb28181 supports registration with password authentication to up platform 2025-04-02 22:22:35 +08:00
pg
6fdc855279 feature: gb28181 Supports registration with password authentication 2025-04-02 21:49:25 +08:00
pg
3f698660ae fix: gb28181 sipgo.NewClient use localip, remove viaheader when invite 2025-04-02 20:35:54 +08:00
pg
dbd3d55237 fix: invite viaheader modify 2025-04-01 16:57:14 +08:00
pg
6f51a15fc7 fix: in nat environment,change device ip and port when router restart 2025-03-31 21:56:34 +08:00
pg
470cab36da fix: gb28181 update source ip,port when recover device 2025-03-27 18:16:17 +08:00
langhuihui
01d41a3426 fix: config 2025-03-27 15:07:00 +08:00
pg
2b462b4c10 feature: upstream cascading supports both UDP and TCP active/passive streaming transmission modes. 2025-03-27 11:22:02 +08:00
langhuihui
cc4ee2a447 chore: add config parse nil value soluition 2025-03-26 11:01:23 +08:00
langhuihui
7998d55b41 fix: time scale 2025-03-25 20:06:04 +08:00
pg
b305f18b2e fix: remove old gb28281,rename gb28181pro to gb28181 2025-03-25 13:55:59 +08:00
pg
9827efe43e feature: Supports manual start and stop of recording. 2025-03-24 17:53:10 +08:00
pg
6583bc21a8 fix: remove via header when build request and invite 2025-03-23 22:51:35 +08:00
pg
349e9f35a4 feature: gb support tcp active 2025-03-23 18:13:59 +08:00
pg
674d149039 fix: delete record in db after succeed delete record file in disk 2025-03-22 22:52:19 +08:00
pg
18e77cd594 feature: support query devicestatus,reposond devicestatus from up platform 2025-03-22 17:19:48 +08:00
pg
6c8c44486c fix: get correct sip port from request or current configuration 2025-03-21 23:27:36 +08:00
langhuihui
69797670be fix: trun flag 2025-03-21 17:12:06 +08:00
pg
262d24d728 fix: gb28181 play video from lan 2025-03-20 13:31:07 +08:00
langhuihui
6ec2de3a82 fix: add some log for wrap error 2025-03-19 15:56:50 +08:00
langhuihui
400e8d17e1 fix: wrap index error 2025-03-18 19:42:11 +08:00
langhuihui
5916c6838f fix: rtsp Netconnection dispose 2025-03-18 12:00:08 +08:00
pg
9818b54ef8 feature: support handle preset request from platform 2025-03-17 23:14:34 +08:00
langhuihui
dfde7c896a fix: register hls puller 2025-03-17 15:57:50 +08:00
pg
df7ccaa952 feature: support preset 2025-03-17 12:41:29 +08:00
langhuihui
f4face865c fix: pull mp4 2025-03-17 11:51:10 +08:00
pg
f5fdb51052 feature: supprt manual start record,stop record 2025-03-15 16:41:50 +08:00
langhuihui
d5187b56d6 fix: mp4 unkown box 2025-03-14 17:16:53 +08:00
pg
551eac055d feature: Support GB28181 cascade play video; 2025-03-12 21:25:09 +08:00
langhuihui
d7872ec492 fix: hevc mp4 muxe 2025-03-11 19:11:13 +08:00
pg
7d83b9dede fix:modify api/records 2025-03-11 15:07:19 +08:00
pg
8866e7a68d feature: continue develop oninvite 2025-03-10 17:57:02 +08:00
langhuihui
6fa5aba7ff fix: pull proxy block 2025-03-10 13:04:01 +08:00
pg
4a52cc89bc feature: reinit device from db 2025-03-06 21:51:20 +08:00
pg
1764a9f7e7 feature: query gb28181 records and playback 2025-03-06 10:36:53 +08:00
pg
0dcfe382fd fix: modify ptz api;modify updateplatform api 2025-03-04 16:18:51 +08:00
pg
1fa85d39d9 fix: api/list get all devices when Page && Count is 0,modify ptz api 2025-03-03 17:34:32 +08:00
pg
4059112b3a fix: api/list change list to data 2025-03-03 09:31:21 +08:00
pg
0cf80cedbf fix: catalog get channellist 2025-03-03 09:25:01 +08:00
langhuihui
8c47c5b513 feat: add codec info to hlsv7 2025-02-28 17:39:58 +08:00
pg
67f979c0d7 fix: stop pulljob when stop pullproxy 2025-02-28 15:58:10 +08:00
pg
76e213cbef fix: deviceinfo,catalog xml 2025-02-27 23:51:57 +08:00
pg
ae3e76b20b fix: api/list add channelcout,KeepAliveTime 2025-02-27 17:32:41 +08:00
langhuihui
61607d54fc fix: registerHandler 2025-02-27 17:11:41 +08:00
pg
75f1b0fa57 fix: mp4/api/list get eventlevel,eventname,eventdesc 2025-02-27 14:22:32 +08:00
langhuihui
90d59eb406 feat: remove settings dir 2025-02-27 12:20:08 +08:00
langhuihui
d92d3b5820 fix: push proxy push on publish 2025-02-26 15:25:58 +08:00
langhuihui
7a7b77d2b4 feat: add rtmp nalu filter 2025-02-26 09:48:50 +08:00
langhuihui
13e4d3fe3d feat: hls vod fmp4 2025-02-26 09:46:05 +08:00
langhuihui
518716f383 feat: add download single fmp4 2025-02-26 09:46:05 +08:00
langhuihui
e9e1d7fe95 feat: multiple resolution 2025-02-26 09:46:05 +08:00
pg
8811e5e0b6 feature: support register to upper platform,post deviceinfo and catalog to upper platform 2025-02-24 22:38:52 +08:00
langhuihui
7f9bdec10b feat: download fmp4 2025-02-23 22:56:08 +08:00
langhuihui
6728be29af fix: mp4 record moov move forward 2025-02-23 17:48:15 +08:00
pg
12555c31eb fix: mp4 recordlist api support search eventlevel 2025-02-22 09:51:05 +08:00
pg
7343e24fb4 feature: support alarm 2025-02-22 09:51:05 +08:00
pg
34c4e9a18d feature: support query record list 2025-02-22 09:51:05 +08:00
pg
a2dcb8a3ef feature: support playback 2025-02-22 09:51:05 +08:00
pg
2cb60d5a9c fix: play stream api 2025-02-22 09:51:05 +08:00
pg
eef8892618 fix: Refactor to resolve circular dependency issues. 2025-02-22 09:51:05 +08:00
pg
d2fe58be6d feature: support handel catalog and deviceinfo message send from platform 2025-02-22 09:51:05 +08:00
pg
8ab2fa29d1 feature: support on invite request 2025-02-22 09:51:05 +08:00
pg
84f4390834 feature: add some file ready to support oninvite 2025-02-22 09:51:05 +08:00
pg
321bba6a0c feature: support register to platform and keepalive 2025-02-22 09:51:05 +08:00
pg
bb92152c15 feature: add platform and ready to send register to server 2025-02-22 09:51:05 +08:00
pg
827a0f3fc1 fix: update device_db_id in channelinfo 2025-02-22 09:51:05 +08:00
pg
45408c78be feature: invite gb device from api 2025-02-22 09:51:05 +08:00
langhuihui
e37b244cc9 fix: mp4 download 2025-02-21 09:57:41 +08:00
langhuihui
81a4d60a1e fix: mp4 timestamp 2025-02-14 16:42:15 +08:00
langhuihui
58dd654617 chore: add play fmp4 file in fmp4.html 2025-02-14 11:20:25 +08:00
langhuihui
467ec2356a fix: rtmp read cts 2025-02-13 15:47:12 +08:00
langhuihui
a5399ed11f fix: demuxer mp4 one more time 2025-02-13 14:02:55 +08:00
langhuihui
942eeb11b0 fix: demuxer mp4 2025-02-13 10:12:39 +08:00
pg
c1a5ebda13 fix: change default value of time in db to gorm:"type:datetime;default:CURRENT_TIMESTAMP" 2025-02-11 22:18:55 +08:00
pg
6c8cd34076 feature: add protoc.bat can run in windows 2025-02-11 22:18:55 +08:00
pg
896f3c107a feature: gb28181pro support gb28181 client 2025-02-11 22:18:55 +08:00
langhuihui
f4923d9df6 in progress 2025-02-11 20:21:37 +08:00
192 changed files with 38229 additions and 7140 deletions

View File

@@ -27,11 +27,10 @@ jobs:
go-version: 1.23.4
- name: Cache Go modules
uses: actions/cache@v1
uses: actions/cache@v4
with:
path: ~/go/pkg/mod
key: runner.osgo{ { hashFiles('**/go.sum') } }
restore-keys: ${{ runner.os }}-go-
key: ${{ runner.os }}go${{ hashFiles('**/go.sum') }}
- name: Run GoReleaser
uses: goreleaser/goreleaser-action@v2
@@ -84,7 +83,7 @@ jobs:
- name: docker build
if: success() && startsWith(github.ref, 'refs/tags/')
run: |
tar -zxvf bin/m7s_linux_amd64.tar.gz
tar -zxvf bin/m7s_v5_linux_amd64.tar.gz
mv m7s monibuca_linux
docker login -u langhuihui -p ${{ secrets.DOCKER_PASSWORD }}
docker build -t langhuihui/monibuca:v5 .

View File

@@ -13,7 +13,7 @@ ENV HOME /monibuca
WORKDIR /
RUN git clone -b v5 --depth 1 https://github.com/langhuihui/monibuca
RUN git clone --depth 1 https://github.com/langhuihui/monibuca
# compile
WORKDIR /monibuca
@@ -28,7 +28,7 @@ WORKDIR /monibuca
COPY --from=builder /monibuca/build /monibuca/
RUN cp -r ./config.yaml /etc/monibuca
# Export necessary ports
EXPOSE 8080 8443 1935 554 5060 9000-20000
EXPOSE 5060/udp
EXPOSE 6000 8080 8443 1935 554 5060 9000-20000
EXPOSE 5060/udp 44944/udp
CMD [ "./monibuca", "-c", "/etc/monibuca/config.yaml" ]

View File

@@ -112,6 +112,7 @@ The following build tags can be used to customize your build:
| postgres | Enables the postgres DB |
| duckdb | Enables the duckdb DB |
| taskpanic | Throws panic, for testing |
| fasthttp | Enables the fasthttp server instead of net/http |
<p align="right">(<a href="#readme-top">back to top</a>)</p>

View File

@@ -115,6 +115,7 @@ go run -tags sqlite main.go
| postgres | 启用 PostgreSQL 存储 |
| duckdb | 启用 DuckDB 存储 |
| taskpanic | 抛出 panic用于测试 |
| fasthttp | 使用 fasthttp 服务器代替标准库 |
<p align="right">(<a href="#readme-top">返回顶部</a>)</p>

101
api.go
View File

@@ -383,27 +383,24 @@ func (s *Server) api_VideoTrack_SSE(rw http.ResponseWriter, r *http.Request) {
http.Error(rw, err.Error(), http.StatusBadRequest)
return
}
sse := util.NewSSE(rw, r.Context())
PlayBlock(suber, (func(frame *pkg.AVFrame) (err error))(nil), func(frame *pkg.AVFrame) (err error) {
var snap pb.TrackSnapShot
snap.Sequence = frame.Sequence
snap.Timestamp = uint32(frame.Timestamp / time.Millisecond)
snap.WriteTime = timestamppb.New(frame.WriteTime)
snap.Wrap = make([]*pb.Wrap, len(frame.Wraps))
snap.KeyFrame = frame.IDR
for i, wrap := range frame.Wraps {
snap.Wrap[i] = &pb.Wrap{
Timestamp: uint32(wrap.GetTimestamp() / time.Millisecond),
Size: uint32(wrap.GetSize()),
Data: wrap.String(),
util.NewSSE(rw, r.Context(), func(sse *util.SSE) {
PlayBlock(suber, (func(frame *pkg.AVFrame) (err error))(nil), func(frame *pkg.AVFrame) (err error) {
var snap pb.TrackSnapShot
snap.Sequence = frame.Sequence
snap.Timestamp = uint32(frame.Timestamp / time.Millisecond)
snap.WriteTime = timestamppb.New(frame.WriteTime)
snap.Wrap = make([]*pb.Wrap, len(frame.Wraps))
snap.KeyFrame = frame.IDR
for i, wrap := range frame.Wraps {
snap.Wrap[i] = &pb.Wrap{
Timestamp: uint32(wrap.GetTimestamp() / time.Millisecond),
Size: uint32(wrap.GetSize()),
Data: wrap.String(),
}
}
}
return sse.WriteJSON(&snap)
return sse.WriteJSON(&snap)
})
})
if err != nil {
http.Error(rw, err.Error(), http.StatusBadRequest)
return
}
}
func (s *Server) api_AudioTrack_SSE(rw http.ResponseWriter, r *http.Request) {
@@ -419,27 +416,24 @@ func (s *Server) api_AudioTrack_SSE(rw http.ResponseWriter, r *http.Request) {
http.Error(rw, err.Error(), http.StatusBadRequest)
return
}
sse := util.NewSSE(rw, r.Context())
PlayBlock(suber, func(frame *pkg.AVFrame) (err error) {
var snap pb.TrackSnapShot
snap.Sequence = frame.Sequence
snap.Timestamp = uint32(frame.Timestamp / time.Millisecond)
snap.WriteTime = timestamppb.New(frame.WriteTime)
snap.Wrap = make([]*pb.Wrap, len(frame.Wraps))
snap.KeyFrame = frame.IDR
for i, wrap := range frame.Wraps {
snap.Wrap[i] = &pb.Wrap{
Timestamp: uint32(wrap.GetTimestamp() / time.Millisecond),
Size: uint32(wrap.GetSize()),
Data: wrap.String(),
util.NewSSE(rw, r.Context(), func(sse *util.SSE) {
PlayBlock(suber, func(frame *pkg.AVFrame) (err error) {
var snap pb.TrackSnapShot
snap.Sequence = frame.Sequence
snap.Timestamp = uint32(frame.Timestamp / time.Millisecond)
snap.WriteTime = timestamppb.New(frame.WriteTime)
snap.Wrap = make([]*pb.Wrap, len(frame.Wraps))
snap.KeyFrame = frame.IDR
for i, wrap := range frame.Wraps {
snap.Wrap[i] = &pb.Wrap{
Timestamp: uint32(wrap.GetTimestamp() / time.Millisecond),
Size: uint32(wrap.GetSize()),
Data: wrap.String(),
}
}
}
return sse.WriteJSON(&snap)
}, (func(frame *pkg.AVFrame) (err error))(nil))
if err != nil {
http.Error(rw, err.Error(), http.StatusBadRequest)
return
}
return sse.WriteJSON(&snap)
}, (func(frame *pkg.AVFrame) (err error))(nil))
})
}
func (s *Server) VideoTrackSnap(ctx context.Context, req *pb.StreamSnapRequest) (res *pb.TrackSnapShotResponse, err error) {
@@ -783,29 +777,6 @@ func (s *Server) GetConfig(_ context.Context, req *pb.GetConfigRequest) (res *pb
return
}
func (s *Server) ModifyConfig(_ context.Context, req *pb.ModifyConfigRequest) (res *pb.SuccessResponse, err error) {
var conf *config.Config
if req.Name == "global" {
conf = &s.Config
defer s.SaveConfig()
} else {
p, ok := s.Plugins.Get(req.Name)
if !ok {
err = pkg.ErrNotFound
return
}
defer p.SaveConfig()
conf = &p.Config
}
var modified map[string]any
err = yaml.Unmarshal([]byte(req.Yaml), &modified)
if err != nil {
return
}
conf.ParseModifyFile(modified)
return
}
func (s *Server) GetRecordList(ctx context.Context, req *pb.ReqRecordList) (resp *pb.ResponseList, err error) {
if s.DB == nil {
err = pkg.ErrNoDB
@@ -842,6 +813,9 @@ func (s *Server) GetRecordList(ctx context.Context, req *pb.ReqRecordList) (resp
query = query.Where("end_time <= ?", endTime)
}
}
if req.EventLevel != "" {
query = query.Where("event_level = ?", req.EventLevel)
}
query.Count(&totalCount)
err = query.Offset(int(offset)).Limit(int(req.PageSize)).Order("start_time desc").Find(&result).Error
@@ -860,6 +834,9 @@ func (s *Server) GetRecordList(ctx context.Context, req *pb.ReqRecordList) (resp
EndTime: timestamppb.New(recordFile.EndTime),
FilePath: recordFile.FilePath,
StreamPath: recordFile.StreamPath,
EventLevel: recordFile.EventLevel,
EventDesc: recordFile.EventDesc,
EventName: recordFile.EventName,
})
}
return

Binary file not shown.

Before

Width:  |  Height:  |  Size: 625 KiB

After

Width:  |  Height:  |  Size: 659 KiB

434
doc/fmp4.md Normal file
View File

@@ -0,0 +1,434 @@
# fMP4 Technology Implementation and Application Based on HLS v7
## Author's Foreword
As developers of the Monibuca streaming server, we have been continuously seeking to provide more efficient and flexible streaming solutions. With the evolution of Web frontend technologies, especially the widespread application of Media Source Extensions (MSE), we gradually recognized that traditional streaming transmission solutions can no longer meet the demands of modern applications. During our exploration and practice, we discovered that fMP4 (fragmented MP4) technology effectively bridges traditional media formats with modern Web technologies, providing users with a smoother video experience.
In the implementation of the MP4 plugin for the Monibuca project, we faced the challenge of efficiently converting recorded MP4 files into a format compatible with MSE playback. Through in-depth research on the HLS v7 protocol and fMP4 container format, we ultimately developed a comprehensive solution supporting real-time conversion from MP4 to fMP4, seamless merging of multiple MP4 segments, and optimizations for frontend MSE playback. This article shares our technical exploration and implementation approach during this process.
## Introduction
As streaming media technology evolves, video distribution methods continue to advance. From traditional complete downloads to progressive downloads, and now to widely used adaptive bitrate streaming technology, each advancement has significantly enhanced the user experience. This article will explore the implementation of fMP4 (fragmented MP4) technology based on HLS v7, and how it integrates with Media Source Extensions (MSE) in modern Web frontends to create efficient and smooth video playback experiences.
## Evolution of HLS Protocol and Introduction of fMP4
### Traditional HLS and Its Limitations
HTTP Live Streaming (HLS) is an HTTP adaptive bitrate streaming protocol developed by Apple. In earlier versions, HLS primarily used TS (Transport Stream) segments as the media container format. Although the TS format has good error resilience and streaming characteristics, it also has several limitations:
1. Larger file size compared to container formats like MP4
2. Each TS segment needs to contain complete initialization information, causing redundancy
3. Lower integration with other parts of the Web technology stack
### HLS v7 and fMP4
HLS v7 introduced support for fMP4 (fragmented MP4) segments, marking a significant advancement in the HLS protocol. As a media container format, fMP4 offers the following advantages over TS:
1. Smaller file size, higher transmission efficiency
2. Shares the same underlying container format with other streaming protocols like DASH, facilitating a unified technology stack
3. Better support for modern codecs
4. Better compatibility with MSE (Media Source Extensions)
In HLS v7, seamless playback of fMP4 segments is achieved by specifying initialization segments using the `#EXT-X-MAP` tag in the playlist.
## MP4 File Structure and fMP4 Basic Principles
### Traditional MP4 Structure
Traditional MP4 files follow the ISO Base Media File Format (ISO BMFF) specification and mainly consist of the following parts:
1. **ftyp** (File Type Box): Indicates the format and compatibility information of the file
2. **moov** (Movie Box): Contains metadata about the media, such as track information, codec parameters, etc.
3. **mdat** (Media Data Box): Contains the actual media data
In traditional MP4, the `moov` is usually located at the beginning or end of the file and contains all the metadata and index data for the entire video. This structure is not friendly for streaming transmission because the player needs to acquire the complete `moov` before playback can begin.
Below is a diagram of the MP4 file box structure:
```mermaid
graph TD
MP4[MP4 File] --> FTYP[ftyp box]
MP4 --> MOOV[moov box]
MP4 --> MDAT[mdat box]
MOOV --> MVHD[mvhd: Movie header]
MOOV --> TRAK1[trak: Video track]
MOOV --> TRAK2[trak: Audio track]
TRAK1 --> TKHD1[tkhd: Track header]
TRAK1 --> MDIA1[mdia: Media info]
TRAK2 --> TKHD2[tkhd: Track header]
TRAK2 --> MDIA2[mdia: Media info]
MDIA1 --> MDHD1[mdhd: Media header]
MDIA1 --> HDLR1[hdlr: Handler]
MDIA1 --> MINF1[minf: Media info container]
MDIA2 --> MDHD2[mdhd: Media header]
MDIA2 --> HDLR2[hdlr: Handler]
MDIA2 --> MINF2[minf: Media info container]
MINF1 --> STBL1[stbl: Sample table]
MINF2 --> STBL2[stbl: Sample table]
STBL1 --> STSD1[stsd: Sample description]
STBL1 --> STTS1[stts: Time-to-sample]
STBL1 --> STSC1[stsc: Sample-to-chunk]
STBL1 --> STSZ1[stsz: Sample size]
STBL1 --> STCO1[stco: Chunk offset]
STBL2 --> STSD2[stsd: Sample description]
STBL2 --> STTS2[stts: Time-to-sample]
STBL2 --> STSC2[stsc: Sample-to-chunk]
STBL2 --> STSZ2[stsz: Sample size]
STBL2 --> STCO2[stco: Chunk offset]
```
### fMP4 Structural Characteristics
fMP4 (fragmented MP4) restructures the traditional MP4 format with the following key features:
1. Divides media data into multiple fragments
2. Each fragment contains its own metadata and media data
3. The file structure is more suitable for streaming transmission
The main components of fMP4:
1. **ftyp**: Same as traditional MP4, located at the beginning of the file
2. **moov**: Contains overall track information, but not specific sample information
3. **moof** (Movie Fragment Box): Contains metadata for specific fragments
4. **mdat**: Contains media data associated with the preceding moof
Below is a diagram of the fMP4 file box structure:
```mermaid
graph TD
FMP4[fMP4 File] --> FTYP[ftyp box]
FMP4 --> MOOV[moov box]
FMP4 --> MOOF1[moof 1: Fragment 1 metadata]
FMP4 --> MDAT1[mdat 1: Fragment 1 media data]
FMP4 --> MOOF2[moof 2: Fragment 2 metadata]
FMP4 --> MDAT2[mdat 2: Fragment 2 media data]
FMP4 -.- MOOFN[moof n: Fragment n metadata]
FMP4 -.- MDATN[mdat n: Fragment n media data]
MOOV --> MVHD[mvhd: Movie header]
MOOV --> MVEX[mvex: Movie extends]
MOOV --> TRAK1[trak: Video track]
MOOV --> TRAK2[trak: Audio track]
MVEX --> TREX1[trex 1: Track extends]
MVEX --> TREX2[trex 2: Track extends]
MOOF1 --> MFHD1[mfhd: Fragment header]
MOOF1 --> TRAF1[traf: Track fragment]
TRAF1 --> TFHD1[tfhd: Track fragment header]
TRAF1 --> TFDT1[tfdt: Track fragment decode time]
TRAF1 --> TRUN1[trun: Track run]
```
This structure allows the player to immediately begin processing subsequent `moof`+`mdat` fragments after receiving the initial `ftyp` and `moov`, making it highly suitable for streaming transmission and real-time playback.
## Conversion Principles from MP4 to fMP4
The MP4 to fMP4 conversion process can be illustrated by the following sequence diagram:
```mermaid
sequenceDiagram
participant MP4 as Source MP4 File
participant Demuxer as MP4 Parser
participant Muxer as fMP4 Muxer
participant fMP4 as Target fMP4 File
MP4->>Demuxer: Read MP4 file
Note over Demuxer: Parse file structure
Demuxer->>Demuxer: Extract ftyp info
Demuxer->>Demuxer: Parse moov box
Demuxer->>Demuxer: Extract tracks info<br>(video, audio tracks)
Demuxer->>Muxer: Pass track metadata
Muxer->>fMP4: Write ftyp box
Muxer->>Muxer: Create streaming-friendly moov
Muxer->>Muxer: Add mvex extension
Muxer->>fMP4: Write moov box
loop For each media sample
Demuxer->>MP4: Read sample data
Demuxer->>Muxer: Pass sample
Muxer->>Muxer: Create moof box<br>(time and position info)
Muxer->>Muxer: Create mdat box<br>(actual media data)
Muxer->>fMP4: Write moof+mdat pair
end
Note over fMP4: Conversion complete
```
As shown in the diagram, the conversion process consists of three key steps:
1. **Parse the source MP4 file**: Read and parse the structure of the original MP4 file, extract information about video and audio tracks, including codec type, frame rate, resolution, and other metadata.
2. **Create the initialization part of fMP4**: Build the file header and initialization section, including the ftyp and moov boxes. These serve as the initialization segment, containing all the information needed by the decoder, but without actual media sample data.
3. **Create fragments for each sample**: Read the sample data from the original MP4 one by one, then create corresponding moof and mdat box pairs for each sample (or group of samples).
This conversion method transforms MP4 files that were only suitable for download-and-play into fMP4 format suitable for streaming transmission.
## Multiple MP4 Segment Merging Technology
### User Requirement: Time-Range Recording Downloads
In scenarios such as video surveillance, course playback, and live broadcast recording, users often need to download recorded content within a specific time range. For example, a security system operator might only need to export video segments containing specific events, or a student on an educational platform might only want to download key parts of a course. However, since systems typically divide recorded files by fixed durations (e.g., 30 minutes or 1 hour) or specific events (such as the start/end of a live broadcast), the time range needed by users often spans multiple independent MP4 files.
In the Monibuca project, we developed a solution based on time range queries and multi-file merging to address this need. Users only need to specify the start and end times of the content they require, and the system will:
1. Query the database to find all recording files that overlap with the specified time range
2. Extract relevant time segments from each file
3. Seamlessly merge these segments into a single downloadable file
This approach greatly enhances the user experience, allowing them to precisely obtain the content they need without having to download and browse through large amounts of irrelevant video content.
### Database Design and Time Range Queries
To support time range queries, our recording file metadata in the database includes the following key fields:
- Stream Path: Identifies the video source
- Start Time: The start time of the recording segment
- End Time: The end time of the recording segment
- File Path: The storage location of the actual recording file
- Type: The file format, such as "mp4"
When a user requests recordings within a specific time range, the system executes a query similar to the following:
```sql
SELECT * FROM record_streams
WHERE stream_path = ? AND type = 'mp4'
AND start_time <= ? AND end_time >= ?
```
This returns all recording segments that intersect with the requested time range, after which the system needs to extract the relevant parts and merge them.
### Technical Challenges of Multiple MP4 Merging
Merging multiple MP4 files is not a simple file concatenation but requires addressing the following technical challenges:
1. **Timestamp Continuity**: Ensuring that the timestamps in the merged video are continuous, without jumps or overlaps
2. **Codec Consistency**: Handling cases where different MP4 files may use different encoding parameters
3. **Metadata Merging**: Correctly merging the moov box information from various files
4. **Precise Cutting**: Precisely extracting content within the user-specified time range from each file
In practical applications, we implemented two merging strategies: regular MP4 merging and fMP4 merging. These strategies each have their advantages and are suitable for different application scenarios.
### Regular MP4 Merging Process
```mermaid
sequenceDiagram
participant User as User
participant API as API Service
participant DB as Database
participant MP4s as Multiple MP4 Files
participant Muxer as MP4 Muxer
participant Output as Output MP4 File
User->>API: Request time-range recording<br>(stream, startTime, endTime)
API->>DB: Query records within specified range
DB-->>API: Return matching recording list
loop For each MP4 file
API->>MP4s: Read file
MP4s->>Muxer: Parse file structure
Muxer->>Muxer: Parse track info
Muxer->>Muxer: Extract media samples
Muxer->>Muxer: Adjust timestamps for continuity
Muxer->>Muxer: Record sample info and offsets
Note over Muxer: Skip samples outside time range
end
Muxer->>Output: Write ftyp box
Muxer->>Output: Write adjusted sample data
Muxer->>Muxer: Create moov containing all sample info
Muxer->>Output: Write merged moov box
Output-->>User: Provide merged file to user
```
In this approach, the merging process primarily involves arranging media samples from different MP4 files in sequence and adjusting timestamps to ensure continuity. Finally, a new `moov` box containing all sample information is generated. The advantage of this method is its good compatibility, as almost all players can play the merged file normally, making it suitable for download and offline playback scenarios.
It's particularly worth noting that in the code implementation, we handle the overlap relationship between the time range in the parameters and the actual recording time, extracting only the content that users truly need:
```go
if i == 0 {
startTimestamp := startTime.Sub(stream.StartTime).Milliseconds()
var startSample *box.Sample
if startSample, err = demuxer.SeekTime(uint64(startTimestamp)); err != nil {
tsOffset = 0
continue
}
tsOffset = -int64(startSample.Timestamp)
}
// In the last file, frames beyond the end time are skipped
if i == streamCount-1 && int64(sample.Timestamp) > endTime.Sub(stream.StartTime).Milliseconds() {
break
}
```
### fMP4 Merging Process
```mermaid
sequenceDiagram
participant User as User
participant API as API Service
participant DB as Database
participant MP4s as Multiple MP4 Files
participant Muxer as fMP4 Muxer
participant Output as Output fMP4 File
User->>API: Request time-range recording<br>(stream, startTime, endTime)
API->>DB: Query records within specified range
DB-->>API: Return matching recording list
Muxer->>Output: Write ftyp box
Muxer->>Output: Write initial moov box<br>(including mvex)
loop For each MP4 file
API->>MP4s: Read file
MP4s->>Muxer: Parse file structure
Muxer->>Muxer: Parse track info
Muxer->>Muxer: Extract media samples
loop For each sample
Note over Muxer: Check if sample is within target time range
Muxer->>Muxer: Adjust timestamp
Muxer->>Muxer: Create moof+mdat pair
Muxer->>Output: Write moof+mdat pair
end
end
Output-->>User: Provide merged file to user
```
The fMP4 merging is more flexible, with each sample packed into an independent `moof`+`mdat` fragment, maintaining independently decodable characteristics, which is more conducive to streaming transmission and random access. This approach is particularly suitable for integration with MSE and HLS, providing support for real-time streaming playback, allowing users to efficiently play merged content directly in the browser without waiting for the entire file to download.
### Handling Codec Compatibility in Merging
In the process of merging multiple recordings, a key challenge we face is handling potential codec parameter differences between files. For example, during long-term recording, a camera might adjust video resolution due to environmental changes, or an encoder might reinitialize, causing changes in encoding parameters.
To solve this problem, Monibuca implements a smart track version management system that identifies changes by comparing encoder-specific data (ExtraData):
```mermaid
sequenceDiagram
participant Muxer as Merger
participant Track as Track Manager
participant History as Track Version History
loop For each new track
Muxer->>Track: Check track encoding parameters
Track->>History: Compare with existing track versions
alt Found matching track version
History-->>Track: Return existing track
Track-->>Muxer: Use existing track
else No matching version
Track->>Track: Create new track version
Track->>History: Add to version history
Track-->>Muxer: Use new track
end
end
```
This design ensures that even if there are encoding parameter changes in the original recordings, the merged file can maintain correct decoding parameters, providing users with a smooth playback experience.
### Performance Optimization
When processing large video files or a large number of concurrent requests, the performance of the merging process is an important consideration. We have adopted the following optimization measures:
1. **Streaming Processing**: Process samples frame by frame to avoid loading entire files into memory
2. **Parallel Processing**: Use parallel processing for multiple independent tasks (such as file parsing)
3. **Smart Caching**: Cache commonly used encoding parameters and file metadata
4. **On-demand Reading**: Only read and process samples within the target time range
These optimizations enable the system to efficiently process large-scale recording merging requests, completing processing within a reasonable time even for long-term recordings spanning hours or days.
The multiple MP4 merging functionality greatly enhances the flexibility and user experience of Monibuca as a streaming server, allowing users to precisely obtain the recorded content they need, regardless of how the original recordings are segmented and stored.
## Media Source Extensions (MSE) and fMP4 Compatibility Implementation
### MSE Technology Overview
Media Source Extensions (MSE) is a JavaScript API that allows web developers to directly manipulate media stream data. It enables custom adaptive bitrate streaming players to be implemented entirely in the browser without relying on external plugins.
The core working principle of MSE is:
1. Create a MediaSource object
2. Create one or more SourceBuffer objects
3. Append media fragments to the SourceBuffer
4. The browser is responsible for decoding and playing these fragments
### Perfect Integration of fMP4 with MSE
The fMP4 format has natural compatibility with MSE, mainly reflected in:
1. Each fragment of fMP4 can be independently decoded
2. The clear separation of initialization segments and media segments conforms to MSE's buffer management model
3. Precise timestamp control enables seamless splicing
The following sequence diagram shows how fMP4 works with MSE:
```mermaid
sequenceDiagram
participant Client as Browser Client
participant Server as Server
participant MSE as MediaSource API
participant Video as HTML5 Video Element
Client->>Video: Create video element
Client->>MSE: Create MediaSource object
Client->>Video: Set video.src = URL.createObjectURL(mediaSource)
MSE-->>Client: sourceopen event
Client->>MSE: Create SourceBuffer
Client->>Server: Request initialization segment (ftyp+moov)
Server-->>Client: Return initialization segment
Client->>MSE: appendBuffer(initialization segment)
loop During playback
Client->>Server: Request media segment (moof+mdat)
Server-->>Client: Return media segment
Client->>MSE: appendBuffer(media segment)
MSE-->>Video: Decode and render frames
end
```
In Monibuca's implementation, we've made special optimizations for MSE: creating independent moof and mdat for each frame. Although this approach adds some overhead, it provides high flexibility, particularly suitable for low-latency real-time streaming scenarios and precise frame-level operations.
## Integration of HLS and fMP4 in Practical Applications
In practical applications, we combine fMP4 technology with the HLS v7 protocol to implement time-range-based on-demand playback. The system can find the corresponding MP4 records from the database based on the time range specified by the user, and then generate an fMP4 format HLS playlist:
```mermaid
sequenceDiagram
participant Client as Client
participant Server as HLS Server
participant DB as Database
participant MP4Plugin as MP4 Plugin
Client->>Server: Request fMP4.m3u8<br>with time range parameters
Server->>DB: Query MP4 records within specified range
DB-->>Server: Return record list
Server->>Server: Create HLS v7 playlist<br>Version: 7
loop For each record
Server->>Server: Calculate duration
Server->>Server: Add media segment URL<br>/mp4/download/{stream}.fmp4?id={id}
end
Server->>Server: Add #EXT-X-ENDLIST marker
Server-->>Client: Return HLS playlist
loop For each segment
Client->>MP4Plugin: Request fMP4 segment
MP4Plugin->>MP4Plugin: Convert to fMP4 format
MP4Plugin-->>Client: Return fMP4 segment
end
```
Through this approach, we maintain compatibility with existing HLS clients while leveraging the advantages of the fMP4 format to provide more efficient streaming services.
## Conclusion
As a modern media container format, fMP4 combines the efficient compression of MP4 with the flexibility of streaming transmission, making it highly suitable for video distribution needs in modern web applications. Through integration with HLS v7 and MSE technologies, more efficient and flexible streaming services can be achieved.
In the practice of the Monibuca project, we have successfully built a complete streaming solution by implementing MP4 to fMP4 conversion, merging multiple MP4 files, and optimizing fMP4 fragment generation for MSE. The application of these technologies enables our system to provide a better user experience, including faster startup times, smoother quality transitions, and lower bandwidth consumption.
As video technology continues to evolve, fMP4, as a bridge connecting traditional media formats with modern Web technologies, will continue to play an important role in the streaming media field. The Monibuca project will also continue to explore and optimize this technology to provide users with higher quality streaming services.

View File

@@ -0,0 +1,434 @@
# 基于HLS v7的fMP4技术实现与应用
## 作者前言
作为Monibuca流媒体服务器的开发者我们一直在寻求提供更高效、更灵活的流媒体解决方案。随着Web前端技术的发展特别是Media Source Extensions (MSE) 的广泛应用我们逐渐认识到传统的流媒体传输方案已难以满足现代应用的需求。在探索与实践中我们发现fMP4(fragmented MP4)技术能够很好地连接传统媒体格式与现代Web技术为用户提供更流畅的视频体验。
Monibuca项目在MP4插件的实现中我们面临着如何将已录制的MP4文件高效转换为支持MSE播放的格式这一挑战。通过深入研究HLS v7协议和fMP4容器格式我们最终实现了一套完整的解决方案支持MP4到fMP4的实时转换、多段MP4的无缝合并以及针对前端MSE播放的优化。本文将分享我们在这一过程中的技术探索和实现思路。
## 引言
随着流媒体技术的发展视频分发方式不断演进。从传统的整体式下载到渐进式下载再到现在广泛使用的自适应码率流媒体技术每一步演进都极大地提升了用户体验。本文将探讨基于HLS v7的fMP4fragmented MP4技术实现以及它如何与现代Web前端中的媒体源扩展Media Source Extensions, MSE结合打造高效流畅的视频播放体验。
## HLS协议演进与fMP4的引入
### 传统HLS与其局限性
HTTP Live Streaming (HLS)是由Apple公司开发的HTTP自适应比特率流媒体通信协议。在早期版本中HLS主要使用TS(Transport Stream)切片作为媒体容器格式。虽然TS格式具有良好的容错性和流式传输特性但也存在一些局限性
1. 相比于MP4等容器格式TS文件体积较大
2. 每个TS切片都需要包含完整的初始化信息导致冗余
3. 与Web技术栈的其他部分集成度不高
### HLS v7与fMP4
HLS v7版本引入了对fMP4(fragmented MP4)切片的支持这是HLS协议的一个重大进步。fMP4作为媒体容器格式相比TS具有以下优势
1. 文件体积更小,传输效率更高
2. 与DASH等其他流媒体协议共享相同的底层容器格式有利于统一技术栈
3. 更好地支持现代编解码器
4. 与MSE(Media Source Extensions)有更好的兼容性
在HLS v7中通过在播放列表中使用`#EXT-X-MAP`标签指定初始化片段可以实现fMP4切片的无缝播放。
## MP4文件结构与fMP4的基本原理
### 传统MP4结构
传统的MP4文件遵循ISO Base Media File Format(ISO BMFF)规范,主要由以下几个部分组成:
1. **ftyp** (File Type Box): 指示文件的格式和兼容性信息
2. **moov** (Movie Box): 包含媒体的元数据信息,如轨道信息、编解码器参数等
3. **mdat** (Media Data Box): 包含实际的媒体数据
在传统MP4中`moov`通常位于文件开头或结尾,包含了整个视频的所有元信息和索引数据。这种结构对于流式传输不友好,因为播放器需要先获取完整的`moov`才能开始播放。
以下是MP4文件的box结构示意图
```mermaid
graph TD
MP4[MP4文件] --> FTYP[ftyp box]
MP4 --> MOOV[moov box]
MP4 --> MDAT[mdat box]
MOOV --> MVHD[mvhd: 电影头信息]
MOOV --> TRAK1[trak: 视频轨道]
MOOV --> TRAK2[trak: 音频轨道]
TRAK1 --> TKHD1[tkhd: 轨道头信息]
TRAK1 --> MDIA1[mdia: 媒体信息]
TRAK2 --> TKHD2[tkhd: 轨道头信息]
TRAK2 --> MDIA2[mdia: 媒体信息]
MDIA1 --> MDHD1[mdhd: 媒体头信息]
MDIA1 --> HDLR1[hdlr: 处理器信息]
MDIA1 --> MINF1[minf: 媒体信息容器]
MDIA2 --> MDHD2[mdhd: 媒体头信息]
MDIA2 --> HDLR2[hdlr: 处理器信息]
MDIA2 --> MINF2[minf: 媒体信息容器]
MINF1 --> STBL1[stbl: 采样表]
MINF2 --> STBL2[stbl: 采样表]
STBL1 --> STSD1[stsd: 采样描述]
STBL1 --> STTS1[stts: 时间戳信息]
STBL1 --> STSC1[stsc: 块到采样映射]
STBL1 --> STSZ1[stsz: 采样大小]
STBL1 --> STCO1[stco: 块偏移]
STBL2 --> STSD2[stsd: 采样描述]
STBL2 --> STTS2[stts: 时间戳信息]
STBL2 --> STSC2[stsc: 块到采样映射]
STBL2 --> STSZ2[stsz: 采样大小]
STBL2 --> STCO2[stco: 块偏移]
```
### fMP4的结构特点
fMP4(fragmented MP4)对传统MP4格式进行了重构主要特点是
1. 将媒体数据分割成多个片段(fragments)
2. 每个片段包含自己的元数据和媒体数据
3. 文件结构更适合流式传输
fMP4的主要组成部分
1. **ftyp**: 与传统MP4相同位于文件开头
2. **moov**: 包含整体的轨道信息,但不包含具体的样本信息
3. **moof** (Movie Fragment Box): 包含特定片段的元数据
4. **mdat**: 包含与前面的moof相关联的媒体数据
以下是fMP4文件的box结构示意图
```mermaid
graph TD
FMP4[fMP4文件] --> FTYP[ftyp box]
FMP4 --> MOOV[moov box]
FMP4 --> MOOF1[moof 1: 片段1元数据]
FMP4 --> MDAT1[mdat 1: 片段1媒体数据]
FMP4 --> MOOF2[moof 2: 片段2元数据]
FMP4 --> MDAT2[mdat 2: 片段2媒体数据]
FMP4 -.- MOOFN[moof n: 片段n元数据]
FMP4 -.- MDATN[mdat n: 片段n媒体数据]
MOOV --> MVHD[mvhd: 电影头信息]
MOOV --> MVEX[mvex: 电影扩展]
MOOV --> TRAK1[trak: 视频轨道]
MOOV --> TRAK2[trak: 音频轨道]
MVEX --> TREX1[trex 1: 轨道扩展]
MVEX --> TREX2[trex 2: 轨道扩展]
MOOF1 --> MFHD1[mfhd: 片段头]
MOOF1 --> TRAF1[traf: 轨道片段]
TRAF1 --> TFHD1[tfhd: 轨道片段头]
TRAF1 --> TFDT1[tfdt: 轨道片段基准时间]
TRAF1 --> TRUN1[trun: 轨道运行信息]
```
这种结构允许播放器在接收到初始的`ftyp``moov`后,可以立即开始处理后续接收到的`moof`+`mdat`片段,非常适合流式传输和实时播放。
## MP4到fMP4的转换原理
MP4到fMP4的转换过程可以通过以下时序图来说明
```mermaid
sequenceDiagram
participant MP4 as 源MP4文件
participant Demuxer as MP4解析器
participant Muxer as fMP4封装器
participant fMP4 as 目标fMP4文件
MP4->>Demuxer: 读取MP4文件
Note over Demuxer: 解析文件结构
Demuxer->>Demuxer: 提取ftyp信息
Demuxer->>Demuxer: 解析moov box
Demuxer->>Demuxer: 提取tracks信息<br>(视频、音频轨道)
Demuxer->>Muxer: 传递tracks元数据
Muxer->>fMP4: 写入ftyp box
Muxer->>Muxer: 创建适合流式传输的moov
Muxer->>Muxer: 添加mvex扩展
Muxer->>fMP4: 写入moov box
loop 对每个媒体样本
Demuxer->>MP4: 读取样本数据
Demuxer->>Muxer: 传递样本
Muxer->>Muxer: 创建moof box<br>(包含时间和位置信息)
Muxer->>Muxer: 创建mdat box<br>(包含实际媒体数据)
Muxer->>fMP4: 写入moof+mdat对
end
Note over fMP4: 完成转换
```
从上图可以看出,转换过程主要包含三个关键步骤:
1. **解析源MP4文件**读取并解析原始MP4文件的结构提取出视频轨、音频轨的相关信息包括编解码器类型、帧率、分辨率等元数据。
2. **创建fMP4的初始化部分**构建文件头和初始化部分包括ftyp和moov box它们作为初始化段(initialization segment),包含了解码器需要的所有信息,但不包含实际的媒体样本数据。
3. **为每个样本创建片段**逐个读取原始MP4中的样本数据然后为每个样本或一组样本创建对应的moof和mdat box对。
这种转换方式使得原本只适合下载后播放的MP4文件变成了适合流式传输的fMP4格式。
## MP4多段合并技术
### 用户需求:时间范围录像下载
在视频监控、课程回放和直播录制等场景中用户经常需要下载特定时间范围内的录像内容。例如一个安防系统的操作员可能只需要导出包含特定事件的视频片段或者一个教育平台的学生可能只想下载课程中的重点部分。然而由于系统通常按照固定时长如30分钟或1小时或特定事件如直播开始/结束来分割录制文件用户需要的时间范围往往横跨多个独立的MP4文件。
在Monibuca项目中我们针对这一需求开发了基于时间范围查询和多文件合并的解决方案。用户只需指定所需内容的起止时间系统会
1. 查询数据库,找出所有与指定时间范围重叠的录像文件
2. 从每个文件中提取相关的时间片段
3. 将这些片段无缝合并为单个下载文件
这种方式极大地提升了用户体验,使其能够精确获取所需内容,而不必下载和浏览大量无关的视频内容。
### 数据库设计与时间范围查询
为支持时间范围查询,我们的录像文件元数据在数据库中包含以下关键字段:
- 流路径StreamPath标识视频源
- 开始时间StartTime录像片段的开始时间
- 结束时间EndTime录像片段的结束时间
- 文件路径FilePath实际录像文件的存储位置
- 文件类型Type文件格式如"mp4"
当用户请求特定时间范围的录像时,系统执行类似以下的查询:
```sql
SELECT * FROM record_streams
WHERE stream_path = ? AND type = 'mp4'
AND start_time <= ? AND end_time >= ?
```
这将返回所有与请求时间范围有交集的录像片段,然后系统需要从中提取相关部分并合并。
### 多段MP4合并的技术挑战
合并多个MP4文件并非简单的文件拼接而是需要处理以下技术挑战
1. **时间戳连续性**:确保合并后视频的时间戳连续,没有跳跃或重叠
2. **编解码一致性**处理不同MP4文件可能使用不同编码参数的情况
3. **元数据合并**正确合并各文件的moov box信息
4. **精确剪切**:从每个文件中精确提取用户指定时间范围的内容
在实际应用中我们实现了两种合并策略普通MP4合并和fMP4合并。这两种策略各有优势适用于不同的应用场景。
### 普通MP4合并流程
```mermaid
sequenceDiagram
participant User as 用户
participant API as API服务
participant DB as 数据库
participant MP4s as 多个MP4文件
participant Muxer as MP4封装器
participant Output as 输出MP4文件
User->>API: 请求时间范围录像<br>(stream, startTime, endTime)
API->>DB: 查询指定范围的录像记录
DB-->>API: 返回符合条件的录像列表
loop 对每个MP4文件
API->>MP4s: 读取文件
MP4s->>Muxer: 解析文件结构
Muxer->>Muxer: 解析轨道信息
Muxer->>Muxer: 提取媒体样本
Muxer->>Muxer: 调整时间戳保持连续性
Muxer->>Muxer: 记录样本信息和偏移量
Note over Muxer: 跳过时间范围外的样本
end
Muxer->>Output: 写入ftyp box
Muxer->>Output: 写入调整后的样本数据
Muxer->>Muxer: 创建包含所有样本信息的moov
Muxer->>Output: 写入合并后的moov box
Output-->>User: 向用户提供合并后的文件
```
这种方式下合并过程主要是将不同MP4文件的媒体样本连续排列并调整时间戳确保连续性。最后重新生成一个包含所有样本信息的`moov` box。这种方法的优点是兼容性好几乎所有播放器都能正常播放合并后的文件适合用于下载和离线播放场景。
特别值得注意的是,在代码实现中,我们会处理参数中时间范围与实际录像时间的重叠关系,只提取用户真正需要的内容:
```go
if i == 0 {
startTimestamp := startTime.Sub(stream.StartTime).Milliseconds()
var startSample *box.Sample
if startSample, err = demuxer.SeekTime(uint64(startTimestamp)); err != nil {
tsOffset = 0
continue
}
tsOffset = -int64(startSample.Timestamp)
}
// 在最后一个文件中,超出结束时间的帧会被跳过
if i == streamCount-1 && int64(sample.Timestamp) > endTime.Sub(stream.StartTime).Milliseconds() {
break
}
```
### fMP4合并流程
```mermaid
sequenceDiagram
participant User as 用户
participant API as API服务
participant DB as 数据库
participant MP4s as 多个MP4文件
participant Muxer as fMP4封装器
participant Output as 输出fMP4文件
User->>API: 请求时间范围录像<br>(stream, startTime, endTime)
API->>DB: 查询指定范围的录像记录
DB-->>API: 返回符合条件的录像列表
Muxer->>Output: 写入ftyp box
Muxer->>Output: 写入初始moov box<br>(包含mvex)
loop 对每个MP4文件
API->>MP4s: 读取文件
MP4s->>Muxer: 解析文件结构
Muxer->>Muxer: 解析轨道信息
Muxer->>Muxer: 提取媒体样本
loop 对每个样本
Note over Muxer: 检查样本是否在目标时间范围内
Muxer->>Muxer: 调整时间戳
Muxer->>Muxer: 创建moof+mdat对
Muxer->>Output: 写入moof+mdat对
end
end
Output-->>User: 向用户提供合并后的文件
```
fMP4的合并更加灵活每个样本都被封装成独立的`moof`+`mdat`片段保持了可独立解码的特性更有利于流式传输和随机访问。这种方式特别适合与MSE和HLS结合为实时流媒体播放提供支持让用户能够在浏览器中直接高效地播放合并后的内容而无需等待整个文件下载完成。
### 合并中的编解码兼容性处理
在多段录像合并过程中,我们面临的一个关键挑战是处理不同文件可能存在的编码参数差异。例如,在长时间录制过程中,摄像头可能因环境变化调整了视频分辨率,或者编码器可能重新初始化导致编码参数变化。
为了解决这一问题Monibuca实现了一个智能的轨道版本管理系统通过比较编码器特定数据(ExtraData)来识别变化:
```mermaid
sequenceDiagram
participant Muxer as 合并器
participant Track as 轨道管理器
participant History as 轨道历史版本
loop 对每个新轨道
Muxer->>Track: 检查轨道编码参数
Track->>History: 比较已有轨道版本
alt 发现匹配的轨道版本
History-->>Track: 返回现有轨道
Track-->>Muxer: 使用已有轨道
else 无匹配版本
Track->>Track: 创建新轨道版本
Track->>History: 添加到历史版本库
Track-->>Muxer: 使用新轨道
end
end
```
这种设计确保了即使原始录像中存在编码参数变化,合并后的文件也能保持正确的解码参数,为用户提供流畅的播放体验。
### 性能优化
在处理大型视频文件或大量并发请求时,合并过程的性能是一个重要考量。我们采取了以下优化措施:
1. **流式处理**:逐帧处理样本,避免将整个文件加载到内存
2. **并行处理**:对多个独立任务(如文件解析)采用并行处理
3. **智能缓存**:缓存常用的编码参数和文件元数据
4. **按需读取**:仅读取和处理目标时间范围内的样本
这些优化使得系统能够高效处理大规模的录像合并请求,即使是跨越数小时或数天的长时间录像,也能在合理的时间内完成处理。
多段MP4合并功能极大地增强了Monibuca作为流媒体服务器的灵活性和用户体验使用户能够精确获取所需的录像内容无论原始录像如何分段存储。
## 媒体源扩展(MSE)与fMP4的兼容实现
### MSE技术概述
媒体源扩展(Media Source Extensions, MSE)是一种JavaScript API允许网页开发者直接操作媒体流数据。它使得自定义的自适应比特率流媒体播放器可以完全在浏览器中实现无需依赖外部插件。
MSE的核心工作原理是
1. 创建一个MediaSource对象
2. 创建一个或多个SourceBuffer对象
3. 将媒体片段追加到SourceBuffer中
4. 浏览器负责解码和播放这些片段
### fMP4与MSE的完美适配
fMP4格式与MSE有着天然的兼容性主要体现在
1. fMP4的每个片段都可以独立解码
2. 初始化段和媒体段的清晰分离符合MSE的缓冲区管理模型
3. 时间戳的精确控制使得无缝拼接成为可能
以下时序图展示了fMP4如何与MSE配合工作
```mermaid
sequenceDiagram
participant Client as 浏览器客户端
participant Server as 服务器
participant MSE as MediaSource API
participant Video as HTML5 Video元素
Client->>Video: 创建video元素
Client->>MSE: 创建MediaSource对象
Client->>Video: 设置video.src = URL.createObjectURL(mediaSource)
MSE-->>Client: sourceopen事件
Client->>MSE: 创建SourceBuffer
Client->>Server: 请求初始化段(ftyp+moov)
Server-->>Client: 返回初始化段
Client->>MSE: appendBuffer(初始化段)
loop 播放过程
Client->>Server: 请求媒体段(moof+mdat)
Server-->>Client: 返回媒体段
Client->>MSE: appendBuffer(媒体段)
MSE-->>Video: 解码并渲染帧
end
```
在Monibuca的实现中我们针对MSE进行了特殊优化为每一帧创建独立的moof和mdat。这种实现方式尽管会增加一些开销但提供了极高的灵活性特别适合于低延迟的实时流媒体场景和精确的帧级操作。
## HLS与fMP4在实际应用中的集成
在实际应用中我们将fMP4技术与HLS v7协议结合实现了基于时间范围的点播功能。系统可以根据用户指定的时间范围从数据库中查找对应的MP4记录然后生成fMP4格式的HLS播放列表
```mermaid
sequenceDiagram
participant Client as 客户端
participant Server as HLS服务
participant DB as 数据库
participant MP4Plugin as MP4插件
Client->>Server: 请求fMP4.m3u8<br>带时间范围参数
Server->>DB: 查询指定时间范围的MP4记录
DB-->>Server: 返回记录列表
Server->>Server: 创建HLS v7播放列表<br>Version: 7
loop 对每个记录
Server->>Server: 计算时长
Server->>Server: 添加媒体片段URL<br>/mp4/download/{stream}.fmp4?id={id}
end
Server->>Server: 添加#EXT-X-ENDLIST标记
Server-->>Client: 返回HLS播放列表
loop 对每个片段
Client->>MP4Plugin: 请求fMP4片段
MP4Plugin->>MP4Plugin: 转换为fMP4格式
MP4Plugin-->>Client: 返回fMP4片段
end
```
通过这种方式我们在保持兼容现有HLS客户端的同时利用了fMP4格式的优势提供了更高效的流媒体服务。
## 结论
fMP4作为一种现代媒体容器格式结合了MP4的高效压缩和流媒体传输的灵活性非常适合现代Web应用中的视频分发需求。通过与HLS v7和MSE技术的结合可以实现更高效、更灵活的流媒体服务。
在Monibuca项目的实践中我们通过实现MP4到fMP4的转换、多段MP4文件的合并以及针对MSE优化fMP4片段生成成功构建了一套完整的流媒体解决方案。这些技术的应用使得我们的系统能够提供更好的用户体验包括更快的启动时间、更平滑的画质切换以及更低的带宽消耗。
随着视频技术的不断发展fMP4作为连接传统媒体格式与现代Web技术的桥梁将继续在流媒体领域发挥重要作用。而Monibuca项目也将持续探索和优化这一技术为用户提供更优质的流媒体服务。

16
example/8080/snap.yaml Normal file
View File

@@ -0,0 +1,16 @@
snap:
onpub:
transform:
.+:
output:
- watermark:
text: "abcd" # 水印文字内容
fontpath: /Users/dexter/Library/Fonts/MapleMono-NF-CN-Medium.ttf # 水印字体文件路径
fontcolor: "rgba(255,165,0,1)" # 水印字体颜色支持rgba格式
fontsize: 36 # 水印字体大小
offsetx: 0 # 水印位置X偏移
offsety: 0 # 水印位置Y偏移
timeinterval: 1s # 截图时间间隔
savepath: "snaps" # 截图保存路径
iframeinterval: 3 # 间隔多少帧截图
querytimedelta: 3 # 查询截图时允许的最大时间差(秒)

View File

@@ -0,0 +1,12 @@
global:
loglevel: debug
tcp: :50052
http: :8081
disableall: true
flv:
enable: true
pull:
live/test: /Users/dexter/Movies/jb-demo.flv
rtsp:
enable: true
tcp: :8554

View File

@@ -4,15 +4,14 @@ global:
loglevel: debug
admin:
enablelogin: false
# db:
# dbtype: mysql
# dsn: root:Monibuca#!4@tcp(sh-cynosdbmysql-grp-kxt43lv6.sql.tencentcdb.com:28520)/lkm7s_v5?parseTime=true
srt:
listenaddr: :6000
passphrase: foobarfoobar
gb28181:
enable: false
autoinvite: true
autoinvite: false
mediaip: 192.168.1.21 #流媒体收流IP
sipip: 192.168.1.21 #SIP通讯IP
sip:
listenaddr:
- udp::5060
@@ -23,14 +22,16 @@ gb28181:
.* : $0
mp4:
# enable: false
publish:
delayclosetimeout: 3s
# publish:
# delayclosetimeout: 3s
# onpub:
# record:
# ^live/.+:
# fragment: 10s
# filepath: record/$0
# type: mp4
# type: fmp4
# pull:
# live/test: /Users/dexter/Movies/1744963190.mp4
onsub:
pull:
^vod_mp4_\d+/(.+)$: $1
@@ -70,26 +71,21 @@ hls:
snap:
enable: false
ismanualmodesave: true # 手动截图是否保存文件
watermark:
text: "Monibuca $T{2006-01-02 15:04:05.000}"
fontpath: "/System/Library/Fonts/STHeiti Light.ttc" # mac字体路径
# fontpath: "/usr/share/fonts/opentype/noto/NotoSansCJK-Regular.ttc" # linux字体路径 思源黑体
# fontpath: "C:/Windows/Fonts/msyh.ttf" # windows字体路径 微软雅黑
fontsize: 16
fontspacing: 2 # 添加字体间距配置
fontcolor: "rgba(255,165,0,1)"
offsetx: 10
offsety: 10
mode: 2 #截图模式0-时间间隔1-关键帧间隔 2-HTTP请求模式手动触发
timeinterval: 3s
savepath: "./snap"
iframeinterval: 3 # 截图i帧间隔默认为3即每隔3个i帧截图一次
querytimedelta: 3 # 查询截图时允许的最大时间差(秒)
filter: "^live/.*"
onpub:
transform:
.* : $0
.+:
output:
- watermark:
text: "abcd" # 水印文字内容
fontpath: /Users/dexter/Library/Fonts/MapleMono-NF-CN-Medium.ttf # 水印字体文件路径
fontcolor: "rgba(255,165,0,1)" # 水印字体颜色支持rgba格式
fontsize: 36 # 水印字体大小
offsetx: 0 # 水印位置X偏移
offsety: 0 # 水印位置Y偏移
timeinterval: 1s # 截图时间间隔
savepath: "snaps" # 截图保存路径
iframeinterval: 3 # 间隔多少帧截图
querytimedelta: 3 # 查询截图时允许的最大时间差(秒)
crypto:
enable: false

View File

@@ -25,6 +25,7 @@ import (
_ "m7s.live/v5/plugin/stress"
_ "m7s.live/v5/plugin/transcode"
_ "m7s.live/v5/plugin/webrtc"
_ "m7s.live/v5/plugin/webtransport"
)
func main() {

46
go.mod
View File

@@ -1,27 +1,32 @@
module m7s.live/v5
go 1.23
go 1.23.0
require (
github.com/Eyevinn/mp4ff v0.45.1
github.com/IOTechSystems/onvif v1.2.0
github.com/VictoriaMetrics/VictoriaMetrics v1.102.0
github.com/asavie/xdp v0.3.3
github.com/beevik/etree v1.4.1
github.com/bluenviron/gohlslib v1.4.0
github.com/c0deltin/duckdb-driver v0.1.0
github.com/cilium/ebpf v0.15.0
github.com/cloudwego/goref v0.0.0-20240724113447-685d2a9523c8
github.com/deepch/vdk v0.0.27
github.com/disintegration/imaging v1.6.2
github.com/emiago/sipgo v0.22.0
github.com/emiago/sipgo v0.29.0
github.com/go-delve/delve v1.23.0
github.com/gobwas/ws v1.3.2
github.com/golang-jwt/jwt/v5 v5.2.1
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0
github.com/google/gopacket v1.1.19
github.com/google/uuid v1.6.0
github.com/grpc-ecosystem/grpc-gateway/v2 v2.19.1
github.com/husanpao/ip v0.0.0-20220711082147-73160bb611a8
github.com/icholy/digest v0.1.22
github.com/jinzhu/copier v0.4.0
github.com/mattn/go-sqlite3 v1.14.24
github.com/mcuadros/go-defaults v1.2.0
github.com/mozillazg/go-pinyin v0.20.0
github.com/ncruces/go-sqlite3 v0.18.1
github.com/ncruces/go-sqlite3/gormlite v0.18.0
github.com/pion/interceptor v0.1.37
@@ -30,14 +35,15 @@ require (
github.com/pion/rtp v1.8.10
github.com/pion/sdp/v3 v3.0.9
github.com/pion/webrtc/v4 v4.0.7
github.com/quic-go/quic-go v0.43.1
github.com/quic-go/quic-go v0.50.1
github.com/rs/zerolog v1.33.0
github.com/samber/slog-common v0.17.1
github.com/shirou/gopsutil/v4 v4.24.8
github.com/stretchr/testify v1.10.0
github.com/vishvananda/netlink v1.1.0
github.com/yapingcat/gomedia v0.0.0-20240601043430-920523f8e5c7
golang.org/x/image v0.22.0
golang.org/x/text v0.20.0
golang.org/x/text v0.24.0
google.golang.org/genproto/googleapis/api v0.0.0-20240711142825-46eb208f015d
google.golang.org/grpc v1.65.0
google.golang.org/protobuf v1.34.2
@@ -47,15 +53,14 @@ require (
)
require (
github.com/IOTechSystems/onvif v1.2.0 // indirect
github.com/VictoriaMetrics/easyproto v0.1.4 // indirect
github.com/VictoriaMetrics/fastcache v1.12.2 // indirect
github.com/VictoriaMetrics/metrics v1.35.1 // indirect
github.com/VictoriaMetrics/metricsql v0.76.0 // indirect
github.com/abema/go-mp4 v1.2.0 // indirect
github.com/andybalholm/brotli v1.1.1 // indirect
github.com/asticode/go-astikit v0.30.0 // indirect
github.com/asticode/go-astits v1.13.0 // indirect
github.com/beevik/etree v1.4.1 // indirect
github.com/benburkert/openpgp v0.0.0-20160410205803-c2471f86866c // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
@@ -68,19 +73,16 @@ require (
github.com/go-sql-driver/mysql v1.7.0 // indirect
github.com/gobwas/httphead v0.1.0 // indirect
github.com/gobwas/pool v0.2.1 // indirect
github.com/golang-jwt/jwt/v5 v5.2.1 // indirect
github.com/golang/snappy v0.0.4 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/hashicorp/golang-lru v1.0.2 // indirect
github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect
github.com/jackc/pgx/v5 v5.5.5 // indirect
github.com/jackc/puddle/v2 v2.2.1 // indirect
github.com/jinzhu/copier v0.4.0 // indirect
github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.5 // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/klauspost/compress v1.17.9 // indirect
github.com/klauspost/compress v1.18.0 // indirect
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect
github.com/mailru/easyjson v0.7.7 // indirect
github.com/marcboeker/go-duckdb v1.0.5 // indirect
@@ -88,41 +90,33 @@ require (
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/mozillazg/go-pinyin v0.20.0 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/ncruces/julianday v1.0.0 // indirect
github.com/pion/datachannel v1.5.10 // indirect
github.com/pion/dtls/v2 v2.2.11 // indirect
github.com/pion/dtls/v3 v3.0.4 // indirect
github.com/pion/ice/v2 v2.3.9 // indirect
github.com/pion/ice/v4 v4.0.3 // indirect
github.com/pion/mdns v0.0.12 // indirect
github.com/pion/mdns/v2 v2.0.7 // indirect
github.com/pion/randutil v0.1.0 // indirect
github.com/pion/sctp v1.8.35 // indirect
github.com/pion/srtp/v2 v2.0.15 // indirect
github.com/pion/srtp/v3 v3.0.4 // indirect
github.com/pion/stun v0.6.1 // indirect
github.com/pion/stun/v3 v3.0.0 // indirect
github.com/pion/transport/v2 v2.2.5 // indirect
github.com/pion/transport/v3 v3.0.7 // indirect
github.com/pion/turn/v2 v2.1.2 // indirect
github.com/pion/turn/v4 v4.0.0 // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect
github.com/prometheus/client_model v0.6.1 // indirect
github.com/prometheus/common v0.55.0 // indirect
github.com/prometheus/procfs v0.15.1 // indirect
github.com/rogpeppe/go-internal v1.12.0 // indirect
github.com/quic-go/qpack v0.5.1 // indirect
github.com/samber/lo v1.44.0 // indirect
github.com/satori/go.uuid v1.2.1-0.20181028125025-b2ce2384e17b // indirect
github.com/shoenig/go-m1cpu v0.1.6 // indirect
github.com/sirupsen/logrus v1.9.3 // indirect
github.com/stretchr/testify v1.10.0 // indirect
github.com/tetratelabs/wazero v1.8.0 // indirect
github.com/tklauser/go-sysconf v0.3.12 // indirect
github.com/tklauser/numcpus v0.6.1 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/valyala/fasthttp v1.61.0 // indirect
github.com/valyala/fastjson v1.6.4 // indirect
github.com/valyala/fastrand v1.1.0 // indirect
github.com/valyala/gozstd v1.21.1 // indirect
@@ -132,7 +126,7 @@ require (
github.com/wlynxg/anet v0.0.5 // indirect
github.com/yusufpapurcu/wmi v1.2.4 // indirect
golang.org/x/arch v0.8.0 // indirect
golang.org/x/sync v0.9.0 // indirect
golang.org/x/sync v0.13.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20240711142825-46eb208f015d // indirect
)
@@ -149,12 +143,12 @@ require (
github.com/phsym/console-slog v0.3.1
github.com/prometheus/client_golang v1.20.4
github.com/quangngotan95/go-m3u8 v0.1.0
go.uber.org/mock v0.4.0 // indirect
golang.org/x/crypto v0.29.0 // indirect
go.uber.org/mock v0.5.0 // indirect
golang.org/x/crypto v0.37.0
golang.org/x/exp v0.0.0-20240716175740-e3f259677ff7
golang.org/x/mod v0.19.0 // indirect
golang.org/x/net v0.31.0
golang.org/x/sys v0.27.0
golang.org/x/net v0.39.0
golang.org/x/sys v0.32.0
golang.org/x/tools v0.23.0 // indirect
gopkg.in/yaml.v3 v3.0.1
)

215
go.sum
View File

@@ -1,5 +1,3 @@
github.com/Eyevinn/mp4ff v0.45.1 h1:Hlx8ZUu8agN7XrHVcZAGIa+dVZ0UW/g/SLv63Pm/+w0=
github.com/Eyevinn/mp4ff v0.45.1/go.mod h1:hJNUUqOBryLAzUW9wpCJyw2HaI+TCd2rUPhafoS5lgg=
github.com/IOTechSystems/onvif v1.2.0 h1:vplyPdhFhMRtIdkEbQIkTlrKjXpeDj+WUTt5UW61ZcI=
github.com/IOTechSystems/onvif v1.2.0/go.mod h1:/dTr5BtFaGojYGJ2rEBIVWh3seGIcSuCJhcK9zwTsk0=
github.com/VictoriaMetrics/VictoriaMetrics v1.102.0 h1:eRi6VGT7ntLG/OW8XTWUYhSvA+qGD3FHaRkzdgYHOOw=
@@ -19,6 +17,8 @@ github.com/alchemy/rotoslog v0.2.2 h1:yzAOjaQBKgJvAdPi0sF5KSPMq5f2vNJZEnPr73CPDz
github.com/alchemy/rotoslog v0.2.2/go.mod h1:pOHF0DKryPLaQzjcUlidLVRTksvk9yW75YIu1yYiiEQ=
github.com/allegro/bigcache v1.2.1-0.20190218064605-e24eb225f156 h1:eMwmnE/GDgah4HI848JfFxHt+iPb26b4zyfspmqY0/8=
github.com/allegro/bigcache v1.2.1-0.20190218064605-e24eb225f156/go.mod h1:Cb/ax3seSYIx7SuZdm2G2xzfwmv3TPSk2ucNfQESPXM=
github.com/andybalholm/brotli v1.1.1 h1:PR2pgnyFznKEugtsUo0xLdDop5SKXd5Qf5ysW+7XdTA=
github.com/andybalholm/brotli v1.1.1/go.mod h1:05ib4cKhjx3OQYUY22hTVd34Bc8upXjOLL2rKwwZBoA=
github.com/asavie/xdp v0.3.3 h1:b5Aa3EkMJYBeUO5TxPTIAa4wyUqYcsQr2s8f6YLJXhE=
github.com/asavie/xdp v0.3.3/go.mod h1:Vv5p+3mZiDh7ImdSvdon3E78wXyre7df5V58ATdIYAY=
github.com/asticode/go-astikit v0.30.0 h1:DkBkRQRIxYcknlaU7W7ksNfn4gMFsB0tqMJflxkRsZA=
@@ -69,11 +69,9 @@ github.com/disintegration/imaging v1.6.2 h1:w1LecBlG2Lnp8B3jk5zSuNqd7b4DXhcjwek1
github.com/disintegration/imaging v1.6.2/go.mod h1:44/5580QXChDfwIclfc/PCwrr44amcmDAg8hxG0Ewe4=
github.com/elgs/gostrgen v0.0.0-20220325073726-0c3e00d082f6 h1:x9TA+vnGEyqmWY+eA9HfgxNRkOQqwiEpFE9IPXSGuEA=
github.com/elgs/gostrgen v0.0.0-20220325073726-0c3e00d082f6/go.mod h1:wruC5r2gHdr/JIUs5Rr1V45YtsAzKXZxAnn/5rPC97g=
github.com/emiago/sipgo v0.22.0 h1:GaQ51m26M9QnVBVY2aDJ/mXqq/BDfZ1A+nW7XgU/4Ts=
github.com/emiago/sipgo v0.22.0/go.mod h1:a77FgPEEjJvfYWYfP3p53u+dNhWEMb/VGVS6guvBzx0=
github.com/emiago/sipgo v0.29.0 h1:dg/FwwhSl6hQTiOTIHzcqemZm3tB7jvGQgIlJmuD2Nw=
github.com/emiago/sipgo v0.29.0/go.mod h1:ZQ/tl5t+3assyOjiKw/AInPkcawBJ2Or+d5buztOZsc=
github.com/frankban/quicktest v1.11.3/go.mod h1:wRf/ReqHper53s+kmmSZizM8NamnL3IM0I9ntUbOk+k=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
github.com/go-delve/delve v1.23.0 h1:jYgZISZ14KAO3ys8kD07kjrowrygE9F9SIwnpz9xXys=
github.com/go-delve/delve v1.23.0/go.mod h1:S3SLuEE2mn7wipKilTvk1p9HdTMnXXElcEpiZ+VcuqU=
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
@@ -84,11 +82,8 @@ github.com/go-quicktest/qt v1.101.0 h1:O1K29Txy5P2OK0dGo59b7b0LR6wKfIhttaAhHUyn7
github.com/go-quicktest/qt v1.101.0/go.mod h1:14Bz/f7NwaXPtdYEgzsx46kqSxVwTbzVZsDC26tQJow=
github.com/go-sql-driver/mysql v1.7.0 h1:ueSltNNllEqE3qcWBTD0iQd3IpL/6U+mJxLkazJ7YPc=
github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI=
github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE=
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI=
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls=
github.com/go-test/deep v1.1.0 h1:WOcxcdHcvdgThNXjw0t76K42FXTU7HpNQWHpA2HHNlg=
github.com/go-test/deep v1.1.0/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE=
github.com/gobwas/httphead v0.1.0 h1:exrUm0f4YX0L7EBwZHuCF4GDp8aJfVeBrlLQrs6NqWU=
github.com/gobwas/httphead v0.1.0/go.mod h1:O/RXo79gxV8G+RqlR/otEwx4Q36zl9rqC5u12GKvMCM=
github.com/gobwas/pool v0.2.1 h1:xfeeEhW7pwmX8nuLVlqbzVc7udMDrwetjEv+TZIz1og=
@@ -100,24 +95,12 @@ github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17w
github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 h1:DACJavvAHhabrF08vX0COfcOBJRhZ8lUbR+ZWIs0Y5g=
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM=
github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
@@ -126,7 +109,6 @@ github.com/google/gopacket v1.1.19/go.mod h1:iJ8V8n6KS+z2U1A8pUwu8bW5SyEMkXJB8Yo
github.com/google/pprof v0.0.0-20240409012703-83162a5b38cd h1:gbpYu9NMq8jhDVbvlGkMFWCjLFlqqEZjEmObmhUy6Vo=
github.com/google/pprof v0.0.0-20240409012703-83162a5b38cd/go.mod h1:kf6iHlnVGwgKolg33glAes7Yg/8iWP8ukqeldJSO7jw=
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gorilla/websocket v1.5.1 h1:gmztn0JnHVt9JZquRuzLw3g4wouNVzKL15iLr/zn/QY=
@@ -135,7 +117,6 @@ github.com/grpc-ecosystem/grpc-gateway/v2 v2.19.1 h1:/c3QmbOGMGTOumP2iT/rCwB7b0Q
github.com/grpc-ecosystem/grpc-gateway/v2 v2.19.1/go.mod h1:5SN9VR2LTsRFsrEC6FHgRbTWrTHu6tqPeKxEQv15giM=
github.com/hashicorp/golang-lru v1.0.2 h1:dV3g9Z/unq5DpblPpw+Oqcv4dU/1omnb4Ok8iPY6p1c=
github.com/hashicorp/golang-lru v1.0.2/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4=
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
github.com/husanpao/ip v0.0.0-20220711082147-73160bb611a8 h1:4Jk58quTZmzJcTrLlbB5L1Q6qXu49EIjCReWxcBFWKo=
github.com/husanpao/ip v0.0.0-20220711082147-73160bb611a8/go.mod h1:medl9/CfYoQlqAXtAARmMW5dAX2UOdwwkhaszYPk0AM=
github.com/ianlancetaylor/demangle v0.0.0-20240912202439-0a2b6291aafd h1:EVX1s+XNss9jkRW9K6XGJn2jL2lB1h5H804oKPsxOec=
@@ -160,6 +141,8 @@ github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8Hm
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA=
github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw=
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
@@ -205,17 +188,8 @@ github.com/ncruces/go-sqlite3/gormlite v0.18.0/go.mod h1:RXeT1hknrz3A0tBDL6IfluD
github.com/ncruces/julianday v1.0.0 h1:fH0OKwa7NWvniGQtxdJRxAgkBMolni2BjDHaWTxqt7M=
github.com/ncruces/julianday v1.0.0/go.mod h1:Dusn2KvZrrovOMJuOt0TNXL6tB7U2E8kvza5fFc9G7g=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU=
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk=
github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0=
github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU=
github.com/onsi/ginkgo/v2 v2.9.5 h1:+6Hr4uxzP4XIUyAkg61dWBw8lb/gc4/X5luuxN/EC+Q=
github.com/onsi/ginkgo/v2 v2.9.5/go.mod h1:tvAoo1QUJwNEU2ITftXTpR7R1RbCzoZUOs3RonqW57k=
github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
github.com/onsi/gomega v1.17.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY=
github.com/onsi/gomega v1.27.6 h1:ENqfyGeS5AX/rlXDd/ETokDz93u0YufY1Pgxuy/PvWE=
github.com/onsi/gomega v1.27.6/go.mod h1:PIQNjfQwkP3aQAH7lf7j87O/5FiNr+ZR8+ipb+qQlhg=
github.com/orcaman/writerseeker v0.0.0-20200621085525-1d3f536ff85e h1:s2RNOM/IGdY0Y6qfTeUKhDawdHDpK9RGBdx80qN4Ttw=
@@ -224,84 +198,36 @@ github.com/orisano/pixelmatch v0.0.0-20220722002657-fb0b55479cde h1:x0TT0RDC7UhA
github.com/orisano/pixelmatch v0.0.0-20220722002657-fb0b55479cde/go.mod h1:nZgzbfBr3hhjoZnS66nKrHmduYNpc34ny7RK4z5/HM0=
github.com/phsym/console-slog v0.3.1 h1:Fuzcrjr40xTc004S9Kni8XfNsk+qrptQmyR+wZw9/7A=
github.com/phsym/console-slog v0.3.1/go.mod h1:oJskjp/X6e6c0mGpfP8ELkfKUsrkDifYRAqJQgmdDS0=
github.com/pion/datachannel v1.5.5/go.mod h1:iMz+lECmfdCMqFRhXhcA/219B0SQlbpoR2V118yimL0=
github.com/pion/datachannel v1.5.6 h1:1IxKJntfSlYkpUj8LlYRSWpYiTTC02nUrOE8T3DqGeg=
github.com/pion/datachannel v1.5.6/go.mod h1:1eKT6Q85pRnr2mHiWHxJwO50SfZRtWHTsNIVb/NfGW4=
github.com/pion/datachannel v1.5.10 h1:ly0Q26K1i6ZkGf42W7D4hQYR90pZwzFOjTq5AuCKk4o=
github.com/pion/datachannel v1.5.10/go.mod h1:p/jJfC9arb29W7WrxyKbepTU20CFgyx5oLo8Rs4Py/M=
github.com/pion/dtls/v2 v2.2.7/go.mod h1:8WiMkebSHFD0T+dIU+UeBaoV7kDhOW5oDCzZ7WZ/F9s=
github.com/pion/dtls/v2 v2.2.11 h1:9U/dpCYl1ySttROPWJgqWKEylUdT0fXp/xst6JwY5Ks=
github.com/pion/dtls/v2 v2.2.11/go.mod h1:d9SYc9fch0CqK90mRk1dC7AkzzpwJj6u2GU3u+9pqFE=
github.com/pion/dtls/v3 v3.0.4 h1:44CZekewMzfrn9pmGrj5BNnTMDCFwr+6sLH+cCuLM7U=
github.com/pion/dtls/v3 v3.0.4/go.mod h1:R373CsjxWqNPf6MEkfdy3aSe9niZvL/JaKlGeFphtMg=
github.com/pion/ice v0.7.18 h1:KbAWlzWRUdX9SmehBh3gYpIFsirjhSQsCw6K2MjYMK0=
github.com/pion/ice/v2 v2.3.9 h1:7yZpHf3PhPxJGT4JkMj1Y8Rl5cQ6fB709iz99aeMd/U=
github.com/pion/ice/v2 v2.3.9/go.mod h1:lT3kv5uUIlHfXHU/ZRD7uKD/ufM202+eTa3C/umgGf4=
github.com/pion/ice/v4 v4.0.3 h1:9s5rI1WKzF5DRqhJ+Id8bls/8PzM7mau0mj1WZb4IXE=
github.com/pion/ice/v4 v4.0.3/go.mod h1:VfHy0beAZ5loDT7BmJ2LtMtC4dbawIkkkejHPRZNB3Y=
github.com/pion/interceptor v0.1.17/go.mod h1:SY8kpmfVBvrbUzvj2bsXz7OJt5JvmVNZ+4Kjq7FcwrI=
github.com/pion/interceptor v0.1.29 h1:39fsnlP1U8gw2JzOFWdfCU82vHvhW9o0rZnZF56wF+M=
github.com/pion/interceptor v0.1.29/go.mod h1:ri+LGNjRUc5xUNtDEPzfdkmSqISixVTBF/z/Zms/6T4=
github.com/pion/interceptor v0.1.37 h1:aRA8Zpab/wE7/c0O3fh1PqY0AJI3fCSEM5lRWJVorwI=
github.com/pion/interceptor v0.1.37/go.mod h1:JzxbJ4umVTlZAf+/utHzNesY8tmRkM2lVmkS82TTj8Y=
github.com/pion/logging v0.2.2 h1:M9+AIj/+pxNsDfAT64+MAVgJO0rsyLnoJKCqf//DoeY=
github.com/pion/logging v0.2.2/go.mod h1:k0/tDVsRCX2Mb2ZEmTqNa7CWsQPc+YYCB7Q+5pahoms=
github.com/pion/mdns v0.0.7/go.mod h1:4iP2UbeFhLI/vWju/bw6ZfwjJzk0z8DNValjGxR/dD8=
github.com/pion/mdns v0.0.12 h1:CiMYlY+O0azojWDmxdNr7ADGrnZ+V6Ilfner+6mSVK8=
github.com/pion/mdns v0.0.12/go.mod h1:VExJjv8to/6Wqm1FXK+Ii/Z9tsVk/F5sD/N70cnYFbk=
github.com/pion/mdns/v2 v2.0.7 h1:c9kM8ewCgjslaAmicYMFQIde2H9/lrZpjBkN8VwoVtM=
github.com/pion/mdns/v2 v2.0.7/go.mod h1:vAdSYNAT0Jy3Ru0zl2YiW3Rm/fJCwIeM0nToenfOJKA=
github.com/pion/randutil v0.1.0 h1:CFG1UdESneORglEsnimhUjf33Rwjubwj6xfiOXBa3mA=
github.com/pion/randutil v0.1.0/go.mod h1:XcJrSMMbbMRhASFVOlj/5hQial/Y8oH/HVo7TBZq+j8=
github.com/pion/rtcp v1.2.10/go.mod h1:ztfEwXZNLGyF1oQDttz/ZKIBaeeg/oWbRYqzBM9TL1I=
github.com/pion/rtcp v1.2.14 h1:KCkGV3vJ+4DAJmvP0vaQShsb0xkRfWkO540Gy102KyE=
github.com/pion/rtcp v1.2.14/go.mod h1:sn6qjxvnwyAkkPzPULIbVqSKI5Dv54Rv7VG0kNxh9L4=
github.com/pion/rtcp v1.2.15 h1:LZQi2JbdipLOj4eBjK4wlVoQWfrZbh3Q6eHtWtJBZBo=
github.com/pion/rtcp v1.2.15/go.mod h1:jlGuAjHMEXwMUHK78RgX0UmEJFV4zUKOFHR7OP+D3D0=
github.com/pion/rtp v1.7.13/go.mod h1:bDb5n+BFZxXx0Ea7E5qe+klMuqiBrP+w8XSjiWtCUko=
github.com/pion/rtp v1.8.6 h1:MTmn/b0aWWsAzux2AmP8WGllusBVw4NPYPVFFd7jUPw=
github.com/pion/rtp v1.8.6/go.mod h1:pBGHaFt/yW7bf1jjWAoUjpSNoDnw98KTMg+jWWvziqU=
github.com/pion/rtp v1.8.10 h1:puphjdbjPB+L+NFaVuZ5h6bt1g5q4kFIoI+r5q/g0CU=
github.com/pion/rtp v1.8.10/go.mod h1:8uMBJj32Pa1wwx8Fuv/AsFhn8jsgw+3rUC2PfoBZ8p4=
github.com/pion/sctp v1.8.5/go.mod h1:SUFFfDpViyKejTAdwD1d/HQsCu+V/40cCs2nZIvC3s0=
github.com/pion/sctp v1.8.7/go.mod h1:g1Ul+ARqZq5JEmoFy87Q/4CePtKnTJ1QCL9dBBdN6AU=
github.com/pion/sctp v1.8.13/go.mod h1:YKSgO/bO/6aOMP9LCie1DuD7m+GamiK2yIiPM6vH+GA=
github.com/pion/sctp v1.8.16 h1:PKrMs+o9EMLRvFfXq59WFsC+V8mN1wnKzqrv+3D/gYY=
github.com/pion/sctp v1.8.16/go.mod h1:P6PbDVA++OJMrVNg2AL3XtYHV4uD6dvfyOovCgMs0PE=
github.com/pion/sctp v1.8.35 h1:qwtKvNK1Wc5tHMIYgTDJhfZk7vATGVHhXbUDfHbYwzA=
github.com/pion/sctp v1.8.35/go.mod h1:EcXP8zCYVTRy3W9xtOF7wJm1L1aXfKRQzaM33SjQlzg=
github.com/pion/sdp/v3 v3.0.6/go.mod h1:iiFWFpQO8Fy3S5ldclBkpXqmWy02ns78NOKoLLL0YQw=
github.com/pion/sdp/v3 v3.0.9 h1:pX++dCHoHUwq43kuwf3PyJfHlwIj4hXA7Vrifiq0IJY=
github.com/pion/sdp/v3 v3.0.9/go.mod h1:B5xmvENq5IXJimIO4zfp6LAe1fD9N+kFv+V/1lOdz8M=
github.com/pion/srtp v1.5.2 h1:25DmvH+fqKZDqvX64vTwnycVwL9ooJxHF/gkX16bDBY=
github.com/pion/srtp/v2 v2.0.15 h1:+tqRtXGsGwHC0G0IUIAzRmdkHvriF79IHVfZGfHrQoA=
github.com/pion/srtp/v2 v2.0.15/go.mod h1:b/pQOlDrbB0HEH5EUAQXzSYxikFbNcNuKmF8tM0hCtw=
github.com/pion/srtp/v3 v3.0.4 h1:2Z6vDVxzrX3UHEgrUyIGM4rRouoC7v+NiF1IHtp9B5M=
github.com/pion/srtp/v3 v3.0.4/go.mod h1:1Jx3FwDoxpRaTh1oRV8A/6G1BnFL+QI82eK4ms8EEJQ=
github.com/pion/stun v0.6.1 h1:8lp6YejULeHBF8NmV8e2787BogQhduZugh5PdhDyyN4=
github.com/pion/stun v0.6.1/go.mod h1:/hO7APkX4hZKu/D0f2lHzNyvdkTGtIy3NDmLR7kSz/8=
github.com/pion/stun/v3 v3.0.0 h1:4h1gwhWLWuZWOJIJR9s2ferRO+W3zA/b6ijOI6mKzUw=
github.com/pion/stun/v3 v3.0.0/go.mod h1:HvCN8txt8mwi4FBvS3EmDghW6aQJ24T+y+1TKjB5jyU=
github.com/pion/transport v0.14.1 h1:XSM6olwW+o8J4SCmOBb/BpwZypkHeyM0PGFCxNQBr40=
github.com/pion/transport v0.14.1/go.mod h1:4tGmbk00NeYA3rUa9+n+dzCCoKkcy3YlYb99Jn2fNnI=
github.com/pion/transport/v2 v2.0.0/go.mod h1:HS2MEBJTwD+1ZI2eSXSvHJx/HnzQqRy2/LXxt6eVMHc=
github.com/pion/transport/v2 v2.1.0/go.mod h1:AdSw4YBZVDkZm8fpoz+fclXyQwANWmZAlDuQdctTThQ=
github.com/pion/transport/v2 v2.2.0/go.mod h1:AdSw4YBZVDkZm8fpoz+fclXyQwANWmZAlDuQdctTThQ=
github.com/pion/transport/v2 v2.2.1/go.mod h1:cXXWavvCnFF6McHTft3DWS9iic2Mftcz1Aq29pGcU5g=
github.com/pion/transport/v2 v2.2.4/go.mod h1:q2U/tf9FEfnSBGSW6w5Qp5PFWRLRj3NjLhCCgpRK4p0=
github.com/pion/transport/v2 v2.2.5 h1:iyi25i/21gQck4hfRhomF6SktmUQjRsRW4WJdhfc3Kc=
github.com/pion/transport/v2 v2.2.5/go.mod h1:q2U/tf9FEfnSBGSW6w5Qp5PFWRLRj3NjLhCCgpRK4p0=
github.com/pion/transport/v3 v3.0.1/go.mod h1:UY7kiITrlMv7/IKgd5eTUcaahZx5oUN3l9SzK5f5xE0=
github.com/pion/transport/v3 v3.0.2 h1:r+40RJR25S9w3jbA6/5uEPTzcdn7ncyU44RWCbHkLg4=
github.com/pion/transport/v3 v3.0.2/go.mod h1:nIToODoOlb5If2jF9y2Igfx3PFYWfuXi37m0IlWa/D0=
github.com/pion/transport/v3 v3.0.7 h1:iRbMH05BzSNwhILHoBoAPxoB9xQgOaJk+591KC9P1o0=
github.com/pion/transport/v3 v3.0.7/go.mod h1:YleKiTZ4vqNxVwh77Z0zytYi7rXHl7j6uPLGhhz9rwo=
github.com/pion/turn/v2 v2.1.2 h1:wj0cAoGKltaZ790XEGW9HwoUewqjliwmhtxCuB2ApyM=
github.com/pion/turn/v2 v2.1.2/go.mod h1:1kjnPkBcex3dhCU2Am+AAmxDcGhLX3WnMfmkNpvSTQU=
github.com/pion/turn/v4 v4.0.0 h1:qxplo3Rxa9Yg1xXDxxH8xaqcyGUtbHYw4QSCvmFWvhM=
github.com/pion/turn/v4 v4.0.0/go.mod h1:MuPDkm15nYSklKpN8vWJ9W2M0PlyQZqYt1McGuxG7mA=
github.com/pion/webrtc/v4 v3.2.12 h1:pVqz5NdtTqyhKIhMcXR8bPp709kCf9blyAhDjoVRLvA=
github.com/pion/webrtc/v4 v3.2.12/go.mod h1:/Oz6K95CGWaN+3No+Z0NYvgOPOr3aY8UyTlMm/dec3A=
github.com/pion/webrtc/v4 v4.0.7 h1:aeq78uVnFZd2umXW0O9A2VFQYuS7+BZxWetQvSp2jPo=
github.com/pion/webrtc/v4 v4.0.7/go.mod h1:oFVBBVSHU3vAEwSgnk3BuKCwAUwpDwQhko1EDwyZWbU=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
@@ -322,8 +248,12 @@ github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0leargg
github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk=
github.com/quangngotan95/go-m3u8 v0.1.0 h1:8oseBjJn5IKHQKdRZwSNskkua3NLrRtlvXXtoVgBzMk=
github.com/quangngotan95/go-m3u8 v0.1.0/go.mod h1:smzfWHlYpBATVNu1GapKLYiCtEo5JxridIgvvudZ+Wc=
github.com/quic-go/qpack v0.5.1 h1:giqksBPnT/HDtZ6VhtFKgoLOWmlyo9Ei6u9PqzIMbhI=
github.com/quic-go/qpack v0.5.1/go.mod h1:+PC4XFrEskIVkcLzpEkbLqq1uCoxPhQuvK5rH1ZgaEg=
github.com/quic-go/quic-go v0.43.1 h1:fLiMNfQVe9q2JvSsiXo4fXOEguXHGGl9+6gLp4RPeZQ=
github.com/quic-go/quic-go v0.43.1/go.mod h1:132kz4kL3F9vxhW3CtQJLDVwcFe5wdWeJXXijhsO57M=
github.com/quic-go/quic-go v0.50.1 h1:unsgjFIUqW8a2oopkY7YNONpV1gYND6Nt9hnt1PN94Q=
github.com/quic-go/quic-go v0.50.1/go.mod h1:Vim6OmUvlYdwBhXP9ZVrtGmCMWa3wEqhq3NgYrI8b4E=
github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8=
github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4=
github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
@@ -339,7 +269,6 @@ github.com/samber/slog-multi v1.0.0 h1:snvP/P5GLQ8TQh5WSqdRaxDANW8AAA3egwEoytLsq
github.com/samber/slog-multi v1.0.0/go.mod h1:uLAvHpGqbYgX4FSL0p1ZwoLuveIAJvBECtE07XmYvFo=
github.com/satori/go.uuid v1.2.1-0.20181028125025-b2ce2384e17b h1:gQZ0qzfKHQIybLANtM3mBXNUtOfsCFXeTsnBqCsx1KM=
github.com/satori/go.uuid v1.2.1-0.20181028125025-b2ce2384e17b/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0=
github.com/sclevine/agouti v3.0.0+incompatible/go.mod h1:b4WX9W9L1sfQKXeJf1mUTLZKJ48R1S7H23Ji7oFO5Bw=
github.com/shirou/gopsutil/v4 v4.24.8 h1:pVQjIenQkIhqO81mwTaXjTzOMT7d3TZkf43PlVFHENI=
github.com/shirou/gopsutil/v4 v4.24.8/go.mod h1:wE0OrJtj4dG+hYkxqDH3QiBICdKSf04/npcvLLc/oRg=
github.com/shoenig/go-m1cpu v0.1.6 h1:nxdKQNcEB6vzgA2E2bvzKIYRuNj7XNJ4S/aRSwKzFtM=
@@ -355,16 +284,11 @@ github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpE
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
@@ -377,6 +301,8 @@ github.com/tklauser/numcpus v0.6.1 h1:ng9scYS7az0Bk4OZLvrNXNSAO2Pxr1XXRAPyjhIx+F
github.com/tklauser/numcpus v0.6.1/go.mod h1:1XfjsgE2zo8GVw7POkMbHENHzVg3GzmoZ9fESEdAacY=
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
github.com/valyala/fasthttp v1.61.0 h1:VV08V0AfoRaFurP1EWKvQQdPTZHiUzaVoulX1aBDgzU=
github.com/valyala/fasthttp v1.61.0/go.mod h1:wRIV/4cMwUPWnRcDno9hGnYZGh78QzODFfo1LTUhBog=
github.com/valyala/fastjson v1.6.4 h1:uAUNq9Z6ymTgGhcm0UynUAB6tlbakBrz6CQFax3BXVQ=
github.com/valyala/fastjson v1.6.4/go.mod h1:CLCAqky6SMuOcxStkYQvblddUtoRxhYMGLrsQns1aXY=
github.com/valyala/fastrand v1.1.0 h1:f+5HkLW4rsgzdNoleUOB69hyT9IlD2ZQh9GyDMfb5G8=
@@ -393,32 +319,23 @@ github.com/vishvananda/netns v0.0.0-20191106174202-0a2b9b5464df h1:OviZH7qLw/7Zo
github.com/vishvananda/netns v0.0.0-20191106174202-0a2b9b5464df/go.mod h1:JP3t17pCcGlemwknint6hfoeCVQrEMVwxRLRjXpq+BU=
github.com/wlynxg/anet v0.0.5 h1:J3VJGi1gvo0JwZ/P1/Yc/8p63SoW98B5dHkYDmpgvvU=
github.com/wlynxg/anet v0.0.5/go.mod h1:eay5PRQr7fIVAMbTbchTnO9gG65Hg/uYGdc7mguHxoA=
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
github.com/yapingcat/gomedia v0.0.0-20240601043430-920523f8e5c7 h1:e9n2WNcfvs20aLgpDhKoaJgrU/EeAvuNnWLBm31Q5Fw=
github.com/yapingcat/gomedia v0.0.0-20240601043430-920523f8e5c7/go.mod h1:WSZ59bidJOO40JSJmLqlkBJrjZCtjbKKkygEMfzY/kc=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=
github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
go.uber.org/mock v0.4.0 h1:VcM4ZOtdbR4f6VXfiOpwpVJDL6lCReaZ6mw31wqh7KU=
go.uber.org/mock v0.4.0/go.mod h1:a6FSlNadKUHUa9IP5Vyt1zh4fC7uAwxMutEAscFbkZc=
golang.org/x/arch v0.6.0 h1:S0JTfE48HbRj80+4tbvZDYsJ3tGv6BUU3XxyZ7CirAc=
golang.org/x/arch v0.6.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys=
go.uber.org/mock v0.5.0 h1:KAMbZvZPyBPWgD14IrIQ38QCyjwpvVVV6K/bHl1IwQU=
go.uber.org/mock v0.5.0/go.mod h1:ge71pBPLYDk7QIi1LupWxdAykm7KIEFchiOqd6z7qMM=
golang.org/x/arch v0.8.0 h1:3wRIsP3pM4yUptoR96otTUOXI367OS0+c9eeRi9doIc=
golang.org/x/arch v0.8.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.8.0/go.mod h1:mRqEX+O9/h5TFCrQhkgjo2yKi0yYA+9ecGkdQoHrywE=
golang.org/x/crypto v0.10.0/go.mod h1:o4eNf7Ede1fv+hwOwZsTHl9EsPFO6q6ZvYR8vYfY45I=
golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw=
golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg=
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs=
golang.org/x/crypto v0.26.0 h1:RrRspgV4mU+YwB4FYnuBoKsUapNIL5cohGAmSH3azsw=
golang.org/x/crypto v0.26.0/go.mod h1:GY7jblb9wI+FOo5y8/S2oY4zWP07AkOJ4+jxCqdqn54=
golang.org/x/crypto v0.29.0 h1:L5SG1JTTXupVV3n6sUqMTeWbjAyfPwoda2DLX8J8FrQ=
golang.org/x/crypto v0.29.0/go.mod h1:+F4F4N5hv6v38hfeYwTdx20oUvLLc+QfrE9Ax9HtgRg=
golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE=
golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc=
golang.org/x/exp v0.0.0-20240716175740-e3f259677ff7 h1:wDLEX9a7YQoKdKNQt88rtydkqDxeGaBUTnIYc3iG/mA=
golang.org/x/exp v0.0.0-20240716175740-e3f259677ff7/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY=
golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
@@ -426,159 +343,77 @@ golang.org/x/image v0.22.0 h1:UtK5yLUzilVrkjMAZAZ34DXGpASN8i8pj8g+O+yd10g=
golang.org/x/image v0.22.0/go.mod h1:9hPFhljd4zZ1GNSIZJ49sqbp45GKK9t6w+iXvGqZUz4=
golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.19.0 h1:fEdghXQSo20giMthA7cd28ZC+jts4amQ3YMXiP5oMQ8=
golang.org/x/mod v0.19.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco=
golang.org/x/net v0.5.0/go.mod h1:DivGGAXEgPSlEBzxGzZI+ZLohi+xUj054jfeKui00ws=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns=
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/net v0.11.0/go.mod h1:2L/ixqYpgIVXmeoSA/4Lu7BzTG4KIyPIryS4IsOd1oQ=
golang.org/x/net v0.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI=
golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY=
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
golang.org/x/net v0.22.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg=
golang.org/x/net v0.27.0 h1:5K3Njcw06/l2y9vpGCSdcxWOYHOUk3dVNGDXN+FvAys=
golang.org/x/net v0.27.0/go.mod h1:dDi0PyhWNoiUOrAS8uXv/vnScO4wnHQO4mj9fn/RytE=
golang.org/x/net v0.31.0 h1:68CPQngjLL0r2AlUKiSxtQFKvzRVbnzLwMUn5SzcLHo=
golang.org/x/net v0.31.0/go.mod h1:P4fl1q7dY2hnZFxEk4pPSkDHF+QqjitcnDjUQyMM+pM=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/net v0.39.0 h1:ZCu7HMWDxpXpaiKdhzIfaltL9Lp31x/3fCP11bc6/fY=
golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.9.0 h1:fEo0HyrW1GIgZdpbhCRO0PkJajUS5H9IFUztCgEo2jQ=
golang.org/x/sync v0.9.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sync v0.13.0 h1:AauUjRAJ9OSnvULf/ARrrVywoJDy0YS2AwQ98I37610=
golang.org/x/sync v0.13.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190606203320-7fc4e5ec1444/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210324051608-47abb6519492/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.9.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34=
golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.27.0 h1:wBqf8DvsY9Y/2P8gAfPDEYNuS30J4lPHJxXSb/nJZ+s=
golang.org/x/sys v0.27.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20=
golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.4.0/go.mod h1:9P2UbLfCdcvo3p/nzKvsmas4TnlujnuoV9hGgYzW1lQ=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U=
golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY=
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
golang.org/x/term v0.9.0/go.mod h1:M6DEAAIenWoTxdKrOltXcmDY3rSplQUkrvaDU5FcQyo=
golang.org/x/term v0.11.0/go.mod h1:zC9APTIj3jG3FdV/Ons+XE1riIZXG4aZ4GTHiPZJPIU=
golang.org/x/term v0.16.0/go.mod h1:yn7UURbUtPyrVJPGPq404EukNFxcm/foM+bV/bfcDsY=
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.6.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.10.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.20.0 h1:gK/Kv2otX8gz+wn7Rmb3vT96ZwuoxnQlY+HlJVj7Qug=
golang.org/x/text v0.20.0/go.mod h1:D4IsuqiFMhST5bX19pQ9ikHC2GsaKyk/oF+pn3ducp4=
golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0=
golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU=
golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190624222133-a101b041ded4/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191216052735-49a3e744a425/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/tools v0.23.0 h1:SGsXPZ+2l4JsgaCKkx+FQ9YZ5XEtA1GZYuoDjenLjvg=
golang.org/x/tools v0.23.0/go.mod h1:pnu6ufv6vQkll6szChhK3C3L/ruaIv5eBeztNG8wtsI=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/genproto/googleapis/api v0.0.0-20240711142825-46eb208f015d h1:kHjw/5UfflP/L5EbledDrcG4C2597RtymmGRZvHiCuY=
google.golang.org/genproto/googleapis/api v0.0.0-20240711142825-46eb208f015d/go.mod h1:mw8MG/Qz5wfgYr6VqVCiZcHe/GJEfI+oGGDCohaVgB0=
google.golang.org/genproto/googleapis/rpc v0.0.0-20240711142825-46eb208f015d h1:JU0iKnSg02Gmb5ZdV8nYsKEKsP6o/FGVWTrw4i1DA9A=
google.golang.org/genproto/googleapis/rpc v0.0.0-20240711142825-46eb208f015d/go.mod h1:Ue6ibwXGpU+dqIcODieyLOcgj7z8+IcskoNIgZxtrFY=
google.golang.org/grpc v1.65.0 h1:bs/cUb4lp1G5iImFFd3u5ixQzweKizoZJAwBNLR42lc=
google.golang.org/grpc v1.65.0/go.mod h1:WgYC2ypjlB0EiQi6wdKixMqukr6lBc0Vo+oOgjrM5ZQ=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg=
google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
gopkg.in/src-d/go-billy.v4 v4.3.2 h1:0SQA1pRztfTFx2miS8sA97XvooFeNOmvUenF4o0EcVg=
gopkg.in/src-d/go-billy.v4 v4.3.2/go.mod h1:nDjArDMp+XMs1aFAESLRjfGSgfvoYN0hDfzEk0GjC98=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

View File

@@ -1,7 +1,7 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.28.1
// protoc v3.19.1
// protoc-gen-go v1.36.5
// protoc v5.28.3
// source: auth.proto
package pb
@@ -12,6 +12,7 @@ import (
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
reflect "reflect"
sync "sync"
unsafe "unsafe"
)
const (
@@ -22,21 +23,18 @@ const (
)
type LoginRequest struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
state protoimpl.MessageState `protogen:"open.v1"`
Username string `protobuf:"bytes,1,opt,name=username,proto3" json:"username,omitempty"`
Password string `protobuf:"bytes,2,opt,name=password,proto3" json:"password,omitempty"`
unknownFields protoimpl.UnknownFields
Username string `protobuf:"bytes,1,opt,name=username,proto3" json:"username,omitempty"`
Password string `protobuf:"bytes,2,opt,name=password,proto3" json:"password,omitempty"`
sizeCache protoimpl.SizeCache
}
func (x *LoginRequest) Reset() {
*x = LoginRequest{}
if protoimpl.UnsafeEnabled {
mi := &file_auth_proto_msgTypes[0]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
mi := &file_auth_proto_msgTypes[0]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *LoginRequest) String() string {
@@ -47,7 +45,7 @@ func (*LoginRequest) ProtoMessage() {}
func (x *LoginRequest) ProtoReflect() protoreflect.Message {
mi := &file_auth_proto_msgTypes[0]
if protoimpl.UnsafeEnabled && x != nil {
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
@@ -77,21 +75,18 @@ func (x *LoginRequest) GetPassword() string {
}
type LoginSuccess struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
state protoimpl.MessageState `protogen:"open.v1"`
Token string `protobuf:"bytes,1,opt,name=token,proto3" json:"token,omitempty"`
UserInfo *UserInfo `protobuf:"bytes,2,opt,name=userInfo,proto3" json:"userInfo,omitempty"`
unknownFields protoimpl.UnknownFields
Token string `protobuf:"bytes,1,opt,name=token,proto3" json:"token,omitempty"`
UserInfo *UserInfo `protobuf:"bytes,2,opt,name=userInfo,proto3" json:"userInfo,omitempty"`
sizeCache protoimpl.SizeCache
}
func (x *LoginSuccess) Reset() {
*x = LoginSuccess{}
if protoimpl.UnsafeEnabled {
mi := &file_auth_proto_msgTypes[1]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
mi := &file_auth_proto_msgTypes[1]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *LoginSuccess) String() string {
@@ -102,7 +97,7 @@ func (*LoginSuccess) ProtoMessage() {}
func (x *LoginSuccess) ProtoReflect() protoreflect.Message {
mi := &file_auth_proto_msgTypes[1]
if protoimpl.UnsafeEnabled && x != nil {
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
@@ -132,22 +127,19 @@ func (x *LoginSuccess) GetUserInfo() *UserInfo {
}
type LoginResponse struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
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"`
Data *LoginSuccess `protobuf:"bytes,3,opt,name=data,proto3" json:"data,omitempty"`
unknownFields protoimpl.UnknownFields
Code int32 `protobuf:"varint,1,opt,name=code,proto3" json:"code,omitempty"`
Message string `protobuf:"bytes,2,opt,name=message,proto3" json:"message,omitempty"`
Data *LoginSuccess `protobuf:"bytes,3,opt,name=data,proto3" json:"data,omitempty"`
sizeCache protoimpl.SizeCache
}
func (x *LoginResponse) Reset() {
*x = LoginResponse{}
if protoimpl.UnsafeEnabled {
mi := &file_auth_proto_msgTypes[2]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
mi := &file_auth_proto_msgTypes[2]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *LoginResponse) String() string {
@@ -158,7 +150,7 @@ func (*LoginResponse) ProtoMessage() {}
func (x *LoginResponse) ProtoReflect() protoreflect.Message {
mi := &file_auth_proto_msgTypes[2]
if protoimpl.UnsafeEnabled && x != nil {
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
@@ -195,20 +187,17 @@ func (x *LoginResponse) GetData() *LoginSuccess {
}
type LogoutRequest struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
state protoimpl.MessageState `protogen:"open.v1"`
Token string `protobuf:"bytes,1,opt,name=token,proto3" json:"token,omitempty"`
unknownFields protoimpl.UnknownFields
Token string `protobuf:"bytes,1,opt,name=token,proto3" json:"token,omitempty"`
sizeCache protoimpl.SizeCache
}
func (x *LogoutRequest) Reset() {
*x = LogoutRequest{}
if protoimpl.UnsafeEnabled {
mi := &file_auth_proto_msgTypes[3]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
mi := &file_auth_proto_msgTypes[3]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *LogoutRequest) String() string {
@@ -219,7 +208,7 @@ func (*LogoutRequest) ProtoMessage() {}
func (x *LogoutRequest) ProtoReflect() protoreflect.Message {
mi := &file_auth_proto_msgTypes[3]
if protoimpl.UnsafeEnabled && x != nil {
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
@@ -242,21 +231,18 @@ func (x *LogoutRequest) GetToken() string {
}
type LogoutResponse struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
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"`
unknownFields protoimpl.UnknownFields
Code int32 `protobuf:"varint,1,opt,name=code,proto3" json:"code,omitempty"`
Message string `protobuf:"bytes,2,opt,name=message,proto3" json:"message,omitempty"`
sizeCache protoimpl.SizeCache
}
func (x *LogoutResponse) Reset() {
*x = LogoutResponse{}
if protoimpl.UnsafeEnabled {
mi := &file_auth_proto_msgTypes[4]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
mi := &file_auth_proto_msgTypes[4]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *LogoutResponse) String() string {
@@ -267,7 +253,7 @@ func (*LogoutResponse) ProtoMessage() {}
func (x *LogoutResponse) ProtoReflect() protoreflect.Message {
mi := &file_auth_proto_msgTypes[4]
if protoimpl.UnsafeEnabled && x != nil {
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
@@ -297,20 +283,17 @@ func (x *LogoutResponse) GetMessage() string {
}
type UserInfoRequest struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
state protoimpl.MessageState `protogen:"open.v1"`
Token string `protobuf:"bytes,1,opt,name=token,proto3" json:"token,omitempty"`
unknownFields protoimpl.UnknownFields
Token string `protobuf:"bytes,1,opt,name=token,proto3" json:"token,omitempty"`
sizeCache protoimpl.SizeCache
}
func (x *UserInfoRequest) Reset() {
*x = UserInfoRequest{}
if protoimpl.UnsafeEnabled {
mi := &file_auth_proto_msgTypes[5]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
mi := &file_auth_proto_msgTypes[5]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *UserInfoRequest) String() string {
@@ -321,7 +304,7 @@ func (*UserInfoRequest) ProtoMessage() {}
func (x *UserInfoRequest) ProtoReflect() protoreflect.Message {
mi := &file_auth_proto_msgTypes[5]
if protoimpl.UnsafeEnabled && x != nil {
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
@@ -344,21 +327,18 @@ func (x *UserInfoRequest) GetToken() string {
}
type UserInfo struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
state protoimpl.MessageState `protogen:"open.v1"`
Username string `protobuf:"bytes,1,opt,name=username,proto3" json:"username,omitempty"`
ExpiresAt int64 `protobuf:"varint,2,opt,name=expires_at,json=expiresAt,proto3" json:"expires_at,omitempty"` // Token expiration timestamp
unknownFields protoimpl.UnknownFields
Username string `protobuf:"bytes,1,opt,name=username,proto3" json:"username,omitempty"`
ExpiresAt int64 `protobuf:"varint,2,opt,name=expires_at,json=expiresAt,proto3" json:"expires_at,omitempty"` // Token expiration timestamp
sizeCache protoimpl.SizeCache
}
func (x *UserInfo) Reset() {
*x = UserInfo{}
if protoimpl.UnsafeEnabled {
mi := &file_auth_proto_msgTypes[6]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
mi := &file_auth_proto_msgTypes[6]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *UserInfo) String() string {
@@ -369,7 +349,7 @@ func (*UserInfo) ProtoMessage() {}
func (x *UserInfo) ProtoReflect() protoreflect.Message {
mi := &file_auth_proto_msgTypes[6]
if protoimpl.UnsafeEnabled && x != nil {
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
@@ -399,22 +379,19 @@ func (x *UserInfo) GetExpiresAt() int64 {
}
type UserInfoResponse struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
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"`
Data *UserInfo `protobuf:"bytes,3,opt,name=data,proto3" json:"data,omitempty"`
unknownFields protoimpl.UnknownFields
Code int32 `protobuf:"varint,1,opt,name=code,proto3" json:"code,omitempty"`
Message string `protobuf:"bytes,2,opt,name=message,proto3" json:"message,omitempty"`
Data *UserInfo `protobuf:"bytes,3,opt,name=data,proto3" json:"data,omitempty"`
sizeCache protoimpl.SizeCache
}
func (x *UserInfoResponse) Reset() {
*x = UserInfoResponse{}
if protoimpl.UnsafeEnabled {
mi := &file_auth_proto_msgTypes[7]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
mi := &file_auth_proto_msgTypes[7]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *UserInfoResponse) String() string {
@@ -425,7 +402,7 @@ func (*UserInfoResponse) ProtoMessage() {}
func (x *UserInfoResponse) ProtoReflect() protoreflect.Message {
mi := &file_auth_proto_msgTypes[7]
if protoimpl.UnsafeEnabled && x != nil {
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
@@ -463,7 +440,7 @@ func (x *UserInfoResponse) GetData() *UserInfo {
var File_auth_proto protoreflect.FileDescriptor
var file_auth_proto_rawDesc = []byte{
var file_auth_proto_rawDesc = string([]byte{
0x0a, 0x0a, 0x61, 0x75, 0x74, 0x68, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x02, 0x70, 0x62,
0x1a, 0x1c, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x61, 0x6e, 0x6e,
0x6f, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x46,
@@ -506,13 +483,13 @@ var file_auth_proto_rawDesc = []byte{
0x48, 0x0a, 0x05, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x12, 0x10, 0x2e, 0x70, 0x62, 0x2e, 0x4c, 0x6f,
0x67, 0x69, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x11, 0x2e, 0x70, 0x62, 0x2e,
0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x1a, 0x82,
0xd3, 0xe4, 0x93, 0x02, 0x14, 0x22, 0x0f, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x61, 0x75, 0x74, 0x68,
0x2f, 0x6c, 0x6f, 0x67, 0x69, 0x6e, 0x3a, 0x01, 0x2a, 0x12, 0x4c, 0x0a, 0x06, 0x4c, 0x6f, 0x67,
0xd3, 0xe4, 0x93, 0x02, 0x14, 0x3a, 0x01, 0x2a, 0x22, 0x0f, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x61,
0x75, 0x74, 0x68, 0x2f, 0x6c, 0x6f, 0x67, 0x69, 0x6e, 0x12, 0x4c, 0x0a, 0x06, 0x4c, 0x6f, 0x67,
0x6f, 0x75, 0x74, 0x12, 0x11, 0x2e, 0x70, 0x62, 0x2e, 0x4c, 0x6f, 0x67, 0x6f, 0x75, 0x74, 0x52,
0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x12, 0x2e, 0x70, 0x62, 0x2e, 0x4c, 0x6f, 0x67, 0x6f,
0x75, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x1b, 0x82, 0xd3, 0xe4, 0x93,
0x02, 0x15, 0x22, 0x10, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x61, 0x75, 0x74, 0x68, 0x2f, 0x6c, 0x6f,
0x67, 0x6f, 0x75, 0x74, 0x3a, 0x01, 0x2a, 0x12, 0x54, 0x0a, 0x0b, 0x47, 0x65, 0x74, 0x55, 0x73,
0x02, 0x15, 0x3a, 0x01, 0x2a, 0x22, 0x10, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x61, 0x75, 0x74, 0x68,
0x2f, 0x6c, 0x6f, 0x67, 0x6f, 0x75, 0x74, 0x12, 0x54, 0x0a, 0x0b, 0x47, 0x65, 0x74, 0x55, 0x73,
0x65, 0x72, 0x49, 0x6e, 0x66, 0x6f, 0x12, 0x13, 0x2e, 0x70, 0x62, 0x2e, 0x55, 0x73, 0x65, 0x72,
0x49, 0x6e, 0x66, 0x6f, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x14, 0x2e, 0x70, 0x62,
0x2e, 0x55, 0x73, 0x65, 0x72, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73,
@@ -520,22 +497,22 @@ var file_auth_proto_rawDesc = []byte{
0x61, 0x75, 0x74, 0x68, 0x2f, 0x75, 0x73, 0x65, 0x72, 0x69, 0x6e, 0x66, 0x6f, 0x42, 0x10, 0x5a,
0x0e, 0x6d, 0x37, 0x73, 0x2e, 0x6c, 0x69, 0x76, 0x65, 0x2f, 0x76, 0x35, 0x2f, 0x70, 0x62, 0x62,
0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
}
})
var (
file_auth_proto_rawDescOnce sync.Once
file_auth_proto_rawDescData = file_auth_proto_rawDesc
file_auth_proto_rawDescData []byte
)
func file_auth_proto_rawDescGZIP() []byte {
file_auth_proto_rawDescOnce.Do(func() {
file_auth_proto_rawDescData = protoimpl.X.CompressGZIP(file_auth_proto_rawDescData)
file_auth_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_auth_proto_rawDesc), len(file_auth_proto_rawDesc)))
})
return file_auth_proto_rawDescData
}
var file_auth_proto_msgTypes = make([]protoimpl.MessageInfo, 8)
var file_auth_proto_goTypes = []interface{}{
var file_auth_proto_goTypes = []any{
(*LoginRequest)(nil), // 0: pb.LoginRequest
(*LoginSuccess)(nil), // 1: pb.LoginSuccess
(*LoginResponse)(nil), // 2: pb.LoginResponse
@@ -567,109 +544,11 @@ func file_auth_proto_init() {
if File_auth_proto != nil {
return
}
if !protoimpl.UnsafeEnabled {
file_auth_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*LoginRequest); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
file_auth_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*LoginSuccess); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
file_auth_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*LoginResponse); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
file_auth_proto_msgTypes[3].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*LogoutRequest); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
file_auth_proto_msgTypes[4].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*LogoutResponse); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
file_auth_proto_msgTypes[5].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*UserInfoRequest); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
file_auth_proto_msgTypes[6].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*UserInfo); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
file_auth_proto_msgTypes[7].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*UserInfoResponse); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
}
type x struct{}
out := protoimpl.TypeBuilder{
File: protoimpl.DescBuilder{
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: file_auth_proto_rawDesc,
RawDescriptor: unsafe.Slice(unsafe.StringData(file_auth_proto_rawDesc), len(file_auth_proto_rawDesc)),
NumEnums: 0,
NumMessages: 8,
NumExtensions: 0,
@@ -680,7 +559,6 @@ func file_auth_proto_init() {
MessageInfos: file_auth_proto_msgTypes,
}.Build()
File_auth_proto = out.File
file_auth_proto_rawDesc = nil
file_auth_proto_goTypes = nil
file_auth_proto_depIdxs = nil
}

View File

@@ -123,6 +123,7 @@ func local_request_Auth_GetUserInfo_0(ctx context.Context, marshaler runtime.Mar
// UnaryRPC :call AuthServer directly.
// StreamingRPC :currently unsupported pending https://github.com/grpc/grpc-go/issues/906.
// Note that using this registration option will cause many gRPC library features to stop working. Consider using RegisterAuthHandlerFromEndpoint instead.
// GRPC interceptors will not work for this type of registration. To use interceptors, you must use the "runtime.WithMiddlewares" option in the "runtime.NewServeMux" call.
func RegisterAuthHandlerServer(ctx context.Context, mux *runtime.ServeMux, server AuthServer) error {
mux.Handle("POST", pattern_Auth_Login_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
@@ -206,21 +207,21 @@ func RegisterAuthHandlerServer(ctx context.Context, mux *runtime.ServeMux, serve
// RegisterAuthHandlerFromEndpoint is same as RegisterAuthHandler but
// automatically dials to "endpoint" and closes the connection when "ctx" gets done.
func RegisterAuthHandlerFromEndpoint(ctx context.Context, mux *runtime.ServeMux, endpoint string, opts []grpc.DialOption) (err error) {
conn, err := grpc.DialContext(ctx, endpoint, opts...)
conn, err := grpc.NewClient(endpoint, opts...)
if err != nil {
return err
}
defer func() {
if err != nil {
if cerr := conn.Close(); cerr != nil {
grpclog.Infof("Failed to close conn to %s: %v", endpoint, cerr)
grpclog.Errorf("Failed to close conn to %s: %v", endpoint, cerr)
}
return
}
go func() {
<-ctx.Done()
if cerr := conn.Close(); cerr != nil {
grpclog.Infof("Failed to close conn to %s: %v", endpoint, cerr)
grpclog.Errorf("Failed to close conn to %s: %v", endpoint, cerr)
}
}()
}()
@@ -238,7 +239,7 @@ func RegisterAuthHandler(ctx context.Context, mux *runtime.ServeMux, conn *grpc.
// to "mux". The handlers forward requests to the grpc endpoint over the given implementation of "AuthClient".
// Note: the gRPC framework executes interceptors within the gRPC handler. If the passed in "AuthClient"
// doesn't go through the normal gRPC flow (creating a gRPC client etc.) then it will be up to the passed in
// "AuthClient" to call the correct interceptors.
// "AuthClient" to call the correct interceptors. This client ignores the HTTP middlewares.
func RegisterAuthHandlerClient(ctx context.Context, mux *runtime.ServeMux, client AuthClient) error {
mux.Handle("POST", pattern_Auth_Login_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {

View File

@@ -1,7 +1,7 @@
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
// versions:
// - protoc-gen-go-grpc v1.2.0
// - protoc v3.19.1
// - protoc-gen-go-grpc v1.5.1
// - protoc v5.28.3
// source: auth.proto
package pb
@@ -15,8 +15,14 @@ import (
// This is a compile-time assertion to ensure that this generated file
// is compatible with the grpc package it is being compiled against.
// Requires gRPC-Go v1.32.0 or later.
const _ = grpc.SupportPackageIsVersion7
// Requires gRPC-Go v1.64.0 or later.
const _ = grpc.SupportPackageIsVersion9
const (
Auth_Login_FullMethodName = "/pb.Auth/Login"
Auth_Logout_FullMethodName = "/pb.Auth/Logout"
Auth_GetUserInfo_FullMethodName = "/pb.Auth/GetUserInfo"
)
// AuthClient is the client API for Auth service.
//
@@ -36,8 +42,9 @@ func NewAuthClient(cc grpc.ClientConnInterface) AuthClient {
}
func (c *authClient) Login(ctx context.Context, in *LoginRequest, opts ...grpc.CallOption) (*LoginResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(LoginResponse)
err := c.cc.Invoke(ctx, "/pb.Auth/Login", in, out, opts...)
err := c.cc.Invoke(ctx, Auth_Login_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
@@ -45,8 +52,9 @@ func (c *authClient) Login(ctx context.Context, in *LoginRequest, opts ...grpc.C
}
func (c *authClient) Logout(ctx context.Context, in *LogoutRequest, opts ...grpc.CallOption) (*LogoutResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(LogoutResponse)
err := c.cc.Invoke(ctx, "/pb.Auth/Logout", in, out, opts...)
err := c.cc.Invoke(ctx, Auth_Logout_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
@@ -54,8 +62,9 @@ func (c *authClient) Logout(ctx context.Context, in *LogoutRequest, opts ...grpc
}
func (c *authClient) GetUserInfo(ctx context.Context, in *UserInfoRequest, opts ...grpc.CallOption) (*UserInfoResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(UserInfoResponse)
err := c.cc.Invoke(ctx, "/pb.Auth/GetUserInfo", in, out, opts...)
err := c.cc.Invoke(ctx, Auth_GetUserInfo_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
@@ -64,7 +73,7 @@ func (c *authClient) GetUserInfo(ctx context.Context, in *UserInfoRequest, opts
// AuthServer is the server API for Auth service.
// All implementations must embed UnimplementedAuthServer
// for forward compatibility
// for forward compatibility.
type AuthServer interface {
Login(context.Context, *LoginRequest) (*LoginResponse, error)
Logout(context.Context, *LogoutRequest) (*LogoutResponse, error)
@@ -72,9 +81,12 @@ type AuthServer interface {
mustEmbedUnimplementedAuthServer()
}
// UnimplementedAuthServer must be embedded to have forward compatible implementations.
type UnimplementedAuthServer struct {
}
// UnimplementedAuthServer must be embedded to have
// forward compatible implementations.
//
// NOTE: this should be embedded by value instead of pointer to avoid a nil
// pointer dereference when methods are called.
type UnimplementedAuthServer struct{}
func (UnimplementedAuthServer) Login(context.Context, *LoginRequest) (*LoginResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method Login not implemented")
@@ -86,6 +98,7 @@ func (UnimplementedAuthServer) GetUserInfo(context.Context, *UserInfoRequest) (*
return nil, status.Errorf(codes.Unimplemented, "method GetUserInfo not implemented")
}
func (UnimplementedAuthServer) mustEmbedUnimplementedAuthServer() {}
func (UnimplementedAuthServer) testEmbeddedByValue() {}
// UnsafeAuthServer may be embedded to opt out of forward compatibility for this service.
// Use of this interface is not recommended, as added methods to AuthServer will
@@ -95,6 +108,13 @@ type UnsafeAuthServer interface {
}
func RegisterAuthServer(s grpc.ServiceRegistrar, srv AuthServer) {
// If the following call pancis, it indicates UnimplementedAuthServer was
// embedded by pointer and is nil. This will cause panics if an
// unimplemented method is ever invoked, so we test this at initialization
// time to prevent it from happening at runtime later due to I/O.
if t, ok := srv.(interface{ testEmbeddedByValue() }); ok {
t.testEmbeddedByValue()
}
s.RegisterService(&Auth_ServiceDesc, srv)
}
@@ -108,7 +128,7 @@ func _Auth_Login_Handler(srv interface{}, ctx context.Context, dec func(interfac
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: "/pb.Auth/Login",
FullMethod: Auth_Login_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(AuthServer).Login(ctx, req.(*LoginRequest))
@@ -126,7 +146,7 @@ func _Auth_Logout_Handler(srv interface{}, ctx context.Context, dec func(interfa
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: "/pb.Auth/Logout",
FullMethod: Auth_Logout_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(AuthServer).Logout(ctx, req.(*LogoutRequest))
@@ -144,7 +164,7 @@ func _Auth_GetUserInfo_Handler(srv interface{}, ctx context.Context, dec func(in
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: "/pb.Auth/GetUserInfo",
FullMethod: Auth_GetUserInfo_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(AuthServer).GetUserInfo(ctx, req.(*UserInfoRequest))

File diff suppressed because it is too large Load Diff

View File

@@ -1192,66 +1192,6 @@ func local_request_Api_GetFormily_0(ctx context.Context, marshaler runtime.Marsh
}
func request_Api_ModifyConfig_0(ctx context.Context, marshaler runtime.Marshaler, client ApiClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
var protoReq ModifyConfigRequest
var metadata runtime.ServerMetadata
if err := marshaler.NewDecoder(req.Body).Decode(&protoReq.Yaml); err != nil && err != io.EOF {
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
}
var (
val string
ok bool
err error
_ = err
)
val, ok = pathParams["name"]
if !ok {
return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "name")
}
protoReq.Name, err = runtime.String(val)
if err != nil {
return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "name", err)
}
msg, err := client.ModifyConfig(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))
return msg, metadata, err
}
func local_request_Api_ModifyConfig_0(ctx context.Context, marshaler runtime.Marshaler, server ApiServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
var protoReq ModifyConfigRequest
var metadata runtime.ServerMetadata
if err := marshaler.NewDecoder(req.Body).Decode(&protoReq.Yaml); err != nil && err != io.EOF {
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
}
var (
val string
ok bool
err error
_ = err
)
val, ok = pathParams["name"]
if !ok {
return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "name")
}
protoReq.Name, err = runtime.String(val)
if err != nil {
return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "name", err)
}
msg, err := server.ModifyConfig(ctx, &protoReq)
return msg, metadata, err
}
func request_Api_GetPullProxyList_0(ctx context.Context, marshaler runtime.Marshaler, client ApiClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
var protoReq emptypb.Empty
var metadata runtime.ServerMetadata
@@ -1904,6 +1844,7 @@ func local_request_Api_DeleteRecord_0(ctx context.Context, marshaler runtime.Mar
// UnaryRPC :call ApiServer directly.
// StreamingRPC :currently unsupported pending https://github.com/grpc/grpc-go/issues/906.
// Note that using this registration option will cause many gRPC library features to stop working. Consider using RegisterApiHandlerFromEndpoint instead.
// GRPC interceptors will not work for this type of registration. To use interceptors, you must use the "runtime.WithMiddlewares" option in the "runtime.NewServeMux" call.
func RegisterApiHandlerServer(ctx context.Context, mux *runtime.ServeMux, server ApiServer) error {
mux.Handle("GET", pattern_Api_SysInfo_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
@@ -2581,31 +2522,6 @@ func RegisterApiHandlerServer(ctx context.Context, mux *runtime.ServeMux, server
})
mux.Handle("POST", pattern_Api_ModifyConfig_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)
var err error
var annotatedContext context.Context
annotatedContext, err = runtime.AnnotateIncomingContext(ctx, mux, req, "/global.Api/ModifyConfig", runtime.WithHTTPPathPattern("/api/config/modify/{name}"))
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
resp, md, err := local_request_Api_ModifyConfig_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_ModifyConfig_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
})
mux.Handle("GET", pattern_Api_GetPullProxyList_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
ctx, cancel := context.WithCancel(req.Context())
defer cancel()
@@ -2739,7 +2655,7 @@ func RegisterApiHandlerServer(ctx context.Context, mux *runtime.ServeMux, server
inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
var err error
var annotatedContext context.Context
annotatedContext, err = runtime.AnnotateIncomingContext(ctx, mux, req, "/global.Api/RemovePullProxy", runtime.WithHTTPPathPattern("/api/device/add/{id}"))
annotatedContext, err = runtime.AnnotateIncomingContext(ctx, mux, req, "/global.Api/RemovePullProxy", runtime.WithHTTPPathPattern("/api/device/remove/{id}"))
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
@@ -3037,21 +2953,21 @@ func RegisterApiHandlerServer(ctx context.Context, mux *runtime.ServeMux, server
// RegisterApiHandlerFromEndpoint is same as RegisterApiHandler but
// automatically dials to "endpoint" and closes the connection when "ctx" gets done.
func RegisterApiHandlerFromEndpoint(ctx context.Context, mux *runtime.ServeMux, endpoint string, opts []grpc.DialOption) (err error) {
conn, err := grpc.DialContext(ctx, endpoint, opts...)
conn, err := grpc.NewClient(endpoint, opts...)
if err != nil {
return err
}
defer func() {
if err != nil {
if cerr := conn.Close(); cerr != nil {
grpclog.Infof("Failed to close conn to %s: %v", endpoint, cerr)
grpclog.Errorf("Failed to close conn to %s: %v", endpoint, cerr)
}
return
}
go func() {
<-ctx.Done()
if cerr := conn.Close(); cerr != nil {
grpclog.Infof("Failed to close conn to %s: %v", endpoint, cerr)
grpclog.Errorf("Failed to close conn to %s: %v", endpoint, cerr)
}
}()
}()
@@ -3069,7 +2985,7 @@ func RegisterApiHandler(ctx context.Context, mux *runtime.ServeMux, conn *grpc.C
// to "mux". The handlers forward requests to the grpc endpoint over the given implementation of "ApiClient".
// Note: the gRPC framework executes interceptors within the gRPC handler. If the passed in "ApiClient"
// doesn't go through the normal gRPC flow (creating a gRPC client etc.) then it will be up to the passed in
// "ApiClient" to call the correct interceptors.
// "ApiClient" to call the correct interceptors. This client ignores the HTTP middlewares.
func RegisterApiHandlerClient(ctx context.Context, mux *runtime.ServeMux, client ApiClient) error {
mux.Handle("GET", pattern_Api_SysInfo_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
@@ -3666,28 +3582,6 @@ func RegisterApiHandlerClient(ctx context.Context, mux *runtime.ServeMux, client
})
mux.Handle("POST", pattern_Api_ModifyConfig_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)
var err error
var annotatedContext context.Context
annotatedContext, err = runtime.AnnotateContext(ctx, mux, req, "/global.Api/ModifyConfig", runtime.WithHTTPPathPattern("/api/config/modify/{name}"))
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
resp, md, err := request_Api_ModifyConfig_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_ModifyConfig_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
})
mux.Handle("GET", pattern_Api_GetPullProxyList_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
ctx, cancel := context.WithCancel(req.Context())
defer cancel()
@@ -3804,7 +3698,7 @@ func RegisterApiHandlerClient(ctx context.Context, mux *runtime.ServeMux, client
inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
var err error
var annotatedContext context.Context
annotatedContext, err = runtime.AnnotateContext(ctx, mux, req, "/global.Api/RemovePullProxy", runtime.WithHTTPPathPattern("/api/device/add/{id}"))
annotatedContext, err = runtime.AnnotateContext(ctx, mux, req, "/global.Api/RemovePullProxy", runtime.WithHTTPPathPattern("/api/device/remove/{id}"))
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
@@ -4120,8 +4014,6 @@ var (
pattern_Api_GetFormily_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 1, 0, 4, 1, 5, 3}, []string{"api", "config", "formily", "name"}, ""))
pattern_Api_ModifyConfig_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 1, 0, 4, 1, 5, 3}, []string{"api", "config", "modify", "name"}, ""))
pattern_Api_GetPullProxyList_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 2, 3}, []string{"api", "proxy", "pull", "list"}, ""))
pattern_Api_GetPullProxyList_1 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2}, []string{"api", "device", "list"}, ""))
@@ -4132,7 +4024,7 @@ var (
pattern_Api_RemovePullProxy_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 2, 3, 1, 0, 4, 1, 5, 4}, []string{"api", "proxy", "pull", "remove", "id"}, ""))
pattern_Api_RemovePullProxy_1 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 1, 0, 4, 1, 5, 3}, []string{"api", "device", "add", "id"}, ""))
pattern_Api_RemovePullProxy_1 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 1, 0, 4, 1, 5, 3}, []string{"api", "device", "remove", "id"}, ""))
pattern_Api_UpdatePullProxy_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 2, 3}, []string{"api", "proxy", "pull", "update"}, ""))
@@ -4212,8 +4104,6 @@ var (
forward_Api_GetFormily_0 = runtime.ForwardResponseMessage
forward_Api_ModifyConfig_0 = runtime.ForwardResponseMessage
forward_Api_GetPullProxyList_0 = runtime.ForwardResponseMessage
forward_Api_GetPullProxyList_1 = runtime.ForwardResponseMessage

View File

@@ -152,12 +152,7 @@ service api {
get: "/api/config/formily/{name}"
};
}
rpc ModifyConfig (ModifyConfigRequest) returns (SuccessResponse) {
option (google.api.http) = {
post: "/api/config/modify/{name}"
body: "yaml"
};
}
rpc GetPullProxyList (google.protobuf.Empty) returns (PullProxyListResponse) {
option (google.api.http) = {
get: "/api/proxy/pull/list"
@@ -181,7 +176,7 @@ service api {
post: "/api/proxy/pull/remove/{id}"
body: "*"
additional_bindings {
post: "/api/device/add/{id}"
post: "/api/device/remove/{id}"
body: "*"
}
};
@@ -671,6 +666,7 @@ message ReqRecordList {
uint32 pageSize = 6;
string mode = 7;
string type = 8;
string eventLevel = 9;
}
message RecordFile {
@@ -679,6 +675,9 @@ message RecordFile {
string streamPath = 3;
google.protobuf.Timestamp startTime = 4;
google.protobuf.Timestamp endTime = 5;
string eventLevel = 6;
string eventName = 7;
string eventDesc = 8;
}
message ResponseList {

View File

@@ -1,7 +1,7 @@
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
// versions:
// - protoc-gen-go-grpc v1.2.0
// - protoc v3.19.1
// - protoc-gen-go-grpc v1.5.1
// - protoc v5.28.3
// source: global.proto
package pb
@@ -16,8 +16,51 @@ import (
// This is a compile-time assertion to ensure that this generated file
// is compatible with the grpc package it is being compiled against.
// Requires gRPC-Go v1.32.0 or later.
const _ = grpc.SupportPackageIsVersion7
// Requires gRPC-Go v1.64.0 or later.
const _ = grpc.SupportPackageIsVersion9
const (
Api_SysInfo_FullMethodName = "/global.api/SysInfo"
Api_DisabledPlugins_FullMethodName = "/global.api/DisabledPlugins"
Api_Summary_FullMethodName = "/global.api/Summary"
Api_Shutdown_FullMethodName = "/global.api/Shutdown"
Api_Restart_FullMethodName = "/global.api/Restart"
Api_TaskTree_FullMethodName = "/global.api/TaskTree"
Api_StopTask_FullMethodName = "/global.api/StopTask"
Api_RestartTask_FullMethodName = "/global.api/RestartTask"
Api_StreamList_FullMethodName = "/global.api/StreamList"
Api_WaitList_FullMethodName = "/global.api/WaitList"
Api_StreamInfo_FullMethodName = "/global.api/StreamInfo"
Api_PauseStream_FullMethodName = "/global.api/PauseStream"
Api_ResumeStream_FullMethodName = "/global.api/ResumeStream"
Api_SetStreamSpeed_FullMethodName = "/global.api/SetStreamSpeed"
Api_SeekStream_FullMethodName = "/global.api/SeekStream"
Api_GetSubscribers_FullMethodName = "/global.api/GetSubscribers"
Api_AudioTrackSnap_FullMethodName = "/global.api/AudioTrackSnap"
Api_VideoTrackSnap_FullMethodName = "/global.api/VideoTrackSnap"
Api_ChangeSubscribe_FullMethodName = "/global.api/ChangeSubscribe"
Api_GetStreamAlias_FullMethodName = "/global.api/GetStreamAlias"
Api_SetStreamAlias_FullMethodName = "/global.api/SetStreamAlias"
Api_StopPublish_FullMethodName = "/global.api/StopPublish"
Api_StopSubscribe_FullMethodName = "/global.api/StopSubscribe"
Api_GetConfigFile_FullMethodName = "/global.api/GetConfigFile"
Api_UpdateConfigFile_FullMethodName = "/global.api/UpdateConfigFile"
Api_GetConfig_FullMethodName = "/global.api/GetConfig"
Api_GetFormily_FullMethodName = "/global.api/GetFormily"
Api_GetPullProxyList_FullMethodName = "/global.api/GetPullProxyList"
Api_AddPullProxy_FullMethodName = "/global.api/AddPullProxy"
Api_RemovePullProxy_FullMethodName = "/global.api/RemovePullProxy"
Api_UpdatePullProxy_FullMethodName = "/global.api/UpdatePullProxy"
Api_GetPushProxyList_FullMethodName = "/global.api/GetPushProxyList"
Api_AddPushProxy_FullMethodName = "/global.api/AddPushProxy"
Api_RemovePushProxy_FullMethodName = "/global.api/RemovePushProxy"
Api_UpdatePushProxy_FullMethodName = "/global.api/UpdatePushProxy"
Api_GetRecording_FullMethodName = "/global.api/GetRecording"
Api_GetTransformList_FullMethodName = "/global.api/GetTransformList"
Api_GetRecordList_FullMethodName = "/global.api/GetRecordList"
Api_GetRecordCatalog_FullMethodName = "/global.api/GetRecordCatalog"
Api_DeleteRecord_FullMethodName = "/global.api/DeleteRecord"
)
// ApiClient is the client API for Api service.
//
@@ -50,7 +93,6 @@ type ApiClient interface {
UpdateConfigFile(ctx context.Context, in *UpdateConfigFileRequest, opts ...grpc.CallOption) (*SuccessResponse, error)
GetConfig(ctx context.Context, in *GetConfigRequest, opts ...grpc.CallOption) (*GetConfigResponse, error)
GetFormily(ctx context.Context, in *GetConfigRequest, opts ...grpc.CallOption) (*GetConfigResponse, error)
ModifyConfig(ctx context.Context, in *ModifyConfigRequest, opts ...grpc.CallOption) (*SuccessResponse, error)
GetPullProxyList(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*PullProxyListResponse, error)
AddPullProxy(ctx context.Context, in *PullProxyInfo, opts ...grpc.CallOption) (*SuccessResponse, error)
RemovePullProxy(ctx context.Context, in *RequestWithId, opts ...grpc.CallOption) (*SuccessResponse, error)
@@ -75,8 +117,9 @@ func NewApiClient(cc grpc.ClientConnInterface) ApiClient {
}
func (c *apiClient) SysInfo(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*SysInfoResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(SysInfoResponse)
err := c.cc.Invoke(ctx, "/global.api/SysInfo", in, out, opts...)
err := c.cc.Invoke(ctx, Api_SysInfo_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
@@ -84,8 +127,9 @@ func (c *apiClient) SysInfo(ctx context.Context, in *emptypb.Empty, opts ...grpc
}
func (c *apiClient) DisabledPlugins(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*DisabledPluginsResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(DisabledPluginsResponse)
err := c.cc.Invoke(ctx, "/global.api/DisabledPlugins", in, out, opts...)
err := c.cc.Invoke(ctx, Api_DisabledPlugins_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
@@ -93,8 +137,9 @@ func (c *apiClient) DisabledPlugins(ctx context.Context, in *emptypb.Empty, opts
}
func (c *apiClient) Summary(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*SummaryResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(SummaryResponse)
err := c.cc.Invoke(ctx, "/global.api/Summary", in, out, opts...)
err := c.cc.Invoke(ctx, Api_Summary_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
@@ -102,8 +147,9 @@ func (c *apiClient) Summary(ctx context.Context, in *emptypb.Empty, opts ...grpc
}
func (c *apiClient) Shutdown(ctx context.Context, in *RequestWithId, opts ...grpc.CallOption) (*SuccessResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(SuccessResponse)
err := c.cc.Invoke(ctx, "/global.api/Shutdown", in, out, opts...)
err := c.cc.Invoke(ctx, Api_Shutdown_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
@@ -111,8 +157,9 @@ func (c *apiClient) Shutdown(ctx context.Context, in *RequestWithId, opts ...grp
}
func (c *apiClient) Restart(ctx context.Context, in *RequestWithId, opts ...grpc.CallOption) (*SuccessResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(SuccessResponse)
err := c.cc.Invoke(ctx, "/global.api/Restart", in, out, opts...)
err := c.cc.Invoke(ctx, Api_Restart_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
@@ -120,8 +167,9 @@ func (c *apiClient) Restart(ctx context.Context, in *RequestWithId, opts ...grpc
}
func (c *apiClient) TaskTree(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*TaskTreeResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(TaskTreeResponse)
err := c.cc.Invoke(ctx, "/global.api/TaskTree", in, out, opts...)
err := c.cc.Invoke(ctx, Api_TaskTree_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
@@ -129,8 +177,9 @@ func (c *apiClient) TaskTree(ctx context.Context, in *emptypb.Empty, opts ...grp
}
func (c *apiClient) StopTask(ctx context.Context, in *RequestWithId64, opts ...grpc.CallOption) (*SuccessResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(SuccessResponse)
err := c.cc.Invoke(ctx, "/global.api/StopTask", in, out, opts...)
err := c.cc.Invoke(ctx, Api_StopTask_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
@@ -138,8 +187,9 @@ func (c *apiClient) StopTask(ctx context.Context, in *RequestWithId64, opts ...g
}
func (c *apiClient) RestartTask(ctx context.Context, in *RequestWithId64, opts ...grpc.CallOption) (*SuccessResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(SuccessResponse)
err := c.cc.Invoke(ctx, "/global.api/RestartTask", in, out, opts...)
err := c.cc.Invoke(ctx, Api_RestartTask_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
@@ -147,8 +197,9 @@ func (c *apiClient) RestartTask(ctx context.Context, in *RequestWithId64, opts .
}
func (c *apiClient) StreamList(ctx context.Context, in *StreamListRequest, opts ...grpc.CallOption) (*StreamListResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(StreamListResponse)
err := c.cc.Invoke(ctx, "/global.api/StreamList", in, out, opts...)
err := c.cc.Invoke(ctx, Api_StreamList_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
@@ -156,8 +207,9 @@ func (c *apiClient) StreamList(ctx context.Context, in *StreamListRequest, opts
}
func (c *apiClient) WaitList(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*StreamWaitListResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(StreamWaitListResponse)
err := c.cc.Invoke(ctx, "/global.api/WaitList", in, out, opts...)
err := c.cc.Invoke(ctx, Api_WaitList_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
@@ -165,8 +217,9 @@ func (c *apiClient) WaitList(ctx context.Context, in *emptypb.Empty, opts ...grp
}
func (c *apiClient) StreamInfo(ctx context.Context, in *StreamSnapRequest, opts ...grpc.CallOption) (*StreamInfoResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(StreamInfoResponse)
err := c.cc.Invoke(ctx, "/global.api/StreamInfo", in, out, opts...)
err := c.cc.Invoke(ctx, Api_StreamInfo_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
@@ -174,8 +227,9 @@ func (c *apiClient) StreamInfo(ctx context.Context, in *StreamSnapRequest, opts
}
func (c *apiClient) PauseStream(ctx context.Context, in *StreamSnapRequest, opts ...grpc.CallOption) (*SuccessResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(SuccessResponse)
err := c.cc.Invoke(ctx, "/global.api/PauseStream", in, out, opts...)
err := c.cc.Invoke(ctx, Api_PauseStream_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
@@ -183,8 +237,9 @@ func (c *apiClient) PauseStream(ctx context.Context, in *StreamSnapRequest, opts
}
func (c *apiClient) ResumeStream(ctx context.Context, in *StreamSnapRequest, opts ...grpc.CallOption) (*SuccessResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(SuccessResponse)
err := c.cc.Invoke(ctx, "/global.api/ResumeStream", in, out, opts...)
err := c.cc.Invoke(ctx, Api_ResumeStream_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
@@ -192,8 +247,9 @@ func (c *apiClient) ResumeStream(ctx context.Context, in *StreamSnapRequest, opt
}
func (c *apiClient) SetStreamSpeed(ctx context.Context, in *SetStreamSpeedRequest, opts ...grpc.CallOption) (*SuccessResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(SuccessResponse)
err := c.cc.Invoke(ctx, "/global.api/SetStreamSpeed", in, out, opts...)
err := c.cc.Invoke(ctx, Api_SetStreamSpeed_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
@@ -201,8 +257,9 @@ func (c *apiClient) SetStreamSpeed(ctx context.Context, in *SetStreamSpeedReques
}
func (c *apiClient) SeekStream(ctx context.Context, in *SeekStreamRequest, opts ...grpc.CallOption) (*SuccessResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(SuccessResponse)
err := c.cc.Invoke(ctx, "/global.api/SeekStream", in, out, opts...)
err := c.cc.Invoke(ctx, Api_SeekStream_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
@@ -210,8 +267,9 @@ func (c *apiClient) SeekStream(ctx context.Context, in *SeekStreamRequest, opts
}
func (c *apiClient) GetSubscribers(ctx context.Context, in *SubscribersRequest, opts ...grpc.CallOption) (*SubscribersResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(SubscribersResponse)
err := c.cc.Invoke(ctx, "/global.api/GetSubscribers", in, out, opts...)
err := c.cc.Invoke(ctx, Api_GetSubscribers_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
@@ -219,8 +277,9 @@ func (c *apiClient) GetSubscribers(ctx context.Context, in *SubscribersRequest,
}
func (c *apiClient) AudioTrackSnap(ctx context.Context, in *StreamSnapRequest, opts ...grpc.CallOption) (*TrackSnapShotResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(TrackSnapShotResponse)
err := c.cc.Invoke(ctx, "/global.api/AudioTrackSnap", in, out, opts...)
err := c.cc.Invoke(ctx, Api_AudioTrackSnap_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
@@ -228,8 +287,9 @@ func (c *apiClient) AudioTrackSnap(ctx context.Context, in *StreamSnapRequest, o
}
func (c *apiClient) VideoTrackSnap(ctx context.Context, in *StreamSnapRequest, opts ...grpc.CallOption) (*TrackSnapShotResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(TrackSnapShotResponse)
err := c.cc.Invoke(ctx, "/global.api/VideoTrackSnap", in, out, opts...)
err := c.cc.Invoke(ctx, Api_VideoTrackSnap_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
@@ -237,8 +297,9 @@ func (c *apiClient) VideoTrackSnap(ctx context.Context, in *StreamSnapRequest, o
}
func (c *apiClient) ChangeSubscribe(ctx context.Context, in *ChangeSubscribeRequest, opts ...grpc.CallOption) (*SuccessResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(SuccessResponse)
err := c.cc.Invoke(ctx, "/global.api/ChangeSubscribe", in, out, opts...)
err := c.cc.Invoke(ctx, Api_ChangeSubscribe_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
@@ -246,8 +307,9 @@ func (c *apiClient) ChangeSubscribe(ctx context.Context, in *ChangeSubscribeRequ
}
func (c *apiClient) GetStreamAlias(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*StreamAliasListResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(StreamAliasListResponse)
err := c.cc.Invoke(ctx, "/global.api/GetStreamAlias", in, out, opts...)
err := c.cc.Invoke(ctx, Api_GetStreamAlias_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
@@ -255,8 +317,9 @@ func (c *apiClient) GetStreamAlias(ctx context.Context, in *emptypb.Empty, opts
}
func (c *apiClient) SetStreamAlias(ctx context.Context, in *SetStreamAliasRequest, opts ...grpc.CallOption) (*SuccessResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(SuccessResponse)
err := c.cc.Invoke(ctx, "/global.api/SetStreamAlias", in, out, opts...)
err := c.cc.Invoke(ctx, Api_SetStreamAlias_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
@@ -264,8 +327,9 @@ func (c *apiClient) SetStreamAlias(ctx context.Context, in *SetStreamAliasReques
}
func (c *apiClient) StopPublish(ctx context.Context, in *StreamSnapRequest, opts ...grpc.CallOption) (*SuccessResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(SuccessResponse)
err := c.cc.Invoke(ctx, "/global.api/StopPublish", in, out, opts...)
err := c.cc.Invoke(ctx, Api_StopPublish_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
@@ -273,8 +337,9 @@ func (c *apiClient) StopPublish(ctx context.Context, in *StreamSnapRequest, opts
}
func (c *apiClient) StopSubscribe(ctx context.Context, in *RequestWithId, opts ...grpc.CallOption) (*SuccessResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(SuccessResponse)
err := c.cc.Invoke(ctx, "/global.api/StopSubscribe", in, out, opts...)
err := c.cc.Invoke(ctx, Api_StopSubscribe_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
@@ -282,8 +347,9 @@ func (c *apiClient) StopSubscribe(ctx context.Context, in *RequestWithId, opts .
}
func (c *apiClient) GetConfigFile(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*GetConfigFileResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(GetConfigFileResponse)
err := c.cc.Invoke(ctx, "/global.api/GetConfigFile", in, out, opts...)
err := c.cc.Invoke(ctx, Api_GetConfigFile_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
@@ -291,8 +357,9 @@ func (c *apiClient) GetConfigFile(ctx context.Context, in *emptypb.Empty, opts .
}
func (c *apiClient) UpdateConfigFile(ctx context.Context, in *UpdateConfigFileRequest, opts ...grpc.CallOption) (*SuccessResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(SuccessResponse)
err := c.cc.Invoke(ctx, "/global.api/UpdateConfigFile", in, out, opts...)
err := c.cc.Invoke(ctx, Api_UpdateConfigFile_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
@@ -300,8 +367,9 @@ func (c *apiClient) UpdateConfigFile(ctx context.Context, in *UpdateConfigFileRe
}
func (c *apiClient) GetConfig(ctx context.Context, in *GetConfigRequest, opts ...grpc.CallOption) (*GetConfigResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(GetConfigResponse)
err := c.cc.Invoke(ctx, "/global.api/GetConfig", in, out, opts...)
err := c.cc.Invoke(ctx, Api_GetConfig_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
@@ -309,17 +377,9 @@ func (c *apiClient) GetConfig(ctx context.Context, in *GetConfigRequest, opts ..
}
func (c *apiClient) GetFormily(ctx context.Context, in *GetConfigRequest, opts ...grpc.CallOption) (*GetConfigResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(GetConfigResponse)
err := c.cc.Invoke(ctx, "/global.api/GetFormily", in, out, opts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *apiClient) ModifyConfig(ctx context.Context, in *ModifyConfigRequest, opts ...grpc.CallOption) (*SuccessResponse, error) {
out := new(SuccessResponse)
err := c.cc.Invoke(ctx, "/global.api/ModifyConfig", in, out, opts...)
err := c.cc.Invoke(ctx, Api_GetFormily_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
@@ -327,8 +387,9 @@ func (c *apiClient) ModifyConfig(ctx context.Context, in *ModifyConfigRequest, o
}
func (c *apiClient) GetPullProxyList(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*PullProxyListResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(PullProxyListResponse)
err := c.cc.Invoke(ctx, "/global.api/GetPullProxyList", in, out, opts...)
err := c.cc.Invoke(ctx, Api_GetPullProxyList_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
@@ -336,8 +397,9 @@ func (c *apiClient) GetPullProxyList(ctx context.Context, in *emptypb.Empty, opt
}
func (c *apiClient) AddPullProxy(ctx context.Context, in *PullProxyInfo, opts ...grpc.CallOption) (*SuccessResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(SuccessResponse)
err := c.cc.Invoke(ctx, "/global.api/AddPullProxy", in, out, opts...)
err := c.cc.Invoke(ctx, Api_AddPullProxy_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
@@ -345,8 +407,9 @@ func (c *apiClient) AddPullProxy(ctx context.Context, in *PullProxyInfo, opts ..
}
func (c *apiClient) RemovePullProxy(ctx context.Context, in *RequestWithId, opts ...grpc.CallOption) (*SuccessResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(SuccessResponse)
err := c.cc.Invoke(ctx, "/global.api/RemovePullProxy", in, out, opts...)
err := c.cc.Invoke(ctx, Api_RemovePullProxy_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
@@ -354,8 +417,9 @@ func (c *apiClient) RemovePullProxy(ctx context.Context, in *RequestWithId, opts
}
func (c *apiClient) UpdatePullProxy(ctx context.Context, in *PullProxyInfo, opts ...grpc.CallOption) (*SuccessResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(SuccessResponse)
err := c.cc.Invoke(ctx, "/global.api/UpdatePullProxy", in, out, opts...)
err := c.cc.Invoke(ctx, Api_UpdatePullProxy_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
@@ -363,8 +427,9 @@ func (c *apiClient) UpdatePullProxy(ctx context.Context, in *PullProxyInfo, opts
}
func (c *apiClient) GetPushProxyList(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*PushProxyListResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(PushProxyListResponse)
err := c.cc.Invoke(ctx, "/global.api/GetPushProxyList", in, out, opts...)
err := c.cc.Invoke(ctx, Api_GetPushProxyList_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
@@ -372,8 +437,9 @@ func (c *apiClient) GetPushProxyList(ctx context.Context, in *emptypb.Empty, opt
}
func (c *apiClient) AddPushProxy(ctx context.Context, in *PushProxyInfo, opts ...grpc.CallOption) (*SuccessResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(SuccessResponse)
err := c.cc.Invoke(ctx, "/global.api/AddPushProxy", in, out, opts...)
err := c.cc.Invoke(ctx, Api_AddPushProxy_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
@@ -381,8 +447,9 @@ func (c *apiClient) AddPushProxy(ctx context.Context, in *PushProxyInfo, opts ..
}
func (c *apiClient) RemovePushProxy(ctx context.Context, in *RequestWithId, opts ...grpc.CallOption) (*SuccessResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(SuccessResponse)
err := c.cc.Invoke(ctx, "/global.api/RemovePushProxy", in, out, opts...)
err := c.cc.Invoke(ctx, Api_RemovePushProxy_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
@@ -390,8 +457,9 @@ func (c *apiClient) RemovePushProxy(ctx context.Context, in *RequestWithId, opts
}
func (c *apiClient) UpdatePushProxy(ctx context.Context, in *PushProxyInfo, opts ...grpc.CallOption) (*SuccessResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(SuccessResponse)
err := c.cc.Invoke(ctx, "/global.api/UpdatePushProxy", in, out, opts...)
err := c.cc.Invoke(ctx, Api_UpdatePushProxy_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
@@ -399,8 +467,9 @@ func (c *apiClient) UpdatePushProxy(ctx context.Context, in *PushProxyInfo, opts
}
func (c *apiClient) GetRecording(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*RecordingListResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(RecordingListResponse)
err := c.cc.Invoke(ctx, "/global.api/GetRecording", in, out, opts...)
err := c.cc.Invoke(ctx, Api_GetRecording_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
@@ -408,8 +477,9 @@ func (c *apiClient) GetRecording(ctx context.Context, in *emptypb.Empty, opts ..
}
func (c *apiClient) GetTransformList(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*TransformListResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(TransformListResponse)
err := c.cc.Invoke(ctx, "/global.api/GetTransformList", in, out, opts...)
err := c.cc.Invoke(ctx, Api_GetTransformList_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
@@ -417,8 +487,9 @@ func (c *apiClient) GetTransformList(ctx context.Context, in *emptypb.Empty, opt
}
func (c *apiClient) GetRecordList(ctx context.Context, in *ReqRecordList, opts ...grpc.CallOption) (*ResponseList, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(ResponseList)
err := c.cc.Invoke(ctx, "/global.api/GetRecordList", in, out, opts...)
err := c.cc.Invoke(ctx, Api_GetRecordList_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
@@ -426,8 +497,9 @@ func (c *apiClient) GetRecordList(ctx context.Context, in *ReqRecordList, opts .
}
func (c *apiClient) GetRecordCatalog(ctx context.Context, in *ReqRecordCatalog, opts ...grpc.CallOption) (*ResponseCatalog, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(ResponseCatalog)
err := c.cc.Invoke(ctx, "/global.api/GetRecordCatalog", in, out, opts...)
err := c.cc.Invoke(ctx, Api_GetRecordCatalog_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
@@ -435,8 +507,9 @@ func (c *apiClient) GetRecordCatalog(ctx context.Context, in *ReqRecordCatalog,
}
func (c *apiClient) DeleteRecord(ctx context.Context, in *ReqRecordDelete, opts ...grpc.CallOption) (*ResponseDelete, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(ResponseDelete)
err := c.cc.Invoke(ctx, "/global.api/DeleteRecord", in, out, opts...)
err := c.cc.Invoke(ctx, Api_DeleteRecord_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
@@ -445,7 +518,7 @@ func (c *apiClient) DeleteRecord(ctx context.Context, in *ReqRecordDelete, opts
// ApiServer is the server API for Api service.
// All implementations must embed UnimplementedApiServer
// for forward compatibility
// for forward compatibility.
type ApiServer interface {
SysInfo(context.Context, *emptypb.Empty) (*SysInfoResponse, error)
DisabledPlugins(context.Context, *emptypb.Empty) (*DisabledPluginsResponse, error)
@@ -474,7 +547,6 @@ type ApiServer interface {
UpdateConfigFile(context.Context, *UpdateConfigFileRequest) (*SuccessResponse, error)
GetConfig(context.Context, *GetConfigRequest) (*GetConfigResponse, error)
GetFormily(context.Context, *GetConfigRequest) (*GetConfigResponse, error)
ModifyConfig(context.Context, *ModifyConfigRequest) (*SuccessResponse, error)
GetPullProxyList(context.Context, *emptypb.Empty) (*PullProxyListResponse, error)
AddPullProxy(context.Context, *PullProxyInfo) (*SuccessResponse, error)
RemovePullProxy(context.Context, *RequestWithId) (*SuccessResponse, error)
@@ -491,9 +563,12 @@ type ApiServer interface {
mustEmbedUnimplementedApiServer()
}
// UnimplementedApiServer must be embedded to have forward compatible implementations.
type UnimplementedApiServer struct {
}
// UnimplementedApiServer must be embedded to have
// forward compatible implementations.
//
// NOTE: this should be embedded by value instead of pointer to avoid a nil
// pointer dereference when methods are called.
type UnimplementedApiServer struct{}
func (UnimplementedApiServer) SysInfo(context.Context, *emptypb.Empty) (*SysInfoResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method SysInfo not implemented")
@@ -576,9 +651,6 @@ func (UnimplementedApiServer) GetConfig(context.Context, *GetConfigRequest) (*Ge
func (UnimplementedApiServer) GetFormily(context.Context, *GetConfigRequest) (*GetConfigResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method GetFormily not implemented")
}
func (UnimplementedApiServer) ModifyConfig(context.Context, *ModifyConfigRequest) (*SuccessResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method ModifyConfig not implemented")
}
func (UnimplementedApiServer) GetPullProxyList(context.Context, *emptypb.Empty) (*PullProxyListResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method GetPullProxyList not implemented")
}
@@ -619,6 +691,7 @@ func (UnimplementedApiServer) DeleteRecord(context.Context, *ReqRecordDelete) (*
return nil, status.Errorf(codes.Unimplemented, "method DeleteRecord not implemented")
}
func (UnimplementedApiServer) mustEmbedUnimplementedApiServer() {}
func (UnimplementedApiServer) testEmbeddedByValue() {}
// UnsafeApiServer may be embedded to opt out of forward compatibility for this service.
// Use of this interface is not recommended, as added methods to ApiServer will
@@ -628,6 +701,13 @@ type UnsafeApiServer interface {
}
func RegisterApiServer(s grpc.ServiceRegistrar, srv ApiServer) {
// If the following call pancis, it indicates UnimplementedApiServer was
// embedded by pointer and is nil. This will cause panics if an
// unimplemented method is ever invoked, so we test this at initialization
// time to prevent it from happening at runtime later due to I/O.
if t, ok := srv.(interface{ testEmbeddedByValue() }); ok {
t.testEmbeddedByValue()
}
s.RegisterService(&Api_ServiceDesc, srv)
}
@@ -641,7 +721,7 @@ func _Api_SysInfo_Handler(srv interface{}, ctx context.Context, dec func(interfa
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: "/global.api/SysInfo",
FullMethod: Api_SysInfo_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(ApiServer).SysInfo(ctx, req.(*emptypb.Empty))
@@ -659,7 +739,7 @@ func _Api_DisabledPlugins_Handler(srv interface{}, ctx context.Context, dec func
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: "/global.api/DisabledPlugins",
FullMethod: Api_DisabledPlugins_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(ApiServer).DisabledPlugins(ctx, req.(*emptypb.Empty))
@@ -677,7 +757,7 @@ func _Api_Summary_Handler(srv interface{}, ctx context.Context, dec func(interfa
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: "/global.api/Summary",
FullMethod: Api_Summary_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(ApiServer).Summary(ctx, req.(*emptypb.Empty))
@@ -695,7 +775,7 @@ func _Api_Shutdown_Handler(srv interface{}, ctx context.Context, dec func(interf
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: "/global.api/Shutdown",
FullMethod: Api_Shutdown_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(ApiServer).Shutdown(ctx, req.(*RequestWithId))
@@ -713,7 +793,7 @@ func _Api_Restart_Handler(srv interface{}, ctx context.Context, dec func(interfa
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: "/global.api/Restart",
FullMethod: Api_Restart_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(ApiServer).Restart(ctx, req.(*RequestWithId))
@@ -731,7 +811,7 @@ func _Api_TaskTree_Handler(srv interface{}, ctx context.Context, dec func(interf
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: "/global.api/TaskTree",
FullMethod: Api_TaskTree_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(ApiServer).TaskTree(ctx, req.(*emptypb.Empty))
@@ -749,7 +829,7 @@ func _Api_StopTask_Handler(srv interface{}, ctx context.Context, dec func(interf
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: "/global.api/StopTask",
FullMethod: Api_StopTask_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(ApiServer).StopTask(ctx, req.(*RequestWithId64))
@@ -767,7 +847,7 @@ func _Api_RestartTask_Handler(srv interface{}, ctx context.Context, dec func(int
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: "/global.api/RestartTask",
FullMethod: Api_RestartTask_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(ApiServer).RestartTask(ctx, req.(*RequestWithId64))
@@ -785,7 +865,7 @@ func _Api_StreamList_Handler(srv interface{}, ctx context.Context, dec func(inte
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: "/global.api/StreamList",
FullMethod: Api_StreamList_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(ApiServer).StreamList(ctx, req.(*StreamListRequest))
@@ -803,7 +883,7 @@ func _Api_WaitList_Handler(srv interface{}, ctx context.Context, dec func(interf
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: "/global.api/WaitList",
FullMethod: Api_WaitList_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(ApiServer).WaitList(ctx, req.(*emptypb.Empty))
@@ -821,7 +901,7 @@ func _Api_StreamInfo_Handler(srv interface{}, ctx context.Context, dec func(inte
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: "/global.api/StreamInfo",
FullMethod: Api_StreamInfo_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(ApiServer).StreamInfo(ctx, req.(*StreamSnapRequest))
@@ -839,7 +919,7 @@ func _Api_PauseStream_Handler(srv interface{}, ctx context.Context, dec func(int
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: "/global.api/PauseStream",
FullMethod: Api_PauseStream_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(ApiServer).PauseStream(ctx, req.(*StreamSnapRequest))
@@ -857,7 +937,7 @@ func _Api_ResumeStream_Handler(srv interface{}, ctx context.Context, dec func(in
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: "/global.api/ResumeStream",
FullMethod: Api_ResumeStream_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(ApiServer).ResumeStream(ctx, req.(*StreamSnapRequest))
@@ -875,7 +955,7 @@ func _Api_SetStreamSpeed_Handler(srv interface{}, ctx context.Context, dec func(
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: "/global.api/SetStreamSpeed",
FullMethod: Api_SetStreamSpeed_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(ApiServer).SetStreamSpeed(ctx, req.(*SetStreamSpeedRequest))
@@ -893,7 +973,7 @@ func _Api_SeekStream_Handler(srv interface{}, ctx context.Context, dec func(inte
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: "/global.api/SeekStream",
FullMethod: Api_SeekStream_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(ApiServer).SeekStream(ctx, req.(*SeekStreamRequest))
@@ -911,7 +991,7 @@ func _Api_GetSubscribers_Handler(srv interface{}, ctx context.Context, dec func(
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: "/global.api/GetSubscribers",
FullMethod: Api_GetSubscribers_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(ApiServer).GetSubscribers(ctx, req.(*SubscribersRequest))
@@ -929,7 +1009,7 @@ func _Api_AudioTrackSnap_Handler(srv interface{}, ctx context.Context, dec func(
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: "/global.api/AudioTrackSnap",
FullMethod: Api_AudioTrackSnap_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(ApiServer).AudioTrackSnap(ctx, req.(*StreamSnapRequest))
@@ -947,7 +1027,7 @@ func _Api_VideoTrackSnap_Handler(srv interface{}, ctx context.Context, dec func(
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: "/global.api/VideoTrackSnap",
FullMethod: Api_VideoTrackSnap_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(ApiServer).VideoTrackSnap(ctx, req.(*StreamSnapRequest))
@@ -965,7 +1045,7 @@ func _Api_ChangeSubscribe_Handler(srv interface{}, ctx context.Context, dec func
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: "/global.api/ChangeSubscribe",
FullMethod: Api_ChangeSubscribe_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(ApiServer).ChangeSubscribe(ctx, req.(*ChangeSubscribeRequest))
@@ -983,7 +1063,7 @@ func _Api_GetStreamAlias_Handler(srv interface{}, ctx context.Context, dec func(
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: "/global.api/GetStreamAlias",
FullMethod: Api_GetStreamAlias_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(ApiServer).GetStreamAlias(ctx, req.(*emptypb.Empty))
@@ -1001,7 +1081,7 @@ func _Api_SetStreamAlias_Handler(srv interface{}, ctx context.Context, dec func(
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: "/global.api/SetStreamAlias",
FullMethod: Api_SetStreamAlias_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(ApiServer).SetStreamAlias(ctx, req.(*SetStreamAliasRequest))
@@ -1019,7 +1099,7 @@ func _Api_StopPublish_Handler(srv interface{}, ctx context.Context, dec func(int
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: "/global.api/StopPublish",
FullMethod: Api_StopPublish_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(ApiServer).StopPublish(ctx, req.(*StreamSnapRequest))
@@ -1037,7 +1117,7 @@ func _Api_StopSubscribe_Handler(srv interface{}, ctx context.Context, dec func(i
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: "/global.api/StopSubscribe",
FullMethod: Api_StopSubscribe_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(ApiServer).StopSubscribe(ctx, req.(*RequestWithId))
@@ -1055,7 +1135,7 @@ func _Api_GetConfigFile_Handler(srv interface{}, ctx context.Context, dec func(i
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: "/global.api/GetConfigFile",
FullMethod: Api_GetConfigFile_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(ApiServer).GetConfigFile(ctx, req.(*emptypb.Empty))
@@ -1073,7 +1153,7 @@ func _Api_UpdateConfigFile_Handler(srv interface{}, ctx context.Context, dec fun
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: "/global.api/UpdateConfigFile",
FullMethod: Api_UpdateConfigFile_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(ApiServer).UpdateConfigFile(ctx, req.(*UpdateConfigFileRequest))
@@ -1091,7 +1171,7 @@ func _Api_GetConfig_Handler(srv interface{}, ctx context.Context, dec func(inter
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: "/global.api/GetConfig",
FullMethod: Api_GetConfig_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(ApiServer).GetConfig(ctx, req.(*GetConfigRequest))
@@ -1109,7 +1189,7 @@ func _Api_GetFormily_Handler(srv interface{}, ctx context.Context, dec func(inte
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: "/global.api/GetFormily",
FullMethod: Api_GetFormily_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(ApiServer).GetFormily(ctx, req.(*GetConfigRequest))
@@ -1117,24 +1197,6 @@ func _Api_GetFormily_Handler(srv interface{}, ctx context.Context, dec func(inte
return interceptor(ctx, in, info, handler)
}
func _Api_ModifyConfig_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(ModifyConfigRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(ApiServer).ModifyConfig(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: "/global.api/ModifyConfig",
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(ApiServer).ModifyConfig(ctx, req.(*ModifyConfigRequest))
}
return interceptor(ctx, in, info, handler)
}
func _Api_GetPullProxyList_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(emptypb.Empty)
if err := dec(in); err != nil {
@@ -1145,7 +1207,7 @@ func _Api_GetPullProxyList_Handler(srv interface{}, ctx context.Context, dec fun
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: "/global.api/GetPullProxyList",
FullMethod: Api_GetPullProxyList_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(ApiServer).GetPullProxyList(ctx, req.(*emptypb.Empty))
@@ -1163,7 +1225,7 @@ func _Api_AddPullProxy_Handler(srv interface{}, ctx context.Context, dec func(in
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: "/global.api/AddPullProxy",
FullMethod: Api_AddPullProxy_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(ApiServer).AddPullProxy(ctx, req.(*PullProxyInfo))
@@ -1181,7 +1243,7 @@ func _Api_RemovePullProxy_Handler(srv interface{}, ctx context.Context, dec func
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: "/global.api/RemovePullProxy",
FullMethod: Api_RemovePullProxy_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(ApiServer).RemovePullProxy(ctx, req.(*RequestWithId))
@@ -1199,7 +1261,7 @@ func _Api_UpdatePullProxy_Handler(srv interface{}, ctx context.Context, dec func
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: "/global.api/UpdatePullProxy",
FullMethod: Api_UpdatePullProxy_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(ApiServer).UpdatePullProxy(ctx, req.(*PullProxyInfo))
@@ -1217,7 +1279,7 @@ func _Api_GetPushProxyList_Handler(srv interface{}, ctx context.Context, dec fun
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: "/global.api/GetPushProxyList",
FullMethod: Api_GetPushProxyList_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(ApiServer).GetPushProxyList(ctx, req.(*emptypb.Empty))
@@ -1235,7 +1297,7 @@ func _Api_AddPushProxy_Handler(srv interface{}, ctx context.Context, dec func(in
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: "/global.api/AddPushProxy",
FullMethod: Api_AddPushProxy_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(ApiServer).AddPushProxy(ctx, req.(*PushProxyInfo))
@@ -1253,7 +1315,7 @@ func _Api_RemovePushProxy_Handler(srv interface{}, ctx context.Context, dec func
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: "/global.api/RemovePushProxy",
FullMethod: Api_RemovePushProxy_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(ApiServer).RemovePushProxy(ctx, req.(*RequestWithId))
@@ -1271,7 +1333,7 @@ func _Api_UpdatePushProxy_Handler(srv interface{}, ctx context.Context, dec func
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: "/global.api/UpdatePushProxy",
FullMethod: Api_UpdatePushProxy_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(ApiServer).UpdatePushProxy(ctx, req.(*PushProxyInfo))
@@ -1289,7 +1351,7 @@ func _Api_GetRecording_Handler(srv interface{}, ctx context.Context, dec func(in
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: "/global.api/GetRecording",
FullMethod: Api_GetRecording_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(ApiServer).GetRecording(ctx, req.(*emptypb.Empty))
@@ -1307,7 +1369,7 @@ func _Api_GetTransformList_Handler(srv interface{}, ctx context.Context, dec fun
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: "/global.api/GetTransformList",
FullMethod: Api_GetTransformList_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(ApiServer).GetTransformList(ctx, req.(*emptypb.Empty))
@@ -1325,7 +1387,7 @@ func _Api_GetRecordList_Handler(srv interface{}, ctx context.Context, dec func(i
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: "/global.api/GetRecordList",
FullMethod: Api_GetRecordList_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(ApiServer).GetRecordList(ctx, req.(*ReqRecordList))
@@ -1343,7 +1405,7 @@ func _Api_GetRecordCatalog_Handler(srv interface{}, ctx context.Context, dec fun
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: "/global.api/GetRecordCatalog",
FullMethod: Api_GetRecordCatalog_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(ApiServer).GetRecordCatalog(ctx, req.(*ReqRecordCatalog))
@@ -1361,7 +1423,7 @@ func _Api_DeleteRecord_Handler(srv interface{}, ctx context.Context, dec func(in
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: "/global.api/DeleteRecord",
FullMethod: Api_DeleteRecord_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(ApiServer).DeleteRecord(ctx, req.(*ReqRecordDelete))
@@ -1484,10 +1546,6 @@ var Api_ServiceDesc = grpc.ServiceDesc{
MethodName: "GetFormily",
Handler: _Api_GetFormily_Handler,
},
{
MethodName: "ModifyConfig",
Handler: _Api_ModifyConfig_Handler,
},
{
MethodName: "GetPullProxyList",
Handler: _Api_GetPullProxyList_Handler,

View File

@@ -172,6 +172,7 @@ func (r *AVRingReader) ReadFrame(conf *config.Subscribe) (err error) {
}
}
r.Delay = r.Track.LastValue.Sequence - r.Value.Sequence
// fmt.Println(r.Delay)
if r.Track.ICodecCtx != nil {
r.Log(context.TODO(), task.TraceLevel, r.Track.FourCC().String(), "ts", r.Value.Timestamp, "delay", r.Delay, "bps", r.BPS)
} else {

View File

@@ -2,6 +2,7 @@ package codec
import (
"fmt"
"github.com/deepch/vdk/codec/aacparser"
"github.com/deepch/vdk/codec/opusparser"
)
@@ -58,6 +59,12 @@ func (ctx *AACCtx) GetSampleRate() int {
func (ctx *AACCtx) GetBase() ICodecCtx {
return ctx
}
func (ctx *AACCtx) String() string {
// https://www.w3.org/TR/webcodecs-aac-codec-registration/
return fmt.Sprintf("mp4a.40.%d", ctx.Config.ObjectType)
}
func (ctx *AACCtx) GetRecord() []byte {
return ctx.ConfigBytes
}
@@ -78,9 +85,18 @@ func (ctx *PCMACtx) GetBase() ICodecCtx {
return ctx
}
func (ctx *PCMACtx) String() string {
return "alaw"
}
func (ctx *PCMUCtx) String() string {
return "ulaw"
}
func (ctx *PCMUCtx) GetBase() ICodecCtx {
return ctx
}
func (*PCMUCtx) GetRecord() []byte {
return []byte{} //TODO
}
@@ -95,6 +111,11 @@ func (*OPUSCtx) FourCC() FourCC {
func (ctx *OPUSCtx) GetBase() ICodecCtx {
return ctx
}
func (ctx *OPUSCtx) String() string {
return "opus"
}
func (ctx *OPUSCtx) GetChannels() int {
return ctx.ChannelLayout().Count()
}

View File

@@ -1,5 +1,7 @@
package codec
import "fmt"
const (
AV1_OBU_SEQUENCE_HEADER = 1
AV1_OBU_TEMPORAL_DELIMITER = 2
@@ -41,3 +43,7 @@ func (*AV1Ctx) FourCC() FourCC {
func (ctx *AV1Ctx) GetRecord() []byte {
return ctx.ConfigOBUs
}
func (ctx *AV1Ctx) String() string {
return fmt.Sprintf("av01.%02X%02X%02X", ctx.ConfigOBUs[0], ctx.ConfigOBUs[1], ctx.ConfigOBUs[2])
}

View File

@@ -2,6 +2,7 @@ package codec
import (
"fmt"
"github.com/deepch/vdk/codec/h264parser"
)
@@ -126,3 +127,7 @@ func (h264 *H264Ctx) GetBase() ICodecCtx {
func (ctx *H264Ctx) GetRecord() []byte {
return ctx.Record
}
func (h264 *H264Ctx) String() string {
return fmt.Sprintf("avc1.%02X%02X%02X", h264.RecordInfo.AVCProfileIndication, h264.RecordInfo.ProfileCompatibility, h264.RecordInfo.AVCLevelIndication)
}

View File

@@ -1,7 +1,10 @@
package codec
import "fmt"
import "github.com/deepch/vdk/codec/h265parser"
import (
"fmt"
"github.com/deepch/vdk/codec/h265parser"
)
type H265NALUType byte
@@ -36,3 +39,7 @@ func (h265 *H265Ctx) GetBase() ICodecCtx {
func (h265 *H265Ctx) GetRecord() []byte {
return h265.Record
}
func (h265 *H265Ctx) String() string {
return fmt.Sprintf("hvc1.%02X%02X%02X", h265.RecordInfo.AVCProfileIndication, h265.RecordInfo.ProfileCompatibility, h265.RecordInfo.AVCLevelIndication)
}

View File

@@ -5,4 +5,5 @@ type ICodecCtx interface {
GetInfo() string
GetBase() ICodecCtx
GetRecord() []byte
String() string
}

View File

@@ -30,6 +30,10 @@ func (f FourCC) String() string {
return string(f[:])
}
func (f FourCC) MatchString(str string) bool {
return string(f[:]) == str[:4]
}
func (f FourCC) Name() string {
switch f {
case FourCC_H264:

View File

@@ -3,7 +3,6 @@ package config
import (
"encoding/json"
"fmt"
"github.com/mcuadros/go-defaults"
"log/slog"
"maps"
"os"
@@ -12,6 +11,8 @@ import (
"strings"
"time"
"github.com/mcuadros/go-defaults"
"gopkg.in/yaml.v3"
)
@@ -100,6 +101,12 @@ func (config *Config) Parse(s any, prefix ...string) {
}
config.Ptr = v
if !v.IsValid() {
fmt.Println("parse to ", prefix, config.name, s, "is not valid")
return
}
config.Default = v.Interface()
if l := len(prefix); l > 0 { // 读取环境变量

View File

@@ -1,13 +1,9 @@
package config
import (
"crypto/sha256"
"crypto/subtle"
"crypto/tls"
"log/slog"
"net/http"
"m7s.live/v5/pkg/task"
"m7s.live/v5/pkg/util"
"github.com/grpc-ecosystem/grpc-gateway/v2/runtime"
@@ -46,7 +42,7 @@ func (config *HTTP) GetHandler() http.Handler {
return config.mux
}
func (config *HTTP) CreateHttpMux() *http.ServeMux {
func (config *HTTP) CreateHttpMux() http.Handler {
config.mux = http.NewServeMux()
return config.mux
}
@@ -73,10 +69,10 @@ func (config *HTTP) Handle(path string, f http.Handler, last bool) {
config.mux = http.NewServeMux()
}
if config.CORS {
f = CORS(f)
f = util.CORS(f)
}
if config.UserName != "" && config.Password != "" {
f = BasicAuth(config.UserName, config.Password, f)
f = util.BasicAuth(config.UserName, config.Password, f)
}
for _, middleware := range config.middlewares {
f = middleware(path, f)
@@ -91,151 +87,3 @@ func (config *HTTP) GetHTTPConfig() *HTTP {
// func (config *HTTP) Handler(r *http.Request) (h http.Handler, pattern string) {
// return config.mux.Handler(r)
// }
func (config *HTTP) CreateHTTPWork(logger *slog.Logger) *ListenHTTPWork {
ret := &ListenHTTPWork{HTTP: config}
ret.Logger = logger.With("addr", config.ListenAddr)
return ret
}
func (config *HTTP) CreateHTTPSWork(logger *slog.Logger) *ListenHTTPSWork {
ret := &ListenHTTPSWork{ListenHTTPWork{HTTP: config}}
ret.Logger = logger.With("addr", config.ListenAddrTLS)
return ret
}
func CORS(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
header := w.Header()
header.Set("Access-Control-Allow-Credentials", "true")
header.Set("Cross-Origin-Resource-Policy", "cross-origin")
header.Set("Access-Control-Allow-Headers", "Content-Type,Access-Token,Authorization")
header.Set("Access-Control-Allow-Private-Network", "true")
origin := r.Header["Origin"]
if len(origin) == 0 {
header.Set("Access-Control-Allow-Origin", "*")
} else {
header.Set("Access-Control-Allow-Origin", origin[0])
}
if next != nil && r.Method != "OPTIONS" {
next.ServeHTTP(w, r)
}
})
}
func BasicAuth(u, p string, next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Extract the username and password from the request
// Authorization header. If no Authentication header is present
// or the header value is invalid, then the 'ok' return value
// will be false.
username, password, ok := r.BasicAuth()
if ok {
// Calculate SHA-256 hashes for the provided and expected
// usernames and passwords.
usernameHash := sha256.Sum256([]byte(username))
passwordHash := sha256.Sum256([]byte(password))
expectedUsernameHash := sha256.Sum256([]byte(u))
expectedPasswordHash := sha256.Sum256([]byte(p))
// 使用 subtle.ConstantTimeCompare() 进行校验
// the provided username and password hashes equal the
// expected username and password hashes. ConstantTimeCompare
// 如果值相等则返回1否则返回0。
// Importantly, we should to do the work to evaluate both the
// username and password before checking the return values to
// 避免泄露信息。
usernameMatch := (subtle.ConstantTimeCompare(usernameHash[:], expectedUsernameHash[:]) == 1)
passwordMatch := (subtle.ConstantTimeCompare(passwordHash[:], expectedPasswordHash[:]) == 1)
// If the username and password are correct, then call
// the next handler in the chain. Make sure to return
// afterwards, so that none of the code below is run.
if usernameMatch && passwordMatch {
if next != nil {
next.ServeHTTP(w, r)
}
return
}
}
// If the Authentication header is not present, is invalid, or the
// username or password is wrong, then set a WWW-Authenticate
// header to inform the client that we expect them to use basic
// authentication and send a 401 Unauthorized response.
w.Header().Set("WWW-Authenticate", `Basic realm="restricted", charset="UTF-8"`)
http.Error(w, "Unauthorized", http.StatusUnauthorized)
})
}
type ListenHTTPWork struct {
task.Task
*HTTP
*http.Server
}
func (task *ListenHTTPWork) Start() (err error) {
task.Server = &http.Server{
Addr: task.ListenAddr,
ReadTimeout: task.HTTP.ReadTimeout,
WriteTimeout: task.HTTP.WriteTimeout,
IdleTimeout: task.HTTP.IdleTimeout,
Handler: task.GetHandler(),
}
return
}
func (task *ListenHTTPWork) Go() error {
task.Info("listen http")
return task.Server.ListenAndServe()
}
func (task *ListenHTTPWork) Dispose() {
task.Info("http server stop")
task.Server.Close()
}
type ListenHTTPSWork struct {
ListenHTTPWork
}
func (task *ListenHTTPSWork) Start() (err error) {
cer, _ := tls.X509KeyPair(LocalCert, LocalKey)
task.Server = &http.Server{
Addr: task.HTTP.ListenAddrTLS,
ReadTimeout: task.HTTP.ReadTimeout,
WriteTimeout: task.HTTP.WriteTimeout,
IdleTimeout: task.HTTP.IdleTimeout,
Handler: task.HTTP.GetHandler(),
TLSConfig: &tls.Config{
Certificates: []tls.Certificate{cer},
CipherSuites: []uint16{
tls.TLS_AES_128_GCM_SHA256,
tls.TLS_CHACHA20_POLY1305_SHA256,
tls.TLS_AES_256_GCM_SHA384,
//tls.TLS_RSA_WITH_AES_128_CBC_SHA,
//tls.TLS_RSA_WITH_AES_256_CBC_SHA,
//tls.TLS_RSA_WITH_AES_128_GCM_SHA256,
//tls.TLS_RSA_WITH_AES_256_GCM_SHA384,
tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305,
tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305,
tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,
tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,
tls.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256,
tls.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA,
tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256,
tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA,
tls.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA,
tls.TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA,
},
},
}
return
}
func (task *ListenHTTPSWork) Go() error {
task.Info("listen https")
return task.Server.ListenAndServeTLS(task.HTTP.CertFile, task.HTTP.KeyFile)
}

View File

@@ -125,7 +125,7 @@ func (task *ListenTCPWork) listen(handler TCPHandler) {
if max := 1 * time.Second; tempDelay > max {
tempDelay = max
}
// slog.Warnf("%s: Accept error: %v; retrying in %v", tcp.ListenAddr, err, tempDelay)
// slog.Warnf("%s: Accept error: %v; retrying in %v", tcp.DownListenAddr, err, tempDelay)
time.Sleep(tempDelay)
continue
}

View File

@@ -36,9 +36,9 @@ type (
IdleTimeout time.Duration `desc:"空闲(无订阅)超时"` // 空闲(无订阅)超时
PauseTimeout time.Duration `default:"30s" desc:"暂停超时时间"` // 暂停超时
BufferTime time.Duration `desc:"缓冲时长0代表取最近关键帧"` // 缓冲长度(单位:秒)0代表取最近关键帧
Speed float64 `default:"0" desc:"发送速率"` // 发送速率0 为不限速
Speed float64 `default:"1" desc:"发送速率"` // 发送速率0 为不限速
Scale float64 `default:"1" desc:"缩放倍数"` // 缩放倍数
MaxFPS int `default:"30" desc:"最大FPS"` // 最大FPS
MaxFPS int `default:"60" desc:"最大FPS"` // 最大FPS
Key string `desc:"发布鉴权key"` // 发布鉴权key
RingSize util.Range[int] `default:"20-1024" desc:"RingSize范围"` // 缓冲区大小范围
RelayMode string `default:"remux" desc:"转发模式" enum:"remux:转格式,relay:纯转发,mix:混合转发"` // 转发模式
@@ -164,3 +164,36 @@ func (v HTTPValues) DeepClone() (ret HTTPValues) {
}
return
}
func (r *TransfromOutput) UnmarshalYAML(node *yaml.Node) error {
if node.Kind == yaml.ScalarNode {
// If it's a string, assign it to Target
return node.Decode(&r.Target)
}
if node.Kind == yaml.MappingNode {
var conf map[string]any
if err := node.Decode(&conf); err != nil {
return err
}
var normal bool
if conf["target"] != nil {
r.Target = conf["target"].(string)
normal = true
}
if conf["streampath"] != nil {
r.StreamPath = conf["streampath"].(string)
normal = true
}
if conf["conf"] != nil {
r.Conf = conf["conf"]
normal = true
}
if !normal {
r.Conf = conf
}
return nil
}
return fmt.Errorf("unsupported node kind: %v", node.Kind)
}

View File

@@ -57,7 +57,7 @@ func (task *ListenUDPWork) Go() error {
if max := 1 * time.Second; tempDelay > max {
tempDelay = max
}
// slog.Warnf("%s: Accept error: %v; retrying in %v", tcp.ListenAddr, err, tempDelay)
// slog.Warnf("%s: Accept error: %v; retrying in %v", tcp.DownListenAddr, err, tempDelay)
time.Sleep(tempDelay)
continue
}

View File

@@ -16,7 +16,7 @@ type User struct {
Username string `gorm:"uniqueIndex;size:64"`
Password string `gorm:"size:60"` // bcrypt hash
Role string `gorm:"size:20;default:'user'"` // admin or user
LastLogin time.Time
LastLogin time.Time `gorm:"type:datetime;default:CURRENT_TIMESTAMP"`
}
// BeforeCreate hook to hash password before saving

109
pkg/http_server_fasthttp.go Normal file
View File

@@ -0,0 +1,109 @@
//go:build fasthttp
package pkg
import (
"crypto/tls"
"log/slog"
"github.com/valyala/fasthttp"
"github.com/valyala/fasthttp/fasthttpadaptor"
"m7s.live/v5/pkg/config"
"m7s.live/v5/pkg/task"
)
func CreateHTTPWork(conf *config.HTTP, logger *slog.Logger) *ListenFastHTTPWork {
ret := &ListenFastHTTPWork{HTTP: conf}
ret.Logger = logger.With("addr", conf.ListenAddr)
return ret
}
func CreateHTTPSWork(conf *config.HTTP, logger *slog.Logger) *ListenFastHTTPSWork {
ret := &ListenFastHTTPSWork{ListenFastHTTPWork{HTTP: conf}}
ret.Logger = logger.With("addr", conf.ListenAddrTLS)
return ret
}
// ListenFastHTTPWork 用于启动 FastHTTP 服务
type ListenFastHTTPWork struct {
task.Task
*config.HTTP
server *fasthttp.Server
}
// 主请求处理函数
func (task *ListenFastHTTPWork) requestHandler(ctx *fasthttp.RequestCtx) {
fasthttpadaptor.NewFastHTTPHandler(task.GetHandler())(ctx)
}
func (task *ListenFastHTTPWork) Start() (err error) {
// 配置 fasthttp 服务器
task.server = &fasthttp.Server{
Handler: task.requestHandler,
ReadTimeout: task.HTTP.ReadTimeout,
WriteTimeout: task.HTTP.WriteTimeout,
IdleTimeout: task.HTTP.IdleTimeout,
Name: "Monibuca FastHTTP Server",
// 启用流式响应支持
StreamRequestBody: true,
}
return nil
}
func (task *ListenFastHTTPWork) Go() error {
task.Info("listen fasthttp")
return task.server.ListenAndServe(task.ListenAddr)
}
func (task *ListenFastHTTPWork) Dispose() {
task.Info("fasthttp server stop")
if task.server != nil {
if err := task.server.Shutdown(); err != nil {
task.Error("shutdown error", "err", err)
}
}
}
// ListenFastHTTPSWork 用于启动 HTTPS FastHTTP 服务
type ListenFastHTTPSWork struct {
ListenFastHTTPWork
}
func (task *ListenFastHTTPSWork) Start() (err error) {
cer, _ := tls.X509KeyPair(config.LocalCert, config.LocalKey)
// 调用基类的 Start
if err = task.ListenFastHTTPWork.Start(); err != nil {
return err
}
task.server.TLSConfig = &tls.Config{
Certificates: []tls.Certificate{cer},
CipherSuites: []uint16{
tls.TLS_AES_128_GCM_SHA256,
tls.TLS_CHACHA20_POLY1305_SHA256,
tls.TLS_AES_256_GCM_SHA384,
//tls.TLS_RSA_WITH_AES_128_CBC_SHA,
//tls.TLS_RSA_WITH_AES_256_CBC_SHA,
//tls.TLS_RSA_WITH_AES_128_GCM_SHA256,
//tls.TLS_RSA_WITH_AES_256_GCM_SHA384,
tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305,
tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305,
tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,
tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,
tls.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256,
tls.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA,
tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256,
tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA,
tls.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA,
tls.TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA,
},
}
return
}
func (task *ListenFastHTTPSWork) Go() error {
task.Info("listen https fasthttp")
return task.server.ListenAndServeTLS(task.ListenAddrTLS, task.CertFile, task.KeyFile)
}

96
pkg/http_server_std.go Normal file
View File

@@ -0,0 +1,96 @@
//go:build !fasthttp
package pkg
import (
"crypto/tls"
"log/slog"
"net/http"
"m7s.live/v5/pkg/config"
"m7s.live/v5/pkg/task"
)
func CreateHTTPWork(conf *config.HTTP, logger *slog.Logger) *ListenHTTPWork {
ret := &ListenHTTPWork{HTTP: conf}
ret.Logger = logger.With("addr", conf.ListenAddr)
return ret
}
func CreateHTTPSWork(conf *config.HTTP, logger *slog.Logger) *ListenHTTPSWork {
ret := &ListenHTTPSWork{ListenHTTPWork{HTTP: conf}}
ret.Logger = logger.With("addr", conf.ListenAddrTLS)
return ret
}
type ListenHTTPWork struct {
task.Task
*config.HTTP
*http.Server
}
func (task *ListenHTTPWork) Start() (err error) {
task.Server = &http.Server{
Addr: task.ListenAddr,
ReadTimeout: task.HTTP.ReadTimeout,
WriteTimeout: task.HTTP.WriteTimeout,
IdleTimeout: task.HTTP.IdleTimeout,
Handler: task.GetHandler(),
}
return
}
func (task *ListenHTTPWork) Go() error {
task.Info("listen http")
return task.Server.ListenAndServe()
}
func (task *ListenHTTPWork) Dispose() {
task.Info("http server stop")
task.Server.Close()
}
type ListenHTTPSWork struct {
ListenHTTPWork
}
func (task *ListenHTTPSWork) Start() (err error) {
cer, _ := tls.X509KeyPair(config.LocalCert, config.LocalKey)
task.Server = &http.Server{
Addr: task.HTTP.ListenAddrTLS,
ReadTimeout: task.HTTP.ReadTimeout,
WriteTimeout: task.HTTP.WriteTimeout,
IdleTimeout: task.HTTP.IdleTimeout,
Handler: task.HTTP.GetHandler(),
TLSConfig: &tls.Config{
Certificates: []tls.Certificate{cer},
CipherSuites: []uint16{
tls.TLS_AES_128_GCM_SHA256,
tls.TLS_CHACHA20_POLY1305_SHA256,
tls.TLS_AES_256_GCM_SHA384,
//tls.TLS_RSA_WITH_AES_128_CBC_SHA,
//tls.TLS_RSA_WITH_AES_256_CBC_SHA,
//tls.TLS_RSA_WITH_AES_128_GCM_SHA256,
//tls.TLS_RSA_WITH_AES_256_GCM_SHA384,
tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305,
tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305,
tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,
tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,
tls.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256,
tls.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA,
tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256,
tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA,
tls.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA,
tls.TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA,
},
},
}
return
}
func (task *ListenHTTPSWork) Go() error {
task.Info("listen https")
return task.Server.ListenAndServeTLS(task.HTTP.CertFile, task.HTTP.KeyFile)
}

View File

@@ -4,6 +4,12 @@ import (
"time"
)
type ITickTask interface {
IChannelTask
GetTickInterval() time.Duration
GetTicker() *time.Ticker
}
type ChannelTask struct {
Task
SignalChan any
@@ -25,16 +31,36 @@ type TickTask struct {
Ticker *time.Ticker
}
func (t *TickTask) GetTicker() *time.Ticker {
return t.Ticker
}
func (t *TickTask) GetTickInterval() time.Duration {
return time.Second
}
func (t *TickTask) Start() (err error) {
t.Ticker = time.NewTicker(t.handler.(interface{ GetTickInterval() time.Duration }).GetTickInterval())
t.Ticker = time.NewTicker(t.handler.(ITickTask).GetTickInterval())
t.SignalChan = t.Ticker.C
return
}
func (t *TickTask) Dispose() {
t.Ticker.Stop()
type AsyncTickTask struct {
TickTask
}
func (t *AsyncTickTask) GetSignal() any {
return t.Task.GetSignal()
}
func (t *AsyncTickTask) Go() error {
t.handler.(ITickTask).Tick(nil)
for {
select {
case c := <-t.Ticker.C:
t.handler.(ITickTask).Tick(c)
case <-t.Done():
return nil
}
}
}

View File

@@ -55,11 +55,17 @@ func (mt *Job) Blocked() ITask {
}
func (mt *Job) waitChildrenDispose() {
defer func() {
// 忽略由于在任务关闭过程中可能存在竞态条件,当父任务关闭时子任务可能已经被释放。
if err := recover(); err != nil {
mt.Debug("waitChildrenDispose panic", "err", err)
}
mt.addSub <- nil
<-mt.childrenDisposed
}()
if blocked := mt.blocked; blocked != nil {
blocked.Stop(mt.StopReason())
}
mt.addSub <- nil
<-mt.childrenDisposed
}
func (mt *Job) OnChildDispose(listener func(ITask)) {
@@ -214,20 +220,28 @@ func (mt *Job) run() {
mt.blocked = mt.children[taskIndex]
switch tt := mt.blocked.(type) {
case IChannelTask:
tt.Tick(rev.Interface())
if tt.IsStopped() {
mt.onChildDispose(mt.blocked)
}
}
if !ok {
if mt.onChildDispose(mt.blocked); mt.blocked.checkRetry(mt.blocked.StopReason()) {
if mt.blocked.reset(); mt.blocked.start() {
mt.cases[chosen].Chan = reflect.ValueOf(mt.blocked.GetSignal())
continue
switch ttt := tt.(type) {
case ITickTask:
ttt.GetTicker().Stop()
}
mt.onChildDispose(mt.blocked)
mt.children = slices.Delete(mt.children, taskIndex, taskIndex+1)
mt.cases = slices.Delete(mt.cases, chosen, chosen+1)
} else {
tt.Tick(rev.Interface())
}
default:
if !ok {
if mt.onChildDispose(mt.blocked); mt.blocked.checkRetry(mt.blocked.StopReason()) {
if mt.blocked.reset(); mt.blocked.start() {
mt.cases[chosen].Chan = reflect.ValueOf(mt.blocked.GetSignal())
continue
}
}
mt.children = slices.Delete(mt.children, taskIndex, taskIndex+1)
mt.cases = slices.Delete(mt.cases, chosen, chosen+1)
}
mt.children = slices.Delete(mt.children, taskIndex, taskIndex+1)
mt.cases = slices.Delete(mt.cases, chosen, chosen+1)
}
}
if !mt.handler.keepalive() && len(mt.children) == 0 {

View File

@@ -36,3 +36,28 @@ func (m *Manager[K, T]) Add(ctx T, opt ...any) *Task {
})
return m.AddTask(ctx, opt...)
}
// SafeGet 用于不同协程获取元素,防止并发请求
func (m *Manager[K, T]) SafeGet(key K) (item T, ok bool) {
if m.L == nil {
m.Call(func() error {
item, ok = m.Collection.Get(key)
return nil
})
} else {
item, ok = m.Collection.Get(key)
}
return
}
// SafeRange 用于不同协程获取元素,防止并发请求
func (m *Manager[K, T]) SafeRange(f func(T) bool) {
if m.L == nil {
m.Call(func() error {
m.Collection.Range(f)
return nil
})
} else {
m.Collection.Range(f)
}
}

View File

@@ -77,6 +77,8 @@ type (
OnDispose(func())
GetState() TaskState
GetLevel() byte
WaitStopped() error
WaitStarted() error
}
IJob interface {
ITask
@@ -324,6 +326,9 @@ func (task *Task) start() bool {
task.ResetRetryCount()
if runHandler, ok := task.handler.(TaskBlock); ok {
task.state = TASK_STATE_RUNNING
if task.Logger != nil {
task.Debug("task run", "taskId", task.ID, "taskType", task.GetTaskType(), "ownerType", task.GetOwnerType())
}
err = runHandler.Run()
if err == nil {
err = ErrTaskComplete
@@ -334,6 +339,9 @@ func (task *Task) start() bool {
if err == nil {
if goHandler, ok := task.handler.(TaskGo); ok {
task.state = TASK_STATE_GOING
if task.Logger != nil {
task.Debug("task go", "taskId", task.ID, "taskType", task.GetTaskType(), "ownerType", task.GetOwnerType())
}
go task.run(goHandler.Go)
}
return true

View File

@@ -14,6 +14,11 @@ import (
"m7s.live/v5/pkg/util"
)
const threshold = 10 * time.Millisecond
const DROP_FRAME_LEVEL_NODROP = 0
const DROP_FRAME_LEVEL_DROP_P = 1
const DROP_FRAME_LEVEL_DROP_ALL = 2
type (
Track struct {
*slog.Logger
@@ -30,8 +35,23 @@ type (
Track
}
TsTamer struct {
BaseTs, LastTs time.Duration
BaseTs, LastTs, BeforeScaleChangedTs time.Duration
LastScale float64
}
SpeedController struct {
speed float64
pausedTime time.Duration
beginTime time.Time
beginTimestamp time.Duration
Delta time.Duration
}
DropController struct {
acceptFrameCount int
accpetFPS int
LastDropLevelChange time.Time
DropFrameLevel int // 0: no drop, 1: drop P-frame, 2: drop all
}
AVTrack struct {
Track
*RingWriter
@@ -40,6 +60,8 @@ type (
SequenceFrame IAVFrame
WrapIndex int
TsTamer
SpeedController
DropController
}
)
@@ -67,7 +89,7 @@ func NewAVTrack(args ...any) (t *AVTrack) {
}
}
//t.ready = util.NewPromise(struct{}{})
t.Info("create")
t.Info("create", "dropFrameLevel", t.DropFrameLevel)
return
}
@@ -87,6 +109,58 @@ func (t *Track) AddBytesIn(n int) {
}
}
func (t *AVTrack) AddBytesIn(n int) {
dur := time.Since(t.lastBPSTime)
t.Track.AddBytesIn(n)
if t.frameCount == 0 {
t.accpetFPS = int(float64(t.acceptFrameCount) / dur.Seconds())
t.acceptFrameCount = 0
}
}
func (t *AVTrack) AcceptFrame(data IAVFrame) {
t.acceptFrameCount++
t.Value.Wraps = append(t.Value.Wraps, data)
}
func (t *AVTrack) changeDropFrameLevel(newLevel int) {
t.Warn("change drop frame level", "from", t.DropFrameLevel, "to", newLevel)
t.DropFrameLevel = newLevel
t.LastDropLevelChange = time.Now()
}
func (t *AVTrack) CheckIfNeedDropFrame(maxFPS int) (drop bool) {
drop = maxFPS > 0 && (t.accpetFPS > maxFPS)
if drop {
defer func() {
if time.Since(t.LastDropLevelChange) > time.Second && t.DropFrameLevel > 0 {
t.changeDropFrameLevel(t.DropFrameLevel + 1)
}
}()
}
// Enhanced frame dropping strategy based on DropFrameLevel
switch t.DropFrameLevel {
case DROP_FRAME_LEVEL_NODROP:
if drop {
t.changeDropFrameLevel(DROP_FRAME_LEVEL_DROP_P)
}
case DROP_FRAME_LEVEL_DROP_P: // Drop P-frame
if !t.Value.IDR {
return true
} else if !drop {
t.changeDropFrameLevel(DROP_FRAME_LEVEL_NODROP)
}
return false
default:
if !drop {
t.changeDropFrameLevel(DROP_FRAME_LEVEL_DROP_P)
} else {
return true
}
}
return
}
func (t *AVTrack) Ready(err error) {
if t.ready.IsPending() {
if err != nil {
@@ -133,13 +207,46 @@ func (t *TsTamer) Tame(ts time.Duration, fps int, scale float64) (result time.Du
result = max(1*time.Millisecond, t.BaseTs+ts)
if fps > 0 {
frameDur := float64(time.Second) / float64(fps)
if math.Abs(float64(result-t.LastTs)) > 10*frameDur { //时间戳突变
if math.Abs(float64(result-t.LastTs)) > 10*frameDur*scale { //时间戳突变
// t.Warn("timestamp mutation", "fps", t.FPS, "lastTs", uint32(t.LastTs/time.Millisecond), "ts", uint32(frame.Timestamp/time.Millisecond), "frameDur", time.Duration(frameDur))
result = t.LastTs + time.Duration(frameDur)
t.BaseTs = result - ts
}
}
t.LastTs = result
result = time.Duration(float64(result) / scale)
if t.LastScale != scale {
t.BeforeScaleChangedTs = result
t.LastScale = scale
}
result = t.BeforeScaleChangedTs + time.Duration(float64(result-t.BeforeScaleChangedTs)/scale)
return
}
func (t *AVTrack) SpeedControl(speed float64) {
t.speedControl(speed, t.LastTs)
}
func (t *AVTrack) AddPausedTime(d time.Duration) {
t.pausedTime += d
}
func (s *SpeedController) speedControl(speed float64, ts time.Duration) {
if speed != s.speed || s.beginTime.IsZero() {
s.speed = speed
s.beginTime = time.Now()
s.beginTimestamp = ts
s.pausedTime = 0
} else {
elapsed := time.Since(s.beginTime) - s.pausedTime
if speed == 0 {
s.Delta = ts - elapsed
return
}
should := time.Duration(float64(ts-s.beginTimestamp) / speed)
s.Delta = should - elapsed
// fmt.Println(speed, elapsed, should, s.Delta)
if s.Delta > threshold {
time.Sleep(min(s.Delta, time.Millisecond*500))
}
}
}

View File

@@ -22,7 +22,7 @@ const (
var (
UnixTimeReg = regexp.MustCompile(`^\d+$`)
UnixTimeRangeReg = regexp.MustCompile(`^(\d+)(~|-)(\d+)$`)
TimeStrRangeReg = regexp.MustCompile(`^(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2})~(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2})$`)
TimeStrRangeReg = regexp.MustCompile(`^(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z?)~(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z?)$`)
)
func TimeRangeQueryParse(query url.Values) (startTime, endTime time.Time, err error) {
@@ -100,6 +100,9 @@ func TimeQueryParseRefer(query string, refer time.Time) (time.Time, error) {
if !strings.Contains(query, "T") {
query = refer.Format("2006-01-02") + "T" + query
}
if strings.Contains(query, "Z") {
return time.Parse(time.RFC3339, query)
}
return time.ParseInLocation("2006-01-02T15:04:05", query, time.Local)
}

View File

@@ -150,16 +150,18 @@ func ReturnFetchValue[T any](fetch func() T, rw http.ResponseWriter, r *http.Req
tickDur = time.Second
}
if r.Header.Get("Accept") == "text/event-stream" {
sse := NewSSE(rw, r.Context())
tick := time.NewTicker(tickDur)
defer tick.Stop()
writer := Conditional(isYaml, sse.WriteYAML, sse.WriteJSON)
writer(fetch())
for range tick.C {
if writer(fetch()) != nil {
return
NewSSE(rw, r.Context(), func(sse *SSE) {
tick := time.NewTicker(tickDur)
defer tick.Stop()
writer := Conditional(isYaml, sse.WriteYAML, sse.WriteJSON)
err := writer(fetch())
for range tick.C {
if err = writer(fetch()); err != nil {
fmt.Println(err)
return
}
}
}
})
} else {
data := fetch()
rw.Header().Set("Content-Type", Conditional(isYaml, "text/yaml", "application/json"))
@@ -217,7 +219,8 @@ func CORS(next http.Handler) http.Handler {
header := w.Header()
header.Set("Access-Control-Allow-Credentials", "true")
header.Set("Cross-Origin-Resource-Policy", "cross-origin")
header.Set("Access-Control-Allow-Headers", "Content-Type,Access-Token")
header.Set("Access-Control-Allow-Headers", "Content-Type,Access-Token,Authorization")
header.Set("Access-Control-Allow-Private-Network", "true")
origin := r.Header["Origin"]
if len(origin) == 0 {
header.Set("Access-Control-Allow-Origin", "*")

View File

@@ -1,3 +1,5 @@
//go:build !fasthttp
package util
import (
@@ -16,6 +18,7 @@ var (
sseEnd = []byte("\n\n")
)
// SSE 标准库实现
type SSE struct {
http.ResponseWriter
context.Context
@@ -45,7 +48,7 @@ func (sse *SSE) WriteEvent(event string, data []byte) (err error) {
return
}
func NewSSE(w http.ResponseWriter, ctx context.Context) *SSE {
func NewSSE(w http.ResponseWriter, ctx context.Context, block func(sse *SSE)) (sse *SSE) {
header := w.Header()
header.Set("Content-Type", "text/event-stream")
header.Set("Cache-Control", "no-cache")
@@ -56,10 +59,12 @@ func NewSSE(w http.ResponseWriter, ctx context.Context) *SSE {
// rw.Header().Set("Access-Control-Allow-Methods", "GET,POST,PUT,DELETE,OPTIONS")
// rw.Header().Set("Access-Control-Allow-Credentials", "true")
// rw.Header().Set("Transfer-Encoding", "chunked")
return &SSE{
sse = &SSE{
ResponseWriter: w,
Context: ctx,
}
block(sse)
return sse
}
func (sse *SSE) WriteJSON(data any) error {

87
pkg/util/sse_fasthttp.go Normal file
View File

@@ -0,0 +1,87 @@
//go:build fasthttp
package util
import (
"bufio"
"context"
"encoding/json"
"net"
"net/http"
"os/exec"
"github.com/valyala/fasthttp"
"gopkg.in/yaml.v3"
)
// 定义 SSE 常量,与 sse.go 中保持一致
var (
// 这些变量需要在这里重新定义,因为使用构建标签后无法共享
sseEent = []byte("event: ")
sseBegin = []byte("data: ")
sseEnd = []byte("\n\n")
)
// SSE 结构体在 fasthttp 构建模式下的实现
type SSE struct {
Writer *bufio.Writer
context.Context
}
func (sse *SSE) Write(data []byte) (n int, err error) {
if err = sse.Err(); err != nil {
return
}
buffers := net.Buffers{sseBegin, data, sseEnd}
nn, err := buffers.WriteTo(sse.Writer)
if err == nil {
sse.Writer.Flush()
}
return int(nn), err
}
func (sse *SSE) WriteEvent(event string, data []byte) (err error) {
if err = sse.Err(); err != nil {
return
}
buffers := net.Buffers{sseEent, []byte(event + "\n"), sseBegin, data, sseEnd}
_, err = buffers.WriteTo(sse.Writer)
if err == nil {
sse.Writer.Flush()
}
return
}
func NewSSE(w http.ResponseWriter, ctx context.Context, block func(sse *SSE)) (sse *SSE) {
reqCtx := ctx.(*fasthttp.RequestCtx)
header := w.Header()
header.Set("Content-Type", "text/event-stream")
header.Set("Cache-Control", "no-cache")
header.Set("Connection", "keep-alive")
header.Set("X-Accel-Buffering", "no")
header.Set("Access-Control-Allow-Origin", "*")
sse = &SSE{
Context: ctx,
}
reqCtx.Response.SetBodyStreamWriter(func(w *bufio.Writer) {
sse.Writer = w
block(sse)
<-ctx.Done()
})
return sse
}
func (sse *SSE) WriteJSON(data any) error {
return json.NewEncoder(sse).Encode(data)
}
func (sse *SSE) WriteYAML(data any) error {
return yaml.NewEncoder(sse).Encode(data)
}
// WriteExec 执行命令并将输出写入 SSE 流
func (sse *SSE) WriteExec(cmd *exec.Cmd) error {
cmd.Stderr = sse
cmd.Stdout = sse
return cmd.Run()
}

148
plugin.go
View File

@@ -43,13 +43,15 @@ type (
Name string
Version string //插件版本
Type reflect.Type
defaultYaml DefaultYaml //默认配置
DefaultYaml DefaultYaml //默认配置
ServiceDesc *grpc.ServiceDesc
RegisterGRPCHandler func(context.Context, *gatewayRuntime.ServeMux, *grpc.ClientConn) error
Puller Puller
Pusher Pusher
Recorder Recorder
Transformer Transformer
NewPuller PullerFactory
NewPusher PusherFactory
NewRecorder RecorderFactory
NewTransformer TransformerFactory
NewPullProxy PullProxyFactory
NewPushProxy PushProxyFactory
OnExit OnExitHandler
OnAuthPub AuthPublisher
OnAuthSub AuthSubscriber
@@ -88,12 +90,6 @@ type (
IQUICPlugin interface {
OnQUICConnect(quic.Connection) task.ITask
}
IPullProxyPlugin interface {
OnPullProxyAdd(pullProxy *PullProxy) any
}
IPushProxyPlugin interface {
OnPushProxyAdd(pushProxy *PushProxy) any
}
)
var plugins []PluginMeta
@@ -121,9 +117,9 @@ func (plugin *PluginMeta) Init(s *Server, userConfig map[string]any) (p *Plugin)
p.Config.Get(name).ParseGlobal(s.Config.Get(name))
}
}
if plugin.defaultYaml != "" {
if plugin.DefaultYaml != "" {
var defaultConf map[string]any
if err := yaml.Unmarshal([]byte(plugin.defaultYaml), &defaultConf); err != nil {
if err := yaml.Unmarshal([]byte(plugin.DefaultYaml), &defaultConf); err != nil {
p.Error("parsing default config", "error", err)
} else {
p.Config.ParseDefaultYaml(defaultConf)
@@ -150,7 +146,11 @@ func (plugin *PluginMeta) Init(s *Server, userConfig map[string]any) (p *Plugin)
p.Warn("plugin disabled")
return
} else {
p.assign()
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
@@ -166,7 +166,7 @@ func (plugin *PluginMeta) Init(s *Server, userConfig map[string]any) (p *Plugin)
}
}
}
if p.DB != nil && p.Meta.Recorder != nil {
if p.DB != nil && p.Meta.NewRecorder != nil {
if err = p.DB.AutoMigrate(&RecordStream{}); err != nil {
p.disable(fmt.Sprintf("auto migrate record stream failed %v", err))
return
@@ -178,35 +178,40 @@ func (plugin *PluginMeta) Init(s *Server, userConfig map[string]any) (p *Plugin)
// InstallPlugin 安装插件
func InstallPlugin[C iPlugin](options ...any) error {
var c *C
t := reflect.TypeOf(c).Elem()
meta := PluginMeta{
Name: strings.TrimSuffix(t.Name(), "Plugin"),
Type: t,
var meta PluginMeta
for _, option := range options {
if m, ok := option.(PluginMeta); ok {
meta = m
}
}
var c *C
meta.Type = reflect.TypeOf(c).Elem()
if meta.Name == "" {
meta.Name = strings.TrimSuffix(meta.Type.Name(), "Plugin")
}
_, pluginFilePath, _, _ := runtime.Caller(1)
configDir := filepath.Dir(pluginFilePath)
if _, after, found := strings.Cut(configDir, "@"); found {
meta.Version = after
} else {
meta.Version = "dev"
if meta.Version == "" {
if _, after, found := strings.Cut(configDir, "@"); found {
meta.Version = after
} else {
meta.Version = "dev"
}
}
for _, option := range options {
switch v := option.(type) {
case OnExitHandler:
meta.OnExit = v
case DefaultYaml:
meta.defaultYaml = v
case Puller:
meta.Puller = v
case Pusher:
meta.Pusher = v
case Recorder:
meta.Recorder = v
case Transformer:
meta.Transformer = v
meta.DefaultYaml = v
case PullerFactory:
meta.NewPuller = v
case PusherFactory:
meta.NewPusher = v
case RecorderFactory:
meta.NewRecorder = v
case TransformerFactory:
meta.NewTransformer = v
case AuthPublisher:
meta.OnAuthPub = v
case AuthSubscriber:
@@ -269,34 +274,12 @@ func (p *Plugin) GetPublicIP(netcardIP string) string {
return localIp
}
func (p *Plugin) settingPath() string {
return filepath.Join(p.Server.SettingDir, strings.ToLower(p.Meta.Name)+".yaml")
}
func (p *Plugin) disable(reason string) {
p.Disabled = true
p.SetDescription("disableReason", reason)
p.Server.disabledPlugins = append(p.Server.disabledPlugins, p)
}
func (p *Plugin) assign() {
f, err := os.Open(p.settingPath())
defer f.Close()
if err == nil {
var modifyConfig map[string]any
err = yaml.NewDecoder(f).Decode(&modifyConfig)
if err != nil {
panic(err)
}
p.Config.ParseModifyFile(modifyConfig)
}
var handlerMap map[string]http.HandlerFunc
if v, ok := p.handler.(IRegisterHandler); ok {
handlerMap = v.RegisterHandler()
}
p.registerHandler(handlerMap)
}
func (p *Plugin) Start() (err error) {
s := p.Server
if p.Meta.ServiceDesc != nil && s.grpcServer != nil {
@@ -337,12 +320,12 @@ func (p *Plugin) listen() (err error) {
if httpConf.ListenAddrTLS != "" && (httpConf.ListenAddrTLS != p.Server.config.HTTP.ListenAddrTLS) {
p.SetDescription("httpTLS", strings.TrimPrefix(httpConf.ListenAddrTLS, ":"))
p.AddDependTask(httpConf.CreateHTTPSWork(p.Logger))
p.AddDependTask(CreateHTTPSWork(httpConf, p.Logger))
}
if httpConf.ListenAddr != "" && (httpConf.ListenAddr != p.Server.config.HTTP.ListenAddr) {
p.SetDescription("http", strings.TrimPrefix(httpConf.ListenAddr, ":"))
p.AddDependTask(httpConf.CreateHTTPWork(p.Logger))
p.AddDependTask(CreateHTTPWork(httpConf, p.Logger))
}
if tcphandler, ok := p.handler.(ITCPPlugin); ok {
@@ -467,14 +450,14 @@ func (p *Plugin) SendWebhook(hookType config.HookType, conf config.Webhook, data
// TODO: use alias stream
func (p *Plugin) OnPublish(pub *Publisher) {
onPublish := p.config.OnPub
if p.Meta.Pusher != nil {
if p.Meta.NewPusher != nil {
for r, pushConf := range onPublish.Push {
if pushConf.URL = r.Replace(pub.StreamPath, pushConf.URL); pushConf.URL != "" {
p.Push(pub.StreamPath, pushConf, nil)
}
}
}
if p.Meta.Recorder != nil {
if p.Meta.NewRecorder != nil {
for r, recConf := range onPublish.Record {
if recConf.FilePath = r.Replace(pub.StreamPath, recConf.FilePath); recConf.FilePath != "" {
p.Record(pub, recConf, nil)
@@ -486,7 +469,7 @@ func (p *Plugin) OnPublish(pub *Publisher) {
if owner != nil {
_, isTransformer = owner.(ITransformer)
}
if p.Meta.Transformer != nil && !isTransformer {
if p.Meta.NewTransformer != nil && !isTransformer {
for r, tranConf := range onPublish.Transform {
if group := r.FindStringSubmatch(pub.StreamPath); group != nil {
for j, to := range tranConf.Output {
@@ -531,7 +514,7 @@ func (p *Plugin) OnSubscribe(streamPath string, args url.Values) {
// }
// }
for reg, conf := range p.config.OnSub.Pull {
if p.Meta.Puller != nil && reg.MatchString(streamPath) {
if p.Meta.NewPuller != nil && reg.MatchString(streamPath) {
conf.Args = config.HTTPValues(args)
conf.URL = reg.Replace(streamPath, conf.URL)
p.handler.Pull(streamPath, conf, nil)
@@ -631,7 +614,7 @@ func (p *Plugin) Subscribe(ctx context.Context, streamPath string) (subscriber *
}
func (p *Plugin) Pull(streamPath string, conf config.Pull, pubConf *config.Publish) {
puller := p.Meta.Puller(conf)
puller := p.Meta.NewPuller(conf)
if puller == nil {
return
}
@@ -639,20 +622,20 @@ func (p *Plugin) Pull(streamPath string, conf config.Pull, pubConf *config.Publi
}
func (p *Plugin) Push(streamPath string, conf config.Push, subConf *config.Subscribe) {
pusher := p.Meta.Pusher()
pusher := p.Meta.NewPusher()
pusher.GetPushJob().Init(pusher, p, streamPath, conf, subConf)
}
func (p *Plugin) Record(pub *Publisher, conf config.Record, subConf *config.Subscribe) *RecordJob {
recorder := p.Meta.Recorder(conf)
recorder := p.Meta.NewRecorder(conf)
job := recorder.GetRecordJob().Init(recorder, p, pub.StreamPath, conf, subConf)
job.Depend(pub)
return job
}
func (p *Plugin) Transform(pub *Publisher, conf config.Transform) {
transformer := p.Meta.Transformer()
job := transformer.GetTransformJob().Init(transformer, p, pub.StreamPath, conf)
transformer := p.Meta.NewTransformer()
job := transformer.GetTransformJob().Init(transformer, p, pub, conf)
job.Depend(pub)
}
@@ -732,35 +715,6 @@ func (p *Plugin) handle(pattern string, handler http.Handler) {
p.Server.apiList = append(p.Server.apiList, pattern)
}
func (p *Plugin) SaveConfig() (err error) {
return Servers.AddTask(&SaveConfig{Plugin: p}).WaitStopped()
}
type SaveConfig struct {
task.Task
Plugin *Plugin
file *os.File
}
func (s *SaveConfig) Start() (err error) {
if s.Plugin.Modify == nil {
err = os.Remove(s.Plugin.settingPath())
if err == nil {
err = task.ErrTaskComplete
}
}
s.file, err = os.OpenFile(s.Plugin.settingPath(), os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0666)
return
}
func (s *SaveConfig) Run() (err error) {
return yaml.NewEncoder(s.file).Encode(s.Plugin.Modify)
}
func (s *SaveConfig) Dispose() {
s.file.Close()
}
func (p *Plugin) sendPublishWebhook(pub *Publisher) {
if p.config.Hook == nil {
return

View File

@@ -24,7 +24,14 @@ type FLVPlugin struct {
const defaultConfig m7s.DefaultYaml = `publish:
speed: 1`
var _ = m7s.InstallPlugin[FLVPlugin](defaultConfig, NewPuller, NewRecorder, pb.RegisterApiServer, &pb.Api_ServiceDesc)
var _ = m7s.InstallPlugin[FLVPlugin](m7s.PluginMeta{
DefaultYaml: defaultConfig,
NewPuller: NewPuller,
NewRecorder: NewRecorder,
RegisterGRPCHandler: pb.RegisterApiHandler,
ServiceDesc: &pb.Api_ServiceDesc,
NewPullProxy: m7s.NewHTTPPullPorxy,
})
func (plugin *FLVPlugin) OnInit() (err error) {
_, port, _ := strings.Cut(plugin.GetCommonConf().HTTP.ListenAddr, ":")
@@ -96,10 +103,3 @@ func (plugin *FLVPlugin) ServeHTTP(w http.ResponseWriter, r *http.Request) {
}
err = live.Run()
}
func (plugin *FLVPlugin) OnPullProxyAdd(pullProxy *m7s.PullProxy) any {
d := &m7s.HTTPPullProxy{}
d.PullProxy = pullProxy
d.Plugin = &plugin.Plugin
return d
}

View File

@@ -175,10 +175,10 @@ func (r *Recorder) createStream(start time.Time) (err error) {
return
}
if sub.Publisher.HasAudioTrack() {
r.stream.AudioCodec = sub.Publisher.AudioTrack.ICodecCtx.FourCC().String()
r.stream.AudioCodec = sub.Publisher.AudioTrack.ICodecCtx.String()
}
if sub.Publisher.HasVideoTrack() {
r.stream.VideoCodec = sub.Publisher.VideoTrack.ICodecCtx.FourCC().String()
r.stream.VideoCodec = sub.Publisher.VideoTrack.ICodecCtx.String()
}
if recordJob.Plugin.DB != nil {
recordJob.Plugin.DB.Save(&r.stream)

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,40 @@
package plugin_gb28181pro
import (
"time"
"m7s.live/v5/pkg/task"
)
// CatalogSubscribeTask 目录订阅任务
type CatalogSubscribeTask struct {
task.TickTask
device *Device
}
// NewCatalogSubscribeTask 创建新的目录订阅任务
func NewCatalogSubscribeTask(device *Device) *CatalogSubscribeTask {
return &CatalogSubscribeTask{
device: device,
}
}
// GetTickInterval 获取定时间隔
func (c *CatalogSubscribeTask) GetTickInterval() time.Duration {
// 如果设备配置了订阅周期则使用设备配置的周期否则使用默认值3600秒
if c.device.SubscribeCatalog > 0 {
return time.Second * time.Duration(c.device.SubscribeCatalog)
}
return time.Second * 3600
}
// Tick 定时执行的方法
func (c *CatalogSubscribeTask) Tick(any) {
// 执行目录订阅
response, err := c.device.subscribeCatalog()
if err != nil {
c.Error("subCatalog", "err", err)
} else {
c.Debug("subCatalog", "response", response.String())
}
}

View File

@@ -1,18 +1,21 @@
package plugin_gb28181
package plugin_gb28181pro
import (
"log/slog"
"strings"
"sync/atomic"
"time"
"m7s.live/v5"
"m7s.live/v5/pkg/task"
"m7s.live/v5/pkg/util"
gb28181 "m7s.live/v5/plugin/gb28181/pkg"
)
type RecordRequest struct {
SN, SumNum int
Response []gb28181.Record
SN, SumNum int
ReceivedNum int // 已接收的记录数
Response []gb28181.Message
*util.Promise
}
@@ -20,24 +23,61 @@ func (r *RecordRequest) GetKey() int {
return r.SN
}
// AddResponse 添加响应并检查是否完成
func (r *RecordRequest) AddResponse(msg gb28181.Message) bool {
r.Response = append(r.Response, msg)
r.ReceivedNum += msg.RecordList.Num
// 当接收到的记录数等于总数时,表示接收完成
return r.ReceivedNum >= msg.SumNum
}
// PresetRequest 预置位请求结构体
type PresetRequest struct {
SN int
Response []gb28181.PresetItem
*util.Promise
}
func (r *PresetRequest) GetKey() int {
return r.SN
}
type Channel struct {
PullProxyTask *PullProxy // 拉流任务
Device *Device // 所属设备
State atomic.Int32 // 通道状态,0:空闲,1:正在invite,2:正在播放/对讲
GpsTime time.Time // gps时间
Longitude, Latitude string // 经度
RecordReqs util.Collection[int, *RecordRequest]
PresetReqs util.Collection[int, *PresetRequest] // 预置位请求集合
*slog.Logger
gb28181.ChannelInfo
AbstractDevice *m7s.PullProxy
gb28181.DeviceChannel
}
func (c *Channel) GetKey() string {
return c.DeviceID
return c.ChannelID
}
func (c *Channel) Pull() {
pubConf := c.Device.plugin.GetCommonConf().Publish
pubConf.PubAudio = c.AbstractDevice.Audio
pubConf.DelayCloseTimeout = util.Conditional(c.AbstractDevice.StopOnIdle, time.Second*5, 0)
c.Device.plugin.Pull(c.AbstractDevice.GetStreamPath(), c.AbstractDevice.Pull, &pubConf)
type PullProxy struct {
task.Task
m7s.BasePullProxy
}
func NewPullProxy() m7s.IPullProxy {
return &PullProxy{}
}
func (p *PullProxy) GetKey() uint {
return p.PullProxyConfig.ID
}
func (p *PullProxy) Start() error {
streamPaths := strings.Split(p.GetStreamPath(), "/")
deviceId, channelId := streamPaths[0], streamPaths[1]
if device, ok := p.Plugin.GetHandler().(*GB28181Plugin).devices.Get(deviceId); ok {
if _, ok := device.channels.Get(channelId); ok {
p.ChangeStatus(m7s.PullProxyStatusOnline)
}
}
return nil
}

View File

@@ -1,4 +1,4 @@
package plugin_gb28181
package plugin_gb28181pro
import (
"context"
@@ -91,7 +91,6 @@ func (c *Client) Start() (err error) {
cred, _ := digest.Digest(chal, digest.Options{
Method: req.Method.String(),
URI: c.recipient.Host,
Username: c.conf.Username,
Password: c.conf.Password,
})

View File

@@ -1,13 +1,21 @@
package plugin_gb28181
package plugin_gb28181pro
import (
"context"
"fmt"
"net"
"net/http"
"strconv"
"strings"
"sync"
"time"
"gorm.io/gorm"
"m7s.live/v5"
"github.com/emiago/sipgo"
"github.com/emiago/sipgo/sip"
"m7s.live/v5"
"m7s.live/v5/pkg/task"
"m7s.live/v5/pkg/util"
gb28181 "m7s.live/v5/plugin/gb28181/pkg"
@@ -24,66 +32,352 @@ const (
)
type Device struct {
task.Task `gorm:"-:all"`
ID string `gorm:"primaryKey"`
Name string
Manufacturer string
Model string
Owner string
UpdateTime time.Time
LastKeepaliveAt time.Time
task.Job `gorm:"-:all"`
DeviceId string `gorm:"primaryKey"` // 设备国标编号
Name string // 设备名
Manufacturer string // 生产厂商
Model string // 型号
Firmware string // 固件版本
Transport string // 传输协议UDP/TCP
StreamMode string // 数据流传输模式UDP:udp传输/TCP-ACTIVEtcp主动模式/TCP-PASSIVEtcp被动模式
IP string // wan地址_ip
Port int // wan地址_port
HostAddress string // wan地址
Online bool // 是否在线true为在线false为离线
RegisterTime time.Time // 注册时间
KeepaliveTime time.Time // 心跳时间
KeepaliveInterval int `gorm:"default:60"` // 心跳间隔
ChannelCount int // 通道个数
Expires int // 注册有效期
CreateTime time.Time // 创建时间
UpdateTime time.Time // 更新时间
Charset string // 字符集, 支持 UTF-8 与 GB2312
SubscribeCatalog int `gorm:"default:0"` // 目录订阅周期0为不订阅
SubscribePosition int `gorm:"default:0"` // 移动设备位置订阅周期0为不订阅
PositionInterval int // 移动设备位置信息上报时间间隔,单位:秒,默认值5
SubscribeAlarm int // 报警订阅周期0为不订阅
SSRCCheck bool // 是否开启ssrc校验默认关闭开启可以防止串流
GeoCoordSys string // 地理坐标系, 目前支持 WGS84,GCJ02
Password string // 密码
SipIp string // SIP交互IP设备访问平台的IP
AsMessageChannel bool // 是否作为消息通道
BroadcastPushAfterAck bool // 控制语音对讲流程释放收到ACK后发流
DeletedAt gorm.DeletedAt `yaml:"-"`
// 删除强关联字段
// channels []gb28181.DeviceChannel `gorm:"foreignKey:DeviceDBID;references:ID"` // 设备通道列表
// 保留原有字段
Status DeviceStatus
SN int
Recipient sip.Uri `gorm:"-:all"`
Transport string
channels util.Collection[string, *Channel]
mediaIp string
GpsTime time.Time //gps时间
Longitude, Latitude string //经度,纬度
Recipient sip.Uri `gorm:"-:all"`
channels util.Collection[string, *Channel] `gorm:"-:all"`
catalogReqs util.Collection[int, *CatalogRequest] `gorm:"-:all"`
MediaIp string `desc:"收流IP"`
Longitude, Latitude string // 经度,纬度
eventChan chan any
client *sipgo.Client
dialogClient *sipgo.DialogClient
contactHDR sip.ContactHeader
fromHDR sip.FromHeader
toHDR sip.ToHeader
plugin *GB28181Plugin
localPort int
}
func (d *Device) TableName() string {
return "device_gb28181"
return "gb28181_device"
}
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_db_id = ?", d.ID).Update("status", "OFF")
}
}
}
func (d *Device) GetKey() string {
return d.ID
return d.DeviceId
}
// CatalogRequest 目录请求结构体
type CatalogRequest struct {
SN, SumNum int
FirstResponse bool // 是否为第一个响应
*util.Promise
sync.Mutex // 保护并发访问
}
func (r *CatalogRequest) GetKey() int {
return r.SN
}
// AddResponse 处理响应并返回是否是第一个响应
func (r *CatalogRequest) AddResponse() bool {
r.Lock()
defer r.Unlock()
wasFirst := r.FirstResponse
r.FirstResponse = false
return wasFirst
}
// IsComplete 检查是否完成接收
func (r *CatalogRequest) IsComplete(channelsLength int) bool {
r.Lock()
defer r.Unlock()
return channelsLength >= r.SumNum
}
func (d *Device) onMessage(req *sip.Request, tx sip.ServerTransaction, msg *gb28181.Message) (err error) {
var body []byte
if d.Status == DeviceRecoverStatus {
d.Status = DeviceOnlineStatus
source := req.Source()
hostname, portStr, _ := net.SplitHostPort(source)
port, _ := strconv.Atoi(portStr)
if d.IP != hostname || d.Port != port {
d.Recipient.Host = hostname
d.Recipient.Port = port
}
d.Debug("OnMessage", "cmdType", msg.CmdType, "body", string(req.Body()))
d.IP = hostname
d.Port = port
d.HostAddress = hostname + ":" + portStr
var body []byte
//d.Online = true
//if d.Status != DeviceOnlineStatus {
// d.Status = DeviceOnlineStatus
//}
//d.Debug("OnMessage", "cmdType", msg.CmdType, "body", string(req.Body()))
switch msg.CmdType {
case "Keepalive":
d.LastKeepaliveAt = time.Now()
d.KeepaliveInterval = int(time.Since(d.KeepaliveTime).Seconds())
d.KeepaliveTime = time.Now()
if d.plugin.DB != nil {
if err := d.plugin.DB.Model(d).Updates(map[string]interface{}{
"keepalive_interval": d.KeepaliveInterval,
"keepalive_time": d.KeepaliveTime,
}).Error; err != nil {
d.Error("update keepalive info failed", "error", err)
}
}
case "Catalog":
d.eventChan <- msg.DeviceList
// 处理目录信息
catalogReq, exists := d.catalogReqs.Get(msg.SN)
if !exists {
// 创建新的目录请求
catalogReq = &CatalogRequest{
SN: msg.SN,
SumNum: msg.SumNum,
FirstResponse: true,
Promise: util.NewPromise(context.Background()),
}
d.catalogReqs.Set(catalogReq)
}
// 添加响应并获取是否是第一个响应
isFirst := catalogReq.AddResponse()
// 更新设备信息到数据库
if d.plugin.DB != nil {
// 如果是第一个响应,先清空现有通道
if isFirst {
d.Debug("清空现有通道", "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)
}
// 清空内存中的通道缓存
d.channels.Clear()
}
// 更新通道信息
for _, c := range msg.DeviceChannelList {
// 设置关联的设备数据库ID
c.ChannelID = c.DeviceID
c.DeviceID = d.DeviceId
c.ID = d.DeviceId + "_" + c.ChannelID
// 使用 Save 进行 upsert 操作
if err := d.plugin.DB.Save(&c).Error; err != nil {
d.Error("save channel failed", "error", err)
}
d.addOrUpdateChannel(c)
}
// 更新当前设备的通道数
d.ChannelCount = msg.SumNum
d.UpdateTime = time.Now()
d.Debug("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,
}).Error; err != nil {
d.Error("save device failed", "error", err)
}
}
// 在所有通道都添加完成后,检查是否完成接收
if catalogReq.IsComplete(d.channels.Length) {
catalogReq.Resolve()
d.catalogReqs.RemoveByKey(msg.SN)
}
case "RecordInfo":
if channel, ok := d.channels.Get(msg.DeviceID); ok {
if req, ok := channel.RecordReqs.Get(msg.SN); ok {
req.Response = msg.RecordList
// 添加响应并检查是否完成
if req.AddResponse(*msg) {
req.Resolve()
}
}
}
case "PresetQuery":
if channel, ok := d.channels.Get(msg.DeviceID); ok {
if req, ok := channel.PresetReqs.Get(msg.SN); ok {
// 添加预置位响应
req.Response = msg.PresetList.Item
req.Resolve()
}
}
// 查询平台信息
type Result struct {
PlatformServerGBID string `gorm:"column:platform_server_gb_id"`
}
var result Result
if d.plugin.DB != nil {
if err := d.plugin.DB.Table("gb28181_platform_channel gpc").
Select("gpc.platform_server_gb_id").
Joins("LEFT JOIN gb28181_channel gc on gpc.channel_db_id= gc.id").
Where("gc.channel_id = ?", msg.DeviceID).
First(&result).Error; err != nil {
d.Error("查询平台信息失败", "error", err)
return err
}
// 从platforms集合中获取平台实例
if platform, ok := d.plugin.platforms.Get(result.PlatformServerGBID); ok {
// 创建并发送响应消息
request := platform.CreateRequest("MESSAGE")
fromTag, _ := req.From().Params.Get("tag")
// 设置From头部
fromHeader := sip.FromHeader{
Address: sip.Uri{
User: platform.PlatformModel.DeviceGBID,
Host: platform.PlatformModel.ServerGBDomain,
},
Params: sip.NewParams(),
}
fromHeader.Params.Add("tag", fromTag)
request.AppendHeader(&fromHeader)
// 添加To头部
toHeader := sip.ToHeader{
Address: sip.Uri{
User: platform.PlatformModel.ServerGBID,
Host: platform.PlatformModel.ServerGBDomain,
},
}
request.AppendHeader(&toHeader)
// 添加Via头部
viaHeader := sip.ViaHeader{
ProtocolName: "SIP",
ProtocolVersion: "2.0",
Transport: platform.PlatformModel.Transport,
Host: platform.PlatformModel.DeviceIP,
Port: platform.PlatformModel.DevicePort,
Params: sip.NewParams(),
}
viaHeader.Params.Add("branch", sip.GenerateBranchN(16)).Add("rport", "")
request.AppendHeader(&viaHeader)
// 设置Content-Type
contentTypeHeader := sip.ContentTypeHeader("Application/MANSCDP+xml")
request.AppendHeader(&contentTypeHeader)
// 直接使用原始消息体
request.SetBody(req.Body())
// 发送请求
_, err = platform.Client.Do(platform.ctx, request)
if err != nil {
d.Error("发送预置位查询响应失败", "error", err)
return err
}
}
}
case "DeviceStatus":
if d.plugin.DB != nil {
d.UpdateTime = time.Now()
if err := d.plugin.DB.Model(d).Updates(map[string]interface{}{
"update_time": d.UpdateTime,
"longitude": msg.Longitude,
"latitude": msg.Latitude,
}).Error; err != nil {
d.Error("save device status failed", "error", err)
}
}
case "DeviceInfo":
// 主设备信息
d.Name = msg.DeviceName
d.Manufacturer = msg.Manufacturer
d.Model = msg.Model
d.Firmware = msg.Firmware
d.UpdateTime = time.Now()
d.Latitude = msg.Latitude
d.Longitude = msg.Longitude
// 更新设备信息到数据库
if d.plugin.DB != nil {
if err := d.plugin.DB.Model(d).Updates(map[string]interface{}{
"name": d.Name,
"manufacturer": d.Manufacturer,
"model": d.Model,
"firmware": d.Firmware,
"update_time": d.UpdateTime,
"longitude": d.Longitude,
"latitude": d.Latitude,
}).Error; err != nil {
d.Error("save device info failed", "error", err)
}
}
case "Alarm":
d.Status = DeviceAlarmedStatus
body = []byte(gb28181.BuildAlarmResponseXML(d.ID))
// 创建报警记录
alarm := &gb28181.DeviceAlarm{
DeviceID: d.DeviceId, // 使用当前设备的ID
DeviceName: d.Name,
ChannelID: msg.DeviceID, // 使用消息中的DeviceID作为通道ID
AlarmPriority: msg.AlarmPriority,
AlarmMethod: msg.AlarmMethod,
AlarmType: msg.Info.AlarmType,
CreateTime: time.Now(),
}
// 尝试解析报警时间
if alarmTime, err := time.Parse("2006-01-02T15:04:05", msg.AlarmTime); err == nil {
alarm.AlarmTime = alarmTime
} else {
alarm.AlarmTime = time.Now()
d.Error("解析报警时间失败", "error", err)
}
// 保存到数据库
if d.plugin.DB != nil {
if err := d.plugin.DB.Create(alarm).Error; err != nil {
d.Error("保存报警信息失败", "error", err)
} else {
d.Info("保存报警信息成功",
"deviceId", alarm.DeviceID,
"channelId", alarm.ChannelID,
"alarmType", alarm.GetAlarmTypeDescription(),
"alarmMethod", alarm.GetAlarmMethodDescription(),
"alarmPriority", alarm.GetAlarmPriorityDescription())
}
}
case "Broadcast":
d.Info("broadcast message", "body", req.Body())
d.Info("Broadcast message", "body", req.Body())
case "DeviceControl":
d.Info("DeviceControl message", "body", req.Body())
default:
d.Warn("Not supported CmdType", "CmdType", msg.CmdType, "body", req.Body())
err = tx.Respond(sip.NewResponseFromRequest(req, http.StatusBadRequest, "", nil))
@@ -96,128 +390,307 @@ 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())
return d.client.Do(d, req)
return d.client.Do(context.Background(), req)
}
func (d *Device) Go() (err error) {
var response *sip.Response
// 初始化catalogReqs
d.catalogReqs.L = new(sync.RWMutex)
response, err = d.queryDeviceInfo()
if err != nil {
d.Error("queryDeviceInfo", "err", err)
}
response, err = d.queryDeviceStatus()
if err != nil {
d.Error("queryDeviceStatus", "err", err)
}
response, err = d.catalog()
if err != nil {
d.Error("catalog", "err", err)
} else {
d.Debug("catalog", "response", response.String())
}
response, err = d.queryDeviceInfo()
if err != nil {
d.Error("deviceInfo", "err", err)
} else {
d.Debug("deviceInfo", "response", response.String())
// 创建并启动目录订阅任务
if d.SubscribeCatalog > 0 {
catalogSubTask := NewCatalogSubscribeTask(d)
d.AddTask(catalogSubTask)
}
subTick := time.NewTicker(time.Second * 3600)
defer subTick.Stop()
catalogTick := time.NewTicker(time.Second * 60)
// 创建并启动位置订阅任务
if d.SubscribePosition > 0 {
positionSubTask := NewPositionSubscribeTask(d)
d.AddTask(positionSubTask)
}
catalogTick := time.NewTicker(time.Minute * 10)
keepaliveSeconds := 60
if d.KeepaliveInterval >= 5 {
keepaliveSeconds = d.KeepaliveInterval
}
keepLiveTick := time.NewTicker(time.Second * 10)
defer keepLiveTick.Stop()
defer catalogTick.Stop()
for {
select {
case <-d.Done():
case <-subTick.C:
response, err = d.subscribeCatalog()
if err != nil {
d.Error("subCatalog", "err", err)
} else {
d.Debug("subCatalog", "response", response.String())
}
response, err = d.subscribePosition(int(6))
if err != nil {
d.Error("subPosition", "err", err)
} else {
d.Debug("subPosition", "response", response.String())
case <-keepLiveTick.C:
if timeDiff := time.Since(d.KeepaliveTime); timeDiff > time.Duration(3*keepaliveSeconds)*time.Second {
d.Online = false
d.Status = DeviceOfflineStatus
// 设置所有通道状态为off
d.channels.Range(func(channel *Channel) bool {
channel.Status = "OFF"
return true
})
d.Stop(fmt.Errorf("device keepalive timeout after %v", timeDiff))
return
}
case <-catalogTick.C:
if time.Since(d.LastKeepaliveAt) > time.Second*3600 {
d.Error("keepalive timeout", "lastKeepaliveAt", d.LastKeepaliveAt)
if time.Since(d.KeepaliveTime) > time.Second*time.Duration(d.Expires) {
d.Error("keepalive timeout", "keepaliveTime", d.KeepaliveTime)
return
}
response, err = d.catalog()
if err != nil {
d.Error("catalog", "err", err)
} else {
d.Debug("catalog", "response", response.String())
}
case event := <-d.eventChan:
switch v := event.(type) {
case []gb28181.ChannelInfo:
for _, c := range v {
//当父设备非空且存在时、父设备节点增加通道
if c.ParentID != "" {
path := strings.Split(c.ParentID, "/")
parentId := path[len(path)-1]
//如果父ID并非本身所属设备一般情况下这是因为下级设备上传了目录信息该信息通常不需要处理。
// 暂时不考虑级联目录的实现
if d.ID != parentId {
if parent, ok := d.plugin.devices.Get(parentId); ok {
parent.addOrUpdateChannel(c)
continue
} else {
c.Model = "Directory " + c.Model
c.Status = "NoParent"
}
}
}
d.addOrUpdateChannel(c)
}
d.Debug("catalogTick", "response", response.String())
}
//case event := <-d.eventChan:
// d.Debug("eventChan", "event", event)
// switch v := event.(type) {
// case []gb28181.DeviceChannel:
// for _, c := range v {
// //当父设备非空且存在时、父设备节点增加通道
// if c.ParentID != "" {
// path := strings.Split(c.ParentID, "/")
// parentId := path[len(path)-1]
// //如果父ID并非本身所属设备一般情况下这是因为下级设备上传了目录信息该信息通常不需要处理。
// // 暂时不考虑级联目录的实现
// if d.DeviceId != parentId {
// if parent, ok := d.plugin.devices.Get(parentId); ok {
// parent.addOrUpdateChannel(c)
// continue
// } else {
// c.Model = "Directory " + c.Model
// c.Status = "NoParent"
// }
// }
// }
// d.addOrUpdateChannel(c)
// }
// }
}
}
}
func (d *Device) createRequest(Method sip.RequestMethod) (req *sip.Request) {
req = sip.NewRequest(Method, d.Recipient)
req.AppendHeader(&d.fromHDR)
func (d *Device) CreateRequest(Method sip.RequestMethod, Recipient any) *sip.Request {
var req *sip.Request
if recipient, ok := Recipient.(sip.Uri); ok {
req = sip.NewRequest(Method, recipient)
} else {
req = sip.NewRequest(Method, d.Recipient)
}
fromHDR := d.fromHDR
fromHDR.Params.Add("tag", sip.GenerateTagN(32))
req.AppendHeader(&fromHDR)
contentType := sip.ContentTypeHeader("Application/MANSCDP+xml")
req.AppendHeader(sip.NewHeader("User-Agent", "M7S/"+m7s.Version))
req.AppendHeader(&contentType)
req.AppendHeader(&d.contactHDR)
return
toHeader := sip.ToHeader{
Address: sip.Uri{User: d.DeviceId, Host: d.HostAddress},
}
req.AppendHeader(&toHeader)
//viaHeader := sip.ViaHeader{
// ProtocolName: "SIP",
// ProtocolVersion: "2.0",
// Transport: "UDP",
// Host: d.SipIp,
// Port: d.localPort,
// Params: sip.HeaderParams(sip.NewParams()),
//}
//viaHeader.Params.Add("branch", sip.GenerateBranchN(10)).Add("rport", "")
//req.AppendHeader(&viaHeader)
//req.AppendHeader(&d.contactHDR)
return req
}
func (d *Device) catalog() (*sip.Response, error) {
request := d.createRequest(sip.MESSAGE)
request := d.CreateRequest(sip.MESSAGE, nil)
//d.subscriber.Timeout = time.Now().Add(time.Second * time.Duration(expires))
request.AppendHeader(sip.NewHeader("Expires", "3600"))
request.SetBody(gb28181.BuildCatalogXML(d.SN, d.ID))
request.SetBody(gb28181.BuildCatalogXML(d.Charset, d.SN, d.DeviceId))
return d.send(request)
}
func (d *Device) subscribeCatalog() (*sip.Response, error) {
request := d.createRequest(sip.SUBSCRIBE)
request.AppendHeader(sip.NewHeader("Expires", "3600"))
request.SetBody(gb28181.BuildCatalogXML(d.SN, d.ID))
request := d.CreateRequest(sip.SUBSCRIBE, nil)
request.AppendHeader(sip.NewHeader("Expires", strconv.Itoa(d.SubscribeCatalog)))
request.SetBody(gb28181.BuildCatalogXML(d.Charset, d.SN, d.DeviceId))
return d.send(request)
}
func (d *Device) queryDeviceInfo() (*sip.Response, error) {
request := d.createRequest(sip.MESSAGE)
request.SetBody(gb28181.BuildDeviceInfoXML(d.SN, d.ID))
request := d.CreateRequest(sip.MESSAGE, nil)
request.SetBody(gb28181.BuildDeviceInfoXML(d.SN, d.DeviceId, d.Charset))
return d.send(request)
}
func (d *Device) queryDeviceStatus() (*sip.Response, error) {
request := d.CreateRequest(sip.MESSAGE, nil)
request.SetBody(gb28181.BuildDeviceStatusXML(d.SN, d.DeviceId, d.Charset))
return d.send(request)
}
func (d *Device) subscribePosition(interval int) (*sip.Response, error) {
request := d.createRequest(sip.SUBSCRIBE)
request.AppendHeader(sip.NewHeader("Expires", "3600"))
request.SetBody(gb28181.BuildDevicePositionXML(d.SN, d.ID, interval))
request := d.CreateRequest(sip.SUBSCRIBE, nil)
request.AppendHeader(sip.NewHeader("Expires", strconv.Itoa(d.SubscribePosition)))
request.SetBody(gb28181.BuildDevicePositionXML(d.SN, d.DeviceId, interval))
return d.send(request)
}
func (d *Device) addOrUpdateChannel(c gb28181.ChannelInfo) {
if channel, ok := d.channels.Get(c.DeviceID); ok {
channel.ChannelInfo = c
// frontEndCmd 前端控制命令包括PTZ指令、FI指令、预置位指令、巡航指令、扫描指令和辅助开关指令
func (d *Device) frontEndCmd(channelId string, cmdStr string) (*sip.Response, error) {
// 构建前端控制指令字符串
//cmdStr := d.frontEndCmdString(cmdCode, parameter1, parameter2, combineCode2)
// 构建XML消息体
ptzXml := strings.Builder{}
ptzXml.WriteString(fmt.Sprintf("<?xml version=\"1.0\" encoding=\"%s\"?>\r\n", d.Charset))
ptzXml.WriteString("<Control>\r\n")
ptzXml.WriteString("<CmdType>DeviceControl</CmdType>\r\n")
ptzXml.WriteString(fmt.Sprintf("<SN>%d</SN>\r\n", int(time.Now().UnixNano()/1e6%1000000)))
ptzXml.WriteString(fmt.Sprintf("<DeviceID>%s</DeviceID>\r\n", channelId))
ptzXml.WriteString(fmt.Sprintf("<PTZCmd>%s</PTZCmd>\r\n", cmdStr))
ptzXml.WriteString("<Info>\r\n")
ptzXml.WriteString("<ControlPriority>5</ControlPriority>\r\n")
ptzXml.WriteString("</Info>\r\n")
ptzXml.WriteString("</Control>\r\n")
// 创建并发送请求
request := d.CreateRequest(sip.MESSAGE, nil)
request.SetBody([]byte(ptzXml.String()))
return d.send(request)
}
// frontEndCmdString 生成前端控制指令字符串
func (d *Device) frontEndCmdString(cmdCode int32, parameter1 int32, parameter2 int32, combineCode2 int32) string {
// 构建指令字符串
var builder strings.Builder
builder.WriteString("A50F01")
// 添加指令码
builder.WriteString(fmt.Sprintf("%02X", cmdCode))
// 添加参数1
builder.WriteString(fmt.Sprintf("%02X", parameter1))
// 添加参数2
builder.WriteString(fmt.Sprintf("%02X", parameter2))
// 添加组合码2左移4位
builder.WriteString(fmt.Sprintf("%02X", combineCode2<<4))
// 计算校验码
checkCode := (0xA5 + 0x0F + 0x01 + int(cmdCode) + int(parameter1) + int(parameter2) + int(combineCode2<<4)) % 0x100
builder.WriteString(fmt.Sprintf("%02X", checkCode))
return builder.String()
}
func (d *Device) addOrUpdateChannel(c gb28181.DeviceChannel) {
if channel, ok := d.channels.Get(c.ChannelID); ok {
channel.DeviceChannel = c
} else {
channel = &Channel{
Device: d,
Logger: d.Logger.With("channel", c.DeviceID),
ChannelInfo: c,
Device: d,
Logger: d.Logger.With("channel", c.ChannelID),
DeviceChannel: c,
}
d.channels.Set(channel)
}
}
func (d *Device) GetID() string {
return d.DeviceId
}
func (d *Device) GetIP() string {
return d.IP
}
func (d *Device) GetStreamMode() string {
return d.StreamMode
}
func (d *Device) Send(req *sip.Request) (*sip.Response, error) {
return d.send(req)
}
func (d *Device) CreateSSRC(serial string) uint16 {
// 使用简单的 hash 函数将设备 ID 转换为 uint16
var hash uint16
for i := 0; i < len(d.DeviceId); i++ {
hash = hash*31 + uint16(d.DeviceId[i])
}
return hash
}
// recordCmd 录制控制命令
func (d *Device) recordCmd(channelId string, cmdType string) (*sip.Response, error) {
// 构建XML消息体
recordXml := strings.Builder{}
recordXml.WriteString(fmt.Sprintf("<?xml version=\"1.0\" encoding=\"%s\"?>\r\n", d.Charset))
recordXml.WriteString("<Control>\r\n")
recordXml.WriteString("<CmdType>DeviceControl</CmdType>\r\n")
recordXml.WriteString(fmt.Sprintf("<SN>%d</SN>\r\n", int(time.Now().UnixNano()/1e6%1000000)))
recordXml.WriteString(fmt.Sprintf("<DeviceID>%s</DeviceID>\r\n", channelId))
recordXml.WriteString(fmt.Sprintf("<RecordCmd>%s</RecordCmd>\r\n", cmdType))
recordXml.WriteString("</Control>\r\n")
// 创建并发送请求
request := d.CreateRequest(sip.MESSAGE, nil)
request.SetBody([]byte(recordXml.String()))
return d.send(request)
}
// SnapshotConfig 抓拍配置结构体
type SnapshotConfig struct {
SnapNum int `json:"snapNum"` // 连拍张数(1-10张)
Interval int `json:"interval"` // 单张抓拍间隔(单位:秒最小1秒)
UploadURL string `json:"uploadUrl"` // 抓拍图片上传路径
SessionID string `json:"sessionId"` // 会话ID用于标识抓拍会话
}
// BuildSnapshotConfigXML 生成抓拍配置XML
func (d *Device) BuildSnapshotConfigXML(config SnapshotConfig, channelID string) string {
// 参数验证和限制
if config.SnapNum < 1 {
config.SnapNum = 1
} else if config.SnapNum > 10 {
config.SnapNum = 10
}
if config.Interval < 1 {
config.Interval = 1
}
xml := strings.Builder{}
xml.WriteString(fmt.Sprintf("<?xml version=\"1.0\" encoding=\"%s\"?>\r\n", d.Charset))
xml.WriteString("<Control>\r\n")
xml.WriteString("<CmdType>DeviceConfig</CmdType>\r\n")
xml.WriteString(fmt.Sprintf("<SN>%d</SN>\r\n", d.SN))
xml.WriteString(fmt.Sprintf("<DeviceID>%s</DeviceID>\r\n", channelID))
xml.WriteString("<SnapShotConfig>\r\n")
xml.WriteString(fmt.Sprintf("<SnapNum>%d</SnapNum>\r\n", config.SnapNum))
xml.WriteString(fmt.Sprintf("<Interval>%d</Interval>\r\n", config.Interval))
xml.WriteString(fmt.Sprintf("<UploadURL>%s</UploadURL>\r\n", config.UploadURL))
xml.WriteString(fmt.Sprintf("<SessionID>%s</SessionID>\r\n", config.SessionID))
xml.WriteString("</SnapShotConfig>\r\n")
xml.WriteString("</Control>\r\n")
return xml.String()
}

View File

@@ -1,15 +1,20 @@
package plugin_gb28181
package plugin_gb28181pro
import (
"errors"
"fmt"
"math/rand"
"net/url"
"strconv"
"strings"
"time"
"github.com/emiago/sipgo"
"m7s.live/v5/pkg/util"
sipgo "github.com/emiago/sipgo"
"github.com/emiago/sipgo/sip"
m7s "m7s.live/v5"
"m7s.live/v5/pkg/task"
"m7s.live/v5/pkg/util"
gb28181 "m7s.live/v5/plugin/gb28181/pkg"
)
@@ -17,9 +22,14 @@ type Dialog struct {
task.Job
Channel *Channel
gb28181.InviteOptions
gb *GB28181Plugin
session *sipgo.DialogClientSession
pullCtx m7s.PullJob
gb *GB28181Plugin
session *sipgo.DialogClientSession
pullCtx m7s.PullJob
start string
end string
StreamMode string // 数据流传输模式UDP:udp传输/TCP-ACTIVEtcp主动模式/TCP-PASSIVEtcp被动模式
targetIP string // 目标设备的IP地址
targetPort int // 目标设备的端口
}
func (d *Dialog) GetCallID() string {
@@ -30,8 +40,23 @@ func (d *Dialog) GetPullJob() *m7s.PullJob {
return &d.pullCtx
}
const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
var seededRand *rand.Rand = rand.New(rand.NewSource(time.Now().UnixNano()))
func GenerateCallID(length int) string {
b := make([]byte, length)
for i := range b {
b[i] = charset[seededRand.Intn(len(charset))]
}
return string(b)
}
func (d *Dialog) Start() (err error) {
if !d.IsLive() {
// 处理时间范围
d.InviteOptions.Start = d.start
d.InviteOptions.End = d.End
if d.IsLive() {
d.pullCtx.PublishConfig.PubType = m7s.PublishTypeVod
}
err = d.pullCtx.Publish()
@@ -39,59 +64,172 @@ func (d *Dialog) Start() (err error) {
return
}
sss := strings.Split(d.pullCtx.RemoteURL, "/")
deviceId, channelId := sss[0], sss[1]
if len(sss) == 2 {
if device, ok := d.gb.devices.Get(deviceId); ok {
if channel, ok := device.channels.Get(channelId); ok {
d.Channel = channel
} else {
return fmt.Errorf("channel %s not found", channelId)
}
} else {
return fmt.Errorf("device %s not found", deviceId)
}
} else if len(sss) == 3 {
var recordRange util.Range[int]
err = recordRange.Resolve(sss[2])
if len(sss) < 2 {
d.Info("remote url is invalid", d.pullCtx.RemoteURL)
return
}
ssrc := d.CreateSSRC(d.gb.Serial)
deviceId, channelId := sss[len(sss)-2], sss[len(sss)-1]
var device *Device
if deviceTmp, ok := d.gb.devices.Get(deviceId); ok {
device = deviceTmp
if channel, ok := deviceTmp.channels.Get(channelId); ok {
d.Channel = channel
d.StreamMode = device.StreamMode
} else {
return fmt.Errorf("channel %s not found", channelId)
}
} else {
return fmt.Errorf("device %s not found", deviceId)
}
d.gb.dialogs.Set(d)
defer d.gb.dialogs.Remove(d)
//defer d.gb.dialogs.Remove(d)
if d.gb.MediaPort.Valid() {
select {
case d.MediaPort = <-d.gb.tcpPorts:
defer func() {
d.gb.tcpPorts <- d.MediaPort
}()
default:
return fmt.Errorf("no available tcp port")
}
} else {
d.MediaPort = d.gb.MediaPort[0]
}
ssrc := d.CreateSSRC(d.gb.Serial)
d.Info("MediaIp is ", device.MediaIp)
// 构建 SDP 内容
sdpInfo := []string{
"v=0",
fmt.Sprintf("o=%s 0 0 IN IP4 %s", d.Channel.DeviceID, d.Channel.Device.mediaIp),
"s=" + util.Conditional(d.IsLive(), "Play", "Playback"),
"u=" + d.Channel.DeviceID + ":0",
"c=IN IP4 " + d.Channel.Device.mediaIp,
d.String(),
fmt.Sprintf("m=video %d TCP/RTP/AVP 96", d.MediaPort),
"a=recvonly",
"a=rtpmap:96 PS/90000",
"a=setup:passive",
"a=connection:new",
"y=" + ssrc,
fmt.Sprintf("o=%s 0 0 IN IP4 %s", channelId, device.MediaIp),
fmt.Sprintf("s=%s", util.Conditional(d.IsLive(), "Play", "Playback")), // 根据是否有时间参数决定
}
contentTypeHeader := sip.ContentTypeHeader("application/sdp")
fromHeader := d.Channel.Device.fromHDR
subjectHeader := sip.NewHeader("Subject", fmt.Sprintf("%s:%s,%s:0", d.Channel.DeviceID, ssrc, d.gb.Serial))
d.session, err = d.Channel.Device.dialogClient.Invite(d.gb, d.Channel.Device.Recipient, []byte(strings.Join(sdpInfo, "\r\n")+"\r\n"), &contentTypeHeader, subjectHeader, &fromHeader, sip.NewHeader("Allow", "INVITE, ACK, CANCEL, REGISTER, MESSAGE, NOTIFY, BYE"))
// 非直播模式下添加u行保持在s=和c=之间
//if !d.IsLive() {
sdpInfo = append(sdpInfo, fmt.Sprintf("u=%s:0", channelId))
//}
// 添加c行
sdpInfo = append(sdpInfo, "c=IN IP4 "+device.MediaIp)
// 将字符串时间转换为 Unix 时间戳
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"))
}
sdpInfo = append(sdpInfo, fmt.Sprintf("t=%d %d", startTime.Unix(), endTime.Unix()))
} else {
sdpInfo = append(sdpInfo, "t=0 0")
}
// 添加媒体行和相关属性
var mediaLine string
switch strings.ToUpper(device.StreamMode) {
case "TCP-PASSIVE", "TCP-ACTIVE":
mediaLine = fmt.Sprintf("m=video %d TCP/RTP/AVP 96", d.MediaPort)
case "UDP":
mediaLine = fmt.Sprintf("m=video %d RTP/AVP 96", d.MediaPort)
default:
mediaLine = fmt.Sprintf("m=video %d TCP/RTP/AVP 96", d.MediaPort)
}
sdpInfo = append(sdpInfo, mediaLine)
sdpInfo = append(sdpInfo, "a=recvonly")
//根据传输模式添加 setup 和 connection 属性
switch strings.ToUpper(device.StreamMode) {
case "TCP-PASSIVE":
sdpInfo = append(sdpInfo,
"a=setup:passive",
"a=connection:new",
)
case "TCP-ACTIVE":
sdpInfo = append(sdpInfo,
"a=setup:active",
"a=connection:new",
)
case "UDP":
d.Stop(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))
// 创建 INVITE 请求
recipient := sip.Uri{
Host: device.IP,
Port: device.Port,
User: channelId,
}
// 设置必需的头部
contentTypeHeader := sip.ContentTypeHeader("APPLICATION/SDP")
subjectHeader := sip.NewHeader("Subject", fmt.Sprintf("%s:%s,%s:0", channelId, ssrc, d.gb.Serial))
//allowHeader := sip.NewHeader("Allow", "INVITE, ACK, CANCEL, REGISTER, MESSAGE, NOTIFY, BYE")
//Toheader里需要放入目录通道的id
toHeader := sip.ToHeader{
Address: sip.Uri{User: channelId, Host: channelId[0:10]},
}
userAgentHeader := sip.NewHeader("User-Agent", "M7S/"+m7s.Version)
//customCallID := fmt.Sprintf("%s-%s-%d@%s", device.DeviceId, channelId, time.Now().Unix(), device.SipIp)
customCallID := fmt.Sprintf("%s@%s", GenerateCallID(32), device.MediaIp)
callID := sip.CallIDHeader(customCallID)
viaHeader := sip.ViaHeader{
ProtocolName: "SIP",
ProtocolVersion: "2.0",
Transport: "UDP",
Host: device.MediaIp,
Port: device.localPort,
Params: sip.NewParams(),
}
viaHeader.Params.Add("branch", sip.GenerateBranchN(10)).Add("rport", "")
maxforward := sip.MaxForwardsHeader(70)
//contentLengthHeader := sip.ContentLengthHeader(len(strings.Join(sdpInfo, "\r\n") + "\r\n"))
csqHeader := sip.CSeqHeader{
SeqNo: uint32(device.SN),
MethodName: "INVITE",
}
//request.AppendHeader(&contentLengthHeader)
contactHDR := sip.ContactHeader{
Address: sip.Uri{
User: d.gb.Serial,
Host: device.SipIp,
Port: device.localPort,
},
}
fromHDR := sip.FromHeader{
Address: sip.Uri{
User: d.gb.Serial,
Host: device.MediaIp,
Port: device.localPort,
},
Params: sip.NewParams(),
}
fromHDR.Params.Add("tag", sip.GenerateTagN(32))
dialogClientCache := sipgo.NewDialogClientCache(device.client, device.contactHDR)
// 创建会话
d.gb.Info("start to invite,recipient:", recipient, " viaHeader:", viaHeader, " fromHDR:", fromHDR, " toHeader:", toHeader, " device.contactHDR:", device.contactHDR, "contactHDR:", contactHDR)
// 判断当前系统类型
//if runtime.GOOS == "windows" {
// d.session, err = dialogClientCache.Invite(d.gb, recipient, []byte(strings.Join(sdpInfo, "\r\n")+"\r\n"), &callID, &csqHeader, &fromHDR, &toHeader, &maxforward, userAgentHeader, subjectHeader, &contentTypeHeader)
//} else {
d.session, err = dialogClientCache.Invite(d.gb, recipient, []byte(strings.Join(sdpInfo, "\r\n")+"\r\n"), &callID, &csqHeader, &fromHDR, &toHeader, &maxforward, userAgentHeader, subjectHeader, &contentTypeHeader)
//}
// 最后添加Content-Length头部
return
}
func (d *Dialog) Run() (err error) {
d.Channel.Info("before WaitAnswer")
err = d.session.WaitAnswer(d.gb, sipgo.AnswerOptions{})
d.Channel.Info("after WaitAnswer")
if err != nil {
return
}
@@ -100,27 +238,48 @@ func (d *Dialog) Run() (err error) {
ds := strings.Split(inviteResponseBody, "\r\n")
for _, l := range ds {
if ls := strings.Split(l, "="); len(ls) > 1 {
if ls[0] == "y" && len(ls[1]) > 0 {
if _ssrc, err := strconv.ParseInt(ls[1], 10, 0); err == nil {
d.SSRC = uint32(_ssrc)
} else {
d.gb.Error("read invite response y ", "err", err)
switch ls[0] {
case "y":
if len(ls[1]) > 0 {
if _ssrc, err := strconv.ParseInt(ls[1], 10, 0); err == nil {
d.SSRC = uint32(_ssrc)
} else {
d.gb.Error("read invite response y ", "err", err)
}
}
// break
}
if ls[0] == "m" && len(ls[1]) > 0 {
netinfo := strings.Split(ls[1], " ")
if strings.ToUpper(netinfo[2]) == "TCP/RTP/AVP" {
d.gb.Debug("device support tcp")
} else {
return fmt.Errorf("device not support tcp")
case "c":
// 解析 c=IN IP4 xxx.xxx.xxx.xxx 格式
parts := strings.Split(ls[1], " ")
if len(parts) >= 3 {
d.targetIP = parts[len(parts)-1]
}
case "m":
// 解析 m=video port xxx 格式
parts := strings.Split(ls[1], " ")
if len(parts) >= 2 {
if port, err := strconv.Atoi(parts[1]); err == nil {
d.targetPort = port
}
}
}
}
}
if d.session.InviteResponse.Contact() != nil {
if &d.session.InviteRequest.Recipient != &d.session.InviteResponse.Contact().Address {
d.session.InviteResponse.Contact().Address = d.session.InviteRequest.Recipient
}
}
err = d.session.Ack(d.gb)
if err != nil {
d.gb.Error("ack session err", err)
}
pub := gb28181.NewPSPublisher(d.pullCtx.Publisher)
pub.Receiver.ListenAddr = fmt.Sprintf(":%d", d.MediaPort)
if d.StreamMode == "TCP-ACTIVE" {
pub.Receiver.ListenAddr = fmt.Sprintf("%s:%d", d.targetIP, d.targetPort)
} else {
pub.Receiver.ListenAddr = fmt.Sprintf(":%d", d.MediaPort)
}
pub.Receiver.StreamMode = d.StreamMode
d.AddTask(&pub.Receiver)
pub.Demux()
return
@@ -131,5 +290,14 @@ func (d *Dialog) GetKey() uint32 {
}
func (d *Dialog) Dispose() {
d.session.Close()
d.gb.tcpPorts <- d.MediaPort
err := d.session.Bye(d)
if err != nil {
d.Error("dialog bye bye err", err)
}
err = d.session.Close()
if err != nil {
d.Error("dialog close session err", err)
}
d.gb.dialogs.Remove(d)
}

View File

@@ -0,0 +1,332 @@
package plugin_gb28181pro
import (
"errors"
"fmt"
sipgo "github.com/emiago/sipgo"
"github.com/emiago/sipgo/sip"
m7s "m7s.live/v5"
"m7s.live/v5/pkg/task"
"m7s.live/v5/pkg/util"
gb28181 "m7s.live/v5/plugin/gb28181/pkg"
"strconv"
"strings"
)
// ForwardDialog 是用于转发RTP流的会话结构体
type ForwardDialog struct {
task.Job
channel *Channel
gb28181.InviteOptions
gb *GB28181Plugin
session *sipgo.DialogClientSession
pullCtx m7s.PullJob
forwarder *gb28181.RTPForwarder
upIP string //上级平台主动模式时接收数据的IP
upPort uint16 //上级平台主动模式时接收数据的端口
platformIP string //上级平台被动模式时发送数据的IP
platformPort int //上级平台被动模式时发送数据的端口
platformSSRC string // 上级平台的SSRC
platformCallId string //上级平台发起invite的callid
// 是否为TCP传输
TCP bool
// 是否为TCP主动模式
TCPActive bool
start int64
end int64
downIP string
downPort int
}
// GetCallID 获取会话的CallID
func (d *ForwardDialog) GetCallID() string {
return d.session.InviteRequest.CallID().Value()
}
// GetPullJob 获取拉流任务
func (d *ForwardDialog) GetPullJob() *m7s.PullJob {
return &d.pullCtx
}
// GetKey 获取会话标识符
func (d *ForwardDialog) GetKey() uint32 {
return d.SSRC
}
// Start 启动会话
func (d *ForwardDialog) Start() (err error) {
// 处理时间范围
isLive := true
if d.start > 0 && d.end > 0 {
isLive = false
d.pullCtx.PublishConfig.PubType = m7s.PublishTypeVod
}
//err = d.pullCtx.Publish()
if err != nil {
return
}
sss := strings.Split(d.pullCtx.RemoteURL, "/")
deviceId, channelId := sss[0], sss[1]
var device *Device
if deviceTmp, ok := d.gb.devices.Get(deviceId); ok {
device = deviceTmp
if channel, ok := deviceTmp.channels.Get(channelId); ok {
d.channel = channel
} else {
return fmt.Errorf("channel %s not found", channelId)
}
} else {
return fmt.Errorf("device %s not found", deviceId)
}
// 注册对话到集合,使用类型转换
d.gb.forwardDialogs.Set(d)
//defer d.gb.forwardDialogs.Remove(d)
if d.gb.MediaPort.Valid() {
select {
case d.MediaPort = <-d.gb.tcpPorts:
defer func() {
d.gb.tcpPorts <- d.MediaPort
}()
default:
return fmt.Errorf("no available tcp port")
}
} else {
d.MediaPort = d.gb.MediaPort[0]
}
// 使用上级平台的SSRC如果有或者设备的CreateSSRC方法
var ssrcValue uint16
if d.platformSSRC != "" {
// 使用上级平台的SSRC
if ssrcInt, err := strconv.ParseUint(d.platformSSRC, 10, 32); err == nil {
d.SSRC = uint32(ssrcInt)
} else {
d.gb.Error("parse platform ssrc error", "err", err)
// 使用设备的CreateSSRC方法作为备选
ssrcValue = device.CreateSSRC(d.gb.Serial)
d.SSRC = uint32(ssrcValue)
}
} else {
// 使用设备的CreateSSRC方法
ssrcValue = device.CreateSSRC(d.gb.Serial)
d.SSRC = uint32(ssrcValue)
}
// 构建 SDP 内容
sdpInfo := []string{
"v=0",
fmt.Sprintf("o=%s 0 0 IN IP4 %s", device.DeviceId, device.MediaIp),
fmt.Sprintf("s=%s", util.Conditional(isLive, "Play", "Playback")), // 根据是否有时间参数决定
}
// 非直播模式下添加u行保持在s=和c=之间
if !isLive {
sdpInfo = append(sdpInfo, fmt.Sprintf("u=%s:0", channelId))
}
// 添加c行
sdpInfo = append(sdpInfo, "c=IN IP4 "+device.MediaIp)
// 将字符串时间转换为 Unix 时间戳
if !isLive {
// 直接使用字符串格式的日期时间转换为秒级时间戳,不考虑时区问题
sdpInfo = append(sdpInfo, fmt.Sprintf("t=%d %d", d.start, d.end))
} else {
sdpInfo = append(sdpInfo, "t=0 0")
}
sdpInfo = append(sdpInfo, fmt.Sprintf("m=video %d TCP/RTP/AVP 96", d.MediaPort))
sdpInfo = append(sdpInfo, "a=recvonly")
sdpInfo = append(sdpInfo, "a=rtpmap:96 PS/90000")
//sdpInfo = append(sdpInfo, "a=rtpmap:98 H264/90000")
//sdpInfo = append(sdpInfo, "a=rtpmap:97 MPEG4/90000")
//根据传输模式添加 setup 和 connection 属性
switch strings.ToUpper(device.StreamMode) {
case "TCP-PASSIVE":
sdpInfo = append(sdpInfo,
"a=setup:passive",
"a=connection:new",
)
case "TCP-ACTIVE":
sdpInfo = append(sdpInfo,
"a=setup:active",
"a=connection:new",
)
case "UDP":
d.Stop(errors.New("do not support udp mode"))
default:
sdpInfo = append(sdpInfo,
"a=setup:passive",
"a=connection:new",
)
}
if d.SSRC == 0 {
d.SSRC = uint32(ssrcValue)
}
// 将SSRC转换为字符串格式
ssrcStr := strconv.FormatUint(uint64(d.SSRC), 10)
sdpInfo = append(sdpInfo, fmt.Sprintf("y=%s", ssrcStr))
// 创建INVITE请求
request := sip.NewRequest(sip.INVITE, sip.Uri{User: channelId, Host: device.IP})
// 使用字符串格式的SSRC
subject := fmt.Sprintf("%s:%s,%s:0", channelId, ssrcStr, deviceId)
// 创建自定义头部
contentTypeHeader := sip.ContentTypeHeader("APPLICATION/SDP")
subjectHeader := sip.NewHeader("Subject", subject)
// 设置请求体
request.SetBody([]byte(strings.Join(sdpInfo, "\r\n") + "\r\n"))
recipient := device.Recipient
recipient.User = channelId
viaHeader := sip.ViaHeader{
ProtocolName: "SIP",
ProtocolVersion: "2.0",
Transport: "UDP",
Host: device.SipIp,
Port: device.localPort,
Params: sip.HeaderParams(sip.NewParams()),
}
viaHeader.Params.Add("branch", sip.GenerateBranchN(16)).Add("rport", "")
fromHDR := sip.FromHeader{
Address: sip.Uri{
User: d.gb.Serial,
Host: device.MediaIp,
Port: device.localPort,
},
Params: sip.NewParams(),
}
toHeader := sip.ToHeader{
Address: sip.Uri{User: channelId, Host: channelId[0:10]},
}
fromHDR.Params.Add("tag", sip.GenerateTagN(16))
// 创建会话 - 使用device的dialogClient创建
dialogClientCache := sipgo.NewDialogClientCache(device.client, device.contactHDR)
//d.session, err = dialogClientCache.Invite(d.gb, recipient, request.Body(), &fromHDR, &toHeader, &viaHeader, subjectHeader, &contentTypeHeader)
d.session, err = dialogClientCache.Invite(d.gb, recipient, []byte(strings.Join(sdpInfo, "\r\n")+"\r\n"), &fromHDR, &toHeader, subjectHeader, &contentTypeHeader)
return
}
// Run 运行会话
func (d *ForwardDialog) Run() (err error) {
d.channel.Info("before WaitAnswer")
err = d.session.WaitAnswer(d.gb, sipgo.AnswerOptions{})
d.channel.Info("after WaitAnswer")
if err != nil {
return
}
inviteResponseBody := string(d.session.InviteResponse.Body())
d.channel.Info("inviteResponse", "body", inviteResponseBody)
ds := strings.Split(inviteResponseBody, "\r\n")
for _, l := range ds {
if ls := strings.Split(l, "="); len(ls) > 1 {
switch ls[0] {
case "y":
if len(ls[1]) > 0 {
if _ssrc, err := strconv.ParseInt(ls[1], 10, 0); err == nil {
d.SSRC = uint32(_ssrc)
} else {
d.gb.Error("read invite response y ", "err", err)
}
}
case "c":
// 解析 c=IN IP4 xxx.xxx.xxx.xxx 格式
parts := strings.Split(ls[1], " ")
if len(parts) >= 3 {
d.downIP = parts[len(parts)-1]
}
case "m":
// 解析 m=video port xxx 格式
parts := strings.Split(ls[1], " ")
if len(parts) >= 2 {
if port, err := strconv.Atoi(parts[1]); err == nil {
d.downPort = port
}
}
}
}
}
if d.session.InviteResponse.Contact() != nil {
if &d.session.InviteRequest.Recipient != &d.session.InviteResponse.Contact().Address {
d.session.InviteResponse.Contact().Address = d.session.InviteRequest.Recipient
}
}
err = d.session.Ack(d.gb)
if err != nil {
d.gb.Error("ack session err", err)
d.Stop(errors.New("ack session err" + err.Error()))
}
// 创建并初始化RTPForwarder
//d.forwarder = gb28181.NewRTPForwarder()
//d.forwarder.TCP = d.TCP
//d.forwarder.TCPActive = d.TCPActive
//d.forwarder.StreamMode = d.channel.Device.StreamMode
//
//if d.TCPActive {
// d.forwarder.UpListenAddr = fmt.Sprintf(":%d", d.upPort)
//} else {
// d.forwarder.UpListenAddr = fmt.Sprintf("%s:%d", d.upIP, d.platformPort)
//}
//
// 设置监听地址和端口
if strings.ToUpper(d.channel.Device.StreamMode) == "TCP-ACTIVE" {
d.forwarder.DownListenAddr = fmt.Sprintf("%s:%d", d.downIP, d.downPort)
} else {
d.forwarder.DownListenAddr = fmt.Sprintf(":%d", d.MediaPort)
}
//
//// 设置转发目标
//if d.platformIP != "" && d.platformPort > 0 {
// err = d.forwarder.SetTarget(d.platformIP, d.platformPort)
// if err != nil {
// d.Error("set target error", "err", err)
// return err
// }
//} else {
// d.Error("no target set, will only receive but not forward")
// return
//}
//
//// 设置目标SSRC
//if d.platformSSRC != "" {
// d.forwarder.TargetSSRC = d.platformSSRC
// d.Info("set target ssrc", "ssrc", d.platformSSRC)
//}
// 将forwarder添加到任务中
d.AddTask(d.forwarder)
d.Info("forwarder started successfully",
"d.forwarder.UpListenAddr", d.forwarder.UpListenAddr,
"TCP", d.forwarder.TCP,
"TCPActive", d.forwarder.TCPActive,
"listen", d.forwarder.DownListenAddr,
"target", fmt.Sprintf("%s:%d", d.platformIP, d.platformPort),
"ssrc", d.platformSSRC)
// 使用goroutine启动Demux避免阻塞
d.forwarder.Demux()
return
}
// Dispose 释放会话资源
func (d *ForwardDialog) Dispose() {
if d.session != nil {
err := d.session.Bye(d)
if err != nil {
d.Error("forwarddialog bye bye err", err)
}
err = d.session.Close()
if err != nil {
d.Error("forwarddialog close session err", err)
}
}
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,27 @@
package gb28181
// CivilCode 行政区划编码信息
type CivilCode struct {
Code string `json:"code"` // 行政区划编码
Name string `json:"name"` // 行政区划名称
ParentCode string `json:"parentCode"` // 父级行政区划编码
}
// NewCivilCodeFromArray 从字符串数组创建 CivilCode 实例
func NewCivilCodeFromArray(infoArray []string) *CivilCode {
if len(infoArray) < 2 {
return nil
}
civilCode := &CivilCode{
Code: infoArray[0],
Name: infoArray[1],
}
// 如果有父级编码
if len(infoArray) > 2 && infoArray[2] != "" {
civilCode.ParentCode = infoArray[2]
}
return civilCode
}

View File

@@ -0,0 +1,126 @@
package gb28181
import (
"strings"
"sync"
)
// CivilCodeUtil 行政区划编码工具类
type CivilCodeUtil struct {
// 用于消息的缓存
civilCodeMap sync.Map // map[string]*CivilCode
}
var (
instance *CivilCodeUtil
once sync.Once
)
// GetInstance 获取单例实例
func GetInstance() *CivilCodeUtil {
once.Do(func() {
instance = &CivilCodeUtil{}
})
return instance
}
// Add 添加多个行政区划编码
func (c *CivilCodeUtil) Add(civilCodeList []*CivilCode) {
if len(civilCodeList) > 0 {
for _, civilCode := range civilCodeList {
c.civilCodeMap.Store(civilCode.Code, civilCode)
}
}
}
// AddOne 添加单个行政区划编码
func (c *CivilCodeUtil) AddOne(civilCode *CivilCode) {
c.civilCodeMap.Store(civilCode.Code, civilCode)
}
// GetParentCode 获取父级编码
func (c *CivilCodeUtil) GetParentCode(code string) *CivilCode {
if len(code) > 8 {
return nil
}
if len(code) == 8 {
parentCode := code[:6]
if value, ok := c.civilCodeMap.Load(parentCode); ok {
return value.(*CivilCode)
}
return nil
}
if value, ok := c.civilCodeMap.Load(code); ok {
civilCode := value.(*CivilCode)
if civilCode.ParentCode == "" {
return nil
}
if parentValue, ok := c.civilCodeMap.Load(civilCode.ParentCode); ok {
return parentValue.(*CivilCode)
}
}
return nil
}
// GetCivilCode 获取行政区划编码对象
func (c *CivilCodeUtil) GetCivilCode(code string) *CivilCode {
if len(code) > 8 {
return nil
}
if value, ok := c.civilCodeMap.Load(code); ok {
return value.(*CivilCode)
}
return nil
}
// GetAllParentCode 获取所有父级编码
func (c *CivilCodeUtil) GetAllParentCode(civilCode string) []*CivilCode {
var civilCodeList []*CivilCode
parentCode := c.GetParentCode(civilCode)
if parentCode != nil {
civilCodeList = append(civilCodeList, parentCode)
allParentCode := c.GetAllParentCode(parentCode.Code)
if len(allParentCode) > 0 {
civilCodeList = append(civilCodeList, allParentCode...)
}
}
return civilCodeList
}
// IsEmpty 判断是否为空
func (c *CivilCodeUtil) IsEmpty() bool {
empty := true
c.civilCodeMap.Range(func(key, value interface{}) bool {
empty = false
return false
})
return empty
}
// Size 获取大小
func (c *CivilCodeUtil) Size() int {
size := 0
c.civilCodeMap.Range(func(key, value interface{}) bool {
size++
return true
})
return size
}
// GetAllChild 获取所有子节点
func (c *CivilCodeUtil) GetAllChild(parent string) []*Region {
var result []*Region
c.civilCodeMap.Range(func(key, value interface{}) bool {
civilCode := value.(*CivilCode)
if parent == "" {
if strings.TrimSpace(civilCode.ParentCode) == "" {
result = append(result, NewRegion(civilCode.Code, civilCode.Name, civilCode.ParentCode))
}
} else if civilCode.ParentCode == parent {
result = append(result, NewRegion(civilCode.Code, civilCode.Name, civilCode.ParentCode))
}
return true
})
return result
}

View File

@@ -0,0 +1,365 @@
package gb28181
import (
"fmt"
)
// CommonGBChannel 通用国标通道信息
type CommonGBChannel struct {
// 数据库自增ID
GbID int `json:"gb_id" gorm:"column:gb_id"`
// 国标编码
GbDeviceID string `json:"gb_device_id" gorm:"column:gb_device_id;default:null"`
// 国标名称
GbName string `json:"gb_name" gorm:"column:gb_name;default:null"`
// 国标设备厂商
GbManufacturer string `json:"gb_manufacturer" gorm:"column:gb_manufacturer;default:null"`
// 国标设备型号
GbModel string `json:"gb_model" gorm:"column:gb_model;default:null"`
// 国标设备归属
GbOwner string `json:"gb_owner" gorm:"column:gb_owner;default:null"`
// 国标行政区域
GbCivilCode string `json:"gb_civil_code" gorm:"column:gb_civil_code"`
// 国标警区
GbBlock string `json:"gb_block" gorm:"column:gb_block"`
// 国标安装地址
GbAddress string `json:"gb_address" gorm:"column:gb_address;default:null"`
// 国标是否有子设备
GbParental int `json:"gb_parental" gorm:"column:gb_parental"`
// 国标父节点ID
GbParentID string `json:"gb_parent_id" gorm:"column:gb_parent_id"`
// 国标信令安全模式
GbSafetyWay int `json:"gb_safety_way" gorm:"column:gb_safety_way"`
// 国标注册方式
GbRegisterWay int `json:"gb_register_way" gorm:"column:gb_register_way"`
// 国标证书序列号
GbCertNum string `json:"gb_cert_num" gorm:"column:gb_cert_num"`
// 国标证书有效标识
GbCertifiable int `json:"gb_certifiable" gorm:"column:gb_certifiable"`
// 国标无效原因码
GbErrCode int `json:"gb_err_code" gorm:"column:gb_err_code"`
// 国标证书终止有效期
GbEndTime string `json:"gb_end_time" gorm:"column:gb_end_time"`
// 国标保密属性
GbSecrecy int `json:"gb_secrecy" gorm:"column:gb_secrecy"`
// 国标IP地址
GbIPAddress string `json:"gb_ip_address" gorm:"column:gb_ip_address"`
// 国标端口
GbPort int `json:"gb_port" gorm:"column:gb_port"`
// 国标密码
GbPassword string `json:"gb_password" gorm:"column:gb_password"`
// 国标状态
GbStatus string `json:"gb_status" gorm:"column:gb_status"`
// 国标经度
GbLongitude float64 `json:"gb_longitude" gorm:"column:gb_longitude"`
// 国标纬度
GbLatitude float64 `json:"gb_latitude" gorm:"column:gb_latitude"`
// 国标业务分组ID
GbBusinessGroupID string `json:"gb_business_group_id" gorm:"column:gb_business_group_id"`
// 国标云台类型
GbPTZType int `json:"gb_ptz_type" gorm:"column:gb_ptz_type"`
// 国标位置类型
GbPositionType int `json:"gb_position_type" gorm:"column:gb_position_type"`
// 国标房间类型
GbRoomType int `json:"gb_room_type" gorm:"column:gb_room_type"`
// 国标用途类型
GbUseType int `json:"gb_use_type" gorm:"column:gb_use_type"`
// 国标补光类型
GbSupplyLightType int `json:"gb_supply_light_type" gorm:"column:gb_supply_light_type"`
// 国标方向类型
GbDirectionType int `json:"gb_direction_type" gorm:"column:gb_direction_type"`
// 国标分辨率
GbResolution string `json:"gb_resolution" gorm:"column:gb_resolution"`
// 国标下载速度
GbDownloadSpeed string `json:"gb_download_speed" gorm:"column:gb_download_speed"`
// 国标空域编码能力
GbSvcSpaceSupportMod int `json:"gb_svc_space_support_mod" gorm:"column:gb_svc_space_support_mod"`
// 国标时域编码能力
GbSvcTimeSupportMode int `json:"gb_svc_time_support_mode" gorm:"column:gb_svc_time_support_mode"`
// 关联的国标设备数据库ID
GbDeviceDbID int `json:"gb_device_db_id" gorm:"column:gb_device_db_id"`
// 二进制保存的录制计划
RecordPlan int64 `json:"record_plan" gorm:"column:record_plan"`
// 关联的推流ID
StreamPushID int `json:"stream_push_id" gorm:"column:stream_push_id"`
// 关联的拉流代理ID
StreamProxyID int `json:"stream_proxy_id" gorm:"column:stream_proxy_id"`
// 创建时间
CreateTime string `json:"create_time" gorm:"column:create_time"`
// 更新时间
UpdateTime string `json:"update_time" gorm:"column:update_time"`
// 流ID存在表示正在推流
StreamID string `json:"stream_id" xml:"-"`
// 是否含有音频
HasAudio bool `json:"has_audio" xml:"-"`
}
// Build 构建通道信息
func (c *CommonGBChannel) Build(deviceID string, name string, manufacturer string, model string, owner string,
civilCode string, block string, address string, parentID string) {
// TODO: 实现构建逻辑
}
// GetFullContent 获取完整的通道信息内容
func (c *CommonGBChannel) GetFullContent(deviceID string, name string, parentID string, event string) string {
content := "<Item>\n"
content += fmt.Sprintf("<DeviceId>%s</DeviceId>\n", deviceID)
content += fmt.Sprintf("<Name>%s</Name>\n", name)
if len(deviceID) > 8 {
deviceType := deviceID[10:13]
switch deviceType {
case "200":
// 业务分组目录项
if c.GbManufacturer != "" {
content += fmt.Sprintf("<Manufacturer>%s</Manufacturer>\n", c.GbManufacturer)
}
if c.GbModel != "" {
content += fmt.Sprintf("<Model>%s</Model>\n", c.GbModel)
}
if c.GbOwner != "" {
content += fmt.Sprintf("<Owner>%s</Owner>\n", c.GbOwner)
}
if c.GbCivilCode != "" {
content += fmt.Sprintf("<CivilCode>%s</CivilCode>\n", c.GbCivilCode)
}
if c.GbAddress != "" {
content += fmt.Sprintf("<Address>%s</Address>\n", c.GbAddress)
}
if c.GbRegisterWay != 0 {
content += fmt.Sprintf("<RegisterWay>%d</RegisterWay>\n", c.GbRegisterWay)
}
content += fmt.Sprintf("<Secrecy>%d</Secrecy>\n", c.GbSecrecy)
case "215":
// 业务分组
if c.GbCivilCode != "" {
content += fmt.Sprintf("<CivilCode>%s</CivilCode>\n", c.GbCivilCode)
}
content += fmt.Sprintf("<ParentID>%s</ParentID>\n", parentID)
case "216":
// 虚拟组织目录项
if c.GbCivilCode != "" {
content += fmt.Sprintf("<CivilCode>%s</CivilCode>\n", c.GbCivilCode)
}
if c.GbParentID != "" {
content += fmt.Sprintf("<ParentID>%s</ParentID>\n", c.GbParentID)
}
content += fmt.Sprintf("<BusinessGroupID>%s</BusinessGroupID>\n", c.GbBusinessGroupID)
default:
// 其他类型
if c.GbManufacturer != "" {
content += fmt.Sprintf("<Manufacturer>%s</Manufacturer>\n", c.GbManufacturer)
}
if c.GbModel != "" {
content += fmt.Sprintf("<Model>%s</Model>\n", c.GbModel)
}
if c.GbOwner != "" {
content += fmt.Sprintf("<Owner>%s</Owner>\n", c.GbOwner)
}
if c.GbCivilCode != "" {
content += fmt.Sprintf("<CivilCode>%s</CivilCode>\n", c.GbCivilCode)
}
if c.GbAddress != "" {
content += fmt.Sprintf("<Address>%s</Address>\n", c.GbAddress)
}
if c.GbParentID != "" {
content += fmt.Sprintf("<ParentID>%s</ParentID>\n", c.GbParentID)
}
content += fmt.Sprintf("<Parental>%d</Parental>\n", c.GbParental)
if c.GbSafetyWay != 0 {
content += fmt.Sprintf("<SafetyWay>%d</SafetyWay>\n", c.GbSafetyWay)
}
if c.GbRegisterWay != 0 {
content += fmt.Sprintf("<RegisterWay>%d</RegisterWay>\n", c.GbRegisterWay)
}
if c.GbCertNum != "" {
content += fmt.Sprintf("<CertNum>%s</CertNum>\n", c.GbCertNum)
}
if c.GbCertifiable != 0 {
content += fmt.Sprintf("<Certifiable>%d</Certifiable>\n", c.GbCertifiable)
}
if c.GbErrCode != 0 {
content += fmt.Sprintf("<ErrCode>%d</ErrCode>\n", c.GbErrCode)
}
if c.GbEndTime != "" {
content += fmt.Sprintf("<EndTime>%s</EndTime>\n", c.GbEndTime)
}
content += fmt.Sprintf("<Secrecy>%d</Secrecy>\n", c.GbSecrecy)
if c.GbIPAddress != "" {
content += fmt.Sprintf("<IPAddress>%s</IPAddress>\n", c.GbIPAddress)
}
if c.GbPort != 0 {
content += fmt.Sprintf("<Port>%d</Port>\n", c.GbPort)
}
if c.GbPassword != "" {
content += fmt.Sprintf("<Password>%s</Password>\n", c.GbPassword)
}
if c.GbStatus != "" {
content += fmt.Sprintf("<Status>%s</Status>\n", c.GbStatus)
}
if c.GbLongitude != 0 {
content += fmt.Sprintf("<Longitude>%f</Longitude>\n", c.GbLongitude)
}
if c.GbLatitude != 0 {
content += fmt.Sprintf("<Latitude>%f</Latitude>\n", c.GbLatitude)
}
// Info 部分
content += "<Info>\n"
if c.GbPTZType != 0 {
content += fmt.Sprintf(" <PTZType>%d</PTZType>\n", c.GbPTZType)
}
if c.GbPositionType != 0 {
content += fmt.Sprintf(" <PositionType>%d</PositionType>\n", c.GbPositionType)
}
if c.GbRoomType != 0 {
content += fmt.Sprintf(" <RoomType>%d</RoomType>\n", c.GbRoomType)
}
if c.GbUseType != 0 {
content += fmt.Sprintf(" <UseType>%d</UseType>\n", c.GbUseType)
}
if c.GbSupplyLightType != 0 {
content += fmt.Sprintf(" <SupplyLightType>%d</SupplyLightType>\n", c.GbSupplyLightType)
}
if c.GbDirectionType != 0 {
content += fmt.Sprintf(" <DirectionType>%d</DirectionType>\n", c.GbDirectionType)
}
if c.GbResolution != "" {
content += fmt.Sprintf(" <Resolution>%s</Resolution>\n", c.GbResolution)
}
if c.GbBusinessGroupID != "" {
content += fmt.Sprintf(" <BusinessGroupID>%s</BusinessGroupID>\n", c.GbBusinessGroupID)
}
if c.GbDownloadSpeed != "" {
content += fmt.Sprintf(" <DownloadSpeed>%s</DownloadSpeed>\n", c.GbDownloadSpeed)
}
if c.GbSvcSpaceSupportMod != 0 {
content += fmt.Sprintf(" <SVCSpaceSupportMode>%d</SVCSpaceSupportMode>\n", c.GbSvcSpaceSupportMod)
}
if c.GbSvcTimeSupportMode != 0 {
content += fmt.Sprintf(" <SVCTimeSupportMode>%d</SVCTimeSupportMode>\n", c.GbSvcTimeSupportMode)
}
content += "</Info>\n"
}
}
if event != "" {
content += fmt.Sprintf("<Event>%s</Event>\n", event)
}
content += "</Item>\n"
return content
}
// Encode 编码通道信息
func (c *CommonGBChannel) Encode(deviceID string, event string) string {
if event == "" {
return c.GetFullContent(deviceID, c.GbName, "", "")
}
switch event {
case "DEL", "DEFECT", "VLOST", "ON", "OFF":
return fmt.Sprintf("<Item>\n<DeviceId>%s</DeviceId>\n<Event>%s</Event>\n</Item>\n", deviceID, event)
case "ADD", "UPDATE":
return c.GetFullContent(deviceID, c.GbName, "", event)
default:
return ""
}
}
// BuildFromGroup 从 Group 构建 CommonGBChannel 实例
func BuildFromGroup(group *Group) *CommonGBChannel {
gbCode := DecodeGBCode(group.DeviceID)
if gbCode == nil {
return nil
}
channel := &CommonGBChannel{
GbName: group.Name,
GbDeviceID: group.DeviceID,
GbCivilCode: group.CivilCode,
}
if gbCode.TypeCode == "215" {
// 业务分组
channel.GbCivilCode = group.CivilCode
} else if gbCode.TypeCode == "216" {
// 虚拟组织
channel.GbParentID = group.ParentDeviceID
channel.GbBusinessGroupID = group.BusinessGroup
channel.GbCivilCode = group.CivilCode
}
return channel
}
// BuildFromPlatform 从 PlatformModel 构建 CommonGBChannel 实例
func BuildFromPlatform(platform *PlatformModel) *CommonGBChannel {
status := "OFF"
if platform.Status {
status = "ON"
}
return &CommonGBChannel{
GbDeviceID: platform.DeviceGBID,
GbName: platform.Name,
GbManufacturer: platform.Manufacturer,
GbModel: platform.Model,
GbCivilCode: platform.CivilCode,
GbAddress: platform.Address,
GbRegisterWay: platform.RegisterWay,
GbSecrecy: platform.Secrecy,
GbStatus: status,
}
}
// BuildFromRegion 从 Region 构建 CommonGBChannel 实例
func BuildFromRegion(region *Region) *CommonGBChannel {
return &CommonGBChannel{
GbDeviceID: region.DeviceID,
GbName: region.Name,
}
}

View File

@@ -0,0 +1,127 @@
package gb28181
import "time"
// DeviceAlarm 报警信息结构体
type DeviceAlarm struct {
ID int64 `gorm:"primaryKey;autoIncrement" json:"id" description:"数据库id"`
DeviceID string `json:"deviceId" description:"设备的国标编号"`
DeviceName string `json:"deviceName" description:"设备名称"`
ChannelID string `json:"channelId" description:"通道的国标编号"`
AlarmPriority string `json:"alarmPriority" description:"报警级别, 1为一级警情, 2为二级警情, 3为三级警情, 4为四级警情"`
AlarmMethod string `json:"alarmMethod" description:"报警方式,1为电话报警,2为设备报警,3为短信报警,4为GPS报警,5为视频报警,6为设备故障报警,7其他报警"`
AlarmTime time.Time `json:"alarmTime" description:"报警时间"`
AlarmDescription string `json:"alarmDescription" description:"报警内容描述"`
Longitude float64 `json:"longitude" description:"经度"`
Latitude float64 `json:"latitude" description:"纬度"`
AlarmType string `json:"alarmType" description:"报警类型"`
CreateTime time.Time `json:"createTime" description:"创建时间"`
}
// GetAlarmPriorityDescription 获取报警级别描述
func (a *DeviceAlarm) GetAlarmPriorityDescription() string {
switch a.AlarmPriority {
case "1":
return "一级警情"
case "2":
return "二级警情"
case "3":
return "三级警情"
case "4":
return "四级警情"
default:
return a.AlarmPriority
}
}
// GetAlarmMethodDescription 获取报警方式描述
func (a *DeviceAlarm) GetAlarmMethodDescription() string {
var desc []rune
for _, c := range a.AlarmMethod {
switch c {
case '1':
desc = append(desc, []rune("-电话报警")...)
case '2':
desc = append(desc, []rune("-设备报警")...)
case '3':
desc = append(desc, []rune("-短信报警")...)
case '4':
desc = append(desc, []rune("-GPS报警")...)
case '5':
desc = append(desc, []rune("-视频报警")...)
case '6':
desc = append(desc, []rune("-设备故障报警")...)
case '7':
desc = append(desc, []rune("-其他报警")...)
}
}
if len(desc) > 0 {
return string(desc[1:]) // 去掉第一个'-'
}
return ""
}
// GetAlarmTypeDescription 获取报警类型描述
func (a *DeviceAlarm) GetAlarmTypeDescription() string {
if a.AlarmType == "" {
return ""
}
// 检查报警方式
methodMap := make(map[string]bool)
for _, c := range a.AlarmMethod {
methodMap[string(c)] = true
}
// 根据不同的报警方式返回对应的描述
if methodMap["2"] { // 设备报警
switch a.AlarmType {
case "1":
return "视频丢失报警"
case "2":
return "设备防拆报警"
case "3":
return "存储设备磁盘满报警"
case "4":
return "设备高温报警"
case "5":
return "设备低温报警"
}
}
if methodMap["5"] || methodMap["6"] { // 视频报警或设备故障报警
switch a.AlarmType {
case "1":
return "人工视频报警"
case "2":
return "运动目标检测报警"
case "3":
return "遗留物检测报警"
case "4":
return "物体移除检测报警"
case "5":
return "绊线检测报警"
case "6":
return "入侵检测报警"
case "7":
return "逆行检测报警"
case "8":
return "徘徊检测报警"
case "9":
return "流量统计报警"
case "10":
return "密度检测报警"
case "11":
return "视频异常检测报警"
case "12":
return "快速移动报警"
}
}
return a.AlarmType
}
// TableName 返回数据库表名
func (DeviceAlarm) TableName() string {
return "gb28181_devicealarm"
}

View File

@@ -0,0 +1,302 @@
package gb28181
import (
"gorm.io/gorm"
"strconv"
"time"
)
// ChannelStatus 通道状态类型
type ChannelStatus string
const (
ChannelOnStatus ChannelStatus = "ON"
ChannelOffStatus ChannelStatus = "OFF"
)
// DeviceChannel 设备通道信息
type DeviceChannel struct {
//CommonGBChannel // 通过组合继承 CommonGBChannel 的字段
ID string `gorm:"primaryKey" json:"ID"` // 数据库自增长ID
ChannelID string `json:"channelID" xml:"ChannelID"`
DeviceID string `json:"deviceID" xml:"DeviceID"` // 设备国标编号
ParentID string `json:"parentId" xml:"ParentID"` // 父节点ID
Name string `json:"name" xml:"Name"` // 通道名称
Manufacturer string `json:"manufacturer" xml:"Manufacturer"` // 设备厂商
Model string `json:"model" xml:"Model"` // 设备型号
Owner string `json:"owner" xml:"Owner"` // 设备归属
CivilCode string `json:"civilCode" xml:"CivilCode"` // 行政区域
Block string `json:"block" xml:"Block"` // 警区
Address string `json:"address" xml:"Address"` // 安装地址
Port int `json:"port" xml:"Port"` // 端口
Parental int `json:"parental" xml:"Parental"` // 是否有子设备
SafetyWay int `json:"safetyWay" xml:"SafetyWay"` // 信令安全模式
RegisterWay int `json:"registerWay" xml:"RegisterWay"` // 注册方式
CertNum string `json:"certNum" xml:"CertNum"` // 证书序列号
Certifiable int `json:"certifiable" xml:"Certifiable"` // 证书有效标识
ErrCode int `json:"errCode" xml:"ErrCode"` // 无效原因码
EndTime string `json:"endTime" xml:"EndTime"` // 证书终止有效期
Secrecy int `json:"secrecy" xml:"Secrecy"` // 保密属性
IPAddress string `json:"ipAddress" xml:"IPAddress"` // 设备/系统IP地址
Password string `json:"password" xml:"Password"` // 设备口令
PTZType int `json:"ptzType" xml:"Info>PTZType"` // 摄像机类型
PositionType int `json:"positionType" xml:"Info>PositionType"` // 摄像机位置类型
RoomType int `json:"roomType" xml:"Info>RoomType"` // 安装位置室内外属性
UseType int `json:"useType" xml:"Info>UseType"` // 用途属性
SupplyLightType int `json:"supplyLightType" xml:"Info>SupplyLightType"` // 摄像机补光属性
DirectionType int `json:"directionType" xml:"Info>DirectionType"` // 摄像机监视方位属性
Resolution string `json:"resolution" xml:"Info>Resolution"` // 摄像机支持的分辨率
BusinessGroupID string `json:"businessGroupId" xml:"Info>BusinessGroupID"` // 虚拟组织所属的业务分组ID
DownloadSpeed string `json:"downloadSpeed" xml:"Info>DownloadSpeed"` // 下载倍速
SVCSpaceSupportMod int `json:"svcSpaceSupportMod" xml:"Info>SVCSpaceSupportMode"` // 空域编码能力
SVCTimeSupportMode int `json:"svcTimeSupportMode" xml:"Info>SVCTimeSupportMode"` // 时域编码能力
StreamPushID int `json:"streamPushId"` // 关联的推流ID
StreamProxyID int `json:"streamProxyId"` // 关联的拉流代理ID
CreateTime string `json:"createTime"` // 创建时间
Status ChannelStatus `json:"status" xml:"Status"` // 设备状态
Longitude float64
Latitude float64
DeletedAt gorm.DeletedAt `yaml:"-"`
PTZTypeText string `json:"ptzTypeText"` // 云台类型描述字符串
GbLongitude float64 `json:"gbLongitude"`
GbLatitude float64 `json:"gbLatitude"`
}
// SetPTZType 设置云台类型并更新描述文本
func (d *DeviceChannel) SetPTZType(ptzType int) {
d.PTZType = ptzType
switch ptzType {
case 0:
d.PTZTypeText = "未知"
case 1:
d.PTZTypeText = "球机"
case 2:
d.PTZTypeText = "半球"
case 3:
d.PTZTypeText = "固定枪机"
case 4:
d.PTZTypeText = "遥控枪机"
case 5:
d.PTZTypeText = "遥控半球"
case 6:
d.PTZTypeText = "多目设备的全景/拼接通道"
case 7:
d.PTZTypeText = "多目设备的分割通道"
}
}
// DecodeFromXML 从 XML 元素解码设备通道信息
func DecodeFromXML(element interface{}) (*DeviceChannel, error) {
// TODO: 实现 XML 解码逻辑
// 这部分需要参考 Java 版本的 decode 方法实现
return nil, nil
}
// DecodeWithOnlyDeviceID 仅解码设备ID
func DecodeWithOnlyDeviceID(element interface{}) (*DeviceChannel, error) {
// TODO: 实现仅解码设备ID的逻辑
return nil, nil
}
// TableName 指定数据库表名
func (d *DeviceChannel) TableName() string {
return "gb28181_channel"
}
// NewDeviceChannel 创建新的设备通道实例
func NewDeviceChannel() *DeviceChannel {
now := time.Now().Format("2006-01-02 15:04:05")
return &DeviceChannel{
CreateTime: now,
Status: ChannelOffStatus,
}
}
// Encode 生成通道信息的XML内容
func (d *DeviceChannel) Encode(event string, serverDeviceID string) string {
if event == "" {
return d.getFullContent("", serverDeviceID)
}
switch event {
case "DEL", "DEFECT", "VLOST":
return "<Item>\n" +
"<DeviceId>" + d.DeviceID + "</DeviceId>\n" +
"<Event>" + event + "</Event>\n" +
"</Item>\n"
case "ON", "OFF":
return "<Item>\n" +
"<DeviceId>" + d.DeviceID + "</DeviceId>\n" +
"<Event>" + event + "</Event>\n" +
"</Item>\n"
case "ADD", "UPDATE":
return d.getFullContent(event, serverDeviceID)
default:
return ""
}
}
// getFullContent 生成完整的通道信息XML内容
func (d *DeviceChannel) getFullContent(event string, serverDeviceID string) string {
content := "<Item>\n" +
"<DeviceId>" + d.DeviceID + "</DeviceId>\n" +
"<Name>" + d.Name + "</Name>\n"
if len(d.DeviceID) > 8 {
typeCode := d.DeviceID[10:13]
switch typeCode {
case "200":
// 业务分组目录项
if d.Manufacturer != "" {
content += "<Manufacturer>" + d.Manufacturer + "</Manufacturer>\n"
}
if d.Model != "" {
content += "<Model>" + d.Model + "</Model>\n"
}
if d.Owner != "" {
content += "<Owner>" + d.Owner + "</Owner>\n"
}
if d.CivilCode != "" {
content += "<CivilCode>" + d.CivilCode + "</CivilCode>\n"
}
if d.Address != "" {
content += "<Address>" + d.Address + "</Address>\n"
}
if d.RegisterWay != 0 {
content += "<RegisterWay>" + strconv.Itoa(d.RegisterWay) + "</RegisterWay>\n"
}
if d.Secrecy != 0 {
content += "<Secrecy>" + strconv.Itoa(d.Secrecy) + "</Secrecy>\n"
}
case "215":
// 业务分组
if d.CivilCode != "" {
content += "<CivilCode>" + d.CivilCode + "</CivilCode>\n"
}
content += "<ParentID>" + serverDeviceID + "</ParentID>\n"
case "216":
// 虚拟组织目录项
if d.CivilCode != "" {
content += "<CivilCode>" + d.CivilCode + "</CivilCode>\n"
}
if d.ParentID != "" {
content += "<ParentID>" + d.ParentID + "</ParentID>\n"
}
content += "<BusinessGroupID>" + d.BusinessGroupID + "</BusinessGroupID>\n"
default:
// 其他类型
d.appendCommonInfo(&content)
}
}
if event != "" {
content += "<Event>" + event + "</Event>\n"
}
content += "</Item>\n"
return content
}
// appendCommonInfo 添加通用信息到XML内容
func (d *DeviceChannel) appendCommonInfo(content *string) {
if d.Manufacturer != "" {
*content += "<Manufacturer>" + d.Manufacturer + "</Manufacturer>\n"
}
if d.Model != "" {
*content += "<Model>" + d.Model + "</Model>\n"
}
if d.Owner != "" {
*content += "<Owner>" + d.Owner + "</Owner>\n"
}
if d.CivilCode != "" {
*content += "<CivilCode>" + d.CivilCode + "</CivilCode>\n"
}
if d.Block != "" {
*content += "<Block>" + d.Block + "</Block>\n"
}
if d.Address != "" {
*content += "<Address>" + d.Address + "</Address>\n"
}
if d.ParentID != "" {
*content += "<ParentID>" + d.ParentID + "</ParentID>\n"
}
if d.SafetyWay != 0 {
*content += "<SafetyWay>" + strconv.Itoa(d.SafetyWay) + "</SafetyWay>\n"
}
if d.RegisterWay != 0 {
*content += "<RegisterWay>" + strconv.Itoa(d.RegisterWay) + "</RegisterWay>\n"
}
if d.CertNum != "" {
*content += "<CertNum>" + d.CertNum + "</CertNum>\n"
}
if d.Certifiable != 0 {
*content += "<Certifiable>" + strconv.Itoa(d.Certifiable) + "</Certifiable>\n"
}
if d.ErrCode != 0 {
*content += "<ErrCode>" + strconv.Itoa(d.ErrCode) + "</ErrCode>\n"
}
if d.EndTime != "" {
*content += "<EndTime>" + d.EndTime + "</EndTime>\n"
}
if d.IPAddress != "" {
*content += "<IPAddress>" + d.IPAddress + "</IPAddress>\n"
}
if d.Port != 0 {
*content += "<Port>" + strconv.Itoa(d.Port) + "</Port>\n"
}
if d.Password != "" {
*content += "<Password>" + d.Password + "</Password>\n"
}
if d.Status != "" {
*content += "<Status>" + string(d.Status) + "</Status>\n"
}
if d.GbLongitude != 0 {
*content += "<Longitude>" + strconv.FormatFloat(d.GbLongitude, 'f', -1, 64) + "</Longitude>\n"
}
if d.GbLatitude != 0 {
*content += "<Latitude>" + strconv.FormatFloat(d.GbLatitude, 'f', -1, 64) + "</Latitude>\n"
}
// 添加Info标签内的信息
*content += "<Info>\n"
d.appendInfoContent(content)
*content += "</Info>\n"
}
// appendInfoContent 添加Info标签内的信息到XML内容
func (d *DeviceChannel) appendInfoContent(content *string) {
if d.PTZType != 0 {
*content += " <PTZType>" + strconv.Itoa(d.PTZType) + "</PTZType>\n"
}
if d.PositionType != 0 {
*content += " <PositionType>" + strconv.Itoa(d.PositionType) + "</PositionType>\n"
}
if d.RoomType != 0 {
*content += " <RoomType>" + strconv.Itoa(d.RoomType) + "</RoomType>\n"
}
if d.UseType != 0 {
*content += " <UseType>" + strconv.Itoa(d.UseType) + "</UseType>\n"
}
if d.SupplyLightType != 0 {
*content += " <SupplyLightType>" + strconv.Itoa(d.SupplyLightType) + "</SupplyLightType>\n"
}
if d.DirectionType != 0 {
*content += " <DirectionType>" + strconv.Itoa(d.DirectionType) + "</DirectionType>\n"
}
if d.Resolution != "" {
*content += " <Resolution>" + d.Resolution + "</Resolution>\n"
}
if d.BusinessGroupID != "" {
*content += " <BusinessGroupID>" + d.BusinessGroupID + "</BusinessGroupID>\n"
}
if d.DownloadSpeed != "" {
*content += " <DownloadSpeed>" + d.DownloadSpeed + "</DownloadSpeed>\n"
}
if d.SVCSpaceSupportMod != 0 {
*content += " <SVCSpaceSupportMode>" + strconv.Itoa(d.SVCSpaceSupportMod) + "</SVCSpaceSupportMode>\n"
}
if d.SVCTimeSupportMode != 0 {
*content += " <SVCTimeSupportMode>" + strconv.Itoa(d.SVCTimeSupportMode) + "</SVCTimeSupportMode>\n"
}
}

View File

@@ -0,0 +1,393 @@
/*
RTPForwarder 是一个RTP包转发器主要功能包括
1. 可通过TCP或UDP协议接收RTP包
2. 接收RTP包后不进行解析直接转发到指定的IP和端口
3. 支持限流控制,可设置发送间隔
4. 提供与Monibuca系统集成的Publisher接口
5. 提供了UDP和TCP两种模式的使用示例
使用场景:
1. 作为GB28181协议中的媒体接收和转发节点
2. 在不需要解析媒体内容的情况下实现RTP流的中转
3. 可用于搭建分发网络将接收到的RTP流转发到多个目标
注意事项:
1. 默认使用TCP协议可通过设置Protocol字段切换为UDP模式
2. 使用前需设置监听地址(DownListenAddr)和转发目标(SetTarget)
3. 资源使用完毕后需调用Dispose方法释放资源
*/
package gb28181
import (
"fmt"
"log/slog"
"net"
"os"
"strconv"
"strings"
"sync"
"time"
"github.com/pion/rtp"
"m7s.live/v5/pkg/task"
"m7s.live/v5/pkg/util"
rtp2 "m7s.live/v5/plugin/rtp/pkg"
)
// RTPForwarder 接收RTP数据包并转发到指定目标的结构体
type RTPForwarder struct {
task.Task
rtp.Packet
FeedChan chan []byte // 接收RTP数据的通道
RTPReader *rtp2.TCP // RTP TCP读取器
UpListenAddr string //用于发送上级设备的监听地址
upListener net.Listener //用于发送上级设备的TCP监听器
DownListenAddr string // 用于接收下级摄像头数据监听地址
downListener net.Listener // 用于接收下级摄像头数据的TCP监听器
udpListener *net.UDPConn // UDP监听器
// 是否为TCP传输
TCP bool
// 是否为TCP主动模式
TCPActive bool
TargetIP string // 目标IP地址
TargetPort int // 目标端口
TargetSSRC string // 目标SSRC用于替换RTP包中的SSRC
udpConn *net.UDPConn // UDP发送连接
tcpConn net.Conn // TCP发送连接
bufferPool sync.Pool // 缓冲池
ForwardCount int64 // 已转发的包数量
SendInterval time.Duration // 发送间隔,可用于限流
lastSendTime time.Time // 上次发送时间
stopChan chan struct{} // 停止信号通道
*slog.Logger
StreamMode string // 数据流传输模式UDP:udp传输/TCP-ACTIVEtcp主动模式/TCP-PASSIVEtcp被动模式
}
// NewRTPForwarder 创建一个新的RTP转发器
func NewRTPForwarder() *RTPForwarder {
ret := &RTPForwarder{
FeedChan: make(chan []byte, 2000), // 增加缓冲区大小,减少丢包风险
SendInterval: time.Millisecond * 0, // 默认不限制发送间隔,最大速度转发
stopChan: make(chan struct{}),
Logger: slog.New(slog.NewTextHandler(os.Stdout, nil)),
}
ret.bufferPool = sync.Pool{
New: func() interface{} {
return make([]byte, 1500) // 常见MTU大小
},
}
return ret
}
// ReadRTP 读取RTP包
func (p *RTPForwarder) ReadRTP(rtpBuf util.Buffer) (err error) {
if err = p.Unmarshal(rtpBuf); err != nil {
p.Error("unmarshal error", "err", err)
return
}
if p.Enabled(p, task.TraceLevel) {
p.Trace("rtp", "len", rtpBuf.Len(), "seq", p.SequenceNumber, "payloadType", p.PayloadType, "ssrc", p.SSRC)
}
// 直接使用原始RTP包数据
rtpData := make([]byte, rtpBuf.Len())
copy(rtpData, rtpBuf)
// 检查是否已经停止
select {
case <-p.stopChan:
// 已经收到停止信号,不再发送数据
return nil
default:
// 将完整的RTP包数据发送到通道
select {
case p.FeedChan <- rtpData:
// 成功发送
case <-p.stopChan:
// 发送过程中收到停止信号
return nil
default:
// 通道已满,记录警告
p.Warn("feed channel full, dropping packet")
}
}
return nil
}
// SetTarget 设置转发目标地址
func (p *RTPForwarder) SetTarget(ip string, port int) error {
p.TargetIP = ip
p.TargetPort = port
// 根据转发协议创建相应的连接
if !p.TCP {
// 关闭已存在的UDP连接
if p.udpConn != nil {
p.udpConn.Close()
}
p.Info("start udp to up platform", "ip", ip, "port", port)
// 创建新的UDP连接
addr, err := net.ResolveUDPAddr("udp", net.JoinHostPort(ip, fmt.Sprintf("%d", port)))
if err != nil {
p.Error("resolve udp addr error", "err", err)
return err
}
p.udpConn, err = net.DialUDP("udp", nil, addr)
if err != nil {
p.Error("dial udp error", "err", err)
return err
}
} else {
go func() {
// 如果是TCP主动模式且还没有建立连接等待连接
p.Info("start to accept uplistener", "p.UpListenAddr,", p.UpListenAddr, "tcpConn is", p.tcpConn == nil, "p.Tcp is", p.TCP, "p.TCPActive", p.TCPActive)
if p.TCP && p.TCPActive && p.tcpConn == nil {
var err error
if p.upListener == nil {
p.upListener, err = net.Listen("tcp4", p.UpListenAddr)
if err != nil {
p.Error("start udp listen error", "err", err)
}
}
p.Info("waiting for upstream connection...")
p.tcpConn, err = p.upListener.Accept()
if err != nil {
p.Error("accept upstream connection failed", "err", err)
}
p.Info("upstream connected", "addr", p.tcpConn.RemoteAddr())
}
}()
}
p.Info("set target success", "ip", ip, "port", port, "TCP", p.TCP, "TCPActive", p.TCPActive)
return nil
}
// Start 启动监听
func (p *RTPForwarder) Start() (err error) {
p.Info("RTPForwarder start", "target", p.TargetIP, "port", p.TargetPort)
if strings.ToUpper(p.StreamMode) == "TCP-ACTIVE" {
// TCP主动模式不需要监听直接返回
p.Info("TCP-ACTIVE mode, no need to listen")
} else if strings.ToUpper(p.StreamMode) == "TCP-PASSIVE" {
p.downListener, err = net.Listen("tcp4", p.DownListenAddr)
if err != nil {
p.Error("start tcp listen error", "err", err)
return err
}
p.Info("start tcp down listen,streammode is ", p.StreamMode, "addr", p.DownListenAddr)
} else {
addr, err := net.ResolveUDPAddr("udp", p.DownListenAddr)
if err != nil {
p.Error("resolve udp addr error", "err", err)
return err
}
p.udpListener, err = net.ListenUDP("udp", addr)
if err != nil {
p.Error("start udp listen error", "err", err)
return err
}
p.Info("start udp listen", "addr", p.DownListenAddr)
}
if !p.TCPActive && p.TCP { //TCP被动模式需要服务器主动连接上级设备
// 创建新的TCP连接
addr := p.UpListenAddr
var err error
p.tcpConn, err = net.Dial("tcp", addr)
p.Info("start tcp listen,now is tcp-passive", "addr", addr)
if err != nil {
p.Error("dial tcp error", "err", err)
return err
}
}
p.Info("RTPForwarder end")
return nil
}
// Go 启动处理任务
func (p *RTPForwarder) Go() error {
p.Info("start go", "addr", p.DownListenAddr)
//if p.TCP {
return p.goTCP()
//} else {
// return p.goUDP()
//}
}
// goTCP 处理TCP连接的RTP包
func (p *RTPForwarder) goTCP() error {
p.Info("start tcp accept")
if strings.ToUpper(p.StreamMode) == "TCP-ACTIVE" {
// TCP主动模式直接连接到设备
addr := p.DownListenAddr
if !strings.Contains(addr, ":") {
return fmt.Errorf("invalid address %s, missing port", addr)
}
conn, err := net.Dial("tcp", addr)
if err != nil {
p.Error("connect to device failed", "err", err)
return err
}
p.RTPReader = (*rtp2.TCP)(conn.(*net.TCPConn))
p.Info("connected to device", "addr", conn.RemoteAddr())
return p.RTPReader.Read(p.ReadRTP)
}
// TCP被动模式等待连接
conn, err := p.downListener.Accept()
if err != nil {
p.Error("accept error", "err", err)
return err
}
p.RTPReader = (*rtp2.TCP)(conn.(*net.TCPConn))
p.Info("accept connection", "addr", conn.RemoteAddr())
return p.RTPReader.Read(p.ReadRTP)
}
// Demux 阻塞读取RTP并转发至目标IP和端口
func (p *RTPForwarder) Demux() {
defer p.Info("demux exit")
// 检查是否设置了目标地址
if !p.TCP && p.udpConn == nil {
p.Error("no udp target set for forwarding")
return
}
//if p.TCP && p.tcpConn == nil {
// p.Error("no tcp target set for forwarding")
// return
//}
p.Info("start demux and forward",
"target", net.JoinHostPort(p.TargetIP, fmt.Sprintf("%d", p.TargetPort)),
"TCP", p.TCP, "TCPActive", p.TCPActive)
// 持续从FeedChan读取RTP数据并转发
for rtpData := range p.FeedChan {
var err error
// 根据转发协议选择不同的发送方式
if !p.TCP {
// 确保发送的是标准RTP包
// 检查是否是有效的RTP包
packet := &rtp.Packet{}
if parseErr := packet.Unmarshal(rtpData); parseErr != nil {
p.Error("invalid RTP packet for UDP forwarding", "err", parseErr)
continue
}
// 如果设置了目标SSRC则修改RTP包中的SSRC
if p.TargetSSRC != "" {
targetSSRCUint, err := strconv.ParseUint(p.TargetSSRC, 10, 32)
if err == nil {
// 修改SSRC
packet.SSRC = uint32(targetSSRCUint)
// 重新编码RTP包
modifiedData, err := packet.Marshal()
if err == nil {
// 发送修改后的RTP包
_, err = p.udpConn.Write(modifiedData)
} else {
p.Error("marshal modified rtp packet error", "err", err)
// 发送原始RTP包
_, err = p.udpConn.Write(rtpData)
}
} else {
p.Error("parse target ssrc error", "err", err)
// 发送原始RTP包
_, err = p.udpConn.Write(rtpData)
}
} else {
// 直接发送原始RTP包
_, err = p.udpConn.Write(rtpData)
}
} else {
// 对于TCP需要添加2字节的长度前缀
if p.tcpConn != nil {
// 创建带长度前缀的数据包
tcpData := make([]byte, len(rtpData)+2)
// 设置长度前缀(大端序)
tcpData[0] = byte((len(rtpData) >> 8) & 0xFF)
tcpData[1] = byte(len(rtpData) & 0xFF)
// 复制RTP数据
copy(tcpData[2:], rtpData)
// 发送到TCP连接
_, err = p.tcpConn.Write(tcpData)
} else {
err = fmt.Errorf("tcp connection not established")
}
}
if err != nil {
p.Error("forward rtp packet error", "err", err, "TCP", p.TCP, "TCPActive", p.TCPActive)
continue
}
p.ForwardCount++
// 控制发送速率
if p.SendInterval > 0 && !p.lastSendTime.IsZero() {
elapsed := time.Since(p.lastSendTime)
if elapsed < p.SendInterval {
time.Sleep(p.SendInterval - elapsed)
}
}
p.lastSendTime = time.Now()
if p.Enabled(p, task.TraceLevel) && p.ForwardCount%1000 == 0 {
p.Trace("forward rtp packet", "count", p.ForwardCount, "TCP", p.TCP, "TCPActive", p.TCPActive)
}
}
}
// Dispose 释放资源
func (p *RTPForwarder) Dispose() {
p.Info("disposing forwarder")
// 发送停止信号
close(p.stopChan)
// 给一些时间让所有goroutine响应停止信号
time.Sleep(100 * time.Millisecond)
if p.downListener != nil {
p.downListener.Close()
}
if p.upListener != nil {
p.upListener.Close()
}
if p.udpListener != nil {
p.udpListener.Close()
}
if p.RTPReader != nil {
p.RTPReader.Close()
}
if p.udpConn != nil {
p.udpConn.Close()
}
if p.tcpConn != nil {
p.tcpConn.Close()
}
// 确保所有goroutine都有机会处理停止信号后再关闭FeedChan
close(p.FeedChan)
p.Info("forwarder disposed", "forwarded_packets", p.ForwardCount)
}

View File

@@ -0,0 +1,30 @@
package gb28181
// GbCode 国标编码对象
type GbCode struct {
CenterCode string `json:"centerCode"` // 中心编码,由监控中心所在地的行政区划代码确定,符合GB/T2260—2007的要求
IndustryCode string `json:"industryCode"` // 行业编码
TypeCode string `json:"typeCode"` // 类型编码
NetCode string `json:"netCode"` // 网络标识
SN string `json:"sn"` // 序号
}
// DecodeGBCode 解析国标编号
func DecodeGBCode(code string) *GbCode {
if code == "" || len(code) != 20 {
return nil
}
return &GbCode{
CenterCode: code[0:8],
IndustryCode: code[8:10],
TypeCode: code[10:13],
NetCode: code[13:14],
SN: code[14:],
}
}
// Encode 编码为完整的国标编号
func (g *GbCode) Encode() string {
return g.CenterCode + g.IndustryCode + g.TypeCode + g.NetCode + g.SN
}

View File

@@ -0,0 +1,61 @@
package gb28181
import (
"strconv"
"time"
)
// Group 表示业务分组
type Group struct {
ID int `gorm:"primaryKey;autoIncrement" json:"id"` // 数据库自增ID
DeviceID string `gorm:"column:device_id" json:"deviceId"` // 区域国标编号
Name string `gorm:"column:name" json:"name"` // 区域名称
ParentID int `gorm:"column:parent_id" json:"parentId"` // 父分组ID
ParentDeviceID string `gorm:"column:parent_device_id" json:"parentDeviceId"` // 父区域国标ID
BusinessGroup string `gorm:"column:business_group" json:"businessGroup"` // 所属的业务分组国标编号
CreateTime string `gorm:"column:create_time" json:"createTime"` // 创建时间
UpdateTime string `gorm:"column:update_time" json:"updateTime"` // 更新时间
CivilCode string `gorm:"column:civil_code" json:"civilCode"` // 行政区划
}
// TableName 指定数据库表名
func (g *Group) TableName() string {
return "group_gb28181pro"
}
// NewGroupFromChannel 从 DeviceChannel 创建 Group 实例
func NewGroupFromChannel(channel *DeviceChannel) *Group {
gbCode := DecodeGBCode(channel.DeviceID)
if gbCode == nil || (gbCode.TypeCode != "215" && gbCode.TypeCode != "216") {
return nil
}
now := time.Now().Format("2006-01-02 15:04:05")
group := &Group{
Name: channel.Name,
DeviceID: channel.DeviceID,
CreateTime: now,
UpdateTime: now,
}
switch gbCode.TypeCode {
case "215":
group.BusinessGroup = channel.DeviceID
case "216":
group.BusinessGroup = channel.BusinessGroupID // 注意:需要在 DeviceChannel 中添加 BusinessGroupID 字段
group.ParentDeviceID = channel.ParentID
}
if group.BusinessGroup == "" {
return nil
}
return group
}
// CompareTo 实现比较功能
func (g *Group) CompareTo(other *Group) int {
thisID, _ := strconv.Atoi(g.DeviceID)
otherID, _ := strconv.Atoi(other.DeviceID)
return thisID - otherID
}

View File

@@ -0,0 +1,107 @@
package gb28181
// GroupsChannelModel 表示分组与通道的关联关系
type GroupsChannelModel struct {
ID int `gorm:"primaryKey;autoIncrement" json:"id"` // ID表示数据库中的唯一标识符
GroupID int `gorm:"column:group_id;index" json:"groupId"` // GroupID表示关联的分组ID
ChannelID string `gorm:"column:channel_id;index" json:"channelId"` // ChannelID表示关联的通道ID
DeviceID string `gorm:"column:device_id;index" json:"deviceId"` // DeviceID表示关联的设备ID
}
// TableName 指定数据库表名
func (g *GroupsChannelModel) TableName() string {
return "groups_channel"
}
// NewGroupsChannel 创建并返回一个新的GroupsChannelModel实例
func NewGroupsChannel(groupID int, channelID string, deviceID string) *GroupsChannelModel {
return &GroupsChannelModel{
GroupID: groupID,
ChannelID: channelID,
DeviceID: deviceID,
}
}
// FindGroupChannels 通过分组ID查找关联的通道
func FindGroupChannels(db interface{}, groupID int) ([]*GroupsChannelModel, error) {
// db 参数应为 *gorm.DB 类型
type DBAPI interface {
Find(dest interface{}, conds ...interface{}) error
}
if gdb, ok := db.(DBAPI); ok {
var channels []*GroupsChannelModel
err := gdb.Find(&channels, "group_id = ?", groupID)
return channels, err
}
return nil, nil
}
// FindChannelGroups 通过通道ID查找关联的分组
func FindChannelGroups(db interface{}, channelID string) ([]*GroupsChannelModel, error) {
// db 参数应为 *gorm.DB 类型
type DBAPI interface {
Find(dest interface{}, conds ...interface{}) error
}
if gdb, ok := db.(DBAPI); ok {
var groups []*GroupsChannelModel
err := gdb.Find(&groups, "channel_id = ?", channelID)
return groups, err
}
return nil, nil
}
// FindDeviceGroups 通过设备ID查找关联的分组
func FindDeviceGroups(db interface{}, deviceID string) ([]*GroupsChannelModel, error) {
// db 参数应为 *gorm.DB 类型
type DBAPI interface {
Find(dest interface{}, conds ...interface{}) error
}
if gdb, ok := db.(DBAPI); ok {
var groups []*GroupsChannelModel
err := gdb.Find(&groups, "device_id = ?", deviceID)
return groups, err
}
return nil, nil
}
// FindGroupChannelsByDevice 通过分组ID和设备ID查找关联的通道
func FindGroupChannelsByDevice(db interface{}, groupID int, deviceID string) ([]*GroupsChannelModel, error) {
// db 参数应为 *gorm.DB 类型
type DBAPI interface {
Find(dest interface{}, conds ...interface{}) error
}
if gdb, ok := db.(DBAPI); ok {
var channels []*GroupsChannelModel
err := gdb.Find(&channels, "group_id = ? AND device_id = ?", groupID, deviceID)
return channels, err
}
return nil, nil
}
// AutoMigrate 执行自动迁移
// 此函数应在插件初始化时调用
func AutoMigrateGroupChannel(db interface{}) error {
// db 参数应为 *gorm.DB 类型
// 使用类型断言获取 DB 实例
type DBAPI interface {
AutoMigrate(dst ...interface{}) error
}
if gdb, ok := db.(DBAPI); ok {
// 执行表结构自动迁移
if err := gdb.AutoMigrate(&GroupsChannelModel{}); err != nil {
return err
}
return nil
}
return nil
}

View File

@@ -0,0 +1,111 @@
package gb28181
import (
"time"
)
// GroupsModel 表示分组结构
type GroupsModel struct {
ID int `gorm:"primaryKey;autoIncrement" json:"id"` // ID表示数据库中的唯一标识符
CreateTime time.Time `gorm:"column:create_time" json:"createTime"` // CreateTime表示记录创建时间
UpdateTime time.Time `gorm:"column:update_time" json:"updateTime"` // UpdateTime表示记录更新时间
Name string `gorm:"column:name" json:"name"` // Name表示分组名称
PID int `gorm:"column:pid;default:0" json:"pid"` // PID表示父分组ID
Level int `gorm:"column:level;default:0" json:"level"` // Level表示分组层级
}
// TableName 指定数据库表名
func (g *GroupsModel) TableName() string {
return "groups"
}
// NewGroup 创建并返回一个新的GroupsModel实例
func NewGroup(name string, pid int, level int) *GroupsModel {
now := time.Now()
return &GroupsModel{
Name: name,
PID: pid,
Level: level,
CreateTime: now,
UpdateTime: now,
}
}
// NewRootGroup 创建根分组实例
func NewRootGroup() *GroupsModel {
return NewGroup("根", 0, 0)
}
// InitRootGroup 初始化根分组记录
// 如果数据库中不存在根分组,则创建一个
func InitRootGroup(db interface{}) error {
// db 参数应为 *gorm.DB 类型
// 使用类型断言获取 DB 实例
// 这里使用 interface{} 是为了避免直接依赖 GORM
type DBAPI interface {
First(dest interface{}, conds ...interface{}) error
Create(value interface{}) error
}
if gdb, ok := db.(DBAPI); ok {
root := &GroupsModel{}
// 检查是否存在根分组
err := gdb.First(root, "pid = ? AND level = ?", 0, 0)
if err != nil {
// 如果不存在,则创建一个根分组
rootGroup := NewRootGroup()
return gdb.Create(rootGroup)
}
return nil
}
return nil
}
// AutoMigrateAll 执行分组及分组-通道关联的自动迁移,并初始化根组织
// 此函数应在插件初始化时调用,一次完成所有相关表的迁移
func AutoMigrateAll(db interface{}) error {
// db 参数应为 *gorm.DB 类型
// 使用类型断言获取 DB 实例
type DBAPI interface {
AutoMigrate(dst ...interface{}) error
First(dest interface{}, conds ...interface{}) error
Create(value interface{}) error
}
if gdb, ok := db.(DBAPI); ok {
// 执行表结构自动迁移 - 分组表和分组-通道关联表
if err := gdb.AutoMigrate(&GroupsModel{}, &GroupsChannelModel{}); err != nil {
return err
}
// 检查是否存在根分组
root := &GroupsModel{}
err := gdb.First(root, "pid = ? AND level = ?", 0, 0)
if err != nil {
// 如果不存在,则创建一个根分组
rootGroup := NewRootGroup()
return gdb.Create(rootGroup)
}
return nil
}
return nil
}
// BeforeCreate GORM钩子在创建记录前设置创建时间和更新时间
func (g *GroupsModel) BeforeCreate() error {
now := time.Now()
g.CreateTime = now
g.UpdateTime = now
return nil
}
// BeforeUpdate GORM钩子在更新记录前设置更新时间
func (g *GroupsModel) BeforeUpdate() error {
g.UpdateTime = time.Now()
return nil
}

View File

@@ -8,8 +8,8 @@ import (
)
type InviteOptions struct {
Start int
End int
Start string
End string
dump string
ssrc string
SSRC uint32
@@ -19,7 +19,7 @@ type InviteOptions struct {
}
func (o InviteOptions) IsLive() bool {
return o.Start == 0 || o.End == 0
return o.Start == "" && o.End == ""
}
func (o InviteOptions) Record() bool {
@@ -27,21 +27,25 @@ func (o InviteOptions) Record() bool {
}
func (o *InviteOptions) Validate(start, end string) error {
var sint int64
var eint int64
if start != "" {
sint, err1 := strconv.ParseInt(start, 10, 0)
sinttmp, err1 := strconv.ParseInt(start, 10, 0)
if err1 != nil {
return err1
}
o.Start = int(sint)
sint = sinttmp
o.Start = start
}
if end != "" {
eint, err2 := strconv.ParseInt(end, 10, 0)
einttmp, err2 := strconv.ParseInt(end, 10, 0)
if err2 != nil {
return err2
}
o.End = int(eint)
eint = einttmp
o.End = end
}
if o.Start >= o.End {
if sint >= eint {
return errors.New("start < end")
}
return nil

View File

@@ -0,0 +1,36 @@
package gb28181
// InviteInfo 从INVITE消息中解析需要的信息
type InviteInfo struct {
// 请求者ID
RequesterId string `json:"requesterId"`
// 目标通道ID
TargetChannelId string `json:"targetChannelId"`
// 源通道ID
SourceChannelId string `json:"sourceChannelId"`
// 会话名称
SessionName string `json:"sessionName"`
// SSRC
SSRC string `json:"ssrc"`
// 是否使用TCP
TCP bool `json:"tcp"`
// TCP是否为主动模式
TCPActive bool `json:"tcpActive"`
// 呼叫ID
CallId string `json:"callId"`
// 开始时间
StartTime int64 `json:"startTime"`
// 结束时间
StopTime int64 `json:"stopTime"`
// 下载速度
DownloadSpeed string `json:"downloadSpeed"`
// IP地址
IP string `json:"ip"`
// 端口
Port int `json:"port"`
}
// NewInviteInfo 创建一个新的 InviteInfo 实例
func NewInviteInfo() *InviteInfo {
return &InviteInfo{}
}

View File

@@ -4,25 +4,28 @@ import (
"bytes"
"encoding/xml"
"fmt"
"golang.org/x/net/html/charset"
"golang.org/x/text/encoding/simplifiedchinese"
"golang.org/x/text/transform"
"io"
"strconv"
"strings"
"time"
"golang.org/x/net/html/charset"
"golang.org/x/text/encoding/simplifiedchinese"
"golang.org/x/text/transform"
)
const (
// CatalogXML 获取设备列表xml样式
CatalogXML = `<?xml version="1.0"?><Query>
CatalogXML = `<?xml version="1.0" encoding="%s"?>
<Query>
<CmdType>Catalog</CmdType>
<SN>%d</SN>
<DeviceID>%s</DeviceID>
</Query>
`
// RecordInfoXML 获取录像文件列表xml样式
RecordInfoXML = `<?xml version="1.0"?><Query>
RecordInfoXML = `<?xml version="1.0"?>
<Query>
<CmdType>RecordInfo</CmdType>
<SN>%d</SN>
<DeviceID>%s</DeviceID>
@@ -33,19 +36,37 @@ const (
</Query>
`
// DeviceInfoXML 查询设备详情xml样式
DeviceInfoXML = `<?xml version="1.0"?><Query>
DeviceInfoXML = `<?xml version="1.0" encoding="%s"?>
<Query>
<CmdType>DeviceInfo</CmdType>
<SN>%d</SN>
<DeviceID>%s</DeviceID>
</Query>
`
// DeviceStatusXML 查询设备详情xml样式
DeviceStatusXML = `<?xml version="1.0" encoding="%s"?>
<Query>
<CmdType>DeviceStatus</CmdType>
<SN>%d</SN>
<DeviceID>%s</DeviceID>
</Query>
`
// DevicePositionXML 订阅设备位置
DevicePositionXML = `<?xml version="1.0"?><Query>
DevicePositionXML = `<?xml version="1.0"?>
<Query>
<CmdType>MobilePosition</CmdType>
<SN>%d</SN>
<DeviceID>%s</DeviceID>
<Interval>%d</Interval>
</Query>
`
// PresetQueryXML 查询预置位指令
PresetQueryXML = `<?xml version="1.0"?>
<Query>
<CmdType>PresetQuery</CmdType>
<SN>%d</SN>
<DeviceID>%s</DeviceID>
</Query>
`
AlarmResponseXML = `<?xml version="1.0"?><Response>
<CmdType>Alarm</CmdType>
@@ -61,8 +82,6 @@ const (
<Status>OK</Status>
</Notify>
`
ChannelOnStatus ChannelStatus = "ON"
ChannelOffStatus ChannelStatus = "OFF"
)
func intTotime(t int64) time.Time {
@@ -83,13 +102,18 @@ func toGB2312(s string) []byte {
}
// BuildDeviceInfoXML 获取设备详情指令
func BuildDeviceInfoXML(sn int, id string) []byte {
return toGB2312(fmt.Sprintf(DeviceInfoXML, sn, id))
func BuildDeviceInfoXML(sn int, id string, charset string) []byte {
return toGB2312(fmt.Sprintf(DeviceInfoXML, charset, sn, id))
}
// BuildDeviceStatusXML 获取设备详情指令
func BuildDeviceStatusXML(sn int, id string, charset string) []byte {
return toGB2312(fmt.Sprintf(DeviceStatusXML, charset, sn, id))
}
// BuildCatalogXML 获取NVR下设备列表指令
func BuildCatalogXML(sn int, id string) []byte {
return toGB2312(fmt.Sprintf(CatalogXML, sn, id))
func BuildCatalogXML(charset string, sn int, id string) []byte {
return toGB2312(fmt.Sprintf(CatalogXML, charset, sn, id))
}
// BuildRecordInfoXML 获取录像文件列表指令
@@ -102,6 +126,11 @@ func BuildDevicePositionXML(sn int, id string, interval int) []byte {
return toGB2312(fmt.Sprintf(DevicePositionXML, sn, id, interval))
}
// BuildPresetQueryXML 构建预置位查询XML
func BuildPresetQueryXML(sn int, id string) []byte {
return toGB2312(fmt.Sprintf(PresetQueryXML, sn, id))
}
func BuildAlarmResponseXML(id string) []byte {
return toGB2312(fmt.Sprintf(AlarmResponseXML, id))
}
@@ -111,45 +140,42 @@ func BuildKeepAliveXML(sn int, id string) []byte {
}
type (
ChannelStatus string
Record struct {
DeviceID string
Name string
FilePath string
Address string
StartTime string
EndTime string
Secrecy int
Type string
}
ChannelInfo struct {
DeviceID string // 通道ID
ParentID string
Name string
Manufacturer string
Model string
Owner string
CivilCode string
Address string
Port int
Parental int
SafetyWay int
RegisterWay int
Secrecy int
Status ChannelStatus
}
Message struct {
XMLName xml.Name
CmdType string
SN int // 请求序列号,一般用于对应 request 和 response
DeviceID string
DeviceName string
Manufacturer string
Model string
Channel string
DeviceList []ChannelInfo `xml:"DeviceList>Item"`
RecordList []Record `xml:"RecordList>Item"`
SumNum int // 录像结果的总数 SumNum录像结果会按照多条消息返回可用于判断是否全部返回
XMLName xml.Name
CmdType string
SN int // 请求序列号,一般用于对应 request 和 response
DeviceID string
Longitude string // 经度
Latitude string // 纬度
DeviceName string
Manufacturer string
Model string
Channel string
Firmware string
DeviceChannelList []DeviceChannel `xml:"DeviceList>Item"`
RecordList struct {
Num int `xml:"Num,attr"`
Item []RecordItem `xml:"Item"`
} `xml:"RecordList"`
PresetList struct {
Num int `xml:"Num,attr"`
Item []PresetItem `xml:"Item"`
} `xml:"PresetList"`
SumNum int // 录像结果的总数 SumNum录像结果会按照多条消息返回可用于判断是否全部返回
Name string // 设备/通道名称
LastTime time.Time `xml:"LastTime"` // 最后时间
// 报警相关字段
AlarmPriority string `xml:"AlarmPriority"` // 报警级别
AlarmMethod string `xml:"AlarmMethod"` // 报警方式
AlarmTime string `xml:"AlarmTime"` // 报警时间
Info struct {
AlarmType string `xml:"AlarmType"` // 报警类型
} `xml:"Info"`
}
PresetItem struct {
PresetID string `xml:"PresetID"`
PresetName string `xml:"PresetName"`
}
)

View File

@@ -0,0 +1,68 @@
package gb28181
// PlatformChannel 表示平台通道信息
type PlatformChannel struct {
//CommonGBChannel `gorm:"-"` // 通过组合继承 CommonGBChannel 的字段
PlatformServerGBID string `gorm:"primaryKey;"` // 平台ID
ChannelDBID string `gorm:"primaryKey;"` // 设备通道ID
CustomDeviceId string `gorm:"default:null"` // 国标-编码
CustomName string `gorm:"default:null"` // 国标-名称
CustomManufacturer string `gorm:"default:null"` // 国标-设备厂商
CustomModel string `gorm:"default:null"` // 国标-设备型号
CustomOwner string `gorm:"default:null"` // 国标-设备归属
CustomCivilCode string // 国标-行政区域
CustomBlock string // 国标-警区
CustomAddress string `gorm:"default:null"` // 国标-安装地址
CustomParental int // 国标-是否有子设备
CustomParentId string // 国标-父节点ID
CustomSafetyWay int // 国标-信令安全模式
CustomRegisterWay int // 国标-注册方式
CustomCertNum int // 国标-证书序列号
CustomCertifiable int // 国标-证书有效标识
CustomErrCode int // 国标-无效原因码
CustomEndTime int // 国标-证书终止有效期
CustomSecurityLevelCode string // 国标-摄像机安全能力等级代码
CustomSecrecy int // 国标-保密属性
CustomIpAddress string // 国标-设备/系统IPv4/IPv6地址
CustomPort int // 国标-设备/系统端口
CustomPassword string // 国标-设备口令
CustomStatus string // 国标-设备状态
CustomLongitude float64 // 国标-经度
CustomLatitude float64 // 国标-纬度
CustomBusinessGroupId string // 国标-虚拟组织所属的业务分组ID
CustomPtzType int // 国标-摄像机结构类型
CustomPositionType int // 国标-摄像机位置类型扩展
CustomPhotoelectricImagingType string // 国标-摄像机光电成像类型
CustomCapturePositionType string // 国标-摄像机采集部位类型
CustomRoomType int // 国标-摄像机安装位置室外、室内属性
CustomUseType int // 国标-用途属性
CustomSupplyLightType int // 国标-摄像机补光属性
CustomDirectionType int // 国标-摄像机监视方位属性
CustomResolution string // 国标-摄像机支持的分辨率
CustomStreamNumberList string // 国标-摄像机支持的码流编号列表
CustomDownloadSpeed string // 国标-下载倍速
CustomSvcSpaceSupportMod int // 国标-空域编码能力
CustomSvcTimeSupportMode int // 国标-时域编码能力
CustomSsvcRatioSupportList string // 国标-SSVC增强层与基本层比例能力
CustomMobileDeviceType int // 国标-移动采集设备类型
CustomHorizontalFieldAngle float64 // 国标-摄像机水平视场角
CustomVerticalFieldAngle float64 // 国标-摄像机竖直视场角
CustomMaxViewDistance float64 // 国标-摄像机可视距离
CustomGrassrootsCode string // 国标-基层组织编码
CustomPoType int // 国标-监控点位类型
CustomPoCommonName string // 国标-点位俗称
CustomMac string // 国标-设备MAC地址
CustomFunctionType string // 国标-摄像机卡口功能类型
CustomEncodeType string // 国标-摄像机视频编码格式
CustomInstallTime string // 国标-摄像机安装使用时间
CustomManagementUnit string // 国标-摄像机所属管理单位名称
CustomContactInfo string // 国标-摄像机所属管理单位联系方式
CustomRecordSaveDays int // 国标-录像保存天数
CustomIndustrialClassification string // 国标-国民经济行业分类代码
}
// TableName 返回数据库表名
func (*PlatformChannel) TableName() string {
return "gb28181_platform_channel"
}

View File

@@ -0,0 +1,69 @@
// Package gb28181 实现了GB28181协议相关的功能
package gb28181
import (
"time"
)
// PlatformModel 表示GB28181平台的配置信息。
// 包含了平台的基本信息、SIP服务配置、设备信息、认证信息等。
// 用于存储和管理GB28181平台的所有相关参数。
type PlatformModel struct {
Enable bool `gorm:"column:enable" json:"enable"` // Enable表示该平台配置是否启用
Name string `gorm:"column:name;omitempty" json:"name"` // Name表示平台的名称
ServerGBID string `gorm:"primaryKey;column:server_gb_id;omitempty" json:"serverGBId"` // ServerGBID表示SIP服务器的国标编码
ServerGBDomain string `gorm:"column:server_gb_domain;omitempty" json:"serverGBDomain"` // ServerGBDomain表示SIP服务器的国标域
ServerIP string `gorm:"column:server_ip;omitempty" json:"serverIp"` // ServerIP表示SIP服务器的IP地址
ServerPort int `gorm:"column:server_port;omitempty" json:"serverPort"` // ServerPort表示SIP服务器的端口号
DeviceGBID string `gorm:"column:device_gb_id;omitempty" json:"deviceGBId"` // DeviceGBID表示设备的国标编号
DeviceIP string `gorm:"column:device_ip;omitempty" json:"deviceIp"` // DeviceIP表示设备的IP地址
DevicePort int `gorm:"column:device_port;omitempty" json:"devicePort"` // DevicePort表示设备的端口号
Username string `gorm:"column:username;omitempty" json:"username"` // Username表示SIP认证的用户名默认使用设备国标编号
Password string `gorm:"column:password;omitempty" json:"password"` // Password表示SIP认证的密码
Expires int `gorm:"column:expires;omitempty" json:"expires"` // Expires表示注册的过期时间单位为秒
KeepTimeout int `gorm:"column:keep_timeout;omitempty" json:"keepTimeout"` // KeepTimeout表示心跳超时时间单位为秒
Transport string `gorm:"column:transport;omitempty" json:"transport"` // Transport表示传输协议类型
CharacterSet string `gorm:"column:character_set;omitempty" json:"characterSet"` // CharacterSet表示字符集编码
PTZ bool `gorm:"column:ptz" json:"ptz"` // PTZ表示是否允许云台控制
RTCP bool `gorm:"column:rtcp" json:"rtcp"` // RTCP表示是否启用RTCP流保活
Status bool `gorm:"column:status" json:"status"` // Status表示平台当前的在线状态
ChannelCount int `gorm:"column:channel_count;omitempty" json:"channelCount"` // ChannelCount表示通道数量
CatalogSubscribe bool `gorm:"column:catalog_subscribe" json:"catalogSubscribe"` // CatalogSubscribe表示是否已订阅目录信息
AlarmSubscribe bool `gorm:"column:alarm_subscribe" json:"alarmSubscribe"` // AlarmSubscribe表示是否已订阅报警信息
MobilePositionSubscribe bool `gorm:"column:mobile_position_subscribe" json:"mobilePositionSubscribe"` // MobilePositionSubscribe表示是否已订阅移动位置信息
CatalogGroup int `gorm:"column:catalog_group;omitempty" json:"catalogGroup"` // CatalogGroup表示目录分组大小每次向上级发送通道数量
UpdateTime string `gorm:"column:update_time;omitempty" json:"updateTime"` // UpdateTime表示最后更新时间
CreateTime string `gorm:"column:create_time;omitempty" json:"createTime"` // CreateTime表示创建时间
AsMessageChannel bool `gorm:"column:as_message_channel" json:"asMessageChannel"` // AsMessageChannel表示是否作为消息通道使用
SendStreamIP string `gorm:"column:send_stream_ip;omitempty" json:"sendStreamIp"` // SendStreamIP表示点播回复200OK时使用的IP地址
AutoPushChannel bool `gorm:"column:auto_push_channel" json:"autoPushChannel"` // AutoPushChannel表示是否自动推送通道变化
CatalogWithPlatform int `gorm:"column:catalog_with_platform;omitempty" json:"catalogWithPlatform"` // CatalogWithPlatform表示目录信息是否包含平台信息(0:关闭,1:打开)
CatalogWithGroup int `gorm:"column:catalog_with_group;omitempty" json:"catalogWithGroup"` // CatalogWithGroup表示目录信息是否包含分组信息(0:关闭,1:打开)
CatalogWithRegion int `gorm:"column:catalog_with_region;omitempty" json:"catalogWithRegion"` // CatalogWithRegion表示目录信息是否包含行政区划(0:关闭,1:打开)
CivilCode string `gorm:"column:civil_code;omitempty" json:"civilCode"` // CivilCode表示行政区划代码
Manufacturer string `gorm:"column:manufacturer;omitempty" json:"manufacturer"` // Manufacturer表示平台厂商
Model string `gorm:"column:model;omitempty" json:"model"` // Model表示平台型号
Address string `gorm:"column:address;omitempty" json:"address"` // Address表示平台安装地址
RegisterWay int `gorm:"column:register_way;omitempty" json:"registerWay"` // RegisterWay表示注册方式(1:标准认证注册,2:口令认证,3:数字证书双向认证,4:数字证书单向认证)
Secrecy int `gorm:"column:secrecy;omitempty" json:"secrecy"` // Secrecy表示保密属性(0:不涉密,1:涉密)
}
// TableName 指定数据库表名
func (p *PlatformModel) TableName() string {
return "gb28181_platform"
}
// NewPlatform 创建并返回一个新的Platform实例。
// 该函数会初始化Platform结构体并设置一些默认值
// - RegisterWay默认设置为1标准认证注册模式
// - Secrecy默认设置为0不涉密
// 返回值为指向新创建的Platform实例的指针。
func NewPlatform() *PlatformModel {
now := time.Now().Format("2006-01-02 15:04:05")
return &PlatformModel{
RegisterWay: 1, // 默认使用标准认证注册模式
Secrecy: 0, // 默认为不涉密
CreateTime: now,
UpdateTime: now,
}
}

View File

@@ -0,0 +1,117 @@
package gb28181
import "time"
// RecordInfo 设备录像信息
type RecordInfo struct {
// 设备编号
DeviceID string `json:"deviceId"`
// 通道编号
ChannelID string `json:"channelId"`
// 命令序列号
SN string `json:"sn"`
// 设备名称
Name string `json:"name"`
// 列表总数
SumNum int `json:"sumNum"`
// 计数
Count int `json:"count"`
// 最后时间
LastTime time.Time `json:"lastTime"`
// 录像列表
RecordList []RecordItem `json:"recordList"`
}
// NewRecordInfo 创建新的 RecordInfo 实例
func NewRecordInfo() *RecordInfo {
return &RecordInfo{
RecordList: make([]RecordItem, 0),
}
}
// GetDeviceID 获取设备编号
func (r *RecordInfo) GetDeviceID() string {
return r.DeviceID
}
// SetDeviceID 设置设备编号
func (r *RecordInfo) SetDeviceID(deviceID string) {
r.DeviceID = deviceID
}
// GetName 获取设备名称
func (r *RecordInfo) GetName() string {
return r.Name
}
// SetName 设置设备名称
func (r *RecordInfo) SetName(name string) {
r.Name = name
}
// GetSumNum 获取列表总数
func (r *RecordInfo) GetSumNum() int {
return r.SumNum
}
// SetSumNum 设置列表总数
func (r *RecordInfo) SetSumNum(sumNum int) {
r.SumNum = sumNum
}
// GetRecordList 获取录像列表
func (r *RecordInfo) GetRecordList() []RecordItem {
return r.RecordList
}
// SetRecordList 设置录像列表
func (r *RecordInfo) SetRecordList(recordList []RecordItem) {
r.RecordList = recordList
}
// GetChannelID 获取通道编号
func (r *RecordInfo) GetChannelID() string {
return r.ChannelID
}
// SetChannelID 设置通道编号
func (r *RecordInfo) SetChannelID(channelID string) {
r.ChannelID = channelID
}
// GetSN 获取命令序列号
func (r *RecordInfo) GetSN() string {
return r.SN
}
// SetSN 设置命令序列号
func (r *RecordInfo) SetSN(sn string) {
r.SN = sn
}
// GetLastTime 获取最后时间
func (r *RecordInfo) GetLastTime() time.Time {
return r.LastTime
}
// SetLastTime 设置最后时间
func (r *RecordInfo) SetLastTime(lastTime time.Time) {
r.LastTime = lastTime
}
// GetCount 获取计数
func (r *RecordInfo) GetCount() int {
return r.Count
}
// SetCount 设置计数
func (r *RecordInfo) SetCount(count int) {
r.Count = count
}

View File

@@ -0,0 +1,77 @@
package gb28181
import (
"strings"
"time"
)
// RecordItem 设备录像信息
type RecordItem struct {
// 设备编号
DeviceID string `xml:"DeviceID" json:"deviceId"`
// 名称
Name string `xml:"Name" json:"name"`
// 文件路径名 (可选)
FilePath string `xml:"FilePath" json:"filePath"`
// 录像文件大小,单位:Byte(可选)
FileSize string `xml:"FileSize" json:"fileSize"`
// 录像地址(可选)
Address string `xml:"Address" json:"address"`
// 录像开始时间(可选)
StartTime string `xml:"StartTime" json:"startTime"`
// 录像结束时间(可选)
EndTime string `xml:"EndTime" json:"endTime"`
// 保密属性(必选)缺省为0;0:不涉密,1:涉密
Secrecy int `xml:"Secrecy" json:"secrecy"`
// 录像产生类型(可选)time或alarm或manual
Type string `xml:"Type" json:"type"`
// 录像触发者ID(可选)
RecorderID string `xml:"RecorderId" json:"recorderId"`
}
// CompareTo 比较两个录像记录的开始时间
// 返回值:
// -1: r < other
//
// 0: r = other
// 1: r > other
func (r *RecordItem) CompareTo(other *RecordItem) int {
startTimeNow, err := time.Parse("2006-01-02T15:04:05", r.StartTime)
if err != nil {
return 0
}
startTimeParam, err := time.Parse("2006-01-02T15:04:05", other.StartTime)
if err != nil {
return 0
}
if startTimeNow.Equal(startTimeParam) {
return 0
} else if startTimeParam.After(startTimeNow) {
return -1
} else {
return 1
}
}
// Less 用于排序
func (r *RecordItem) Less(other *RecordItem) bool {
return r.CompareTo(other) < 0
}
// Equal 判断两个录像记录是否相等
func (r *RecordItem) Equal(other *RecordItem) bool {
if other == nil {
return false
}
return strings.EqualFold(r.StartTime, other.StartTime)
}

View File

@@ -0,0 +1,110 @@
package gb28181
import (
"strconv"
"time"
)
// Region 区域信息
type Region struct {
ID int `gorm:"primaryKey;autoIncrement" json:"id"` // 数据库自增ID
DeviceID string `gorm:"column:device_id" json:"deviceId"` // 区域国标编号
Name string `gorm:"column:name" json:"name"` // 区域名称
ParentID int `gorm:"column:parent_id" json:"parentId"` // 父区域ID
ParentDeviceID string `gorm:"column:parent_device_id" json:"parentDeviceId"` // 父区域国标ID
BusinessGroup string `gorm:"column:business_group" json:"businessGroup"` // 所属的业务分组国标编号
CreateTime string `gorm:"column:create_time" json:"createTime"` // 创建时间
UpdateTime string `gorm:"column:update_time" json:"updateTime"` // 更新时间
CivilCode string `gorm:"column:civil_code" json:"civilCode"` // 行政区划
}
// TableName 指定数据库表名
func (r *Region) TableName() string {
return "region_gb28181pro"
}
// NewRegion 创建新的区域实例
func NewRegion(deviceID, name, parentDeviceID string) *Region {
now := time.Now().Format("2006-01-02 15:04:05")
return &Region{
DeviceID: deviceID,
Name: name,
ParentDeviceID: parentDeviceID,
CreateTime: now,
UpdateTime: now,
}
}
// NewRegionFromCivilCode 从行政区划编码创建区域实例
func NewRegionFromCivilCode(civilCode *CivilCode) *Region {
if civilCode == nil {
return nil
}
now := time.Now().Format("2006-01-02 15:04:05")
region := &Region{
Name: civilCode.Name,
DeviceID: civilCode.Code,
CreateTime: now,
UpdateTime: now,
}
// 如果编码长度大于2设置父级编码
if len(civilCode.Code) > 2 {
region.ParentDeviceID = civilCode.ParentCode
}
return region
}
// NewRegionFromChannel 从设备通道创建区域实例
func NewRegionFromChannel(channel *DeviceChannel) *Region {
if channel == nil {
return nil
}
now := time.Now().Format("2006-01-02 15:04:05")
region := &Region{
Name: channel.Name,
DeviceID: channel.DeviceID,
CreateTime: now,
UpdateTime: now,
}
// 获取父级编码
parentCode := GetInstance().GetParentCode(channel.DeviceID)
if parentCode != nil {
region.ParentDeviceID = parentCode.Code
}
return region
}
// CompareTo 实现比较功能
func (r *Region) CompareTo(other *Region) int {
thisID, _ := strconv.Atoi(r.DeviceID)
otherID, _ := strconv.Atoi(other.DeviceID)
return thisID - otherID
}
// Equals 判断两个区域是否相等
func (r *Region) Equals(other interface{}) bool {
if other == nil {
return false
}
if r == other {
return true
}
otherRegion, ok := other.(*Region)
if !ok {
return false
}
return r.ID == otherRegion.ID
}
// GetParentCode 获取父级编码(这个函数需要在 civilcodeutil.go 中实现)
func GetParentCode(deviceID string) *CivilCode {
// TODO: 实现获取父级编码的逻辑
// 这部分需要参考 CivilCodeUtil.java 的实现
return nil
}

View File

@@ -0,0 +1,193 @@
package gb28181
import (
"fmt"
"strconv"
"strings"
"github.com/emiago/sipgo/sip"
)
// DecodeSDP 从 SIP 请求中解析 SDP 信息
func DecodeSDP(req *sip.Request) (*InviteInfo, error) {
inviteInfo := NewInviteInfo()
// 获取请求者ID
from := req.From()
if from == nil || from.Address.User == "" {
return nil, fmt.Errorf("无法从请求中获取来源id")
}
inviteInfo.RequesterId = from.Address.User
// 获取目标通道ID
channelIDArray := getChannelIDFromRequest(req)
// 获取CallID
callID := req.CallID()
if callID != nil {
inviteInfo.CallId = callID.Value()
}
// 解析SDP消息
sdpStr := string(req.Body())
if sdpStr == "" {
return nil, fmt.Errorf("SDP内容为空")
}
// 解析SDP各个字段
lines := strings.Split(sdpStr, "\r\n")
var channelIdFromSdp string
var port int = -1
var mediaTransmissionTCP bool
var tcpActive *bool
var supportedMediaFormat bool
var sessionName string
for _, line := range lines {
if line == "" {
continue
}
switch {
case strings.HasPrefix(line, "s="):
sessionName = strings.TrimPrefix(line, "s=")
inviteInfo.SessionName = sessionName
// 如果是回放从URI中获取通道ID
if strings.EqualFold(sessionName, "Playback") {
for _, l := range lines {
if strings.HasPrefix(l, "u=") {
uriField := strings.TrimPrefix(l, "u=")
parts := strings.Split(uriField, ":")
if len(parts) > 0 {
channelIdFromSdp = parts[0]
}
break
}
}
}
case strings.HasPrefix(line, "c="):
// c=IN IP4 192.168.1.100
parts := strings.Split(line, " ")
if len(parts) >= 3 {
inviteInfo.IP = parts[2]
}
case strings.HasPrefix(line, "t="):
// t=开始时间 结束时间
parts := strings.Split(strings.TrimPrefix(line, "t="), " ")
if len(parts) >= 2 {
startTime, err := strconv.ParseInt(parts[0], 10, 64)
if err == nil {
inviteInfo.StartTime = startTime
}
stopTime, err := strconv.ParseInt(parts[1], 10, 64)
if err == nil {
inviteInfo.StopTime = stopTime
}
}
case strings.HasPrefix(line, "m="):
mediaDesc := strings.Split(strings.TrimPrefix(line, "m="), " ")
if len(mediaDesc) >= 4 { // 必须有足够的元素:类型、端口、传输协议和格式
portVal, err := strconv.Atoi(mediaDesc[1])
if err == nil {
port = portVal
}
// 检查传输协议
if strings.EqualFold(mediaDesc[2], "TCP/RTP/AVP") {
mediaTransmissionTCP = true
}
// 检查是否包含支持的媒体格式96或8
for i := 3; i < len(mediaDesc); i++ {
if mediaDesc[i] == "96" || mediaDesc[i] == "8" {
supportedMediaFormat = true
break
}
}
}
case strings.HasPrefix(line, "a=setup:"):
val := strings.TrimPrefix(line, "a=setup:")
if strings.EqualFold(val, "active") {
activeVal := true
tcpActive = &activeVal
} else if strings.EqualFold(val, "passive") {
passiveVal := false
tcpActive = &passiveVal
}
case strings.HasPrefix(line, "y="):
inviteInfo.SSRC = strings.TrimPrefix(line, "y=")
case strings.HasPrefix(line, "a=downloadspeed:"):
inviteInfo.DownloadSpeed = strings.TrimPrefix(line, "a=downloadspeed:")
}
}
// 确定最终的通道ID优先使用SDP中的通道ID
var finalChannelId string
if channelIdFromSdp != "" {
finalChannelId = channelIdFromSdp
} else if len(channelIDArray) > 0 {
finalChannelId = channelIDArray[0]
}
// 验证通道ID和请求者ID
if inviteInfo.RequesterId == "" || finalChannelId == "" {
return nil, fmt.Errorf("无法从请求中获取通道id或来源id")
}
// 设置目标通道ID
inviteInfo.TargetChannelId = finalChannelId
// 设置源通道ID如果有
if len(channelIDArray) >= 2 {
inviteInfo.SourceChannelId = channelIDArray[1]
}
// 验证媒体格式支持
if port == -1 || !supportedMediaFormat {
return nil, fmt.Errorf("不支持的媒体格式")
}
// 设置传输相关信息
inviteInfo.TCP = mediaTransmissionTCP
if tcpActive != nil {
inviteInfo.TCPActive = *tcpActive
} else {
inviteInfo.TCPActive = false // 默认值
}
inviteInfo.Port = port
return inviteInfo, nil
}
// getChannelIDFromRequest 从请求中获取通道ID
func getChannelIDFromRequest(req *sip.Request) []string {
subjectHeaders := req.GetHeaders("Subject")
if len(subjectHeaders) == 0 {
// 如果缺失subject
return nil
}
// 获取第一个Subject头部的值
subjectStr := subjectHeaders[0].Value()
result := make([]string, 2)
if strings.Contains(subjectStr, ",") {
subjectSplit := strings.Split(subjectStr, ",")
result[0] = strings.Split(subjectSplit[0], ":")[0]
if len(subjectSplit) > 1 {
result[1] = strings.Split(subjectSplit[1], ":")[0]
}
} else {
result[0] = strings.Split(subjectStr, ":")[0]
}
return result
}

View File

@@ -2,8 +2,10 @@ package gb28181
import (
"errors"
"fmt"
"net"
"os"
"strings"
"github.com/pion/rtp"
"m7s.live/v5"
@@ -43,6 +45,7 @@ type Receiver struct {
RTPReader *rtp2.TCP
ListenAddr string
listener net.Listener
StreamMode string // 数据流传输模式UDP:udp传输/TCP-ACTIVEtcp主动模式/TCP-PASSIVEtcp被动模式
}
func NewPSPublisher(puber *m7s.Publisher) *PSPublisher {
@@ -141,39 +144,71 @@ func (dec *PSPublisher) decProgramStreamMap() (err error) {
func (p *Receiver) ReadRTP(rtp util.Buffer) (err error) {
lastSeq := p.SequenceNumber
if err = p.Unmarshal(rtp); err != nil {
p.Error("unmarshal error", "err", err)
return
}
if p.SequenceNumber != lastSeq+1 {
return ErrRTPReceiveLost
if lastSeq == 0 || p.SequenceNumber == lastSeq+1 {
if p.Enabled(p, task.TraceLevel) {
p.Trace("rtp", "len", rtp.Len(), "seq", p.SequenceNumber, "payloadType", p.PayloadType, "ssrc", p.SSRC)
}
copyData := make([]byte, len(p.Payload))
copy(copyData, p.Payload)
p.FeedChan <- copyData
return
}
if p.Enabled(p, task.TraceLevel) {
p.Trace("rtp", "len", rtp.Len(), "seq", p.SequenceNumber, "payloadType", p.PayloadType, "ssrc", p.SSRC)
}
copyData := make([]byte, len(p.Payload))
copy(copyData, p.Payload)
p.FeedChan <- copyData
return
return ErrRTPReceiveLost
}
func (p *Receiver) Start() (err error) {
p.listener, err = net.Listen("tcp", p.ListenAddr)
if strings.ToUpper(p.StreamMode) == "TCP-ACTIVE" {
// TCP主动模式不需要监听直接返回
p.Info("TCP-ACTIVE mode, no need to listen")
return nil
}
// TCP被动模式
p.listener, err = net.Listen("tcp4", p.ListenAddr)
if err != nil {
p.Error("start listen", "err", err)
return
return errors.New("start listen,err" + err.Error())
}
p.Info("start listen", "addr", p.ListenAddr)
return
}
func (p *Receiver) Dispose() {
p.listener.Close()
if p.listener != nil {
p.listener.Close()
}
if p.RTPReader != nil {
p.RTPReader.Close()
}
//close(p.FeedChan)
if p.FeedChan != nil {
close(p.FeedChan)
}
}
func (p *Receiver) Go() error {
if strings.ToUpper(p.StreamMode) == "TCP-ACTIVE" {
// TCP主动模式主动连接设备
addr := p.ListenAddr
if !strings.Contains(addr, ":") {
addr = ":" + addr
}
if strings.HasPrefix(addr, ":") {
p.Error("invalid address, missing IP", "addr", addr)
return fmt.Errorf("invalid address %s, missing IP", addr)
}
p.Info("TCP-ACTIVE mode, connecting to device", "addr", addr)
conn, err := net.Dial("tcp", addr)
if err != nil {
p.Error("connect to device failed", "err", err)
return err
}
p.RTPReader = (*rtp2.TCP)(conn.(*net.TCPConn))
p.Info("connected to device", "addr", conn.RemoteAddr())
return p.RTPReader.Read(p.ReadRTP)
}
// TCP被动模式
p.Info("start accept")
conn, err := p.listener.Accept()
if err != nil {

1377
plugin/gb28181/platform.go Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,45 @@
package plugin_gb28181pro
import (
"time"
"m7s.live/v5/pkg/task"
)
// PositionSubscribeTask 位置订阅任务
type PositionSubscribeTask struct {
task.TickTask
device *Device
}
// NewPositionSubscribeTask 创建新的位置订阅任务
func NewPositionSubscribeTask(device *Device) *PositionSubscribeTask {
return &PositionSubscribeTask{
device: device,
}
}
// GetTickInterval 获取定时间隔
func (p *PositionSubscribeTask) GetTickInterval() time.Duration {
// 如果设备配置了位置订阅周期则使用设备配置的周期否则使用默认值3600秒
if p.device.SubscribePosition > 0 {
return time.Second * time.Duration(p.device.SubscribePosition)
}
return time.Second * 3600
}
// Tick 定时执行的方法
func (p *PositionSubscribeTask) Tick(any) {
// 执行位置订阅使用设备配置的位置间隔如果未配置则使用默认值6
interval := 6
if p.device.PositionInterval > 0 {
interval = p.device.PositionInterval
}
response, err := p.device.subscribePosition(interval)
if err != nil {
p.Error("subPosition", "err", err)
} else {
p.Debug("subPosition", "response", response.String())
}
}

View File

@@ -0,0 +1,69 @@
package plugin_gb28181pro
import (
"context"
"fmt"
"time"
"github.com/emiago/sipgo/sip"
"m7s.live/v5/pkg/util"
)
// RecordInfoQuery 发送录像查询请求
// startTime 和 endTime 的格式为 "2006-01-02 15:04:05"
func (gb *GB28181Plugin) RecordInfoQuery(deviceID string, channelID string, startTime time.Time, endTime time.Time, sn int) (*util.Promise, error) {
device, ok := gb.devices.Get(deviceID)
if !ok {
return nil, fmt.Errorf("device not found: %s", deviceID)
}
channel, ok := device.channels.Get(channelID)
if !ok {
return nil, fmt.Errorf("channel not found: %s", channelID)
}
// 构建XML消息
charset := "GB2312"
if device.Charset != "" {
charset = device.Charset
}
msgBody := fmt.Sprintf(`<?xml version="1.0" encoding="%s"?>
<Query>
<CmdType>RecordInfo</CmdType>
<SN>%d</SN>
<DeviceID>%s</DeviceID>
<StartTime>%s</StartTime>
<EndTime>%s</EndTime>
<Secrecy>0</Secrecy>
<Type>all</Type>
</Query>`, charset, sn, channelID, startTime.Format("2006-01-02T15:04:05"), endTime.Format("2006-01-02T15:04:05"))
// 创建 MESSAGE 请求
request := device.CreateRequest(sip.MESSAGE, nil)
if request == nil {
return nil, fmt.Errorf("create request failed")
}
// 设置消息体
request.SetBody([]byte(msgBody))
// 创建Promise并保存到channel的RecordReqs中
promise := util.NewPromise(context.Background())
recordReq := &RecordRequest{
SN: sn,
Promise: promise,
}
// 先保存请求到RecordReqs确保能接收到响应
channel.RecordReqs.Set(recordReq)
// 发送请求
_, err := device.send(request)
if err != nil {
channel.RecordReqs.Remove(recordReq)
return nil, err
}
return promise, nil
}

View File

@@ -0,0 +1,70 @@
package plugin_gb28181pro
import (
"errors"
"time"
"m7s.live/v5/pkg/task"
)
type Register struct {
task.TickTask
platform *Platform
registerType string
seconds time.Duration
platformKeepAliveTask *PlatformKeepAliveTask
}
func NewRegister(platform *Platform, registerType string) *Register {
register := &Register{
registerType: registerType,
}
register.platform = platform
if registerType == "firstRegister" {
register.seconds = time.Second * 60
} else {
register.seconds = time.Second * time.Duration(platform.PlatformModel.Expires)
}
return register
}
func (r *Register) GetTickInterval() time.Duration {
return r.seconds
}
func (r *Register) Tick(any) {
r.Register()
}
func (r *Register) Register() {
if err := r.platform.DoRegister(); err != nil {
if r.registerType == "keepaliveRegister" { //保活注册失败,需要回到首次注册类型
r.Error("keepaliveRegister err", err, "register type is ", r.registerType, "DeviceGBId is", r.platform.PlatformModel.DeviceGBID)
//r.platform.eventChan <- r
r.platformKeepAliveTask.Stop(errors.New("keepaliveRegister failed,start to firstRegister,DeviceGBId is" + r.platform.PlatformModel.DeviceGBID))
r.Ticker.Reset(time.Second * 60)
//register := NewRegister(r.platform, "firstRegister")
//r.platform.AddTask(register)
//r.Stop(errors.New("keepaliveRegister failed,start to firstRegister,DeviceGBId is" + r.platform.PlatformModel.DeviceGBID))
}
} else {
if r.registerType == "firstRegister" {
r.platform.Info("firstRegister success", "register type is ", r.registerType, "DeviceGBId is", r.platform.PlatformModel.DeviceGBID)
//r.platform.eventChan <- r
//register := NewRegister(r.platform, "keepaliveRegister")
//r.platform.AddTask(register)
pat := PlatformKeepAliveTask{
platform: r.platform,
}
r.platformKeepAliveTask = &pat
r.platform.AddTask(&pat)
r.Ticker.Reset(time.Second * time.Duration(r.platform.PlatformModel.Expires))
//r.Stop(errors.New("firstRegister success,start to keepaliveRegister,DeviceGBId is" + r.platform.PlatformModel.DeviceGBID))
}
}
}
//func (r *Register) Dispose() {
// r.platform.Info("into dispose,DeviceGBId is", r.platform.PlatformModel.DeviceGBID)
//}

View File

@@ -21,6 +21,8 @@
width: 70%;
padding: 8px;
margin-right: 10px;
margin-bottom: 10px;
display: block;
}
button {
@@ -29,6 +31,7 @@
color: white;
border: none;
cursor: pointer;
margin-right: 10px;
}
button:hover {
@@ -48,32 +51,623 @@
font-family: monospace;
white-space: pre-wrap;
}
.drop-zone {
width: 100%;
height: 100px;
border: 2px dashed #4CAF50;
border-radius: 5px;
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 20px;
background-color: #f8f8f8;
transition: all 0.3s ease;
}
.drop-zone.drag-over {
background-color: #e8f5e9;
border-color: #2e7d32;
}
.drop-zone p {
margin: 0;
color: #666;
text-align: center;
}
</style>
</head>
<body>
<div class="input-container">
<input type="text" id="m3u8Url" placeholder="输入 M3U8 地址">
<button onclick="loadM3U8()">加载</button>
<input type="text" id="m3u8Url" placeholder="输入 M3U8 地址"
value="http://localhost:8080/hls/vod/fmp4.m3u8?start=1740116409&streamPath=live/test">
<button onclick="loadM3U8()">加载 M3U8</button>
<div id="m3u8Content"
style="margin: 10px 0; padding: 10px; background-color: #f0f0f0; border: 1px solid #ddd; font-family: monospace; white-space: pre; max-height: 200px; overflow-y: auto;">
</div>
<input type="text" id="fmp4Url" placeholder="输入 FMP4 地址">
<button onclick="testFMP4()">测试 FMP4</button>
<input type="text" id="wsUrl" placeholder="输入 WebSocket 地址" value="ws://localhost:8080/mp4/live/test.mp4">
<button onclick="connectWebSocket()">连接 WebSocket</button>
<div style="margin: 10px 0;">
<label for="bufferCount">缓存包数量: <span id="bufferCountValue">1</span></label>
<input type="range" id="bufferCount" min="1" max="50" value="1" style="width: 200px; margin-left: 10px;">
</div>
<div class="drop-zone" id="dropZone">
<p>拖放 FMP4 文件到这里<br>或点击选择文件</p>
<input type="file" id="fileInput" style="display: none" accept=".mp4,.fmp4">
</div>
</div>
<video id="videoPlayer" controls></video>
<video id="videoPlayer" controls autoplay></video>
<div id="debug"></div>
<script>
// MSE Player Class
class MSEPlayer {
constructor(videoElement, onLog = console.log) {
this.video = videoElement;
this.mediaSource = null;
this.sourceBuffer = null;
this.pendingBuffers = [];
this.isBuffering = false;
this.onLog = onLog;
this.codecConfigs = [
// 'video/mp4; codecs="avc1.4d001f, mp4a.40.2"',
'video/mp4; codecs="avc1.4d001f"',
'video/mp4'
];
this.MAX_BUFFER_LENGTH = 30;
this.hasError = false;
this.isDestroyed = false;
this.retryCount = 0;
this.MAX_RETRIES = 3;
this.isSourceBufferReady = false;
this.hasMetadata = false;
}
log(message) {
this.onLog(message);
}
async init() {
if (this.mediaSource) {
if (this.mediaSource.readyState === 'open') {
try {
// 等待 SourceBuffer 更新完成
if (this.sourceBuffer && this.sourceBuffer.updating) {
await new Promise((resolve) => {
const onUpdate = () => {
this.sourceBuffer.removeEventListener('updateend', onUpdate);
resolve();
};
this.sourceBuffer.addEventListener('updateend', onUpdate);
});
}
// 不在这里调用 endOfStream而是等待视频元数据加载完成
} catch (e) {
this.log(`清理旧的 MediaSource 失败: ${e.message}`);
}
}
URL.revokeObjectURL(this.video.src);
this.log('清理旧的 MediaSource');
}
this.mediaSource = new MediaSource();
this.video.src = URL.createObjectURL(this.mediaSource);
this.pendingBuffers = [];
this.isBuffering = false;
this.hasError = false;
this.isDestroyed = false;
this.retryCount = 0;
this.isSourceBufferReady = false;
this.hasMetadata = false;
// 监听视频元数据加载事件
this.video.addEventListener('loadedmetadata', () => {
this.hasMetadata = true;
this.log('视频元数据已加载');
});
return new Promise((resolve, reject) => {
let timeoutId;
let sourceOpenHandler, errorHandler;
sourceOpenHandler = async () => {
this.log('MediaSource 已打开');
clearTimeout(timeoutId);
try {
await this.initSourceBuffer();
this.mediaSource.removeEventListener('sourceopen', sourceOpenHandler);
this.mediaSource.removeEventListener('error', errorHandler);
resolve();
} catch (error) {
this.log(`初始化失败: ${error.message}`);
this.handleError(error.message);
reject(error);
}
};
errorHandler = (e) => {
clearTimeout(timeoutId);
this.log(`MediaSource 错误: ${e}`);
this.mediaSource.removeEventListener('sourceopen', sourceOpenHandler);
this.mediaSource.removeEventListener('error', errorHandler);
this.handleError(e);
reject(e);
};
this.mediaSource.addEventListener('sourceopen', sourceOpenHandler);
this.mediaSource.addEventListener('error', errorHandler);
// 添加超时处理
timeoutId = setTimeout(() => {
if (!this.isDestroyed && this.mediaSource && this.mediaSource.readyState !== 'open') {
const error = new Error('MediaSource 打开超时');
this.mediaSource.removeEventListener('sourceopen', sourceOpenHandler);
this.mediaSource.removeEventListener('error', errorHandler);
this.handleError(error.message);
reject(error);
}
}, 5000); // 5秒超时
});
}
async initSourceBuffer() {
if (!this.mediaSource || this.mediaSource.readyState !== 'open') {
debugger;
throw new Error('MediaSource 未准备好');
}
let sourceBufferCreated = false;
for (const codec of this.codecConfigs) {
try {
if (MediaSource.isTypeSupported(codec)) {
this.sourceBuffer = this.mediaSource.addSourceBuffer(codec);
sourceBufferCreated = true;
this.log(`成功创建 SourceBuffer使用编解码器: ${codec}`);
break;
}
} catch (e) {
this.log(`尝试编解码器 ${codec} 失败: ${e.message}`);
}
}
if (!sourceBufferCreated) {
throw new Error('无法创建支持的 SourceBuffer');
}
this.sourceBuffer.mode = 'sequence';
const handleUpdateEnd = () => this.handleUpdateEnd();
const handleError = (e) => {
const errorMessage = e.message || e.toString();
this.log(`SourceBuffer 错误: ${errorMessage}`);
this.handleError(new Error(`SourceBuffer 错误: ${errorMessage}`));
};
this.sourceBuffer.addEventListener('updateend', handleUpdateEnd);
this.sourceBuffer.addEventListener('error', handleError);
// 保存事件处理函数的引用,以便在销毁时正确移除
this._updateEndHandler = handleUpdateEnd;
this._errorHandler = handleError;
// 等待一小段时间确保 SourceBuffer 完全准备好
await new Promise(resolve => setTimeout(resolve, 100));
this.isSourceBufferReady = true;
}
async appendBuffer(buffer) {
if (this.hasError || this.isDestroyed) {
this.log('播放器处于错误状态或已销毁,忽略新数据');
return;
}
// 如果 SourceBuffer 还未准备好,将数据加入队列
if (!this.isSourceBufferReady) {
if (this.pendingBuffers.length < 10) {
this.pendingBuffers.push(buffer);
this.log('SourceBuffer 未准备好,将数据加入队列');
}
return;
}
if (!this.sourceBuffer || this.sourceBuffer.updating || this.pendingBuffers.length > 0) {
if (this.pendingBuffers.length < 10) {
this.pendingBuffers.push(buffer);
this.log('缓冲区正忙,将数据加入队列');
} else {
this.log('等待队列已满,丢弃数据');
}
return;
}
try {
if (!buffer || buffer.byteLength === 0) {
throw new Error('收到空数据');
}
// 检查 MediaSource 状态
if (!this.mediaSource || this.mediaSource.readyState !== 'open') {
if (this.retryCount < this.MAX_RETRIES) {
this.retryCount++;
this.log(`MediaSource 未准备好,重试 ${this.retryCount}/${this.MAX_RETRIES}`);
this.pendingBuffers.unshift(buffer);
setTimeout(() => this.processNextBuffer(), 500);
return;
}
throw new Error('MediaSource 未准备好或已关闭');
}
await this.removeOldBuffers();
this.sourceBuffer.appendBuffer(buffer);
this.isBuffering = true;
this.retryCount = 0; // 重置重试计数
this.log(`添加数据到缓冲区,大小: ${buffer.byteLength} 字节`);
} catch (error) {
const errorMessage = error.message || '未知错误';
this.log(`添加缓冲区失败: ${errorMessage}`);
console.error('添加缓冲区失败:', error);
// 只有在重试次数用完后才触发致命错误
if (this.retryCount >= this.MAX_RETRIES) {
this.handleError(new Error(`添加缓冲区失败: ${errorMessage}`));
throw error;
}
}
}
async processNextBuffer() {
if (this.pendingBuffers.length > 0 && !this.sourceBuffer.updating) {
const nextBuffer = this.pendingBuffers.shift();
await this.appendBuffer(nextBuffer);
}
}
async removeOldBuffers() {
if (!this.sourceBuffer || !this.video.buffered.length) return;
const currentTime = this.video.currentTime;
const buffered = this.video.buffered;
for (let i = 0; i < buffered.length; i++) {
const start = buffered.start(i);
const end = buffered.end(i);
if (end - currentTime > this.MAX_BUFFER_LENGTH) {
const removeEnd = currentTime - 1;
if (removeEnd > start) {
try {
this.log(`清理缓冲区: ${start.toFixed(2)} - ${removeEnd.toFixed(2)}`);
await new Promise((resolve, reject) => {
this.sourceBuffer.remove(start, removeEnd);
const onUpdate = () => {
this.sourceBuffer.removeEventListener('updateend', onUpdate);
resolve();
};
this.sourceBuffer.addEventListener('updateend', onUpdate);
});
} catch (e) {
this.log(`清理缓冲区失败: ${e.message}`);
}
}
}
}
}
handleUpdateEnd() {
this.isBuffering = false;
this.log('缓冲区更新完成');
// 处理队列中的下一个缓冲区
this.processNextBuffer();
if (!this.hasError && !this.video.playing) {
this.video.play().catch(e => {
this.log(`播放失败: ${e.message}`);
this.handleError(e.message);
});
}
}
handleError(error) {
if (this.hasError || this.isDestroyed) {
return; // 防止重复处理错误
}
this.hasError = true;
const errorMessage = error instanceof Error ? error.message : String(error);
this.log(`播放器错误: ${errorMessage}`);
this.destroy().catch(e => {
this.log(`销毁播放器时发生错误: ${e.message}`);
});
}
async destroy() {
if (this.isDestroyed) {
return; // 防止重复销毁
}
this.isDestroyed = true;
this.hasError = true;
try {
this.video.pause();
// 等待 SourceBuffer 更新完成
if (this.sourceBuffer && this.sourceBuffer.updating) {
await new Promise((resolve) => {
const onUpdate = () => {
if (this.sourceBuffer) {
this.sourceBuffer.removeEventListener('updateend', onUpdate);
}
resolve();
};
this.sourceBuffer.addEventListener('updateend', onUpdate);
});
}
// 清理 SourceBuffer 事件监听器
if (this.sourceBuffer) {
if (this._updateEndHandler) {
this.sourceBuffer.removeEventListener('updateend', this._updateEndHandler);
}
if (this._errorHandler) {
this.sourceBuffer.removeEventListener('error', this._errorHandler);
}
}
// 清理 MediaSource
if (this.mediaSource && this.mediaSource.readyState === 'open') {
// 移除所有 SourceBuffers
if (this.sourceBuffer && !this.sourceBuffer.updating) {
try {
this.mediaSource.removeSourceBuffer(this.sourceBuffer);
} catch (e) {
this.log(`移除 SourceBuffer 失败: ${e.message}`);
}
}
// 只有在视频元数据加载完成后才调用 endOfStream
if (this.hasMetadata) {
try {
await new Promise(resolve => {
// 确保在下一个事件循环中执行 endOfStream
setTimeout(() => {
try {
this.mediaSource.endOfStream();
} catch (e) {
this.log(`关闭 MediaSource 失败: ${e.message}`);
}
resolve();
}, 0);
});
} catch (e) {
this.log(`关闭 MediaSource 失败: ${e.message}`);
}
}
}
if (this.video.src) {
URL.revokeObjectURL(this.video.src);
this.video.removeAttribute('src');
this.video.load();
}
} catch (error) {
this.log(`销毁播放器时发生错误: ${error.message}`);
} finally {
// 确保清理所有资源
this.sourceBuffer = null;
this.mediaSource = null;
this.pendingBuffers = [];
this.isBuffering = false;
this._updateEndHandler = null;
this._errorHandler = null;
this.hasMetadata = false;
}
}
}
// Global variables
let currentPlaylist = [];
let currentIndex = 0;
let mediaSource;
let sourceBuffer;
let pendingBuffers = [];
let isBuffering = false;
const MAX_BUFFER_LENGTH = 30; // 保持30秒的缓冲区
let msePlayer = null;
let wsConnection = null;
let bufferMergeCount = 10; // 默认缓存包数量
// 添加滑动条事件监听
const bufferCountSlider = document.getElementById('bufferCount');
const bufferCountValue = document.getElementById('bufferCountValue');
bufferCountSlider.addEventListener('input', (e) => {
bufferMergeCount = parseInt(e.target.value);
bufferCountValue.textContent = bufferMergeCount;
if (wsConnection) {
log(`已更新缓存包数量为: ${bufferMergeCount}`);
}
});
function log(message) {
const debug = document.getElementById('debug');
const time = new Date().toLocaleTimeString();
debug.textContent = `[${time}] ${message}\n` + debug.textContent;
const newLine = document.createElement('div');
newLine.innerHTML = `[${time}] ${message}`;
if (debug.firstChild) {
debug.insertBefore(newLine, debug.firstChild);
} else {
debug.appendChild(newLine);
}
}
// Initialize MSE player
function initPlayer() {
const video = document.getElementById('videoPlayer');
if (msePlayer) {
msePlayer.destroy();
}
msePlayer = new MSEPlayer(video, log);
return msePlayer.init();
}
// WebSocket handling
async function connectWebSocket() {
const wsUrl = document.getElementById('wsUrl').value;
if (!wsUrl) {
alert('请输入 WebSocket 地址');
return;
}
try {
await initPlayer();
if (wsConnection) {
wsConnection.close();
wsConnection = null;
}
wsConnection = new WebSocket(wsUrl);
wsConnection.binaryType = 'arraybuffer';
log(`正在连接 WebSocket: ${wsUrl}`);
wsConnection.onopen = () => {
log('WebSocket 连接已建立');
};
wsConnection.onmessage = async (event) => {
if (!msePlayer || msePlayer.hasError || msePlayer.isDestroyed) {
if (wsConnection) {
wsConnection.close();
wsConnection = null;
}
return;
}
try {
if (!event.data || event.data.byteLength === 0) {
throw new Error('收到空数据');
}
// 为前两个 buffer 创建 Blob URL
if (!wsConnection.bufferCount) {
wsConnection.bufferCount = 0;
}
if (wsConnection.bufferCount < 2) {
const blob = new Blob([event.data], { type: 'application/octet-stream' });
const url = URL.createObjectURL(blob);
const linkElement = document.createElement('a');
linkElement.href = url;
linkElement.download = `buffer-${wsConnection.bufferCount + 1}.mp4`;
linkElement.textContent = `下载第 ${wsConnection.bufferCount + 1} 个 buffer`;
linkElement.target = '_blank';
const debug = document.getElementById('debug');
const time = new Date().toLocaleTimeString();
const newLine = document.createElement('div');
newLine.textContent = `[${time}] 第 ${wsConnection.bufferCount + 1} 个 buffer 大小: ${event.data.byteLength} 字节 `;
newLine.appendChild(linkElement);
debug.insertBefore(newLine, debug.firstChild);
wsConnection.bufferCount++;
}
// 初始化缓存数组
if (!wsConnection.cachedBuffers) {
wsConnection.cachedBuffers = [];
}
// 添加到缓存
wsConnection.cachedBuffers.push(new Uint8Array(event.data));
log(`已缓存 ${wsConnection.cachedBuffers.length} 个数据包`);
// 当累积到指定数量的数据包时,合并并添加到 buffer
if (wsConnection.cachedBuffers.length >= bufferMergeCount) {
// 计算总长度
const totalLength = wsConnection.cachedBuffers.reduce((acc, curr) => acc + curr.byteLength, 0);
// 创建合并后的 buffer
const mergedBuffer = new Uint8Array(totalLength);
let offset = 0;
// 合并所有缓存的数据
for (const buffer of wsConnection.cachedBuffers) {
mergedBuffer.set(buffer, offset);
offset += buffer.byteLength;
}
log(`合并 ${wsConnection.cachedBuffers.length} 个数据包,总大小: ${totalLength} 字节`);
// 清空缓存
wsConnection.cachedBuffers = [];
// 添加到 MSE
await msePlayer.appendBuffer(mergedBuffer);
}
} catch (error) {
const errorMessage = error.message || '未知错误';
log(`处理数据失败: ${errorMessage}`);
// 只有在发生致命错误时才关闭连接
if (msePlayer.hasError) {
if (wsConnection) {
wsConnection.close();
wsConnection = null;
}
}
}
};
wsConnection.onclose = (event) => {
const reason = event.reason || '未知原因';
log(`WebSocket 连接已关闭: ${reason} (code: ${event.code})`);
if (msePlayer && !msePlayer.isDestroyed) {
msePlayer.destroy();
}
};
wsConnection.onerror = (error) => {
const errorMessage = error.message || '未知错误';
log(`WebSocket 错误: ${errorMessage}`);
if (msePlayer && !msePlayer.isDestroyed) {
msePlayer.destroy();
}
if (wsConnection) {
wsConnection.close();
wsConnection = null;
}
};
} catch (error) {
const errorMessage = error.message || '未知错误';
log(`WebSocket 初始化失败: ${errorMessage}`);
if (msePlayer && !msePlayer.isDestroyed) {
msePlayer.destroy();
}
if (wsConnection) {
wsConnection.close();
wsConnection = null;
}
}
}
// Handle local file
async function handleLocalFile(file) {
if (!file.name.toLowerCase().endsWith('.mp4') && !file.name.toLowerCase().endsWith('.fmp4')) {
alert('请选择 FMP4/MP4 文件');
return;
}
try {
log(`开始处理本地文件: ${file.name}`);
await initPlayer();
const buffer = await file.arrayBuffer();
log(`本地文件加载完成,大小: ${buffer.byteLength} 字节`);
await msePlayer.appendBuffer(buffer);
} catch (error) {
log(`处理本地文件失败: ${error.message}`);
}
}
// M3U8 handling
async function loadM3U8() {
const m3u8Url = document.getElementById('m3u8Url').value;
if (!m3u8Url) {
@@ -84,7 +678,15 @@
try {
log(`开始加载 M3U8: ${m3u8Url}`);
const response = await fetch(m3u8Url);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const content = await response.text();
// 显示 M3U8 内容
const m3u8ContentDiv = document.getElementById('m3u8Content');
m3u8ContentDiv.textContent = content;
const mp4Urls = parseM3U8(content, m3u8Url);
log(`解析到 ${mp4Urls.length} 个 MP4 文件`);
@@ -95,10 +697,10 @@
currentPlaylist = mp4Urls;
currentIndex = 0;
initMSE();
await initPlayer();
await loadNextSegment();
} catch (error) {
console.error('加载 M3U8 文件失败:', error);
log(`加载失败: ${error.message}`);
log(`加载 M3U8 文件失败: ${error.message}`);
alert('加载 M3U8 文件失败');
}
}
@@ -106,224 +708,135 @@
function parseM3U8(content, baseUrl) {
const lines = content.split('\n');
const mp4Urls = [];
let duration = 0;
for (const line of lines) {
if (line.trim() && !line.startsWith('#')) {
const url = line.startsWith('http') ? line : new URL(line, baseUrl).href;
if (url.endsWith('.mp4')) {
mp4Urls.push(url);
log(`找到 MP4: ${url}`);
}
for (let i = 0; i < lines.length; i++) {
const line = lines[i].trim();
// 解析 EXTINF 获取时长
if (line.startsWith('#EXTINF:')) {
duration = parseFloat(line.split(':')[1]);
continue;
}
// 跳过注释和空行
if (line === '' || line.startsWith('#')) {
continue;
}
// 处理 MP4 文件 URL
const url = line.startsWith('http') ? line : new URL(line, baseUrl).href;
mp4Urls.push({
url,
duration
});
log(`找到 MP4: ${url} (时长: ${duration}秒)`);
}
return mp4Urls;
}
function initMSE() {
const video = document.getElementById('videoPlayer');
log('初始化 MSE');
if (mediaSource) {
if (mediaSource.readyState === 'open') {
mediaSource.endOfStream();
}
URL.revokeObjectURL(video.src);
log('清理旧的 MediaSource');
}
mediaSource = new MediaSource();
video.src = URL.createObjectURL(mediaSource);
pendingBuffers = [];
isBuffering = false;
mediaSource.addEventListener('sourceopen', async () => {
log('MediaSource 已打开');
try {
// Try different codec combinations
const codecConfigs = [
'video/mp4; codecs="avc1.64001f"', // Video only
'video/mp4; codecs="avc1.64001f,mp4a.40.2"', // Video + AAC
'video/mp4' // Let the browser figure it out
];
let sourceBufferCreated = false;
for (const codec of codecConfigs) {
try {
if (MediaSource.isTypeSupported(codec)) {
sourceBuffer = mediaSource.addSourceBuffer(codec);
sourceBufferCreated = true;
log(`成功创建 SourceBuffer使用编解码器: ${codec}`);
break;
}
} catch (e) {
log(`尝试编解码器 ${codec} 失败: ${e.message}`);
}
}
if (!sourceBufferCreated) {
throw new Error('无法创建支持的 SourceBuffer');
}
sourceBuffer.mode = 'sequence';
sourceBuffer.addEventListener('updateend', handleUpdateEnd);
sourceBuffer.addEventListener('error', (e) => {
log(`SourceBuffer 错误: ${e}`);
});
// 先加载第一个片段,等待缓冲完成后再播放
log('等待第一个片段加载完成...');
await loadNextSegment();
await new Promise(resolve => {
const checkBuffer = () => {
if (!sourceBuffer.updating && video.buffered.length > 0) {
log('首个片段缓冲完成,开始播放');
resolve();
} else {
setTimeout(checkBuffer, 100);
}
};
checkBuffer();
});
video.play().catch(e => {
log(`播放失败: ${e.message}`);
console.error('播放失败:', e);
});
} catch (error) {
log(`创建 SourceBuffer 失败: ${error.message}`);
}
});
mediaSource.addEventListener('sourceended', () => {
log('MediaSource 已结束');
});
mediaSource.addEventListener('error', (e) => {
log(`MediaSource 错误: ${e}`);
});
video.addEventListener('error', (e) => {
log(`视频错误: ${video.error.message}`);
});
}
async function loadNextSegment() {
if (currentIndex >= currentPlaylist.length) {
if (mediaSource.readyState === 'open') {
mediaSource.endOfStream();
if (msePlayer.mediaSource.readyState === 'open') {
msePlayer.mediaSource.endOfStream();
log('已到达播放列表末尾');
}
return;
}
try {
// 在加载新片段前检查并清理缓冲区
await removeOldBuffers();
log(`加载视频片段 ${currentIndex + 1}`);
const response = await fetch(currentPlaylist[currentIndex]);
const segment = currentPlaylist[currentIndex];
log(`加载视频片段 ${currentIndex + 1}/${currentPlaylist.length}`);
const response = await fetch(segment.url);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const buffer = await response.arrayBuffer();
log(`视频片段 ${currentIndex + 1} 加载完成,大小: ${buffer.byteLength} 字节`);
appendBuffer(buffer);
await msePlayer.appendBuffer(buffer);
// 预加载下一个片段,但要控制预加载的数量
if (currentIndex < currentPlaylist.length - 1 && pendingBuffers.length < 2) {
// 预加载下一个片段
if (currentIndex < currentPlaylist.length - 1 && msePlayer.pendingBuffers.length < 2) {
currentIndex++;
loadNextSegment();
}
} catch (error) {
log(`加载视频片段失败: ${error.message}`);
console.error('加载视频片段失败:', error);
}
}
async function removeOldBuffers() {
if (!sourceBuffer || !video.buffered.length) return;
const currentTime = video.currentTime;
const buffered = video.buffered;
// 计算当前缓冲区的范围
for (let i = 0; i < buffered.length; i++) {
const start = buffered.start(i);
const end = buffered.end(i);
// 如果缓冲区超过了最大长度,移除旧的部分
if (end - currentTime > MAX_BUFFER_LENGTH) {
const removeEnd = currentTime - 1; // 保留当前播放位置前1秒
if (removeEnd > start) {
try {
log(`清理缓冲区: ${start.toFixed(2)} - ${removeEnd.toFixed(2)}`);
await new Promise((resolve, reject) => {
sourceBuffer.remove(start, removeEnd);
const onUpdate = () => {
sourceBuffer.removeEventListener('updateend', onUpdate);
resolve();
};
sourceBuffer.addEventListener('updateend', onUpdate);
});
} catch (e) {
log(`清理缓冲区失败: ${e.message}`);
}
}
}
}
}
function appendBuffer(buffer) {
if (!sourceBuffer || sourceBuffer.updating || pendingBuffers.length > 0) {
// 限制等待队列的长度
if (pendingBuffers.length < 3) {
pendingBuffers.push(buffer);
log('缓冲区正忙,将数据加入队列');
} else {
log('等待队列已满,丢弃数据');
}
// FMP4 testing
async function testFMP4() {
const fmp4Url = document.getElementById('fmp4Url').value;
if (!fmp4Url) {
alert('请输入 FMP4 地址');
return;
}
try {
sourceBuffer.appendBuffer(buffer);
isBuffering = true;
log('添加数据到缓冲区');
} catch (error) {
if (error.name === 'QuotaExceededError') {
log('缓冲区已满,将进行清理');
pendingBuffers.push(buffer);
removeOldBuffers();
} else {
log(`添加缓冲区失败: ${error.message}`);
console.error('添加缓冲区失败:', error);
log(`开始测试 FMP4: ${fmp4Url}`);
await initPlayer();
const response = await fetch(fmp4Url);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const buffer = await response.arrayBuffer();
log(`FMP4 文件加载完成,大小: ${buffer.byteLength} 字节`);
await msePlayer.appendBuffer(buffer);
} catch (error) {
log(`加载 FMP4 文件失败: ${error.message}`);
}
}
function handleUpdateEnd() {
isBuffering = false;
log('缓冲区更新完成');
// Event listeners
const dropZone = document.getElementById('dropZone');
const fileInput = document.getElementById('fileInput');
const video = document.getElementById('videoPlayer');
if (pendingBuffers.length > 0) {
const nextBuffer = pendingBuffers.shift();
appendBuffer(nextBuffer);
dropZone.addEventListener('click', () => fileInput.click());
dropZone.addEventListener('dragover', (e) => {
e.preventDefault();
dropZone.classList.add('drag-over');
});
dropZone.addEventListener('dragleave', (e) => {
e.preventDefault();
dropZone.classList.remove('drag-over');
});
dropZone.addEventListener('drop', (e) => {
e.preventDefault();
dropZone.classList.remove('drag-over');
const files = e.dataTransfer.files;
if (files.length > 0) {
handleLocalFile(files[0]);
}
});
fileInput.addEventListener('change', (e) => {
if (e.target.files.length > 0) {
handleLocalFile(e.target.files[0]);
}
}
document.getElementById('videoPlayer').addEventListener('ended', () => {
log('视频播放结束,重新开始');
currentIndex = 0;
initMSE();
});
const video = document.getElementById('videoPlayer');
video.addEventListener('ended', () => {
log('视频播放结束');
});
video.addEventListener('playing', () => log('视频开始播放'));
video.addEventListener('pause', () => log('视频暂停'));
video.addEventListener('waiting', () => log('视频缓冲中'));
video.addEventListener('canplay', () => log('视频可以播放'));
video.addEventListener('loadedmetadata', () => log('视频元数据已加载'));
video.addEventListener('error', (e) => {
if (msePlayer && !msePlayer.isDestroyed) {
log(`视频错误: ${video.error ? video.error.message : '未知错误'}`);
msePlayer.handleError(video.error ? video.error.message : '未知错误');
}
if (wsConnection) {
wsConnection.close();
wsConnection = null;
}
});
</script>
</body>

View File

@@ -14,7 +14,12 @@ import (
hls "m7s.live/v5/plugin/hls/pkg"
)
var _ = m7s.InstallPlugin[HLSPlugin](hls.NewTransform, hls.NewRecorder)
var _ = m7s.InstallPlugin[HLSPlugin](m7s.PluginMeta{
NewTransformer: hls.NewTransform,
NewRecorder: hls.NewRecorder,
NewPuller: hls.NewPuller,
NewPullProxy: m7s.NewHTTPPullPorxy,
})
//go:embed hls.js
var hls_js embed.FS
@@ -45,13 +50,6 @@ func (p *HLSPlugin) RegisterHandler() map[string]http.HandlerFunc {
}
}
func (p *HLSPlugin) OnPullProxyAdd(pullProxy *m7s.PullProxy) any {
d := &m7s.HTTPPullProxy{}
d.PullProxy = pullProxy
d.Plugin = &p.Plugin
return d
}
func (config *HLSPlugin) vod(w http.ResponseWriter, r *http.Request) {
recordType := "ts"
if r.PathValue("streamPath") == "mp4.m3u8" {
@@ -76,13 +74,35 @@ func (config *HLSPlugin) vod(w http.ResponseWriter, r *http.Request) {
if !startTime.IsZero() {
if config.DB != nil {
var records []m7s.RecordStream
if endTime.IsZero() {
query := `stream_path = ? AND type = ? AND start_time IS NOT NULL AND end_time > ?`
config.DB.Where(query, streamPath, recordType, startTime).Find(&records)
} else {
if recordType == "fmp4" {
query := `stream_path = ? AND type = ? AND start_time IS NOT NULL AND end_time IS NOT NULL AND ? <= end_time AND ? >= start_time`
config.DB.Where(query, streamPath, recordType, startTime, endTime).Find(&records)
config.DB.Where(query, streamPath, "mp4", startTime, endTime).Find(&records)
if len(records) == 0 {
return
}
playlist := hls.Playlist{
Version: 7,
Sequence: 0,
Targetduration: 90,
}
var plBuffer util.Buffer
playlist.Writer = &plBuffer
playlist.Init()
for _, record := range records {
duration := record.EndTime.Sub(record.StartTime).Seconds()
playlist.WriteInf(hls.PlaylistInf{
Duration: duration,
URL: fmt.Sprintf("/mp4/download/%s.fmp4?id=%d", streamPath, record.ID),
Title: record.StartTime.Format(time.RFC3339),
})
}
plBuffer.WriteString("#EXT-X-ENDLIST\n")
w.Write(plBuffer)
return
}
query := `stream_path = ? AND type = ? AND start_time IS NOT NULL AND end_time IS NOT NULL AND ? <= end_time AND ? >= start_time`
config.DB.Where(query, streamPath, recordType, startTime, endTime).Find(&records)
if len(records) > 0 {
playlist := hls.Playlist{
Version: 7,
@@ -97,8 +117,7 @@ func (config *HLSPlugin) vod(w http.ResponseWriter, r *http.Request) {
duration := record.EndTime.Sub(record.StartTime).Seconds()
playlist.WriteInf(hls.PlaylistInf{
Duration: duration,
Title: record.FilePath,
FilePath: record.FilePath,
URL: record.FilePath,
})
}
plBuffer.WriteString("#EXT-X-ENDLIST\n")
@@ -178,6 +197,7 @@ func (config *HLSPlugin) ServeHTTP(w http.ResponseWriter, r *http.Request) {
Sequence: 0,
Targetduration: 90,
}
var plBuffer util.Buffer
playlist.Writer = &plBuffer
playlist.Init()
@@ -186,7 +206,7 @@ func (config *HLSPlugin) ServeHTTP(w http.ResponseWriter, r *http.Request) {
duration := record.EndTime.Sub(record.StartTime).Seconds()
playlist.WriteInf(hls.PlaylistInf{
Duration: duration,
Title: path.Base(record.FilePath),
URL: path.Base(record.FilePath),
FilePath: record.FilePath,
})
}

View File

@@ -96,6 +96,7 @@ type PlaylistKey struct {
type PlaylistInf struct {
Duration float64
URL string
Title string
FilePath string
}
@@ -113,13 +114,14 @@ func (pl *Playlist) Init() (err error) {
"#EXT-X-VERSION:%d\n"+
"#EXT-X-MEDIA-SEQUENCE:%d\n"+
"#EXT-X-TARGETDURATION:%d\n", pl.Version, pl.Sequence, pl.Targetduration)
pl.Sequence++
return
}
func (pl *Playlist) WriteInf(inf PlaylistInf) (err error) {
_, err = fmt.Fprintf(pl, "#EXTINF:%.3f,\n"+
"%s\n", inf.Duration, inf.Title)
_, err = fmt.Fprintf(pl, "#EXTINF:%.3f,%s\n"+
"%s\n", inf.Duration, inf.Title, inf.URL)
pl.tsCount++
return
}

View File

@@ -21,6 +21,10 @@ import (
mpegts "m7s.live/v5/plugin/hls/pkg/ts"
)
func NewPuller(conf config.Pull) m7s.IPuller {
return &Puller{}
}
type Puller struct {
task.Job
PullJob m7s.PullJob
@@ -276,7 +280,7 @@ func (p *Puller) pull(info *M3u8Info) (err error) {
tsFilePath := p.PullJob.StreamPath + "/" + tsFilename
ss := strings.Split(p.PullJob.StreamPath, "/")
var plInfo = PlaylistInf{
Title: fmt.Sprintf("%s/%s", ss[len(ss)-1], tsFilename),
URL: fmt.Sprintf("%s/%s", ss[len(ss)-1], tsFilename),
Duration: v.dur,
FilePath: tsFilePath,
}

View File

@@ -61,10 +61,10 @@ func (r *Recorder) createStream(start time.Time) (err error) {
return
}
if sub.Publisher.HasAudioTrack() {
r.stream.AudioCodec = sub.Publisher.AudioTrack.ICodecCtx.FourCC().String()
r.stream.AudioCodec = sub.Publisher.AudioTrack.ICodecCtx.String()
}
if sub.Publisher.HasVideoTrack() {
r.stream.VideoCodec = sub.Publisher.VideoTrack.ICodecCtx.FourCC().String()
r.stream.VideoCodec = sub.Publisher.VideoTrack.ICodecCtx.String()
}
if recordJob.Plugin.DB != nil {
recordJob.Plugin.DB.Save(&r.stream)

View File

@@ -135,7 +135,7 @@ func (w *HLSWriter) checkFragment(ts time.Duration) (err error) {
inf := PlaylistInf{
//浮点计算精度
Duration: dur.Seconds(),
Title: fmt.Sprintf("%s/%s", ss[len(ss)-1], tsFilename),
URL: fmt.Sprintf("%s/%s", ss[len(ss)-1], tsFilename),
FilePath: tsFilePath,
}

View File

@@ -52,14 +52,15 @@ func (h *LogRotatePlugin) ServeHTTP(w http.ResponseWriter, r *http.Request) {
}
func (l *LogRotatePlugin) API_trail(w http.ResponseWriter, r *http.Request) {
writer := util.NewSSE(w, r.Context())
file, err := os.Open(filepath.Join(l.Path, "current.log"))
if err == nil {
io.Copy(writer, file)
file.Close()
}
h := console.NewHandler(writer, &console.HandlerOptions{NoColor: true})
l.Server.LogHandler.Add(h)
<-r.Context().Done()
l.Server.LogHandler.Remove(h)
util.NewSSE(w, r.Context(), func(sse *util.SSE) {
file, err := os.Open(filepath.Join(l.Path, "current.log"))
if err == nil {
io.Copy(sse, file)
file.Close()
}
h := console.NewHandler(sse, &console.HandlerOptions{NoColor: true})
l.Server.LogHandler.Add(h)
<-r.Context().Done()
l.Server.LogHandler.Remove(h)
})
}

View File

@@ -1,12 +1,14 @@
package plugin_mp4
import (
"bytes"
"context"
"fmt"
"io"
"net/http"
"os"
"path/filepath"
"strings"
"time"
"unsafe"
@@ -16,6 +18,7 @@ import (
"m7s.live/v5/pb"
"m7s.live/v5/pkg"
"m7s.live/v5/pkg/config"
"m7s.live/v5/pkg/task"
"m7s.live/v5/pkg/util"
mp4pb "m7s.live/v5/plugin/mp4/pb"
mp4 "m7s.live/v5/plugin/mp4/pkg"
@@ -24,8 +27,80 @@ import (
type ContentPart struct {
*os.File
Start int64
Size int
Start int64
Size int
boxies []box.IBox
}
func (p *MP4Plugin) downloadSingleFile(stream *m7s.RecordStream, flag mp4.Flag, w http.ResponseWriter, r *http.Request) {
if flag == 0 {
http.ServeFile(w, r, stream.FilePath)
} else if flag == mp4.FLAG_FRAGMENT {
file, err := os.Open(stream.FilePath)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
p.Info("read", "file", file.Name())
demuxer := mp4.NewDemuxer(file)
err = demuxer.Demux()
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
var trackMap = make(map[box.MP4_CODEC_TYPE]*mp4.Track)
muxer := mp4.NewMuxer(mp4.FLAG_FRAGMENT)
for _, track := range demuxer.Tracks {
t := muxer.AddTrack(track.Cid)
t.ExtraData = track.ExtraData
trackMap[track.Cid] = t
if track.Cid.IsAudio() {
t.SampleSize = track.SampleSize
t.SampleRate = track.SampleRate
t.ChannelCount = track.ChannelCount
} else if track.Cid.IsVideo() {
t.Width = track.Width
t.Height = track.Height
}
}
moov := muxer.MakeMoov()
var parts []*ContentPart
var part *ContentPart
for track, sample := range demuxer.RangeSample {
if part == nil {
part = &ContentPart{
File: file,
Start: sample.Offset,
}
parts = append(parts, part)
}
fixSample := *sample
part.Seek(sample.Offset, io.SeekStart)
fixSample.Data = make([]byte, sample.Size)
part.Read(fixSample.Data)
moof, mdat := muxer.CreateFlagment(trackMap[track.Cid], fixSample)
if moof != nil {
part.boxies = append(part.boxies, moof, mdat)
part.Size += int(moof.Size() + mdat.Size())
}
}
var children []box.IBox
var totalSize uint64
ftyp := muxer.CreateFTYPBox()
children = append(children, ftyp, moov)
totalSize += uint64(ftyp.Size() + moov.Size())
for _, part := range parts {
totalSize += uint64(part.Size)
children = append(children, part.boxies...)
part.Close()
}
w.Header().Set("Content-Length", fmt.Sprintf("%d", totalSize))
_, err = box.WriteTo(w, children...)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
}
}
func (p *MP4Plugin) download(w http.ResponseWriter, r *http.Request) {
@@ -35,7 +110,13 @@ func (p *MP4Plugin) download(w http.ResponseWriter, r *http.Request) {
}
w.Header().Set("Content-Type", "video/mp4")
streamPath := r.PathValue("streamPath")
var flag mp4.Flag
if strings.HasSuffix(streamPath, ".fmp4") {
flag = mp4.FLAG_FRAGMENT
streamPath = strings.TrimSuffix(streamPath, ".fmp4")
} else {
streamPath = strings.TrimSuffix(streamPath, ".mp4")
}
query := r.URL.Query()
var streams []m7s.RecordStream
if id := query.Get("id"); id != "" {
@@ -45,10 +126,10 @@ func (p *MP4Plugin) download(w http.ResponseWriter, r *http.Request) {
http.Error(w, "record not found", http.StatusNotFound)
return
}
http.ServeFile(w, r, streams[0].FilePath)
p.downloadSingleFile(&streams[0], flag, w, r)
return
}
// 合并多个 mp4
startTime, endTime, err := util.TimeRangeQueryParse(query)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
@@ -59,23 +140,90 @@ 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)
muxer := mp4.NewMuxer(0)
ftyp := box.CreateFTYPBox(box.TypeISOM, 0x200, box.TypeISOM, box.TypeISO2, box.TypeAVC1, box.TypeMP41)
var n int64
n, err = box.WriteTo(w, ftyp)
if err != nil {
return
}
muxer.CurrentOffset = n
muxer := mp4.NewMuxer(flag)
ftyp := muxer.CreateFTYPBox()
n := ftyp.Size()
muxer.CurrentOffset = int64(n)
var lastTs, tsOffset int64
var parts []*ContentPart
sampleOffset := muxer.CurrentOffset + box.BasicBoxLen*2
sampleOffset := muxer.CurrentOffset + mp4.BeforeMdatData
mdatOffset := sampleOffset
var audioTrack, videoTrack *mp4.Track
var file *os.File
var moov box.IBox
streamCount := len(streams)
// Track ExtraData history for each track
type TrackHistory struct {
Track *mp4.Track
ExtraData []byte
}
var audioHistory, videoHistory []TrackHistory
addAudioTrack := func(track *mp4.Track) {
t := muxer.AddTrack(track.Cid)
t.ExtraData = track.ExtraData
t.SampleSize = track.SampleSize
t.SampleRate = track.SampleRate
t.ChannelCount = track.ChannelCount
if len(audioHistory) > 0 {
t.Samplelist = audioHistory[len(audioHistory)-1].Track.Samplelist
}
audioTrack = t
audioHistory = append(audioHistory, TrackHistory{Track: t, ExtraData: track.ExtraData})
}
addVideoTrack := func(track *mp4.Track) {
t := muxer.AddTrack(track.Cid)
t.ExtraData = track.ExtraData
t.Width = track.Width
t.Height = track.Height
if len(videoHistory) > 0 {
t.Samplelist = videoHistory[len(videoHistory)-1].Track.Samplelist
}
videoTrack = t
videoHistory = append(videoHistory, TrackHistory{Track: t, ExtraData: track.ExtraData})
}
addTrack := func(track *mp4.Track) {
var lastAudioTrack, lastVideoTrack *TrackHistory
if len(audioHistory) > 0 {
lastAudioTrack = &audioHistory[len(audioHistory)-1]
}
if len(videoHistory) > 0 {
lastVideoTrack = &videoHistory[len(videoHistory)-1]
}
if track.Cid.IsAudio() {
if lastAudioTrack == nil {
addAudioTrack(track)
} else if !bytes.Equal(lastAudioTrack.ExtraData, track.ExtraData) {
for _, history := range audioHistory {
if bytes.Equal(history.ExtraData, track.ExtraData) {
audioTrack = history.Track
audioTrack.Samplelist = audioHistory[len(audioHistory)-1].Track.Samplelist
return
}
}
addAudioTrack(track)
}
} else if track.Cid.IsVideo() {
if lastVideoTrack == nil {
addVideoTrack(track)
} else if !bytes.Equal(lastVideoTrack.ExtraData, track.ExtraData) {
for _, history := range videoHistory {
if bytes.Equal(history.ExtraData, track.ExtraData) {
videoTrack = history.Track
videoTrack.Samplelist = videoHistory[len(videoHistory)-1].Track.Samplelist
return
}
}
addVideoTrack(track)
}
}
}
for i, stream := range streams {
tsOffset = lastTs
file, err = os.Open(stream.FilePath)
@@ -88,32 +236,29 @@ func (p *MP4Plugin) download(w http.ResponseWriter, r *http.Request) {
if err != nil {
return
}
if i == 0 {
trackCount := len(demuxer.Tracks)
if i == 0 || flag == mp4.FLAG_FRAGMENT {
for _, track := range demuxer.Tracks {
t := muxer.AddTrack(track.Cid)
t.ExtraData = track.ExtraData
if track.Cid.IsAudio() {
audioTrack = t
t.SampleSize = track.SampleSize
t.SampleRate = track.SampleRate
t.ChannelCount = track.ChannelCount
} else if track.Cid.IsVideo() {
videoTrack = t
t.Width = track.Width
t.Height = track.Height
}
addTrack(track)
}
}
if trackCount != len(muxer.Tracks) {
if flag == mp4.FLAG_FRAGMENT {
moov = muxer.MakeMoov()
}
}
if i == 0 {
startTimestamp := startTime.Sub(stream.StartTime).Milliseconds()
var startSample *box.Sample
if startSample, err = demuxer.SeekTime(uint64(startTimestamp)); err != nil {
tsOffset = 0
continue
}
tsOffset = -int64(startSample.DTS)
tsOffset = -int64(startSample.Timestamp)
}
var part *ContentPart
for track, sample := range demuxer.RangeSample {
if i == streamCount-1 && int64(sample.DTS) > endTime.Sub(stream.StartTime).Milliseconds() {
if i == streamCount-1 && int64(sample.Timestamp) > endTime.Sub(stream.StartTime).Milliseconds() {
break
}
if part == nil {
@@ -122,16 +267,31 @@ func (p *MP4Plugin) download(w http.ResponseWriter, r *http.Request) {
Start: sample.Offset,
}
}
part.Size += sample.Size
lastTs = int64(sample.DTS + uint64(tsOffset))
lastTs = int64(sample.Timestamp + uint32(tsOffset))
fixSample := *sample
fixSample.DTS += uint64(tsOffset)
fixSample.PTS += uint64(tsOffset)
fixSample.Offset += sampleOffset - part.Start
if track.Cid.IsAudio() {
audioTrack.AddSampleEntry(fixSample)
} else if track.Cid.IsVideo() {
videoTrack.AddSampleEntry(fixSample)
fixSample.Timestamp += uint32(tsOffset)
if flag == 0 {
fixSample.Offset = sampleOffset + (fixSample.Offset - part.Start)
part.Size += sample.Size
if track.Cid.IsAudio() {
audioTrack.AddSampleEntry(fixSample)
} else if track.Cid.IsVideo() {
videoTrack.AddSampleEntry(fixSample)
}
} else {
part.Seek(sample.Offset, io.SeekStart)
fixSample.Data = make([]byte, sample.Size)
part.Read(fixSample.Data)
var moof, mdat box.IBox
if track.Cid.IsAudio() {
moof, mdat = muxer.CreateFlagment(audioTrack, fixSample)
} else if track.Cid.IsVideo() {
moof, mdat = muxer.CreateFlagment(videoTrack, fixSample)
}
if moof != nil {
part.boxies = append(part.boxies, moof, mdat)
part.Size += int(moof.Size() + mdat.Size())
}
}
}
if part != nil {
@@ -139,39 +299,67 @@ func (p *MP4Plugin) download(w http.ResponseWriter, r *http.Request) {
parts = append(parts, part)
}
}
moovSize := muxer.GetMoovSize()
for _, track := range muxer.Tracks {
for i := range track.Samplelist {
track.Samplelist[i].Offset += int64(moovSize)
if flag == 0 {
moovSize := muxer.MakeMoov().Size()
dataSize := uint64(sampleOffset - mdatOffset)
w.Header().Set("Content-Length", fmt.Sprintf("%d", uint64(sampleOffset)+moovSize))
for _, track := range muxer.Tracks {
for i := range track.Samplelist {
track.Samplelist[i].Offset += int64(moovSize)
}
}
}
err = muxer.WriteMoov(w)
if err != nil {
return
}
var mdatBox = box.CreateBaseBox(box.TypeMDAT, uint64(sampleOffset-mdatOffset)+box.BasicBoxLen)
var freeBox *box.FreeBox
if mdatBox.HeaderSize() == box.BasicBoxLen {
freeBox = box.CreateFreeBox(nil)
}
_, err = box.WriteTo(w, freeBox, mdatBox)
if err != nil {
return
}
var written, totalWritten int64
for _, part := range parts {
part.Seek(part.Start, io.SeekStart)
written, err = io.CopyN(w, part.File, int64(part.Size))
mdatBox := box.CreateBaseBox(box.TypeMDAT, dataSize+box.BasicBoxLen)
var freeBox *box.FreeBox
if mdatBox.HeaderSize() == box.BasicBoxLen {
freeBox = box.CreateFreeBox(nil)
}
var written, totalWritten int64
totalWritten, err = box.WriteTo(w, ftyp, muxer.MakeMoov(), freeBox, mdatBox)
if err != nil {
return
}
for _, part := range parts {
part.Seek(part.Start, io.SeekStart)
written, err = io.CopyN(w, part.File, int64(part.Size))
if err != nil {
return
}
totalWritten += written
part.Close()
}
} else {
var children []box.IBox
var totalSize uint64
children = append(children, ftyp, moov)
totalSize += uint64(ftyp.Size() + moov.Size())
for _, part := range parts {
totalSize += uint64(part.Size)
children = append(children, part.boxies...)
part.Close()
}
w.Header().Set("Content-Length", fmt.Sprintf("%d", totalSize))
_, err = box.WriteTo(w, children...)
if err != nil {
return
}
totalWritten += written
part.Close()
}
}
func (p *MP4Plugin) StartRecord(ctx context.Context, req *mp4pb.ReqStartRecord) (res *mp4pb.ResponseStartRecord, err error) {
var recordExists bool
var filePath = "."
var fragment = time.Minute
if req.Fragment != nil {
fragment = req.Fragment.AsDuration()
}
if req.FilePath != "" {
filePath = req.FilePath
}
res = &mp4pb.ResponseStartRecord{}
p.Server.Records.Call(func() error {
_, recordExists = p.Server.Records.Find(func(job *m7s.RecordJob) bool {
@@ -187,11 +375,32 @@ func (p *MP4Plugin) StartRecord(ctx context.Context, req *mp4pb.ReqStartRecord)
if stream, ok := p.Server.Streams.Get(req.StreamPath); ok {
recordConf := config.Record{
Append: false,
Fragment: req.Fragment.AsDuration(),
FilePath: req.FilePath,
Fragment: fragment,
FilePath: filePath,
}
job := p.Record(stream, recordConf, nil)
res.Data = uint64(uintptr(unsafe.Pointer(job.GetTask())))
} else {
err = pkg.ErrNotFound
}
return nil
})
return
}
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
})
@@ -214,7 +423,7 @@ func (p *MP4Plugin) EventStart(ctx context.Context, req *mp4pb.ReqEventRecord) (
p.Error("EventStart", "error", err)
}
}
recorder := p.Meta.Recorder(config.Record{})
//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 {
@@ -230,7 +439,11 @@ func (p *MP4Plugin) EventStart(ctx context.Context, req *mp4pb.ReqEventRecord) (
Fragment: 0,
FilePath: filepath.Join(p.EventRecordFilePath, stream.StreamPath, time.Now().Local().Format("2006-01-02-15-04-05")),
}
recordJob := recorder.GetRecordJob()
//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
@@ -238,10 +451,6 @@ func (p *MP4Plugin) EventStart(ctx context.Context, req *mp4pb.ReqEventRecord) (
recordJob.AfterDuration = afterDuration
recordJob.BeforeDuration = beforeDuration
recordJob.Mode = m7s.RecordModeEvent
var subconfig config.Subscribe
defaults.SetDefaults(&subconfig)
subconfig.BufferTime = beforeDuration
p.Record(stream, recordConf, &subconfig)
}
return nil
})
@@ -258,6 +467,7 @@ func (p *MP4Plugin) EventStart(ctx context.Context, req *mp4pb.ReqEventRecord) (
Mode: m7s.RecordModeEvent,
BeforeDuration: beforeDuration,
AfterDuration: afterDuration,
Type: "mp4",
}
now := time.Now()
startTime := now.Add(-beforeDuration)
@@ -282,6 +492,7 @@ func (p *MP4Plugin) List(ctx context.Context, req *mp4pb.ReqRecordList) (resp *p
PageSize: req.PageSize,
Mode: req.Mode,
Type: "mp4",
EventLevel: req.EventLevel,
}
return p.Server.GetRecordList(ctx, globalReq)
}

View File

@@ -2,6 +2,8 @@ package plugin_mp4
import (
"os"
"path/filepath"
"strings"
"time"
"github.com/shirou/gopsutil/v4/disk"
@@ -57,60 +59,75 @@ type Exception struct {
// }
// 判断磁盘使用量是否中超限
func (p *DeleteRecordTask) getDiskOutOfSpace(max float64) bool {
exePath, err := os.Getwd()
//pwd, _ := os.Getwd()
//fmt.Printf("当前pwd是: %v\n", pwd)
//if err != nil {
// fmt.Printf("Error getting executable path: %v\n", err)
// return false
//}
//// 获取路径的根目录部分
//root := filepath.VolumeName(exePath)
//if root == "" {
// // 在Unix-like系统中根目录是 "/"
// root = "/"
//}
func (p *DeleteRecordTask) getDiskOutOfSpace(filePath string) bool {
exePath := filepath.Dir(filePath)
d, err := disk.Usage(exePath)
if err != nil {
if err != nil || d == nil {
p.Error("getDiskOutOfSpace", "error", err)
}
p.Debug("getDiskOutOfSpace", "current path", exePath, "disk UsedPercent", d.UsedPercent, "total disk space", d.Total,
"disk free", d.Free, "disk usage", d.Used, "AutoOverWriteDiskPercent", p.AutoOverWriteDiskPercent, "DiskMaxPercent", p.DiskMaxPercent)
if d.UsedPercent >= max {
return true
} else {
return false
}
p.plugin.Debug("getDiskOutOfSpace", "current path", exePath, "disk UsedPercent", d.UsedPercent, "total disk space", d.Total,
"disk free", d.Free, "disk usage", d.Used, "AutoOverWriteDiskPercent", p.AutoOverWriteDiskPercent, "DiskMaxPercent", p.DiskMaxPercent)
return d.UsedPercent >= p.AutoOverWriteDiskPercent
}
func (p *DeleteRecordTask) deleteOldestFile() {
//当当前磁盘使用量大于AutoOverWriteDiskPercent自动覆盖磁盘使用量配置时自动删除最旧的文件
//连续录像删除最旧的文件
for p.getDiskOutOfSpace(p.AutoOverWriteDiskPercent) {
queryRecord := m7s.RecordStream{
EventLevel: m7s.EventLevelLow, // 查询条件event_level = 1,非重要事件
// 创建一个数组来存储所有的conf.FilePath
var filePaths []string
if len(p.plugin.GetCommonConf().OnPub.Record) > 0 {
for _, conf := range p.plugin.GetCommonConf().OnPub.Record {
// 处理路径,去掉最后的/$0部分只保留目录部分
dirPath := filepath.Dir(conf.FilePath)
p.Info("deleteOldestFile", "original filepath", conf.FilePath, "processed filepath", dirPath)
filePaths = append(filePaths, dirPath)
}
var eventRecords []m7s.RecordStream
err := p.DB.Where(&queryRecord).Where("end_time != '1970-01-01 00:00:00'").Order("end_time ASC").Limit(1).Find(&eventRecords).Error
if err == nil {
if len(eventRecords) > 0 {
for _, record := range eventRecords {
p.Info("deleteOldestFile", "ready to delete oldestfile,ID", record.ID, "create time", record.EndTime, "filepath", record.FilePath)
err = os.Remove(record.FilePath)
if err != nil {
p.Error("deleteOldestFile", "delete file from disk error", err)
}
err = p.DB.Delete(&record).Error
if err != nil {
p.Error("deleteOldestFile", "delete record from disk error", err)
}
if p.plugin.EventRecordFilePath != "" {
// 同样处理EventRecordFilePath
dirPath := filepath.Dir(p.plugin.EventRecordFilePath)
filePaths = append(filePaths, dirPath)
}
for _, filePath := range filePaths {
for p.getDiskOutOfSpace(filePath) {
queryRecord := m7s.RecordStream{
EventLevel: m7s.EventLevelLow, // 查询条件event_level = 1,非重要事件
}
var eventRecords []m7s.RecordStream
// 使用不同的方法进行路径匹配避免ESCAPE语法问题
// 解决方案用MySQL能理解的简单方式匹配路径前缀
basePath := filePath
// 直接替换所有反斜杠,不需要判断是否包含
basePath = strings.Replace(basePath, "\\", "\\\\", -1)
searchPattern := basePath + "%"
p.Info("deleteOldestFile", "searching with path pattern", searchPattern)
err := p.DB.Where(&queryRecord).Where("end_time IS NOT NULL").
Where("file_path LIKE ?", searchPattern).
Order("end_time ASC").Find(&eventRecords).Error
if err == nil {
if len(eventRecords) > 0 {
p.Info("deleteOldestFile", "found %d records", len(eventRecords))
for _, record := range eventRecords {
p.Info("deleteOldestFile", "ready to delete oldestfile,ID", record.ID, "create time", record.EndTime, "filepath", record.FilePath)
err = os.Remove(record.FilePath)
if err != nil {
p.Error("deleteOldestFile", "delete file from disk error", err)
continue
} else {
err = p.DB.Delete(&record).Error
if err != nil {
p.Error("deleteOldestFile", "delete record from disk error", err)
}
}
}
}
} else {
p.Error("deleteOldestFile", "search record from db error", err)
}
} else {
p.Error("deleteOldestFile", "search record from db error", err)
time.Sleep(time.Second * 3)
}
time.Sleep(time.Second * 3)
}
}
@@ -120,6 +137,7 @@ type DeleteRecordTask struct {
AutoOverWriteDiskPercent float64
RecordFileExpireDays int
DB *gorm.DB
plugin *MP4Plugin
}
func (t *DeleteRecordTask) GetTickInterval() time.Duration {
@@ -138,7 +156,7 @@ func (t *DeleteRecordTask) Tick(any) {
queryRecord := m7s.RecordStream{
EventLevel: m7s.EventLevelLow, // 查询条件event_level = low,非重要事件
}
err := t.DB.Where(&queryRecord).Find(&eventRecords, "end_time < ? AND end_time != '1970-01-01 00:00:00'", expireTime).Error
err := t.DB.Where(&queryRecord).Find(&eventRecords, "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)

View File

@@ -8,7 +8,7 @@ import (
"strings"
"time"
"github.com/Eyevinn/mp4ff/mp4"
"github.com/gobwas/ws/wsutil"
"m7s.live/v5"
v5 "m7s.live/v5/pkg"
"m7s.live/v5/pkg/codec"
@@ -20,83 +20,34 @@ import (
type MediaContext struct {
io.Writer
conn net.Conn
wto time.Duration
seqNumber uint32
muxer *pkg.Muxer
audio, video *pkg.Track
buffer []byte
offset int64
conn net.Conn
wto time.Duration
ws bool
buffer []byte
}
func (m *MediaContext) Write(p []byte) (n int, err error) {
if m.conn != nil {
if m.ws {
m.buffer = append(m.buffer, p...)
return len(p), nil
}
if m.conn != nil && m.wto > 0 {
m.conn.SetWriteDeadline(time.Now().Add(m.wto))
}
return m.Writer.Write(p)
}
func (m *MediaContext) Read(p []byte) (n int, err error) {
if m.offset >= int64(len(m.buffer)) {
return 0, io.EOF
func (m *MediaContext) Flush() (err error) {
if m.ws {
if m.wto > 0 {
m.conn.SetWriteDeadline(time.Now().Add(m.wto))
}
err = wsutil.WriteServerBinary(m.conn, m.buffer)
m.buffer = m.buffer[:0]
}
n = copy(p, m.buffer[m.offset:])
m.offset += int64(n)
return
}
func (m *MediaContext) Seek(offset int64, whence int) (int64, error) {
switch whence {
case io.SeekStart:
m.offset = offset
case io.SeekCurrent:
m.offset += offset
case io.SeekEnd:
m.offset = int64(len(m.buffer)) + offset
}
if m.offset < 0 {
m.offset = 0
}
if m.offset > int64(len(m.buffer)) {
m.offset = int64(len(m.buffer))
}
return m.offset, nil
}
type TrackContext struct {
TrackId uint32
fragment *mp4.Fragment
ts uint32 // 每个小片段起始时间戳
abs uint32 // 绝对起始时间戳
absSet bool // 是否设置过abs
}
func (m *TrackContext) Push(ctx *MediaContext, dt uint32, dur uint32, data []byte, flags uint32) {
if !m.absSet {
m.abs = dt
m.absSet = true
}
dt -= m.abs
if m.fragment != nil && dt-m.ts > 1000 {
m.fragment.Encode(ctx)
m.fragment = nil
}
if m.fragment == nil {
ctx.seqNumber++
m.fragment, _ = mp4.CreateFragment(ctx.seqNumber, m.TrackId)
m.ts = dt
}
m.fragment.AddFullSample(mp4.FullSample{
Data: data,
DecodeTime: uint64(dt),
Sample: mp4.Sample{
Flags: flags,
Dur: dur,
Size: uint32(len(data)),
},
})
}
type MP4Plugin struct {
pb.UnimplementedApiServer
m7s.Plugin
@@ -104,7 +55,8 @@ type MP4Plugin struct {
AfterDuration time.Duration `default:"30s" desc:"事件录像结束时长不配置则默认30s"`
RecordFileExpireDays int `desc:"录像自动删除的天数,0或未设置表示不自动删除"`
DiskMaxPercent float64 `default:"90" desc:"硬盘使用百分之上限值,超上限后触发报警,并停止当前所有磁盘写入动作。"`
AutoOverWriteDiskPercent float64 `default:"80" desc:"自动覆盖功能磁盘占用上限值,超过上限时连续录像自动删除日有录像,事件录像自动删除非重要事件录像,删除规则为删除距离当日最久日期的连续录像或非重要事件录像。"`
AutoOverWriteDiskPercent float64 `default:"0" desc:"自动覆盖功能磁盘占用上限值,超过上限时连续录像自动删除日有录像,事件录像自动删除非重要事件录像,删除规则为删除距离当日最久日期的连续录像或非重要事件录像。"`
AutoRecovery bool `default:"true" desc:"是否自动恢复"`
ExceptionPostUrl string `desc:"第三方异常上报地址"`
EventRecordFilePath string `desc:"事件录像存放地址"`
}
@@ -113,22 +65,42 @@ const defaultConfig m7s.DefaultYaml = `publish:
speed: 1`
// var exceptionChannel = make(chan *Exception)
var _ = m7s.InstallPlugin[MP4Plugin](defaultConfig, &pb.Api_ServiceDesc, pb.RegisterApiHandler, pkg.NewPuller, pkg.NewRecorder)
var _ = m7s.InstallPlugin[MP4Plugin](m7s.PluginMeta{
DefaultYaml: defaultConfig,
ServiceDesc: &pb.Api_ServiceDesc,
RegisterGRPCHandler: pb.RegisterApiHandler,
NewPuller: pkg.NewPuller,
NewRecorder: pkg.NewRecorder,
NewPullProxy: m7s.NewHTTPPullPorxy,
})
func (p *MP4Plugin) RegisterHandler() map[string]http.HandlerFunc {
return map[string]http.HandlerFunc{
"/download/{streamPath...}": p.download,
}
}
func (p *MP4Plugin) OnInit() (err error) {
if p.DB != nil {
err = p.DB.AutoMigrate(&Exception{})
var deleteRecordTask DeleteRecordTask
deleteRecordTask.DB = p.DB
deleteRecordTask.DiskMaxPercent = p.DiskMaxPercent
deleteRecordTask.AutoOverWriteDiskPercent = p.AutoOverWriteDiskPercent
deleteRecordTask.RecordFileExpireDays = p.RecordFileExpireDays
p.AddTask(&deleteRecordTask)
if err != nil {
return
}
if p.AutoOverWriteDiskPercent > 0 {
var deleteRecordTask DeleteRecordTask
deleteRecordTask.DB = p.DB
deleteRecordTask.DiskMaxPercent = p.DiskMaxPercent
deleteRecordTask.AutoOverWriteDiskPercent = p.AutoOverWriteDiskPercent
deleteRecordTask.RecordFileExpireDays = p.RecordFileExpireDays
deleteRecordTask.plugin = p
p.AddTask(&deleteRecordTask)
}
if p.AutoRecovery {
var recoveryTask RecordRecoveryTask
recoveryTask.DB = p.DB
recoveryTask.plugin = p
p.AddTask(&recoveryTask)
}
}
// go func() { //处理所有异常,录像中断异常、录像读取异常、录像导出文件中断、磁盘容量低于阈值异常、磁盘异常
// for exception := range exceptionChannel {
@@ -149,6 +121,7 @@ func (p *MP4Plugin) OnInit() (err error) {
}
return
}
func (p *MP4Plugin) ServeHTTP(w http.ResponseWriter, r *http.Request) {
streamPath := strings.TrimSuffix(strings.TrimPrefix(r.URL.Path, "/"), ".mp4")
if r.URL.RawQuery != "" {
@@ -165,30 +138,34 @@ func (p *MP4Plugin) ServeHTTP(w http.ResponseWriter, r *http.Request) {
if err != nil {
return
}
wto := p.GetCommonConf().WriteTimeout
ctx.wto = p.GetCommonConf().WriteTimeout
if ctx.conn == nil {
w.Header().Set("Transfer-Encoding", "chunked")
w.Header().Set("Content-Type", "video/mp4")
w.WriteHeader(http.StatusOK)
if hijacker, ok := w.(http.Hijacker); ok && wto > 0 {
if hijacker, ok := w.(http.Hijacker); ok && ctx.wto > 0 {
ctx.conn, _, _ = hijacker.Hijack()
ctx.conn.SetWriteDeadline(time.Now().Add(wto))
ctx.conn.SetWriteDeadline(time.Now().Add(ctx.wto))
ctx.Writer = ctx.conn
} else {
ctx.Writer = w
w.(http.Flusher).Flush()
}
}
if ctx.conn != nil {
ctx.Writer = ctx.conn
} else {
ctx.Writer = w
w.(http.Flusher).Flush()
ctx.ws = true
ctx.Writer = ctx.conn
}
ctx.wto = p.GetCommonConf().WriteTimeout
ctx.muxer = pkg.NewMuxer(pkg.FLAG_FRAGMENT)
ctx.muxer.WriteInitSegment(ctx.Writer)
muxer := pkg.NewMuxer(pkg.FLAG_FRAGMENT)
err = muxer.WriteInitSegment(&ctx)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
var offsetAudio, offsetVideo = 1, 5
if sub.Publisher.HasVideoTrack() {
var audio, video *pkg.Track
var nextFragmentId uint32
if sub.Publisher.HasVideoTrack() && sub.SubVideo {
v := sub.Publisher.VideoTrack.AVTrack
if err = v.WaitReady(); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
@@ -201,24 +178,33 @@ func (p *MP4Plugin) ServeHTTP(w http.ResponseWriter, r *http.Request) {
case codec.FourCC_H265:
codecID = box.MP4_CODEC_H265
}
ctx.video = ctx.muxer.AddTrack(codecID)
ctx.video.Timescale = 1000
video = muxer.AddTrack(codecID)
video.Timescale = 1000
video.Samplelist = []box.Sample{
{
Offset: 0,
Data: nil,
Size: 0,
Timestamp: 0,
Duration: 0,
KeyFrame: true,
},
}
switch v.ICodecCtx.FourCC() {
case codec.FourCC_H264:
h264Ctx := v.ICodecCtx.GetBase().(*codec.H264Ctx)
ctx.video.ExtraData = h264Ctx.Record
ctx.video.Width = uint32(h264Ctx.Width())
ctx.video.Height = uint32(h264Ctx.Height())
video.ExtraData = h264Ctx.Record
video.Width = uint32(h264Ctx.Width())
video.Height = uint32(h264Ctx.Height())
case codec.FourCC_H265:
h265Ctx := v.ICodecCtx.GetBase().(*codec.H265Ctx)
ctx.video.ExtraData = h265Ctx.Record
ctx.video.Width = uint32(h265Ctx.Width())
ctx.video.Height = uint32(h265Ctx.Height())
video.ExtraData = h265Ctx.Record
video.Width = uint32(h265Ctx.Width())
video.Height = uint32(h265Ctx.Height())
}
}
if sub.Publisher.HasAudioTrack() {
if sub.Publisher.HasAudioTrack() && sub.SubAudio {
a := sub.Publisher.AudioTrack.AVTrack
if err = a.WaitReady(); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
@@ -228,60 +214,98 @@ func (p *MP4Plugin) ServeHTTP(w http.ResponseWriter, r *http.Request) {
switch a.ICodecCtx.FourCC() {
case codec.FourCC_MP4A:
codecID = box.MP4_CODEC_AAC
case codec.FourCC_ALAW:
codecID = box.MP4_CODEC_G711A
case codec.FourCC_ULAW:
codecID = box.MP4_CODEC_G711U
case codec.FourCC_OPUS:
codecID = box.MP4_CODEC_OPUS
}
ctx.audio = ctx.muxer.AddTrack(codecID)
ctx.audio.Timescale = 1000
audio = muxer.AddTrack(codecID)
audio.Timescale = 1000
audioCtx := a.ICodecCtx.(v5.IAudioCodecCtx)
ctx.audio.SampleRate = uint32(audioCtx.GetSampleRate())
ctx.audio.ChannelCount = uint8(audioCtx.GetChannels())
ctx.audio.SampleSize = uint16(audioCtx.GetSampleSize())
audio.SampleRate = uint32(audioCtx.GetSampleRate())
audio.ChannelCount = uint8(audioCtx.GetChannels())
audio.SampleSize = uint16(audioCtx.GetSampleSize())
audio.Samplelist = []box.Sample{
{
Offset: 0,
Data: nil,
Size: 0,
Timestamp: 0,
Duration: 0,
KeyFrame: true,
},
}
switch a.ICodecCtx.FourCC() {
case codec.FourCC_MP4A:
offsetAudio = 2
ctx.audio.ExtraData = a.ICodecCtx.GetBase().(*codec.AACCtx).ConfigBytes
audio.ExtraData = a.ICodecCtx.GetBase().(*codec.AACCtx).ConfigBytes
default:
offsetAudio = 1
}
}
err = ctx.muxer.WriteInitSegment(&ctx)
err = muxer.WriteMoov(&ctx)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
m7s.PlayBlock(sub, func(audio *rtmp.RTMPAudio) error {
bs := audio.Memory.ToBytes()
if ctx.ws {
ctx.Flush()
}
m7s.PlayBlock(sub, func(frame *rtmp.RTMPAudio) (err error) {
bs := frame.Memory.ToBytes()
if offsetAudio == 2 && bs[1] == 0 {
return nil
}
sample := box.Sample{
Offset: 0,
Data: bs[offsetAudio:],
Size: len(bs) - offsetAudio,
DTS: uint64(audio.Timestamp),
PTS: uint64(audio.Timestamp),
KeyFrame: true,
if audio.Samplelist[0].Data != nil {
audio.Samplelist[0].Duration = sub.AudioReader.AbsTime - audio.Samplelist[0].Timestamp
nextFragmentId++
// Create moof box for this track
moof := audio.MakeMoof(nextFragmentId)
// Create mdat box for this track
mdat := box.CreateDataBox(box.TypeMDAT, audio.Samplelist[0].Data)
box.WriteTo(&ctx, moof, mdat)
if ctx.ws {
err = ctx.Flush()
}
}
ctx.audio.AddSampleEntry(sample)
return nil
}, func(video *rtmp.RTMPVideo) error {
bs := video.Memory.ToBytes()
if ctx, ok := sub.VideoReader.Track.ICodecCtx.(*rtmp.H265Ctx); ok && ctx.Enhanced && bs[0]&0b1111 == rtmp.PacketTypeCodedFrames {
offsetVideo = 8
audio.Samplelist[0].Timestamp = sub.AudioReader.AbsTime
audio.Samplelist[0].Data = bs[offsetAudio:]
audio.Samplelist[0].Size = len(audio.Samplelist[0].Data)
return
}, func(frame *rtmp.RTMPVideo) (err error) {
bs := frame.Memory.ToBytes()
if ctx, ok := sub.VideoReader.Track.ICodecCtx.(*rtmp.H265Ctx); ok && ctx.Enhanced {
switch bs[0] & 0b1111 {
case rtmp.PacketTypeCodedFrames:
offsetVideo = 8
case rtmp.PacketTypeSequenceStart:
return nil
}
} else {
if bs[1] == 0 {
return nil
}
offsetVideo = 5
}
sample := box.Sample{
Offset: 0,
Data: bs[offsetVideo:],
Size: len(bs) - offsetVideo,
DTS: uint64(video.Timestamp),
PTS: uint64(video.Timestamp),
KeyFrame: sub.VideoReader.Value.IDR,
if video.Samplelist[0].Data != nil {
video.Samplelist[0].Duration = sub.VideoReader.AbsTime - video.Samplelist[0].Timestamp
nextFragmentId++
// Create moof box for this track
moof := video.MakeMoof(nextFragmentId)
// Create mdat box for this track
mdat := box.CreateDataBox(box.TypeMDAT, video.Samplelist[0].Data)
box.WriteTo(&ctx, moof, mdat)
if ctx.ws {
err = ctx.Flush()
}
}
ctx.video.AddSampleEntry(sample)
return nil
video.Samplelist[0].Data = bs[offsetVideo:]
video.Samplelist[0].Size = len(bs) - offsetVideo
video.Samplelist[0].Timestamp = sub.VideoReader.AbsTime
video.Samplelist[0].CTS = frame.CTS
video.Samplelist[0].KeyFrame = sub.VideoReader.Value.IDR
return
})
}

View File

@@ -1,7 +1,7 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.28.1
// protoc v3.19.1
// protoc-gen-go v1.36.5
// protoc v5.28.3
// source: mp4.proto
package pb
@@ -12,10 +12,10 @@ import (
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
durationpb "google.golang.org/protobuf/types/known/durationpb"
emptypb "google.golang.org/protobuf/types/known/emptypb"
_ "google.golang.org/protobuf/types/known/timestamppb"
pb "m7s.live/v5/pb"
reflect "reflect"
sync "sync"
unsafe "unsafe"
)
const (
@@ -26,26 +26,24 @@ const (
)
type ReqRecordList struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
state protoimpl.MessageState `protogen:"open.v1"`
StreamPath string `protobuf:"bytes,1,opt,name=streamPath,proto3" json:"streamPath,omitempty"`
Range string `protobuf:"bytes,2,opt,name=range,proto3" json:"range,omitempty"`
Start string `protobuf:"bytes,3,opt,name=start,proto3" json:"start,omitempty"`
End string `protobuf:"bytes,4,opt,name=end,proto3" json:"end,omitempty"`
PageNum uint32 `protobuf:"varint,5,opt,name=pageNum,proto3" json:"pageNum,omitempty"`
PageSize uint32 `protobuf:"varint,6,opt,name=pageSize,proto3" json:"pageSize,omitempty"`
Mode string `protobuf:"bytes,7,opt,name=mode,proto3" json:"mode,omitempty"`
EventLevel string `protobuf:"bytes,8,opt,name=eventLevel,proto3" json:"eventLevel,omitempty"`
unknownFields protoimpl.UnknownFields
StreamPath string `protobuf:"bytes,1,opt,name=streamPath,proto3" json:"streamPath,omitempty"`
Range string `protobuf:"bytes,2,opt,name=range,proto3" json:"range,omitempty"`
Start string `protobuf:"bytes,3,opt,name=start,proto3" json:"start,omitempty"`
End string `protobuf:"bytes,4,opt,name=end,proto3" json:"end,omitempty"`
PageNum uint32 `protobuf:"varint,5,opt,name=pageNum,proto3" json:"pageNum,omitempty"`
PageSize uint32 `protobuf:"varint,6,opt,name=pageSize,proto3" json:"pageSize,omitempty"`
Mode string `protobuf:"bytes,7,opt,name=mode,proto3" json:"mode,omitempty"`
sizeCache protoimpl.SizeCache
}
func (x *ReqRecordList) Reset() {
*x = ReqRecordList{}
if protoimpl.UnsafeEnabled {
mi := &file_mp4_proto_msgTypes[0]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
mi := &file_mp4_proto_msgTypes[0]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *ReqRecordList) String() string {
@@ -56,7 +54,7 @@ func (*ReqRecordList) ProtoMessage() {}
func (x *ReqRecordList) ProtoReflect() protoreflect.Message {
mi := &file_mp4_proto_msgTypes[0]
if protoimpl.UnsafeEnabled && x != nil {
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
@@ -120,25 +118,29 @@ func (x *ReqRecordList) GetMode() string {
return ""
}
type ReqRecordDelete struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
func (x *ReqRecordList) GetEventLevel() string {
if x != nil {
return x.EventLevel
}
return ""
}
StreamPath string `protobuf:"bytes,1,opt,name=streamPath,proto3" json:"streamPath,omitempty"`
Ids []uint32 `protobuf:"varint,2,rep,packed,name=ids,proto3" json:"ids,omitempty"`
StartTime string `protobuf:"bytes,3,opt,name=startTime,proto3" json:"startTime,omitempty"`
EndTime string `protobuf:"bytes,4,opt,name=endTime,proto3" json:"endTime,omitempty"`
Range string `protobuf:"bytes,5,opt,name=range,proto3" json:"range,omitempty"`
type ReqRecordDelete struct {
state protoimpl.MessageState `protogen:"open.v1"`
StreamPath string `protobuf:"bytes,1,opt,name=streamPath,proto3" json:"streamPath,omitempty"`
Ids []uint32 `protobuf:"varint,2,rep,packed,name=ids,proto3" json:"ids,omitempty"`
StartTime string `protobuf:"bytes,3,opt,name=startTime,proto3" json:"startTime,omitempty"`
EndTime string `protobuf:"bytes,4,opt,name=endTime,proto3" json:"endTime,omitempty"`
Range string `protobuf:"bytes,5,opt,name=range,proto3" json:"range,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *ReqRecordDelete) Reset() {
*x = ReqRecordDelete{}
if protoimpl.UnsafeEnabled {
mi := &file_mp4_proto_msgTypes[1]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
mi := &file_mp4_proto_msgTypes[1]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *ReqRecordDelete) String() string {
@@ -149,7 +151,7 @@ func (*ReqRecordDelete) ProtoMessage() {}
func (x *ReqRecordDelete) ProtoReflect() protoreflect.Message {
mi := &file_mp4_proto_msgTypes[1]
if protoimpl.UnsafeEnabled && x != nil {
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
@@ -200,28 +202,24 @@ func (x *ReqRecordDelete) GetRange() string {
}
type ReqEventRecord struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
StreamPath string `protobuf:"bytes,1,opt,name=streamPath,proto3" json:"streamPath,omitempty"`
EventId string `protobuf:"bytes,2,opt,name=eventId,proto3" json:"eventId,omitempty"`
Mode string `protobuf:"bytes,3,opt,name=mode,proto3" json:"mode,omitempty"` //auto=连续录像模式event=事件录像模式
EventName string `protobuf:"bytes,4,opt,name=eventName,proto3" json:"eventName,omitempty"`
BeforeDuration string `protobuf:"bytes,5,opt,name=beforeDuration,proto3" json:"beforeDuration,omitempty"`
AfterDuration string `protobuf:"bytes,6,opt,name=afterDuration,proto3" json:"afterDuration,omitempty"`
EventDesc string `protobuf:"bytes,7,opt,name=eventDesc,proto3" json:"eventDesc,omitempty"`
EventLevel string `protobuf:"bytes,8,opt,name=eventLevel,proto3" json:"eventLevel,omitempty"` //事件级别,0表示重要事件无法删除且表示无需自动删除,1表示非重要事件,达到自动删除时间后,自动删除
Fragment string `protobuf:"bytes,9,opt,name=fragment,proto3" json:"fragment,omitempty"`
state protoimpl.MessageState `protogen:"open.v1"`
StreamPath string `protobuf:"bytes,1,opt,name=streamPath,proto3" json:"streamPath,omitempty"`
EventId string `protobuf:"bytes,2,opt,name=eventId,proto3" json:"eventId,omitempty"`
EventName string `protobuf:"bytes,3,opt,name=eventName,proto3" json:"eventName,omitempty"`
BeforeDuration string `protobuf:"bytes,4,opt,name=beforeDuration,proto3" json:"beforeDuration,omitempty"`
AfterDuration string `protobuf:"bytes,5,opt,name=afterDuration,proto3" json:"afterDuration,omitempty"`
EventDesc string `protobuf:"bytes,6,opt,name=eventDesc,proto3" json:"eventDesc,omitempty"`
EventLevel string `protobuf:"bytes,7,opt,name=eventLevel,proto3" json:"eventLevel,omitempty"` //事件级别,0表示重要事件无法删除且表示无需自动删除,1表示非重要事件,达到自动删除时间后,自动删除
Fragment string `protobuf:"bytes,8,opt,name=fragment,proto3" json:"fragment,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *ReqEventRecord) Reset() {
*x = ReqEventRecord{}
if protoimpl.UnsafeEnabled {
mi := &file_mp4_proto_msgTypes[2]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
mi := &file_mp4_proto_msgTypes[2]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *ReqEventRecord) String() string {
@@ -232,7 +230,7 @@ func (*ReqEventRecord) ProtoMessage() {}
func (x *ReqEventRecord) ProtoReflect() protoreflect.Message {
mi := &file_mp4_proto_msgTypes[2]
if protoimpl.UnsafeEnabled && x != nil {
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
@@ -261,13 +259,6 @@ func (x *ReqEventRecord) GetEventId() string {
return ""
}
func (x *ReqEventRecord) GetMode() string {
if x != nil {
return x.Mode
}
return ""
}
func (x *ReqEventRecord) GetEventName() string {
if x != nil {
return x.EventName
@@ -311,22 +302,19 @@ func (x *ReqEventRecord) GetFragment() string {
}
type ResponseEventRecord struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
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"`
Data uint32 `protobuf:"varint,3,opt,name=data,proto3" json:"data,omitempty"`
unknownFields protoimpl.UnknownFields
Code int32 `protobuf:"varint,1,opt,name=code,proto3" json:"code,omitempty"`
Message string `protobuf:"bytes,2,opt,name=message,proto3" json:"message,omitempty"`
Data uint32 `protobuf:"varint,3,opt,name=data,proto3" json:"data,omitempty"`
sizeCache protoimpl.SizeCache
}
func (x *ResponseEventRecord) Reset() {
*x = ResponseEventRecord{}
if protoimpl.UnsafeEnabled {
mi := &file_mp4_proto_msgTypes[3]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
mi := &file_mp4_proto_msgTypes[3]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *ResponseEventRecord) String() string {
@@ -337,7 +325,7 @@ func (*ResponseEventRecord) ProtoMessage() {}
func (x *ResponseEventRecord) ProtoReflect() protoreflect.Message {
mi := &file_mp4_proto_msgTypes[3]
if protoimpl.UnsafeEnabled && x != nil {
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
@@ -374,22 +362,19 @@ func (x *ResponseEventRecord) GetData() uint32 {
}
type ReqStartRecord struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
state protoimpl.MessageState `protogen:"open.v1"`
StreamPath string `protobuf:"bytes,1,opt,name=streamPath,proto3" json:"streamPath,omitempty"`
Fragment *durationpb.Duration `protobuf:"bytes,2,opt,name=fragment,proto3" json:"fragment,omitempty"`
FilePath string `protobuf:"bytes,3,opt,name=filePath,proto3" json:"filePath,omitempty"`
unknownFields protoimpl.UnknownFields
StreamPath string `protobuf:"bytes,1,opt,name=streamPath,proto3" json:"streamPath,omitempty"`
Fragment *durationpb.Duration `protobuf:"bytes,2,opt,name=fragment,proto3" json:"fragment,omitempty"`
FilePath string `protobuf:"bytes,3,opt,name=filePath,proto3" json:"filePath,omitempty"`
sizeCache protoimpl.SizeCache
}
func (x *ReqStartRecord) Reset() {
*x = ReqStartRecord{}
if protoimpl.UnsafeEnabled {
mi := &file_mp4_proto_msgTypes[4]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
mi := &file_mp4_proto_msgTypes[4]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *ReqStartRecord) String() string {
@@ -400,7 +385,7 @@ func (*ReqStartRecord) ProtoMessage() {}
func (x *ReqStartRecord) ProtoReflect() protoreflect.Message {
mi := &file_mp4_proto_msgTypes[4]
if protoimpl.UnsafeEnabled && x != nil {
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
@@ -437,22 +422,19 @@ func (x *ReqStartRecord) GetFilePath() string {
}
type ResponseStartRecord struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
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"`
Data uint64 `protobuf:"varint,3,opt,name=data,proto3" json:"data,omitempty"`
unknownFields protoimpl.UnknownFields
Code int32 `protobuf:"varint,1,opt,name=code,proto3" json:"code,omitempty"`
Message string `protobuf:"bytes,2,opt,name=message,proto3" json:"message,omitempty"`
Data uint64 `protobuf:"varint,3,opt,name=data,proto3" json:"data,omitempty"`
sizeCache protoimpl.SizeCache
}
func (x *ResponseStartRecord) Reset() {
*x = ResponseStartRecord{}
if protoimpl.UnsafeEnabled {
mi := &file_mp4_proto_msgTypes[5]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
mi := &file_mp4_proto_msgTypes[5]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *ResponseStartRecord) String() string {
@@ -463,7 +445,7 @@ func (*ResponseStartRecord) ProtoMessage() {}
func (x *ResponseStartRecord) ProtoReflect() protoreflect.Message {
mi := &file_mp4_proto_msgTypes[5]
if protoimpl.UnsafeEnabled && x != nil {
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
@@ -499,152 +481,274 @@ func (x *ResponseStartRecord) GetData() uint64 {
return 0
}
type ReqStopRecord struct {
state protoimpl.MessageState `protogen:"open.v1"`
StreamPath string `protobuf:"bytes,1,opt,name=streamPath,proto3" json:"streamPath,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *ReqStopRecord) Reset() {
*x = ReqStopRecord{}
mi := &file_mp4_proto_msgTypes[6]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *ReqStopRecord) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*ReqStopRecord) ProtoMessage() {}
func (x *ReqStopRecord) ProtoReflect() protoreflect.Message {
mi := &file_mp4_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 ReqStopRecord.ProtoReflect.Descriptor instead.
func (*ReqStopRecord) Descriptor() ([]byte, []int) {
return file_mp4_proto_rawDescGZIP(), []int{6}
}
func (x *ReqStopRecord) GetStreamPath() string {
if x != nil {
return x.StreamPath
}
return ""
}
type ResponseStopRecord 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"`
Data uint64 `protobuf:"varint,3,opt,name=data,proto3" json:"data,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *ResponseStopRecord) Reset() {
*x = ResponseStopRecord{}
mi := &file_mp4_proto_msgTypes[7]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *ResponseStopRecord) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*ResponseStopRecord) ProtoMessage() {}
func (x *ResponseStopRecord) ProtoReflect() protoreflect.Message {
mi := &file_mp4_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 ResponseStopRecord.ProtoReflect.Descriptor instead.
func (*ResponseStopRecord) Descriptor() ([]byte, []int) {
return file_mp4_proto_rawDescGZIP(), []int{7}
}
func (x *ResponseStopRecord) GetCode() int32 {
if x != nil {
return x.Code
}
return 0
}
func (x *ResponseStopRecord) GetMessage() string {
if x != nil {
return x.Message
}
return ""
}
func (x *ResponseStopRecord) GetData() uint64 {
if x != nil {
return x.Data
}
return 0
}
var File_mp4_proto protoreflect.FileDescriptor
var file_mp4_proto_rawDesc = []byte{
var file_mp4_proto_rawDesc = string([]byte{
0x0a, 0x09, 0x6d, 0x70, 0x34, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x03, 0x6d, 0x70, 0x34,
0x1a, 0x1c, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x61, 0x6e, 0x6e,
0x6f, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x1b,
0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f,
0x65, 0x6d, 0x70, 0x74, 0x79, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x1f, 0x67, 0x6f, 0x6f,
0x67, 0x6c, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x74, 0x69, 0x6d,
0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x1e, 0x67, 0x6f,
0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x64, 0x75,
0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x0c, 0x67, 0x6c,
0x6f, 0x62, 0x61, 0x6c, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0xb7, 0x01, 0x0a, 0x0d, 0x52,
0x65, 0x71, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x4c, 0x69, 0x73, 0x74, 0x12, 0x1e, 0x0a, 0x0a,
0x73, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x50, 0x61, 0x74, 0x68, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09,
0x52, 0x0a, 0x73, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x50, 0x61, 0x74, 0x68, 0x12, 0x14, 0x0a, 0x05,
0x72, 0x61, 0x6e, 0x67, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x72, 0x61, 0x6e,
0x67, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x73, 0x74, 0x61, 0x72, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28,
0x09, 0x52, 0x05, 0x73, 0x74, 0x61, 0x72, 0x74, 0x12, 0x10, 0x0a, 0x03, 0x65, 0x6e, 0x64, 0x18,
0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x65, 0x6e, 0x64, 0x12, 0x18, 0x0a, 0x07, 0x70, 0x61,
0x67, 0x65, 0x4e, 0x75, 0x6d, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x07, 0x70, 0x61, 0x67,
0x65, 0x4e, 0x75, 0x6d, 0x12, 0x1a, 0x0a, 0x08, 0x70, 0x61, 0x67, 0x65, 0x53, 0x69, 0x7a, 0x65,
0x18, 0x06, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x08, 0x70, 0x61, 0x67, 0x65, 0x53, 0x69, 0x7a, 0x65,
0x12, 0x12, 0x0a, 0x04, 0x6d, 0x6f, 0x64, 0x65, 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04,
0x6d, 0x6f, 0x64, 0x65, 0x22, 0x91, 0x01, 0x0a, 0x0f, 0x52, 0x65, 0x71, 0x52, 0x65, 0x63, 0x6f,
0x72, 0x64, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x12, 0x1e, 0x0a, 0x0a, 0x73, 0x74, 0x72, 0x65,
0x61, 0x6d, 0x50, 0x61, 0x74, 0x68, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x73, 0x74,
0x72, 0x65, 0x61, 0x6d, 0x50, 0x61, 0x74, 0x68, 0x12, 0x10, 0x0a, 0x03, 0x69, 0x64, 0x73, 0x18,
0x02, 0x20, 0x03, 0x28, 0x0d, 0x52, 0x03, 0x69, 0x64, 0x73, 0x12, 0x1c, 0x0a, 0x09, 0x73, 0x74,
0x61, 0x72, 0x74, 0x54, 0x69, 0x6d, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x73,
0x74, 0x61, 0x72, 0x74, 0x54, 0x69, 0x6d, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x65, 0x6e, 0x64, 0x54,
0x69, 0x6d, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x65, 0x6e, 0x64, 0x54, 0x69,
0x6d, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x72, 0x61, 0x6e, 0x67, 0x65, 0x18, 0x05, 0x20, 0x01, 0x28,
0x09, 0x52, 0x05, 0x72, 0x61, 0x6e, 0x67, 0x65, 0x22, 0xa4, 0x02, 0x0a, 0x0e, 0x52, 0x65, 0x71,
0x45, 0x76, 0x65, 0x6e, 0x74, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x12, 0x1e, 0x0a, 0x0a, 0x73,
0x65, 0x6d, 0x70, 0x74, 0x79, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x1e, 0x67, 0x6f, 0x6f,
0x67, 0x6c, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x64, 0x75, 0x72,
0x61, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x0c, 0x67, 0x6c, 0x6f,
0x62, 0x61, 0x6c, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0xd7, 0x01, 0x0a, 0x0d, 0x52, 0x65,
0x71, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x4c, 0x69, 0x73, 0x74, 0x12, 0x1e, 0x0a, 0x0a, 0x73,
0x74, 0x72, 0x65, 0x61, 0x6d, 0x50, 0x61, 0x74, 0x68, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52,
0x0a, 0x73, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x50, 0x61, 0x74, 0x68, 0x12, 0x18, 0x0a, 0x07, 0x65,
0x76, 0x65, 0x6e, 0x74, 0x49, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x65, 0x76,
0x65, 0x6e, 0x74, 0x49, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x6d, 0x6f, 0x64, 0x65, 0x18, 0x03, 0x20,
0x01, 0x28, 0x09, 0x52, 0x04, 0x6d, 0x6f, 0x64, 0x65, 0x12, 0x1c, 0x0a, 0x09, 0x65, 0x76, 0x65,
0x6e, 0x74, 0x4e, 0x61, 0x6d, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x65, 0x76,
0x65, 0x6e, 0x74, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x26, 0x0a, 0x0e, 0x62, 0x65, 0x66, 0x6f, 0x72,
0x65, 0x44, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52,
0x0e, 0x62, 0x65, 0x66, 0x6f, 0x72, 0x65, 0x44, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12,
0x24, 0x0a, 0x0d, 0x61, 0x66, 0x74, 0x65, 0x72, 0x44, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e,
0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x61, 0x66, 0x74, 0x65, 0x72, 0x44, 0x75, 0x72,
0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x1c, 0x0a, 0x09, 0x65, 0x76, 0x65, 0x6e, 0x74, 0x44, 0x65,
0x73, 0x63, 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x65, 0x76, 0x65, 0x6e, 0x74, 0x44,
0x65, 0x73, 0x63, 0x12, 0x1e, 0x0a, 0x0a, 0x65, 0x76, 0x65, 0x6e, 0x74, 0x4c, 0x65, 0x76, 0x65,
0x0a, 0x73, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x50, 0x61, 0x74, 0x68, 0x12, 0x14, 0x0a, 0x05, 0x72,
0x61, 0x6e, 0x67, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x72, 0x61, 0x6e, 0x67,
0x65, 0x12, 0x14, 0x0a, 0x05, 0x73, 0x74, 0x61, 0x72, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09,
0x52, 0x05, 0x73, 0x74, 0x61, 0x72, 0x74, 0x12, 0x10, 0x0a, 0x03, 0x65, 0x6e, 0x64, 0x18, 0x04,
0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x65, 0x6e, 0x64, 0x12, 0x18, 0x0a, 0x07, 0x70, 0x61, 0x67,
0x65, 0x4e, 0x75, 0x6d, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x07, 0x70, 0x61, 0x67, 0x65,
0x4e, 0x75, 0x6d, 0x12, 0x1a, 0x0a, 0x08, 0x70, 0x61, 0x67, 0x65, 0x53, 0x69, 0x7a, 0x65, 0x18,
0x06, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x08, 0x70, 0x61, 0x67, 0x65, 0x53, 0x69, 0x7a, 0x65, 0x12,
0x12, 0x0a, 0x04, 0x6d, 0x6f, 0x64, 0x65, 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6d,
0x6f, 0x64, 0x65, 0x12, 0x1e, 0x0a, 0x0a, 0x65, 0x76, 0x65, 0x6e, 0x74, 0x4c, 0x65, 0x76, 0x65,
0x6c, 0x18, 0x08, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x65, 0x76, 0x65, 0x6e, 0x74, 0x4c, 0x65,
0x76, 0x65, 0x6c, 0x12, 0x1a, 0x0a, 0x08, 0x66, 0x72, 0x61, 0x67, 0x6d, 0x65, 0x6e, 0x74, 0x18,
0x09, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x66, 0x72, 0x61, 0x67, 0x6d, 0x65, 0x6e, 0x74, 0x22,
0x57, 0x0a, 0x13, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x45, 0x76, 0x65, 0x6e, 0x74,
0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x63, 0x6f, 0x64, 0x65, 0x18, 0x01,
0x20, 0x01, 0x28, 0x05, 0x52, 0x04, 0x63, 0x6f, 0x64, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x6d, 0x65,
0x73, 0x73, 0x61, 0x67, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x6d, 0x65, 0x73,
0x73, 0x61, 0x67, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x64, 0x61, 0x74, 0x61, 0x18, 0x03, 0x20, 0x01,
0x28, 0x0d, 0x52, 0x04, 0x64, 0x61, 0x74, 0x61, 0x22, 0x83, 0x01, 0x0a, 0x0e, 0x52, 0x65, 0x71,
0x53, 0x74, 0x61, 0x72, 0x74, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x12, 0x1e, 0x0a, 0x0a, 0x73,
0x74, 0x72, 0x65, 0x61, 0x6d, 0x50, 0x61, 0x74, 0x68, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52,
0x0a, 0x73, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x50, 0x61, 0x74, 0x68, 0x12, 0x35, 0x0a, 0x08, 0x66,
0x72, 0x61, 0x67, 0x6d, 0x65, 0x6e, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x19, 0x2e,
0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e,
0x44, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x08, 0x66, 0x72, 0x61, 0x67, 0x6d, 0x65,
0x6e, 0x74, 0x12, 0x1a, 0x0a, 0x08, 0x66, 0x69, 0x6c, 0x65, 0x50, 0x61, 0x74, 0x68, 0x18, 0x03,
0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x66, 0x69, 0x6c, 0x65, 0x50, 0x61, 0x74, 0x68, 0x22, 0x57,
0x0a, 0x13, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x53, 0x74, 0x61, 0x72, 0x74, 0x52,
0x65, 0x63, 0x6f, 0x72, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x63, 0x6f, 0x64, 0x65, 0x18, 0x01, 0x20,
0x01, 0x28, 0x05, 0x52, 0x04, 0x63, 0x6f, 0x64, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x6d, 0x65, 0x73,
0x73, 0x61, 0x67, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x6d, 0x65, 0x73, 0x73,
0x61, 0x67, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x64, 0x61, 0x74, 0x61, 0x18, 0x03, 0x20, 0x01, 0x28,
0x04, 0x52, 0x04, 0x64, 0x61, 0x74, 0x61, 0x32, 0xdf, 0x03, 0x0a, 0x03, 0x61, 0x70, 0x69, 0x12,
0x57, 0x0a, 0x04, 0x4c, 0x69, 0x73, 0x74, 0x12, 0x12, 0x2e, 0x6d, 0x70, 0x34, 0x2e, 0x52, 0x65,
0x71, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x4c, 0x69, 0x73, 0x74, 0x1a, 0x14, 0x2e, 0x67, 0x6c,
0x6f, 0x62, 0x61, 0x6c, 0x2e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x4c, 0x69, 0x73,
0x74, 0x22, 0x25, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x1f, 0x12, 0x1d, 0x2f, 0x6d, 0x70, 0x34, 0x2f,
0x61, 0x70, 0x69, 0x2f, 0x6c, 0x69, 0x73, 0x74, 0x2f, 0x7b, 0x73, 0x74, 0x72, 0x65, 0x61, 0x6d,
0x50, 0x61, 0x74, 0x68, 0x3d, 0x2a, 0x2a, 0x7d, 0x12, 0x54, 0x0a, 0x07, 0x43, 0x61, 0x74, 0x61,
0x6c, 0x6f, 0x67, 0x12, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f,
0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x17, 0x2e, 0x67, 0x6c,
0x6f, 0x62, 0x61, 0x6c, 0x2e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x43, 0x61, 0x74,
0x61, 0x6c, 0x6f, 0x67, 0x22, 0x18, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x12, 0x12, 0x10, 0x2f, 0x6d,
0x70, 0x34, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x63, 0x61, 0x74, 0x61, 0x6c, 0x6f, 0x67, 0x12, 0x62,
0x0a, 0x06, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x12, 0x14, 0x2e, 0x6d, 0x70, 0x34, 0x2e, 0x52,
0x65, 0x71, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x1a, 0x16,
0x2e, 0x67, 0x6c, 0x6f, 0x62, 0x61, 0x6c, 0x2e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65,
0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x22, 0x2a, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x24, 0x22, 0x1f,
0x2f, 0x6d, 0x70, 0x34, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x64, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x2f,
0x7b, 0x73, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x50, 0x61, 0x74, 0x68, 0x3d, 0x2a, 0x2a, 0x7d, 0x3a,
0x01, 0x2a, 0x12, 0x5c, 0x0a, 0x0a, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x53, 0x74, 0x61, 0x72, 0x74,
0x12, 0x13, 0x2e, 0x6d, 0x70, 0x34, 0x2e, 0x52, 0x65, 0x71, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x52,
0x65, 0x63, 0x6f, 0x72, 0x64, 0x1a, 0x18, 0x2e, 0x6d, 0x70, 0x34, 0x2e, 0x52, 0x65, 0x73, 0x70,
0x6f, 0x6e, 0x73, 0x65, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x22,
0x1f, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x19, 0x22, 0x14, 0x2f, 0x6d, 0x70, 0x34, 0x2f, 0x61, 0x70,
0x69, 0x2f, 0x65, 0x76, 0x65, 0x6e, 0x74, 0x2f, 0x73, 0x74, 0x61, 0x72, 0x74, 0x3a, 0x01, 0x2a,
0x12, 0x67, 0x0a, 0x0b, 0x53, 0x74, 0x61, 0x72, 0x74, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x12,
0x13, 0x2e, 0x6d, 0x70, 0x34, 0x2e, 0x52, 0x65, 0x71, 0x53, 0x74, 0x61, 0x72, 0x74, 0x52, 0x65,
0x63, 0x6f, 0x72, 0x64, 0x1a, 0x18, 0x2e, 0x6d, 0x70, 0x34, 0x2e, 0x52, 0x65, 0x73, 0x70, 0x6f,
0x6e, 0x73, 0x65, 0x53, 0x74, 0x61, 0x72, 0x74, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x22, 0x29,
0x82, 0xd3, 0xe4, 0x93, 0x02, 0x23, 0x22, 0x1e, 0x2f, 0x6d, 0x70, 0x34, 0x2f, 0x61, 0x70, 0x69,
0x2f, 0x73, 0x74, 0x61, 0x72, 0x74, 0x2f, 0x7b, 0x73, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x50, 0x61,
0x74, 0x68, 0x3d, 0x2a, 0x2a, 0x7d, 0x3a, 0x01, 0x2a, 0x42, 0x1b, 0x5a, 0x19, 0x6d, 0x37, 0x73,
0x2e, 0x6c, 0x69, 0x76, 0x65, 0x2f, 0x76, 0x35, 0x2f, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x2f,
0x6d, 0x70, 0x34, 0x2f, 0x70, 0x62, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
}
0x76, 0x65, 0x6c, 0x22, 0x91, 0x01, 0x0a, 0x0f, 0x52, 0x65, 0x71, 0x52, 0x65, 0x63, 0x6f, 0x72,
0x64, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x12, 0x1e, 0x0a, 0x0a, 0x73, 0x74, 0x72, 0x65, 0x61,
0x6d, 0x50, 0x61, 0x74, 0x68, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x73, 0x74, 0x72,
0x65, 0x61, 0x6d, 0x50, 0x61, 0x74, 0x68, 0x12, 0x10, 0x0a, 0x03, 0x69, 0x64, 0x73, 0x18, 0x02,
0x20, 0x03, 0x28, 0x0d, 0x52, 0x03, 0x69, 0x64, 0x73, 0x12, 0x1c, 0x0a, 0x09, 0x73, 0x74, 0x61,
0x72, 0x74, 0x54, 0x69, 0x6d, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x73, 0x74,
0x61, 0x72, 0x74, 0x54, 0x69, 0x6d, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x65, 0x6e, 0x64, 0x54, 0x69,
0x6d, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x65, 0x6e, 0x64, 0x54, 0x69, 0x6d,
0x65, 0x12, 0x14, 0x0a, 0x05, 0x72, 0x61, 0x6e, 0x67, 0x65, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09,
0x52, 0x05, 0x72, 0x61, 0x6e, 0x67, 0x65, 0x22, 0x90, 0x02, 0x0a, 0x0e, 0x52, 0x65, 0x71, 0x45,
0x76, 0x65, 0x6e, 0x74, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x12, 0x1e, 0x0a, 0x0a, 0x73, 0x74,
0x72, 0x65, 0x61, 0x6d, 0x50, 0x61, 0x74, 0x68, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a,
0x73, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x50, 0x61, 0x74, 0x68, 0x12, 0x18, 0x0a, 0x07, 0x65, 0x76,
0x65, 0x6e, 0x74, 0x49, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x65, 0x76, 0x65,
0x6e, 0x74, 0x49, 0x64, 0x12, 0x1c, 0x0a, 0x09, 0x65, 0x76, 0x65, 0x6e, 0x74, 0x4e, 0x61, 0x6d,
0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x65, 0x76, 0x65, 0x6e, 0x74, 0x4e, 0x61,
0x6d, 0x65, 0x12, 0x26, 0x0a, 0x0e, 0x62, 0x65, 0x66, 0x6f, 0x72, 0x65, 0x44, 0x75, 0x72, 0x61,
0x74, 0x69, 0x6f, 0x6e, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0e, 0x62, 0x65, 0x66, 0x6f,
0x72, 0x65, 0x44, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x24, 0x0a, 0x0d, 0x61, 0x66,
0x74, 0x65, 0x72, 0x44, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x05, 0x20, 0x01, 0x28,
0x09, 0x52, 0x0d, 0x61, 0x66, 0x74, 0x65, 0x72, 0x44, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e,
0x12, 0x1c, 0x0a, 0x09, 0x65, 0x76, 0x65, 0x6e, 0x74, 0x44, 0x65, 0x73, 0x63, 0x18, 0x06, 0x20,
0x01, 0x28, 0x09, 0x52, 0x09, 0x65, 0x76, 0x65, 0x6e, 0x74, 0x44, 0x65, 0x73, 0x63, 0x12, 0x1e,
0x0a, 0x0a, 0x65, 0x76, 0x65, 0x6e, 0x74, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x18, 0x07, 0x20, 0x01,
0x28, 0x09, 0x52, 0x0a, 0x65, 0x76, 0x65, 0x6e, 0x74, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x12, 0x1a,
0x0a, 0x08, 0x66, 0x72, 0x61, 0x67, 0x6d, 0x65, 0x6e, 0x74, 0x18, 0x08, 0x20, 0x01, 0x28, 0x09,
0x52, 0x08, 0x66, 0x72, 0x61, 0x67, 0x6d, 0x65, 0x6e, 0x74, 0x22, 0x57, 0x0a, 0x13, 0x52, 0x65,
0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x52, 0x65, 0x63, 0x6f, 0x72,
0x64, 0x12, 0x12, 0x0a, 0x04, 0x63, 0x6f, 0x64, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x05, 0x52,
0x04, 0x63, 0x6f, 0x64, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65,
0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x12,
0x12, 0x0a, 0x04, 0x64, 0x61, 0x74, 0x61, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x04, 0x64,
0x61, 0x74, 0x61, 0x22, 0x83, 0x01, 0x0a, 0x0e, 0x52, 0x65, 0x71, 0x53, 0x74, 0x61, 0x72, 0x74,
0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x12, 0x1e, 0x0a, 0x0a, 0x73, 0x74, 0x72, 0x65, 0x61, 0x6d,
0x50, 0x61, 0x74, 0x68, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x73, 0x74, 0x72, 0x65,
0x61, 0x6d, 0x50, 0x61, 0x74, 0x68, 0x12, 0x35, 0x0a, 0x08, 0x66, 0x72, 0x61, 0x67, 0x6d, 0x65,
0x6e, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c,
0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x44, 0x75, 0x72, 0x61, 0x74,
0x69, 0x6f, 0x6e, 0x52, 0x08, 0x66, 0x72, 0x61, 0x67, 0x6d, 0x65, 0x6e, 0x74, 0x12, 0x1a, 0x0a,
0x08, 0x66, 0x69, 0x6c, 0x65, 0x50, 0x61, 0x74, 0x68, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52,
0x08, 0x66, 0x69, 0x6c, 0x65, 0x50, 0x61, 0x74, 0x68, 0x22, 0x57, 0x0a, 0x13, 0x52, 0x65, 0x73,
0x70, 0x6f, 0x6e, 0x73, 0x65, 0x53, 0x74, 0x61, 0x72, 0x74, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64,
0x12, 0x12, 0x0a, 0x04, 0x63, 0x6f, 0x64, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x05, 0x52, 0x04,
0x63, 0x6f, 0x64, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x18,
0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x12, 0x12,
0x0a, 0x04, 0x64, 0x61, 0x74, 0x61, 0x18, 0x03, 0x20, 0x01, 0x28, 0x04, 0x52, 0x04, 0x64, 0x61,
0x74, 0x61, 0x22, 0x2f, 0x0a, 0x0d, 0x52, 0x65, 0x71, 0x53, 0x74, 0x6f, 0x70, 0x52, 0x65, 0x63,
0x6f, 0x72, 0x64, 0x12, 0x1e, 0x0a, 0x0a, 0x73, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x50, 0x61, 0x74,
0x68, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x73, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x50,
0x61, 0x74, 0x68, 0x22, 0x56, 0x0a, 0x12, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x53,
0x74, 0x6f, 0x70, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x63, 0x6f, 0x64,
0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x05, 0x52, 0x04, 0x63, 0x6f, 0x64, 0x65, 0x12, 0x18, 0x0a,
0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07,
0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x64, 0x61, 0x74, 0x61, 0x18,
0x03, 0x20, 0x01, 0x28, 0x04, 0x52, 0x04, 0x64, 0x61, 0x74, 0x61, 0x32, 0xc4, 0x04, 0x0a, 0x03,
0x61, 0x70, 0x69, 0x12, 0x57, 0x0a, 0x04, 0x4c, 0x69, 0x73, 0x74, 0x12, 0x12, 0x2e, 0x6d, 0x70,
0x34, 0x2e, 0x52, 0x65, 0x71, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x4c, 0x69, 0x73, 0x74, 0x1a,
0x14, 0x2e, 0x67, 0x6c, 0x6f, 0x62, 0x61, 0x6c, 0x2e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73,
0x65, 0x4c, 0x69, 0x73, 0x74, 0x22, 0x25, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x1f, 0x12, 0x1d, 0x2f,
0x6d, 0x70, 0x34, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x6c, 0x69, 0x73, 0x74, 0x2f, 0x7b, 0x73, 0x74,
0x72, 0x65, 0x61, 0x6d, 0x50, 0x61, 0x74, 0x68, 0x3d, 0x2a, 0x2a, 0x7d, 0x12, 0x54, 0x0a, 0x07,
0x43, 0x61, 0x74, 0x61, 0x6c, 0x6f, 0x67, 0x12, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65,
0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a,
0x17, 0x2e, 0x67, 0x6c, 0x6f, 0x62, 0x61, 0x6c, 0x2e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73,
0x65, 0x43, 0x61, 0x74, 0x61, 0x6c, 0x6f, 0x67, 0x22, 0x18, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x12,
0x12, 0x10, 0x2f, 0x6d, 0x70, 0x34, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x63, 0x61, 0x74, 0x61, 0x6c,
0x6f, 0x67, 0x12, 0x62, 0x0a, 0x06, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x12, 0x14, 0x2e, 0x6d,
0x70, 0x34, 0x2e, 0x52, 0x65, 0x71, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x44, 0x65, 0x6c, 0x65,
0x74, 0x65, 0x1a, 0x16, 0x2e, 0x67, 0x6c, 0x6f, 0x62, 0x61, 0x6c, 0x2e, 0x52, 0x65, 0x73, 0x70,
0x6f, 0x6e, 0x73, 0x65, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x22, 0x2a, 0x82, 0xd3, 0xe4, 0x93,
0x02, 0x24, 0x3a, 0x01, 0x2a, 0x22, 0x1f, 0x2f, 0x6d, 0x70, 0x34, 0x2f, 0x61, 0x70, 0x69, 0x2f,
0x64, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x2f, 0x7b, 0x73, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x50, 0x61,
0x74, 0x68, 0x3d, 0x2a, 0x2a, 0x7d, 0x12, 0x5c, 0x0a, 0x0a, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x53,
0x74, 0x61, 0x72, 0x74, 0x12, 0x13, 0x2e, 0x6d, 0x70, 0x34, 0x2e, 0x52, 0x65, 0x71, 0x45, 0x76,
0x65, 0x6e, 0x74, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x1a, 0x18, 0x2e, 0x6d, 0x70, 0x34, 0x2e,
0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x52, 0x65, 0x63,
0x6f, 0x72, 0x64, 0x22, 0x1f, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x19, 0x3a, 0x01, 0x2a, 0x22, 0x14,
0x2f, 0x6d, 0x70, 0x34, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x65, 0x76, 0x65, 0x6e, 0x74, 0x2f, 0x73,
0x74, 0x61, 0x72, 0x74, 0x12, 0x67, 0x0a, 0x0b, 0x53, 0x74, 0x61, 0x72, 0x74, 0x52, 0x65, 0x63,
0x6f, 0x72, 0x64, 0x12, 0x13, 0x2e, 0x6d, 0x70, 0x34, 0x2e, 0x52, 0x65, 0x71, 0x53, 0x74, 0x61,
0x72, 0x74, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x1a, 0x18, 0x2e, 0x6d, 0x70, 0x34, 0x2e, 0x52,
0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x53, 0x74, 0x61, 0x72, 0x74, 0x52, 0x65, 0x63, 0x6f,
0x72, 0x64, 0x22, 0x29, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x23, 0x3a, 0x01, 0x2a, 0x22, 0x1e, 0x2f,
0x6d, 0x70, 0x34, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x73, 0x74, 0x61, 0x72, 0x74, 0x2f, 0x7b, 0x73,
0x74, 0x72, 0x65, 0x61, 0x6d, 0x50, 0x61, 0x74, 0x68, 0x3d, 0x2a, 0x2a, 0x7d, 0x12, 0x63, 0x0a,
0x0a, 0x53, 0x74, 0x6f, 0x70, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x12, 0x12, 0x2e, 0x6d, 0x70,
0x34, 0x2e, 0x52, 0x65, 0x71, 0x53, 0x74, 0x6f, 0x70, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x1a,
0x17, 0x2e, 0x6d, 0x70, 0x34, 0x2e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x53, 0x74,
0x6f, 0x70, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x22, 0x28, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x22,
0x3a, 0x01, 0x2a, 0x22, 0x1d, 0x2f, 0x6d, 0x70, 0x34, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x73, 0x74,
0x6f, 0x70, 0x2f, 0x7b, 0x73, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x50, 0x61, 0x74, 0x68, 0x3d, 0x2a,
0x2a, 0x7d, 0x42, 0x1b, 0x5a, 0x19, 0x6d, 0x37, 0x73, 0x2e, 0x6c, 0x69, 0x76, 0x65, 0x2f, 0x76,
0x35, 0x2f, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x2f, 0x6d, 0x70, 0x34, 0x2f, 0x70, 0x62, 0x62,
0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
})
var (
file_mp4_proto_rawDescOnce sync.Once
file_mp4_proto_rawDescData = file_mp4_proto_rawDesc
file_mp4_proto_rawDescData []byte
)
func file_mp4_proto_rawDescGZIP() []byte {
file_mp4_proto_rawDescOnce.Do(func() {
file_mp4_proto_rawDescData = protoimpl.X.CompressGZIP(file_mp4_proto_rawDescData)
file_mp4_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_mp4_proto_rawDesc), len(file_mp4_proto_rawDesc)))
})
return file_mp4_proto_rawDescData
}
var file_mp4_proto_msgTypes = make([]protoimpl.MessageInfo, 6)
var file_mp4_proto_goTypes = []interface{}{
var file_mp4_proto_msgTypes = make([]protoimpl.MessageInfo, 8)
var file_mp4_proto_goTypes = []any{
(*ReqRecordList)(nil), // 0: mp4.ReqRecordList
(*ReqRecordDelete)(nil), // 1: mp4.ReqRecordDelete
(*ReqEventRecord)(nil), // 2: mp4.ReqEventRecord
(*ResponseEventRecord)(nil), // 3: mp4.ResponseEventRecord
(*ReqStartRecord)(nil), // 4: mp4.ReqStartRecord
(*ResponseStartRecord)(nil), // 5: mp4.ResponseStartRecord
(*durationpb.Duration)(nil), // 6: google.protobuf.Duration
(*emptypb.Empty)(nil), // 7: google.protobuf.Empty
(*pb.ResponseList)(nil), // 8: global.ResponseList
(*pb.ResponseCatalog)(nil), // 9: global.ResponseCatalog
(*pb.ResponseDelete)(nil), // 10: global.ResponseDelete
(*ReqStopRecord)(nil), // 6: mp4.ReqStopRecord
(*ResponseStopRecord)(nil), // 7: mp4.ResponseStopRecord
(*durationpb.Duration)(nil), // 8: google.protobuf.Duration
(*emptypb.Empty)(nil), // 9: google.protobuf.Empty
(*pb.ResponseList)(nil), // 10: global.ResponseList
(*pb.ResponseCatalog)(nil), // 11: global.ResponseCatalog
(*pb.ResponseDelete)(nil), // 12: global.ResponseDelete
}
var file_mp4_proto_depIdxs = []int32{
6, // 0: mp4.ReqStartRecord.fragment:type_name -> google.protobuf.Duration
8, // 0: mp4.ReqStartRecord.fragment:type_name -> google.protobuf.Duration
0, // 1: mp4.api.List:input_type -> mp4.ReqRecordList
7, // 2: mp4.api.Catalog:input_type -> google.protobuf.Empty
9, // 2: mp4.api.Catalog:input_type -> google.protobuf.Empty
1, // 3: mp4.api.Delete:input_type -> mp4.ReqRecordDelete
2, // 4: mp4.api.EventStart:input_type -> mp4.ReqEventRecord
4, // 5: mp4.api.StartRecord:input_type -> mp4.ReqStartRecord
8, // 6: mp4.api.List:output_type -> global.ResponseList
9, // 7: mp4.api.Catalog:output_type -> global.ResponseCatalog
10, // 8: mp4.api.Delete:output_type -> global.ResponseDelete
3, // 9: mp4.api.EventStart:output_type -> mp4.ResponseEventRecord
5, // 10: mp4.api.StartRecord:output_type -> mp4.ResponseStartRecord
6, // [6:11] is the sub-list for method output_type
1, // [1:6] is the sub-list for method input_type
6, // 6: mp4.api.StopRecord:input_type -> mp4.ReqStopRecord
10, // 7: mp4.api.List:output_type -> global.ResponseList
11, // 8: mp4.api.Catalog:output_type -> global.ResponseCatalog
12, // 9: mp4.api.Delete:output_type -> global.ResponseDelete
3, // 10: mp4.api.EventStart:output_type -> mp4.ResponseEventRecord
5, // 11: mp4.api.StartRecord:output_type -> mp4.ResponseStartRecord
7, // 12: mp4.api.StopRecord:output_type -> mp4.ResponseStopRecord
7, // [7:13] is the sub-list for method output_type
1, // [1:7] is the sub-list for method input_type
1, // [1:1] is the sub-list for extension type_name
1, // [1:1] is the sub-list for extension extendee
0, // [0:1] is the sub-list for field type_name
@@ -655,87 +759,13 @@ func file_mp4_proto_init() {
if File_mp4_proto != nil {
return
}
if !protoimpl.UnsafeEnabled {
file_mp4_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*ReqRecordList); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
file_mp4_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*ReqRecordDelete); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
file_mp4_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*ReqEventRecord); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
file_mp4_proto_msgTypes[3].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*ResponseEventRecord); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
file_mp4_proto_msgTypes[4].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*ReqStartRecord); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
file_mp4_proto_msgTypes[5].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*ResponseStartRecord); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
}
type x struct{}
out := protoimpl.TypeBuilder{
File: protoimpl.DescBuilder{
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: file_mp4_proto_rawDesc,
RawDescriptor: unsafe.Slice(unsafe.StringData(file_mp4_proto_rawDesc), len(file_mp4_proto_rawDesc)),
NumEnums: 0,
NumMessages: 6,
NumMessages: 8,
NumExtensions: 0,
NumServices: 1,
},
@@ -744,7 +774,6 @@ func file_mp4_proto_init() {
MessageInfos: file_mp4_proto_msgTypes,
}.Build()
File_mp4_proto = out.File
file_mp4_proto_rawDesc = nil
file_mp4_proto_goTypes = nil
file_mp4_proto_depIdxs = nil
}

View File

@@ -266,10 +266,71 @@ func local_request_Api_StartRecord_0(ctx context.Context, marshaler runtime.Mars
}
func request_Api_StopRecord_0(ctx context.Context, marshaler runtime.Marshaler, client ApiClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
var protoReq ReqStopRecord
var metadata runtime.ServerMetadata
if err := marshaler.NewDecoder(req.Body).Decode(&protoReq); err != nil && err != io.EOF {
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
}
var (
val string
ok bool
err error
_ = 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.StopRecord(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))
return msg, metadata, err
}
func local_request_Api_StopRecord_0(ctx context.Context, marshaler runtime.Marshaler, server ApiServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
var protoReq ReqStopRecord
var metadata runtime.ServerMetadata
if err := marshaler.NewDecoder(req.Body).Decode(&protoReq); err != nil && err != io.EOF {
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
}
var (
val string
ok bool
err error
_ = 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.StopRecord(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.
// Note that using this registration option will cause many gRPC library features to stop working. Consider using RegisterApiHandlerFromEndpoint instead.
// GRPC interceptors will not work for this type of registration. To use interceptors, you must use the "runtime.WithMiddlewares" option in the "runtime.NewServeMux" call.
func RegisterApiHandlerServer(ctx context.Context, mux *runtime.ServeMux, server ApiServer) error {
mux.Handle("GET", pattern_Api_List_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
@@ -397,27 +458,52 @@ func RegisterApiHandlerServer(ctx context.Context, mux *runtime.ServeMux, server
})
mux.Handle("POST", pattern_Api_StopRecord_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)
var err error
var annotatedContext context.Context
annotatedContext, err = runtime.AnnotateIncomingContext(ctx, mux, req, "/mp4.Api/StopRecord", runtime.WithHTTPPathPattern("/mp4/api/stop/{streamPath=**}"))
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
resp, md, err := local_request_Api_StopRecord_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_StopRecord_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
})
return nil
}
// RegisterApiHandlerFromEndpoint is same as RegisterApiHandler but
// automatically dials to "endpoint" and closes the connection when "ctx" gets done.
func RegisterApiHandlerFromEndpoint(ctx context.Context, mux *runtime.ServeMux, endpoint string, opts []grpc.DialOption) (err error) {
conn, err := grpc.DialContext(ctx, endpoint, opts...)
conn, err := grpc.NewClient(endpoint, opts...)
if err != nil {
return err
}
defer func() {
if err != nil {
if cerr := conn.Close(); cerr != nil {
grpclog.Infof("Failed to close conn to %s: %v", endpoint, cerr)
grpclog.Errorf("Failed to close conn to %s: %v", endpoint, cerr)
}
return
}
go func() {
<-ctx.Done()
if cerr := conn.Close(); cerr != nil {
grpclog.Infof("Failed to close conn to %s: %v", endpoint, cerr)
grpclog.Errorf("Failed to close conn to %s: %v", endpoint, cerr)
}
}()
}()
@@ -435,7 +521,7 @@ func RegisterApiHandler(ctx context.Context, mux *runtime.ServeMux, conn *grpc.C
// to "mux". The handlers forward requests to the grpc endpoint over the given implementation of "ApiClient".
// Note: the gRPC framework executes interceptors within the gRPC handler. If the passed in "ApiClient"
// doesn't go through the normal gRPC flow (creating a gRPC client etc.) then it will be up to the passed in
// "ApiClient" to call the correct interceptors.
// "ApiClient" to call the correct interceptors. This client ignores the HTTP middlewares.
func RegisterApiHandlerClient(ctx context.Context, mux *runtime.ServeMux, client ApiClient) error {
mux.Handle("GET", pattern_Api_List_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
@@ -548,6 +634,28 @@ func RegisterApiHandlerClient(ctx context.Context, mux *runtime.ServeMux, client
})
mux.Handle("POST", pattern_Api_StopRecord_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)
var err error
var annotatedContext context.Context
annotatedContext, err = runtime.AnnotateContext(ctx, mux, req, "/mp4.Api/StopRecord", runtime.WithHTTPPathPattern("/mp4/api/stop/{streamPath=**}"))
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
resp, md, err := request_Api_StopRecord_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_StopRecord_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
})
return nil
}
@@ -561,6 +669,8 @@ var (
pattern_Api_EventStart_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 2, 3}, []string{"mp4", "api", "event", "start"}, ""))
pattern_Api_StartRecord_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 3, 0, 4, 1, 5, 3}, []string{"mp4", "api", "start", "streamPath"}, ""))
pattern_Api_StopRecord_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 3, 0, 4, 1, 5, 3}, []string{"mp4", "api", "stop", "streamPath"}, ""))
)
var (
@@ -573,4 +683,6 @@ var (
forward_Api_EventStart_0 = runtime.ForwardResponseMessage
forward_Api_StartRecord_0 = runtime.ForwardResponseMessage
forward_Api_StopRecord_0 = runtime.ForwardResponseMessage
)

View File

@@ -1,7 +1,6 @@
syntax = "proto3";
import "google/api/annotations.proto";
import "google/protobuf/empty.proto";
import "google/protobuf/timestamp.proto";
import "google/protobuf/duration.proto";
import "global.proto";
package mp4;
@@ -36,6 +35,12 @@ service api {
body: "*"
};
}
rpc StopRecord (ReqStopRecord) returns (ResponseStopRecord) {
option (google.api.http) = {
post: "/mp4/api/stop/{streamPath=**}"
body: "*"
};
}
}
message ReqRecordList {
@@ -46,6 +51,7 @@ message ReqRecordList {
uint32 pageNum = 5;
uint32 pageSize = 6;
string mode = 7;
string eventLevel = 8;
}
message ReqRecordDelete {
@@ -59,13 +65,12 @@ message ReqRecordDelete {
message ReqEventRecord {
string streamPath = 1;
string eventId = 2;
string mode = 3;//auto=连续录像模式event=事件录像模式
string eventName = 4;
string beforeDuration = 5;
string afterDuration = 6;
string eventDesc = 7;
string eventLevel = 8;//事件级别,0表示重要事件无法删除且表示无需自动删除,1表示非重要事件,达到自动删除时间后,自动删除
string fragment = 9;
string eventName = 3;
string beforeDuration = 4;
string afterDuration = 5;
string eventDesc = 6;
string eventLevel = 7;//事件级别,0表示重要事件无法删除且表示无需自动删除,1表示非重要事件,达到自动删除时间后,自动删除
string fragment = 8;
}
message ResponseEventRecord {
@@ -84,4 +89,14 @@ message ResponseStartRecord {
int32 code = 1;
string message = 2;
uint64 data = 3;
}
message ReqStopRecord {
string streamPath = 1;
}
message ResponseStopRecord {
int32 code = 1;
string message = 2;
uint64 data = 3;
}

View File

@@ -1,7 +1,7 @@
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
// versions:
// - protoc-gen-go-grpc v1.2.0
// - protoc v3.19.1
// - protoc-gen-go-grpc v1.5.1
// - protoc v5.28.3
// source: mp4.proto
package pb
@@ -17,8 +17,17 @@ import (
// This is a compile-time assertion to ensure that this generated file
// is compatible with the grpc package it is being compiled against.
// Requires gRPC-Go v1.32.0 or later.
const _ = grpc.SupportPackageIsVersion7
// Requires gRPC-Go v1.64.0 or later.
const _ = grpc.SupportPackageIsVersion9
const (
Api_List_FullMethodName = "/mp4.api/List"
Api_Catalog_FullMethodName = "/mp4.api/Catalog"
Api_Delete_FullMethodName = "/mp4.api/Delete"
Api_EventStart_FullMethodName = "/mp4.api/EventStart"
Api_StartRecord_FullMethodName = "/mp4.api/StartRecord"
Api_StopRecord_FullMethodName = "/mp4.api/StopRecord"
)
// ApiClient is the client API for Api service.
//
@@ -29,6 +38,7 @@ type ApiClient interface {
Delete(ctx context.Context, in *ReqRecordDelete, opts ...grpc.CallOption) (*pb.ResponseDelete, error)
EventStart(ctx context.Context, in *ReqEventRecord, opts ...grpc.CallOption) (*ResponseEventRecord, error)
StartRecord(ctx context.Context, in *ReqStartRecord, opts ...grpc.CallOption) (*ResponseStartRecord, error)
StopRecord(ctx context.Context, in *ReqStopRecord, opts ...grpc.CallOption) (*ResponseStopRecord, error)
}
type apiClient struct {
@@ -40,8 +50,9 @@ func NewApiClient(cc grpc.ClientConnInterface) ApiClient {
}
func (c *apiClient) List(ctx context.Context, in *ReqRecordList, opts ...grpc.CallOption) (*pb.ResponseList, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(pb.ResponseList)
err := c.cc.Invoke(ctx, "/mp4.api/List", in, out, opts...)
err := c.cc.Invoke(ctx, Api_List_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
@@ -49,8 +60,9 @@ func (c *apiClient) List(ctx context.Context, in *ReqRecordList, opts ...grpc.Ca
}
func (c *apiClient) Catalog(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*pb.ResponseCatalog, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(pb.ResponseCatalog)
err := c.cc.Invoke(ctx, "/mp4.api/Catalog", in, out, opts...)
err := c.cc.Invoke(ctx, Api_Catalog_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
@@ -58,8 +70,9 @@ func (c *apiClient) Catalog(ctx context.Context, in *emptypb.Empty, opts ...grpc
}
func (c *apiClient) Delete(ctx context.Context, in *ReqRecordDelete, opts ...grpc.CallOption) (*pb.ResponseDelete, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(pb.ResponseDelete)
err := c.cc.Invoke(ctx, "/mp4.api/Delete", in, out, opts...)
err := c.cc.Invoke(ctx, Api_Delete_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
@@ -67,8 +80,9 @@ func (c *apiClient) Delete(ctx context.Context, in *ReqRecordDelete, opts ...grp
}
func (c *apiClient) EventStart(ctx context.Context, in *ReqEventRecord, opts ...grpc.CallOption) (*ResponseEventRecord, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(ResponseEventRecord)
err := c.cc.Invoke(ctx, "/mp4.api/EventStart", in, out, opts...)
err := c.cc.Invoke(ctx, Api_EventStart_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
@@ -76,8 +90,19 @@ func (c *apiClient) EventStart(ctx context.Context, in *ReqEventRecord, opts ...
}
func (c *apiClient) StartRecord(ctx context.Context, in *ReqStartRecord, opts ...grpc.CallOption) (*ResponseStartRecord, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(ResponseStartRecord)
err := c.cc.Invoke(ctx, "/mp4.api/StartRecord", in, out, opts...)
err := c.cc.Invoke(ctx, Api_StartRecord_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *apiClient) StopRecord(ctx context.Context, in *ReqStopRecord, opts ...grpc.CallOption) (*ResponseStopRecord, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(ResponseStopRecord)
err := c.cc.Invoke(ctx, Api_StopRecord_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
@@ -86,19 +111,23 @@ func (c *apiClient) StartRecord(ctx context.Context, in *ReqStartRecord, opts ..
// ApiServer is the server API for Api service.
// All implementations must embed UnimplementedApiServer
// for forward compatibility
// for forward compatibility.
type ApiServer interface {
List(context.Context, *ReqRecordList) (*pb.ResponseList, error)
Catalog(context.Context, *emptypb.Empty) (*pb.ResponseCatalog, error)
Delete(context.Context, *ReqRecordDelete) (*pb.ResponseDelete, error)
EventStart(context.Context, *ReqEventRecord) (*ResponseEventRecord, error)
StartRecord(context.Context, *ReqStartRecord) (*ResponseStartRecord, error)
StopRecord(context.Context, *ReqStopRecord) (*ResponseStopRecord, error)
mustEmbedUnimplementedApiServer()
}
// UnimplementedApiServer must be embedded to have forward compatible implementations.
type UnimplementedApiServer struct {
}
// UnimplementedApiServer must be embedded to have
// forward compatible implementations.
//
// NOTE: this should be embedded by value instead of pointer to avoid a nil
// pointer dereference when methods are called.
type UnimplementedApiServer struct{}
func (UnimplementedApiServer) List(context.Context, *ReqRecordList) (*pb.ResponseList, error) {
return nil, status.Errorf(codes.Unimplemented, "method List not implemented")
@@ -115,7 +144,11 @@ func (UnimplementedApiServer) EventStart(context.Context, *ReqEventRecord) (*Res
func (UnimplementedApiServer) StartRecord(context.Context, *ReqStartRecord) (*ResponseStartRecord, error) {
return nil, status.Errorf(codes.Unimplemented, "method StartRecord not implemented")
}
func (UnimplementedApiServer) StopRecord(context.Context, *ReqStopRecord) (*ResponseStopRecord, error) {
return nil, status.Errorf(codes.Unimplemented, "method StopRecord not implemented")
}
func (UnimplementedApiServer) mustEmbedUnimplementedApiServer() {}
func (UnimplementedApiServer) testEmbeddedByValue() {}
// UnsafeApiServer may be embedded to opt out of forward compatibility for this service.
// Use of this interface is not recommended, as added methods to ApiServer will
@@ -125,6 +158,13 @@ type UnsafeApiServer interface {
}
func RegisterApiServer(s grpc.ServiceRegistrar, srv ApiServer) {
// If the following call pancis, it indicates UnimplementedApiServer was
// embedded by pointer and is nil. This will cause panics if an
// unimplemented method is ever invoked, so we test this at initialization
// time to prevent it from happening at runtime later due to I/O.
if t, ok := srv.(interface{ testEmbeddedByValue() }); ok {
t.testEmbeddedByValue()
}
s.RegisterService(&Api_ServiceDesc, srv)
}
@@ -138,7 +178,7 @@ func _Api_List_Handler(srv interface{}, ctx context.Context, dec func(interface{
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: "/mp4.api/List",
FullMethod: Api_List_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(ApiServer).List(ctx, req.(*ReqRecordList))
@@ -156,7 +196,7 @@ func _Api_Catalog_Handler(srv interface{}, ctx context.Context, dec func(interfa
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: "/mp4.api/Catalog",
FullMethod: Api_Catalog_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(ApiServer).Catalog(ctx, req.(*emptypb.Empty))
@@ -174,7 +214,7 @@ func _Api_Delete_Handler(srv interface{}, ctx context.Context, dec func(interfac
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: "/mp4.api/Delete",
FullMethod: Api_Delete_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(ApiServer).Delete(ctx, req.(*ReqRecordDelete))
@@ -192,7 +232,7 @@ func _Api_EventStart_Handler(srv interface{}, ctx context.Context, dec func(inte
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: "/mp4.api/EventStart",
FullMethod: Api_EventStart_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(ApiServer).EventStart(ctx, req.(*ReqEventRecord))
@@ -210,7 +250,7 @@ func _Api_StartRecord_Handler(srv interface{}, ctx context.Context, dec func(int
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: "/mp4.api/StartRecord",
FullMethod: Api_StartRecord_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(ApiServer).StartRecord(ctx, req.(*ReqStartRecord))
@@ -218,6 +258,24 @@ func _Api_StartRecord_Handler(srv interface{}, ctx context.Context, dec func(int
return interceptor(ctx, in, info, handler)
}
func _Api_StopRecord_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(ReqStopRecord)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(ApiServer).StopRecord(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: Api_StopRecord_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(ApiServer).StopRecord(ctx, req.(*ReqStopRecord))
}
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)
@@ -245,6 +303,10 @@ var Api_ServiceDesc = grpc.ServiceDesc{
MethodName: "StartRecord",
Handler: _Api_StartRecord_Handler,
},
{
MethodName: "StopRecord",
Handler: _Api_StopRecord_Handler,
},
},
Streams: []grpc.StreamDesc{},
Metadata: "mp4.proto",

View File

@@ -1,59 +0,0 @@
package bits
// Reader is a bit stream reader
type Reader struct {
Data []byte
Offset int
}
// Skip skips n bits
func (r *Reader) Skip(n int) {
r.Offset += n
}
// ReadBit reads a single bit
func (r *Reader) ReadBit() (uint, error) {
if r.Offset/8 >= len(r.Data) {
return 0, nil
}
b := r.Data[r.Offset/8]
v := (b >> (7 - (r.Offset % 8))) & 0x01
r.Offset++
return uint(v), nil
}
// ReadExpGolomb reads an Exp-Golomb code
func (r *Reader) ReadExpGolomb() (uint, error) {
leadingZeroBits := 0
for {
b, err := r.ReadBit()
if err != nil {
return 0, err
}
if b == 1 {
break
}
leadingZeroBits++
}
result := uint(1<<leadingZeroBits) - 1
for i := 0; i < leadingZeroBits; i++ {
b, err := r.ReadBit()
if err != nil {
return 0, err
}
result = (result << 1) | uint(b)
}
return result, nil
}
// ReadSE reads a signed Exp-Golomb code
func (r *Reader) ReadSE() (int, error) {
val, err := r.ReadExpGolomb()
if err != nil {
return 0, err
}
sign := ((val & 0x01) << 1) - 1
val = ((val >> 1) + (val & 0x01)) * uint(sign)
return int(val), nil
}

View File

@@ -2,7 +2,6 @@ package box
import (
"encoding/binary"
"fmt"
"io"
"net"
"reflect"
@@ -15,7 +14,7 @@ type (
BoxHeader interface {
Type() BoxType
HeaderSize() uint32
Size() uint32
Size() uint64
Header() BoxHeader
HeaderWriteTo(w io.Writer) (n int64, err error)
}
@@ -85,7 +84,7 @@ func CreateContainerBox(typ BoxType, children ...IBox) *ContainerBox {
if reflect.ValueOf(child).IsNil() {
continue
}
size += child.Size()
size += uint32(child.Size())
realChildren = append(realChildren, child)
}
return &ContainerBox{
@@ -101,9 +100,9 @@ func (b *BigBox) HeaderSize() uint32 { return BasicBoxLen + 8 }
func (b *BaseBox) Header() BoxHeader { return b }
func (b *BaseBox) HeaderSize() uint32 { return BasicBoxLen }
func (b *BaseBox) Size() uint32 { return b.size }
func (b *BaseBox) Type() BoxType { return b.typ }
func (b *BaseBox) Size() uint64 { return uint64(b.size) }
func (b *BigBox) Size() uint64 { return uint64(b.size) }
func (b *BaseBox) Type() BoxType { return b.typ }
func (b *BaseBox) HeaderWriteTo(w io.Writer) (n int64, err error) {
var tmp [4]byte
@@ -161,7 +160,7 @@ func (b *FullBox) HeaderSize() uint32 { return FullBoxLen }
func WriteTo(w io.Writer, box ...IBox) (n int64, err error) {
var n1, n2 int64
for _, b := range box {
if b == nil {
if reflect.ValueOf(b).IsNil() {
continue
}
n1, err = b.HeaderWriteTo(w)
@@ -172,8 +171,8 @@ func WriteTo(w io.Writer, box ...IBox) (n int64, err error) {
if err != nil {
return
}
if n1 + n2 != int64(b.Size()) {
panic(fmt.Sprintf("write to %s size error, %d != %d", b.Type(), n1 + n2, b.Size()))
if n1+n2 != int64(b.Size()) {
// panic(fmt.Sprintf("write to %s size error, %d != %d", b.Type(), n1+n2, b.Size()))
}
n += n1 + n2
}
@@ -191,9 +190,10 @@ func ReadFrom(r io.Reader) (box IBox, err error) {
baseBox.typ = BoxType(tmp[4:])
t, exists := registry[baseBox.typ.Uint32I()]
if !exists {
return nil, fmt.Errorf("unknown box type: %s", baseBox.typ)
io.CopyN(io.Discard, r, int64(baseBox.size-BasicBoxLen))
return &baseBox, nil
}
b := reflect.New(t).Interface().(IBox)
b := reflect.New(t.Elem()).Interface().(IBox)
var payload []byte
if baseBox.size == 1 {
if _, err = io.ReadFull(r, tmp[:]); err != nil {
@@ -203,8 +203,10 @@ func ReadFrom(r io.Reader) (box IBox, err error) {
} else {
payload = make([]byte, baseBox.size-BasicBoxLen)
}
_, err = io.ReadFull(r, payload)
if err != nil {
return nil, err
}
boxHeader := b.Header()
switch header := boxHeader.(type) {
case *BaseBox:
@@ -216,6 +218,9 @@ func ReadFrom(r io.Reader) (box IBox, err error) {
header.Flags = [3]byte(payload[1:4])
box, err = b.Unmarshal(payload[4:])
}
if err == io.EOF {
return box, nil
}
return
}
@@ -301,12 +306,14 @@ var (
TypeEDTS = f("edts")
TypeELST = f("elst")
TypeMVEX = f("mvex")
TypeMEHD = f("mehd")
TypeMOOF = f("moof")
TypeMFHD = f("mfhd")
TypeTRAF = f("traf")
TypeTFHD = f("tfhd")
TypeTFDT = f("tfdt")
TypeTRUN = f("trun")
TypeSDTP = f("sdtp")
TypeSENC = f("senc")
TypeSAIZ = f("saiz")
TypeSAIO = f("saio")
@@ -335,6 +342,8 @@ var (
TypeMETA = f("meta")
TypeAUXV = f("auxv")
TypeHINT = f("hint")
TypeUDTA = f("udta")
TypeM7SP = f("m7sp") // Custom box type for M7S StreamPath
)
// aligned(8) class Box (unsigned int(32) boxtype, optional unsigned int(8)[16] extended_type) {
@@ -370,3 +379,7 @@ type SampleToChunkEntry struct {
SamplesPerChunk uint32
SampleDescriptionIndex uint32
}
func ConvertUnixTimeToISO14496(unixTime uint64) uint64 {
return unixTime + 0x7C25B080
}

Some files were not shown because too many files have changed in this diff Show More