Compare commits

...

256 Commits

Author SHA1 Message Date
pggiroro
56c4ea5907 fix: api getDevices,getDevice,getChannels 2025-07-11 23:11:27 +08:00
pggiroro
28c71545db fix: groupchannel page select 2025-07-11 18:03:39 +08:00
langhuihui
17faf3f064 feat: pulse interval can be 0 2025-07-11 16:23:00 +08:00
pggiroro
131af312f1 fix: improve webhook task 2025-07-08 21:39:14 +08:00
pggiroro
cf3b7dfabe fix: improve packet replayer 2025-07-08 21:39:14 +08:00
pggiroro
584c2e9932 fix: dialogs.getKey change to string(callid) 2025-07-07 09:14:42 +08:00
pggiroro
a7f04faa23 fix: search hls use type "ts" in db 2025-07-07 09:14:42 +08:00
pggiroro
966153f873 fix: dialog.getKey() change from ssrc to callid;data source of api device/list from db change to memory 2025-07-06 23:06:38 +08:00
pggiroro
4391ad2d8d fix: alarminfo add alarmName 2025-07-06 23:06:38 +08:00
langhuihui
747a5a1104 fix: hls record ts 2025-07-06 10:03:35 +08:00
langhuihui
97d8de523d fix: hls play record ts 2025-07-03 19:52:49 +08:00
pggiroro
cad47aec5c feat: send alarminfo through hook 2025-07-02 21:49:11 +08:00
pggiroro
baf3640b23 feat: send alarm through hook 2025-07-01 11:09:59 +08:00
langhuihui
3d68712ff6 fix: docker action 2025-07-01 08:43:50 +08:00
langhuihui
f06f43dbe9 docs: update v5.0.3 release note, covering major enhancements and fixes for recording, protocols, plugins, and configuration. 2025-06-30 22:38:48 +08:00
langhuihui
75efcba311 fix: mp4 pull record file 2025-06-26 16:36:04 +08:00
pggiroro
6b58e2a9b5 fix: flv record 2025-06-24 20:37:04 +08:00
pggiroro
7b6259ed67 feat: add pullproxy support gb28181 type 2025-06-23 23:39:55 +08:00
langhuihui
0d3d86518d fix: record write time 2025-06-23 16:48:31 +08:00
langhuihui
ac3ad009a7 feat: suber wait video default 2025-06-23 09:00:02 +08:00
langhuihui
5731c2e8da fix: rtmp clone buffers 2025-06-22 23:10:57 +08:00
langhuihui
cf6153fa91 feat: add get all config yaml example 2025-06-22 22:29:09 +08:00
pggiroro
70e1ea51ac fix: Send RTMP data based on timestamps in data of tcpdump file. 2025-06-22 21:37:55 +08:00
langhuihui
8f5a829900 feat: add wait track conf to subscribe 2025-06-21 21:55:17 +08:00
langhuihui
10f4fe3fc6 fix: pull proxy check to pull when on sub 2025-06-20 08:10:58 +08:00
langhuihui
3a2901fa5f fix: correct SQL syntax for event level comparison in eventRecordCheck 2025-06-19 23:17:57 +08:00
dexter
55f5408f64 feat: cut mp4 when avcc changed 2025-06-19 10:46:20 +00:00
dexter
9e45c3eb71 fix: remove event_id from normal record table query 2025-06-19 02:25:15 +00:00
langhuihui
01fa1f3ed8 gifix: replay cap script 2025-06-17 23:51:19 +08:00
langhuihui
830da3aaab fix: mp4 demuxer 2025-06-17 20:22:51 +08:00
langhuihui
5a04dc814d fix: event record check 2025-06-17 19:32:53 +08:00
langhuihui
af5d2bc1f2 fix: set record type 2025-06-17 18:34:10 +08:00
langhuihui
a3e0c1864e feat: add ping pong to batchv2 2025-06-17 14:03:37 +08:00
langhuihui
33d385d2bf fix: record bug 2025-06-17 11:36:32 +08:00
langhuihui
29c47a8d08 fix: hls demo page 2025-06-17 11:26:11 +08:00
langhuihui
5bf5e7bb20 feat: mp4 conert to ts format 2025-06-17 11:09:35 +08:00
langhuihui
4b74ea5841 doc: auth 2025-06-17 09:41:36 +08:00
langhuihui
43710fb017 fix: record 2025-06-16 22:41:55 +08:00
langhuihui
962dda8d08 refactor: mp4 and record system 2025-06-16 20:28:49 +08:00
erroot
ec56bba75a Erroot v5 (#286)
* 插件数据库不同时,新建DB 对象赋值给插件

* MP4 plugin adds extraction, clips, images, compressed video, GOP clicp

* remove mp4/util panic code
2025-06-16 08:29:14 +08:00
pggiroro
b2b511d755 fix: user.LastLogin set gorm type:timestamp, gb28181 api GetGroupChannels modify 2025-06-15 22:19:14 +08:00
pggiroro
42acf47250 feature: gb28181 support single mediaport 2025-06-15 16:58:52 +08:00
langhuihui
6206ee847d fix: record table fit pg database 2025-06-15 15:58:12 +08:00
langhuihui
6cfdc03e4a fix: user mode fit pg database 2025-06-15 15:21:21 +08:00
pggiroro
b425b8da1f fix: ignore RecordEvent in gorm 2025-06-13 12:52:57 +08:00
langhuihui
e105243cd5 refactor: record 2025-06-13 12:52:57 +08:00
langhuihui
20ec6c55cd fix: plugin init error 2025-06-12 15:08:47 +08:00
langhuihui
e478a1972e fix: webrtc batch bug 2025-06-12 14:21:59 +08:00
langhuihui
94be02cd79 feat: consider pull proxy disable status 2025-06-12 13:50:47 +08:00
langhuihui
bacda6f5a0 feat: webrtc fit client codecs 2025-06-12 12:49:36 +08:00
langhuihui
61fae4cc97 fix: webrtc h265 subscribe 2025-06-11 23:43:22 +08:00
pggiroro
e0752242b2 feat: crontab support record with plan like nvr 2025-06-11 22:18:45 +08:00
pggiroro
23f2ed39a1 fix: gb28181 check from.Address.User when onRegister,delete device from db when device is not register 2025-06-11 22:18:45 +08:00
erroot
0b731e468b 插件数据库不同时,新建DB 对象赋值给插件 2025-06-11 21:43:34 +08:00
langhuihui
4fe1472117 refactor: init plugin faild do not register http handle 2025-06-11 13:57:45 +08:00
langhuihui
a8b3a644c3 feat: record recover 2025-06-10 20:16:39 +08:00
pggiroro
4f0a097dac feat: crontab support plat with streampath in database 2025-06-08 21:01:36 +08:00
pggiroro
4df3de00af fix: gb28181 subscriber and invite sdp 2025-06-08 10:40:17 +08:00
langhuihui
9c16905f28 feat: add evn check to debug plugin 2025-06-07 21:07:28 +08:00
pggiroro
0470f78ed7 fix: register to up platform change cseq when need password, get deviceinfo do not update device name when name is not nil in db,return error when DB is nil in Oninit 2025-06-06 22:45:50 +08:00
pggiroro
7282f1f44d fix: add platform from config.yaml,add example into default/config.yaml 2025-06-06 09:03:58 +08:00
pggiroro
67186cd669 fix: subscribe stream before start mp4 record 2025-06-06 09:03:58 +08:00
pggiroro
09e9761083 feat: Added the association feature between plan and streampath, which has not been tested yet. 2025-06-06 09:03:58 +08:00
langhuihui
4acdc19beb feat: add duration to record 2025-06-05 23:51:33 +08:00
langhuihui
80e19726d4 fix: use safeGet insteadof Call and get
feat: multi buddy support
2025-06-05 20:33:59 +08:00
langhuihui
8ff14931fe feat: disable replay protection on tcp webrtc 2025-06-04 23:02:24 +08:00
pggiroro
9c7dc7e628 fix: modify gb.Logger.With 2025-06-04 20:39:49 +08:00
pggiroro
75791fe93f feat: gb28181 support add platform and platform channel from config.yaml 2025-06-04 20:36:48 +08:00
langhuihui
cf218215ff fix: tcp read block 2025-06-04 14:13:28 +08:00
langhuihui
dbf820b845 feat: downlad flv format from mp4 record file 2025-06-03 17:20:58 +08:00
langhuihui
86b9969954 feat: config support more format 2025-06-03 09:06:43 +08:00
langhuihui
b3143e8c14 fix: mp4 download 2025-06-02 22:31:25 +08:00
langhuihui
7f859e6139 fix: mp4 recovery 2025-06-02 21:12:02 +08:00
pggiroro
6eb2941087 fix: use task.Manager to resolve register handler 2025-06-02 20:09:22 +08:00
pggiroro
e8b4cea007 fix: plan.length is 168 2025-06-02 20:09:22 +08:00
pggiroro
3949773e63 fix: update config.yaml add comment about autoinvite,mediaip,sipip 2025-06-02 20:09:22 +08:00
langhuihui
d67279a404 feat: add raw check no frame 2025-05-30 14:01:18 +08:00
langhuihui
043c62f38f feat: add loop read mp4 2025-05-29 20:25:26 +08:00
pggiroro
acf9f0c677 fix: gb28181 make invite sdp mediaip or sipip correct;linux remove viaheader in sip request 2025-05-28 09:22:34 +08:00
langhuihui
49d1e7c784 feat: add s3 plugin 2025-05-28 08:40:53 +08:00
langhuihui
40bc7d4675 feat: add writerBuffer config to tcp 2025-05-27 16:56:01 +08:00
langhuihui
5aa8503aeb feat: add pull testMode 2025-05-27 10:43:34 +08:00
langhuihui
09175f0255 fix: use total insteadof totalCount 2025-05-26 16:04:20 +08:00
pggiroro
dd1a398ca2 feat: gb28181 support play sub stream 2025-05-25 21:33:14 +08:00
pggiroro
50cdfad931 fix: d.conn.NetConnection.Conn maybe nil 2025-05-25 21:33:14 +08:00
langhuihui
6df793a8fb feat: add more format for sei api 2025-05-23 17:18:43 +08:00
langhuihui
74c948d0c3 fix: rtsp memory leak 2025-05-23 10:02:36 +08:00
pggiroro
80ad1044e3 fix: gb28181 register too fast will start too many task 2025-05-22 22:56:41 +08:00
langhuihui
47884b6880 fix: rtmp timestamp start with 1 2025-05-22 22:52:21 +08:00
langhuihui
a38ddd68aa feat: add tcp dump to docker 2025-05-22 20:34:50 +08:00
banshan
a2bc3d94c1 fix: rtsp no audio or video flag 2025-05-22 20:10:17 +08:00
langhuihui
8d6bcc7b1b feat: add more hooks 2025-05-22 10:03:13 +08:00
pggiroro
f475419b7b fix: gb28181 get wrong contact 2025-05-22 09:06:06 +08:00
pggiroro
b8772f62c1 fix: gb28181 save localport into db 2025-05-22 08:58:40 +08:00
pggiroro
962f2450e5 feat: plugin crontab 2025-05-22 08:58:40 +08:00
langhuihui
aa683af001 fix: docker build 2025-05-20 18:44:22 +08:00
langhuihui
e8a1e9e014 fix: docker build 2025-05-20 09:36:11 +08:00
langhuihui
7a7e461f77 chore: docker multi-arch build 2025-05-19 15:45:20 +08:00
langhuihui
4db5d8fc9f feat: add maxcount error 2025-05-19 14:00:26 +08:00
langhuihui
718d752ea8 fix: rtmp first chunk type 3 need add timestamp 2025-05-18 21:20:50 +08:00
langhuihui
eb833ef2de chore: update dockerfile 2025-05-17 23:03:20 +08:00
pggiroro
91ddd03c19 fix: Modify the regular expression matching for playing the stream path in the GB28181 module. 2025-05-17 21:52:40 +08:00
langhuihui
fc8ec2ce70 fix: stsz box sample size 2025-05-17 08:08:18 +08:00
langhuihui
ba1d16c91c fix: pull mp4 file read g711 2025-05-16 17:43:31 +08:00
langhuihui
83bb03be72 feat: add ffmpeg to docker 2025-05-16 11:45:37 +08:00
langhuihui
bcc7defa97 feat: add webrtc h265 support 2025-05-15 19:16:40 +08:00
langhuihui
ecc3947016 doc: update readme 2025-05-15 09:27:36 +08:00
langhuihui
73e101bc6c fix: parse h265 mp4 2025-05-14 13:43:05 +08:00
langhuihui
84558ce434 fix: no need build in docker 2025-05-13 20:22:03 +08:00
banshan
9a808a7b30 readme和go mod 2025-05-13 20:18:11 +08:00
langhuihui
8586ffcb5a feat: reduce js code try build arm docker 2025-05-13 20:04:44 +08:00
banshan
55af915a50 readme和go mod 2025-05-13 19:53:20 +08:00
banshan
c2e49f1092 mcp 2025-05-13 19:36:06 +08:00
pggiroro
793936e88d feat: gb28181 subscribe alarm,mobileposition,catalog,receive notify request 2025-05-13 16:19:33 +08:00
langhuihui
06579ba60c fix: remove filter rtmp bad data temp 2025-05-12 16:34:20 +08:00
pggiroro
917f757f97 feat: gb28181 subscribe catalog and mobile position 2025-05-09 21:26:17 +08:00
langhuihui
78ce406609 feat: add hls record http api 2025-05-08 15:55:21 +08:00
langhuihui
bfed307fa2 feat: add openRTPServer at gb28181 2025-05-08 11:44:03 +08:00
pggiroro
721d4279d5 fix: updatedevice api first stop device task 2025-05-07 22:24:16 +08:00
pggiroro
97a2906377 fix: change remove device api from get to post 2025-05-07 16:47:26 +08:00
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
332 changed files with 61048 additions and 109174 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
@@ -81,16 +80,29 @@ jobs:
AWS_S3_BUCKET: monibuca
SOURCE_DIR: 'bin'
DEST_DIR: ${{ env.dest }}
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: docker build
if: success() && startsWith(github.ref, 'refs/tags/')
run: |
tar -zxvf bin/m7s_linux_amd64.tar.gz
mv m7s monibuca_linux
curl -L https://download.m7s.live/bin/admin.zip -o admin.zip
tar -zxvf bin/m7s_v5_linux_amd64.tar.gz
mv m7s monibuca_amd64
tar -zxvf bin/m7s_v5_linux_arm64.tar.gz
mv m7s monibuca_arm64
docker login -u langhuihui -p ${{ secrets.DOCKER_PASSWORD }}
docker build -t langhuihui/monibuca:v5 .
docker push langhuihui/monibuca:v5
- name: docker push
if: success() && !contains(env.version, 'beta')
if [[ "${{ env.version }}" == *"beta"* ]]; then
docker buildx build --platform linux/amd64,linux/arm64 -t langhuihui/monibuca:v5 --push .
else
docker buildx build --platform linux/amd64,linux/arm64 -t langhuihui/monibuca:v5 -t langhuihui/monibuca:${{ env.version }} --push .
fi
- name: docker build lite version
if: success() && startsWith(github.ref, 'refs/tags/')
run: |
docker tag langhuihui/monibuca:v5 langhuihui/monibuca:${{ env.version }}
docker push langhuihui/monibuca:${{ env.version }}
if [[ "${{ env.version }}" == *"beta"* ]]; then
docker buildx build --platform linux/amd64,linux/arm64 -f DockerfileLite -t monibuca/v5:latest --push .
else
docker buildx build --platform linux/amd64,linux/arm64 -f DockerfileLite -t monibuca/v5:latest -t monibuca/v5:${{ env.version }} --push .
fi

2
.gitignore vendored
View File

@@ -13,8 +13,10 @@ bin
*.flv
pullcf.yaml
*.zip
!plugin/hls/hls.js.zip
__debug*
.cursorrules
example/default/*
!example/default/main.go
!example/default/config.yaml
shutdown.sh

View File

@@ -1,34 +1,34 @@
# Compile Stage
FROM golang:1.23.2-bullseye AS builder
LABEL stage=gobuilder
# Env
ENV CGO_ENABLED 0
ENV GOOS linux
ENV GOARCH amd64
#ENV GOPROXY https://goproxy.cn,direct
ENV HOME /monibuca
WORKDIR /
RUN git clone -b v5 --depth 1 https://github.com/langhuihui/monibuca
# compile
WORKDIR /monibuca
RUN go build -tags sqlite -o ./build/monibuca ./example/default/main.go
RUN cp -r /monibuca/example/default/config.yaml /monibuca/build
# Running Stage
FROM alpine:3.20
FROM linuxserver/ffmpeg:latest
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
CMD [ "./monibuca", "-c", "/etc/monibuca/config.yaml" ]
# Copy the pre-compiled binary from the build context
# The GitHub Actions workflow prepares 'monibuca_linux' in the context root
COPY monibuca_amd64 ./monibuca_amd64
COPY monibuca_arm64 ./monibuca_arm64
COPY admin.zip ./admin.zip
# Install tcpdump
RUN apt-get update && apt-get install -y tcpdump && rm -rf /var/lib/apt/lists/*
# Copy the configuration file from the build context
COPY example/default/config.yaml /etc/monibuca/config.yaml
# Export necessary ports
EXPOSE 6000 8080 8443 1935 554 5060 9000-20000
EXPOSE 5060/udp 44944/udp
RUN if [ "$(uname -m)" = "aarch64" ]; then \
mv ./monibuca_arm64 ./monibuca_linux; \
rm ./monibuca_amd64; \
else \
mv ./monibuca_amd64 ./monibuca_linux; \
rm ./monibuca_arm64; \
fi
ENTRYPOINT [ "./monibuca_linux"]
CMD ["-c", "/etc/monibuca/config.yaml"]

31
DockerfileLite Normal file
View File

@@ -0,0 +1,31 @@
# Running Stage
FROM alpine:latest
WORKDIR /monibuca
# Copy the pre-compiled binary from the build context
# The GitHub Actions workflow prepares 'monibuca_linux' in the context root
COPY monibuca_amd64 ./monibuca_amd64
COPY monibuca_arm64 ./monibuca_arm64
COPY admin.zip ./admin.zip
# Copy the configuration file from the build context
COPY example/default/config.yaml /etc/monibuca/config.yaml
# Export necessary ports
EXPOSE 6000 8080 8443 1935 554 5060 9000-20000
EXPOSE 5060/udp 44944/udp
RUN if [ "$(uname -m)" = "aarch64" ]; then \
mv ./monibuca_arm64 ./monibuca_linux; \
rm ./monibuca_amd64; \
else \
mv ./monibuca_amd64 ./monibuca_linux; \
rm ./monibuca_arm64; \
fi
ENTRYPOINT [ "./monibuca_linux"]
CMD ["-c", "/etc/monibuca/config.yaml"]

View File

@@ -10,6 +10,9 @@
<!-- PROJECT LOGO -->
<br />
<div align="center">
<a href="https://m7s.live">
<img src="https://m7s.live/logo+.svg" alt="Logo" width="200">
</a>
<h1 align="center">Monibuca v5</h1>
<p align="center">
@@ -37,6 +40,7 @@
<li><a href="#monitoring">Monitoring</a></li>
<li><a href="#plugin-development">Plugin Development</a></li>
<li><a href="#arch">Architecture</a></li>
<li><a href="#third-party-plugins">Third-party Plugins</a></li>
<li><a href="#contributing">Contributing</a></li>
<li><a href="#license">License</a></li>
<li><a href="#contact">Contact</a></li>
@@ -112,6 +116,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>
@@ -141,6 +146,12 @@ For detailed architecture design documentation, please refer to the [Architectur
<p align="right">(<a href="#readme-top">back to top</a>)</p>
## Third-party Plugins
- https://github.com/cuteLittleDevil/m7s-jt1078
<p align="right">(<a href="#readme-top">back to top</a>)</p>
## Contributing
Contributions are what make the open source community such an amazing place to learn, inspire, and create. Any contributions you make are **greatly appreciated**.

View File

@@ -8,11 +8,11 @@
[![Issues][issues-shield]][issues-url]
[![AGPL License][license-shield]][license-url]
[![Go Reference](https://pkg.go.dev/badge/m7s.live/v5.svg)](https://pkg.go.dev/m7s.live/v5)
<a href="https://hellogithub.com/repository/6d7916d851c2481f87568ffd9f1c21d9" target="_blank"><img src="https://api.hellogithub.com/v1/widgets/recommend.svg?rid=6d7916d851c2481f87568ffd9f1c21d9&claim_uid=riBYPOGenUf7kbc&theme=small" alt="FeaturedHelloGitHub" /></a>
<br />
<div align="center">
<a href="https://monibuca.com">
<img src="https://monibuca.com/svg/logo.svg" alt="Logo" width="200">
<img src="https://monibuca.com/logo+.svg" alt="Logo" width="200">
</a>
<h1 align="center">Monibuca v5</h1>
@@ -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>
@@ -144,6 +145,10 @@ Monibuca 支持通过插件扩展功能。查看[插件开发指南](./plugin/RE
<p align="right">(<a href="#readme-top">返回顶部</a>)</p>
## 第三方插件
- - https://github.com/cuteLittleDevil/m7s-jt1078
## 贡献指南
我们非常欢迎社区贡献,您的参与将使开源社区变得更加精彩!

139
RELEASE_NOTES_5.0.x_CN.md Normal file
View File

@@ -0,0 +1,139 @@
# Monibuca v5.0.x Release Notes
## v5.0.3 (2025-06-27)
### 🎉 新功能 (New Features)
#### 录像与流媒体协议增强
- **MP4/FLV录像优化**:多项修复和优化录像拉取、分片、写入、格式转换等功能,提升兼容性和稳定性。
- **GB28181协议增强**支持pullproxy代理GB28181流完善平台配置、子码流播放、单独media port等能力。
- **插件与配置系统**插件初始化、配置加载、数据库适配等增强支持获取全部配置yaml示例。
- **WebRTC/HLS/RTMP协议适配**WebRTC支持更多编解码器HLS/RTMP协议兼容性提升。
- **crontab计划录像**:定时任务插件支持计划录像,拉流代理支持禁用。
### 🐛 问题修复 (Bug Fixes)
- **录像/流媒体相关**修复mp4、flv、rtmp、hls等协议的多项bug包括clone buffer、SQL语法、表结构适配等。
- **GB28181/数据库**修复注册、流订阅、表结构、SQL语法等问题适配PostgreSQL。
- **插件系统**:修复插件初始化、数据库对象赋值、配置加载等问题。
### 🛠️ 优化改进 (Improvements)
- **代码结构重构**重构mp4、record、插件等系统提升可维护性。
- **文档与示例**完善文档说明增加配置和API示例。
- **Docker镜像**优化tcpdump、ffmpeg等工具集成。
### 👥 贡献者 (Contributors)
- langhuihui
- pggiroro
- banshan
---
## v5.0.2 (2025-06-05)
### 🎉 新功能 (New Features)
#### 核心功能
- **降低延迟** - 禁用了TCP WebRTC的重放保护功能降低了延迟
- **配置系统增强** - 支持更多配置格式(支持配置项中插入`-``_`和大写字母),提升配置灵活性
- **原始数据检查** - 新增原始数据无帧检查功能,提升数据处理稳定性
- **MP4循环读取** - 支持MP4文件循环读取功能通过配置 pull 配置下的 `loop` 配置)
- **S3插件** - 新增S3存储插件支持云存储集成
- **TCP读写缓冲配置** - 新增TCP连接读写缓冲区配置选项针对高并发下的吞吐能力增强
- **拉流测试模式** - 新增拉流测试模式选项(可以选择拉流时不发布),便于调试和测试
- **SEI API格式扩展** - 扩展SEI API支持更多数据格式
- **Hook扩展** - 新增更多Hook回调点增强扩展性
- **定时任务插件** - 新增crontab定时任务插件
- **服务器抓包** - 新增服务器抓包功能(调用`tcpdump`支持TCP和UDP协议,API 说明见 [tcpdump](https://api.monibuca.com/api-301117332)
#### GB28181协议增强
- **平台配置支持** - GB28181现在支持从config.yaml中添加平台和平台通道配置
- **子码流播放** - 支持GB28181子码流播放功能
- **SDP优化** - 优化invite SDP中的mediaip和sipip处理
- **本地端口保存** - 修复GB28181本地端口保存到数据库的问题
#### MP4功能增强
- **FLV格式下载** - 支持从MP4录制文件下载FLV格式
- **下载功能修复** - 修复MP4下载功能的相关问题
- **恢复功能修复** - 修复MP4恢复功能
### 🐛 问题修复 (Bug Fixes)
#### 网络通信
- **TCP读取阻塞** - 修复TCP读取阻塞问题增加了读取超时设置
- **RTSP内存泄漏** - 修复RTSP协议的内存泄漏问题
- **RTSP音视频标识** - 修复RTSP无音频或视频标识的问题
#### GB28181协议
- **任务管理** - 使用task.Manager解决注册处理器的问题
- **计划长度** - 修复plan.length为168的问题
- **注册频率** - 修复GB28181注册过快导致启动过多任务的问题
- **联系信息** - 修复GB28181获取错误联系信息的问题
#### RTMP协议
- **时间戳处理** - 修复RTMP时间戳开头跳跃问题
### 🛠️ 优化改进 (Improvements)
#### Docker支持
- **tcpdump工具** - Docker镜像中新增tcpdump网络诊断工具
#### Linux平台优化
- **SIP请求优化** - Linux平台移除SIP请求中的viaheader
### 👥 贡献者 (Contributors)
- langhuihui
- pggiroro
- banshan
---
## v5.0.1 (2025-05-21)
### 🎉 新功能 (New Features)
#### WebRTC增强
- **H265支持** - 新增WebRTC对H265编码的支持提升视频质量和压缩效率
#### GB28181协议增强
- **订阅功能扩展** - GB28181模块现在支持订阅报警、移动位置、目录信息
- **通知请求** - 支持接收通知请求,增强与设备的交互能力
#### Docker优化
- **FFmpeg集成** - Docker镜像中新增FFmpeg工具支持更多音视频处理场景
- **多架构支持** - 新增Docker多架构构建支持
### 🐛 问题修复 (Bug Fixes)
#### Docker相关
- **构建问题** - 修复Docker构建过程中的多个问题
- **构建优化** - 优化Docker构建流程提升构建效率
#### RTMP协议
- **时间戳处理** - 修复RTMP第一个chunk类型3需要添加时间戳的问题
#### GB28181协议
- **路径匹配** - 修复GB28181模块中播放流路径的正则表达式匹配问题
#### MP4处理
- **stsz box** - 修复stsz box采样大小的问题
- **G711音频** - 修复拉取MP4文件时读取G711音频的问题
- **H265解析** - 修复H265 MP4文件解析问题
### 🛠️ 优化改进 (Improvements)
#### 代码质量
- **错误处理** - 新增maxcount错误处理机制
- **文档更新** - 更新README文档和go.mod配置
#### 构建系统
- **ARM架构** - 减少JavaScript代码优化ARM架构Docker构建
- **构建标签** - 移除Docker中不必要的构建标签
### 📦 其他更新 (Other Updates)
- **MCP相关** - 更新Model Context Protocol相关功能
- **依赖更新** - 更新项目依赖和模块配置
### 👥 贡献者 (Contributors)
- langhuihui
---

25
alarm.go Normal file
View File

@@ -0,0 +1,25 @@
package m7s
import (
"time"
)
// AlarmInfo 报警信息实体,用于存储到数据库
type AlarmInfo struct {
ID uint `gorm:"primaryKey;autoIncrement" json:"id"` // 主键自增ID
ServerInfo string `gorm:"type:varchar(255);not null" json:"serverInfo"` // 服务器信息
StreamName string `gorm:"type:varchar(255);index" json:"streamName"` // 流名称
StreamPath string `gorm:"type:varchar(500)" json:"streamPath"` // 流的streampath
AlarmName string `gorm:"type:varchar(255);not null" json:"alarmName"` // 报警名称
AlarmDesc string `gorm:"type:varchar(500);not null" json:"alarmDesc"` // 报警描述
AlarmType int `gorm:"not null;index" json:"alarmType"` // 报警类型(对应之前定义的常量)
IsSent bool `gorm:"default:false" json:"isSent"` // 是否已成功发送
CreatedAt time.Time `gorm:"autoCreateTime" json:"createdAt"` // 创建时间,报警时间
UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updatedAt"` // 更新时间
FilePath string `gorm:"type:varchar(255)" json:"filePath"` // 文件路径
}
// TableName 指定表名
func (AlarmInfo) TableName() string {
return "alarm_info"
}

640
api.go
View File

@@ -7,7 +7,6 @@ import (
"net/http"
"net/url"
"os"
"path/filepath"
"reflect"
"runtime"
"strings"
@@ -79,7 +78,7 @@ func (s *Server) DisabledPlugins(ctx context.Context, _ *emptypb.Empty) (res *pb
// /api/stream/annexb/{streamPath}
func (s *Server) api_Stream_AnnexB_(rw http.ResponseWriter, r *http.Request) {
publisher, ok := s.Streams.Get(r.PathValue("streamPath"))
publisher, ok := s.Streams.SafeGet(r.PathValue("streamPath"))
if !ok || publisher.VideoTrack.AVTrack == nil {
http.Error(rw, pkg.ErrNotFound.Error(), http.StatusNotFound)
return
@@ -97,22 +96,14 @@ func (s *Server) api_Stream_AnnexB_(rw http.ResponseWriter, r *http.Request) {
return
}
defer reader.StopRead()
if reader.Value.Raw == nil {
if err = reader.Value.Demux(publisher.VideoTrack.ICodecCtx); err != nil {
http.Error(rw, err.Error(), http.StatusInternalServerError)
return
}
}
var annexb pkg.AnnexB
var t pkg.AVTrack
t.ICodecCtx, t.SequenceFrame, err = annexb.ConvertCtx(publisher.VideoTrack.ICodecCtx)
if t.ICodecCtx == nil {
http.Error(rw, "unsupported codec", http.StatusInternalServerError)
var annexb *pkg.AnnexB
var converter = pkg.NewAVFrameConvert[*pkg.AnnexB](publisher.VideoTrack.AVTrack, nil)
annexb, err = converter.ConvertFromAVFrame(&reader.Value)
if err != nil {
http.Error(rw, err.Error(), http.StatusInternalServerError)
return
}
annexb.Mux(t.ICodecCtx, &reader.Value)
_, err = annexb.WriteTo(rw)
annexb.WriteTo(rw)
}
func (s *Server) getStreamInfo(pub *Publisher) (res *pb.StreamInfoResponse, err error) {
@@ -181,32 +172,27 @@ func (s *Server) getStreamInfo(pub *Publisher) (res *pb.StreamInfoResponse, err
func (s *Server) StreamInfo(ctx context.Context, req *pb.StreamSnapRequest) (res *pb.StreamInfoResponse, err error) {
var recordings []*pb.RecordingDetail
s.Records.Call(func() error {
for record := range s.Records.Range {
if record.StreamPath == req.StreamPath {
recordings = append(recordings, &pb.RecordingDetail{
FilePath: record.RecConf.FilePath,
Mode: record.Mode,
Fragment: durationpb.New(record.RecConf.Fragment),
Append: record.RecConf.Append,
PluginName: record.Plugin.Meta.Name,
})
}
s.Records.SafeRange(func(record *RecordJob) bool {
if record.StreamPath == req.StreamPath {
recordings = append(recordings, &pb.RecordingDetail{
FilePath: record.RecConf.FilePath,
Mode: record.RecConf.Mode,
Fragment: durationpb.New(record.RecConf.Fragment),
Append: record.RecConf.Append,
PluginName: record.Plugin.Meta.Name,
})
}
return nil
return true
})
s.Streams.Call(func() error {
if pub, ok := s.Streams.Get(req.StreamPath); ok {
res, err = s.getStreamInfo(pub)
if err != nil {
return err
}
res.Data.Recording = recordings
} else {
err = pkg.ErrNotFound
if pub, ok := s.Streams.SafeGet(req.StreamPath); ok {
res, err = s.getStreamInfo(pub)
if err != nil {
return
}
return nil
})
res.Data.Recording = recordings
} else {
err = pkg.ErrNotFound
}
return
}
@@ -264,17 +250,15 @@ func (s *Server) RestartTask(ctx context.Context, req *pb.RequestWithId64) (resp
}
func (s *Server) GetRecording(ctx context.Context, req *emptypb.Empty) (resp *pb.RecordingListResponse, err error) {
s.Records.Call(func() error {
resp = &pb.RecordingListResponse{}
for record := range s.Records.Range {
resp.Data = append(resp.Data, &pb.Recording{
StreamPath: record.StreamPath,
StartTime: timestamppb.New(record.StartTime),
Type: reflect.TypeOf(record.recorder).String(),
Pointer: uint64(record.GetTaskPointer()),
})
}
return nil
resp = &pb.RecordingListResponse{}
s.Records.SafeRange(func(record *RecordJob) bool {
resp.Data = append(resp.Data, &pb.Recording{
StreamPath: record.StreamPath,
StartTime: timestamppb.New(record.StartTime),
Type: reflect.TypeOf(record.recorder).String(),
Pointer: uint64(record.GetTaskPointer()),
})
return true
})
return
}
@@ -324,50 +308,47 @@ func (s *Server) GetSubscribers(context.Context, *pb.SubscribersRequest) (res *p
return
}
func (s *Server) AudioTrackSnap(_ context.Context, req *pb.StreamSnapRequest) (res *pb.TrackSnapShotResponse, err error) {
s.Streams.Call(func() error {
if pub, ok := s.Streams.Get(req.StreamPath); ok && pub.HasAudioTrack() {
data := &pb.TrackSnapShotData{}
if pub.AudioTrack.Allocator != nil {
for _, memlist := range pub.AudioTrack.Allocator.GetChildren() {
var list []*pb.MemoryBlock
for _, block := range memlist.GetBlocks() {
list = append(list, &pb.MemoryBlock{
S: uint32(block.Start),
E: uint32(block.End),
})
}
data.Memory = append(data.Memory, &pb.MemoryBlockGroup{List: list, Size: uint32(memlist.Size)})
if pub, ok := s.Streams.SafeGet(req.StreamPath); ok && pub.HasAudioTrack() {
data := &pb.TrackSnapShotData{}
if pub.AudioTrack.Allocator != nil {
for _, memlist := range pub.AudioTrack.Allocator.GetChildren() {
var list []*pb.MemoryBlock
for _, block := range memlist.GetBlocks() {
list = append(list, &pb.MemoryBlock{
S: uint32(block.Start),
E: uint32(block.End),
})
}
data.Memory = append(data.Memory, &pb.MemoryBlockGroup{List: list, Size: uint32(memlist.Size)})
}
pub.AudioTrack.Ring.Do(func(v *pkg.AVFrame) {
if len(v.Wraps) > 0 {
var snap pb.TrackSnapShot
snap.Sequence = v.Sequence
snap.Timestamp = uint32(v.Timestamp / time.Millisecond)
snap.WriteTime = timestamppb.New(v.WriteTime)
snap.Wrap = make([]*pb.Wrap, len(v.Wraps))
snap.KeyFrame = v.IDR
data.RingDataSize += uint32(v.Wraps[0].GetSize())
for i, wrap := range v.Wraps {
snap.Wrap[i] = &pb.Wrap{
Timestamp: uint32(wrap.GetTimestamp() / time.Millisecond),
Size: uint32(wrap.GetSize()),
Data: wrap.String(),
}
}
data.Ring = append(data.Ring, &snap)
}
})
res = &pb.TrackSnapShotResponse{
Code: 0,
Message: "success",
Data: data,
}
} else {
err = pkg.ErrNotFound
}
return nil
})
pub.AudioTrack.Ring.Do(func(v *pkg.AVFrame) {
if len(v.Wraps) > 0 {
var snap pb.TrackSnapShot
snap.Sequence = v.Sequence
snap.Timestamp = uint32(v.Timestamp / time.Millisecond)
snap.WriteTime = timestamppb.New(v.WriteTime)
snap.Wrap = make([]*pb.Wrap, len(v.Wraps))
snap.KeyFrame = v.IDR
data.RingDataSize += uint32(v.Wraps[0].GetSize())
for i, wrap := range v.Wraps {
snap.Wrap[i] = &pb.Wrap{
Timestamp: uint32(wrap.GetTimestamp() / time.Millisecond),
Size: uint32(wrap.GetSize()),
Data: wrap.String(),
}
}
data.Ring = append(data.Ring, &snap)
}
})
res = &pb.TrackSnapShotResponse{
Code: 0,
Message: "success",
Data: data,
}
} else {
err = pkg.ErrNotFound
}
return
}
func (s *Server) api_VideoTrack_SSE(rw http.ResponseWriter, r *http.Request) {
@@ -383,27 +364,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,74 +397,68 @@ 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) {
s.Streams.Call(func() error {
if pub, ok := s.Streams.Get(req.StreamPath); ok && pub.HasVideoTrack() {
data := &pb.TrackSnapShotData{}
if pub.VideoTrack.Allocator != nil {
for _, memlist := range pub.VideoTrack.Allocator.GetChildren() {
var list []*pb.MemoryBlock
for _, block := range memlist.GetBlocks() {
list = append(list, &pb.MemoryBlock{
S: uint32(block.Start),
E: uint32(block.End),
})
}
data.Memory = append(data.Memory, &pb.MemoryBlockGroup{List: list, Size: uint32(memlist.Size)})
if pub, ok := s.Streams.SafeGet(req.StreamPath); ok && pub.HasVideoTrack() {
data := &pb.TrackSnapShotData{}
if pub.VideoTrack.Allocator != nil {
for _, memlist := range pub.VideoTrack.Allocator.GetChildren() {
var list []*pb.MemoryBlock
for _, block := range memlist.GetBlocks() {
list = append(list, &pb.MemoryBlock{
S: uint32(block.Start),
E: uint32(block.End),
})
}
data.Memory = append(data.Memory, &pb.MemoryBlockGroup{List: list, Size: uint32(memlist.Size)})
}
pub.VideoTrack.Ring.Do(func(v *pkg.AVFrame) {
if len(v.Wraps) > 0 {
var snap pb.TrackSnapShot
snap.Sequence = v.Sequence
snap.Timestamp = uint32(v.Timestamp / time.Millisecond)
snap.WriteTime = timestamppb.New(v.WriteTime)
snap.Wrap = make([]*pb.Wrap, len(v.Wraps))
snap.KeyFrame = v.IDR
data.RingDataSize += uint32(v.Wraps[0].GetSize())
for i, wrap := range v.Wraps {
snap.Wrap[i] = &pb.Wrap{
Timestamp: uint32(wrap.GetTimestamp() / time.Millisecond),
Size: uint32(wrap.GetSize()),
Data: wrap.String(),
}
}
data.Ring = append(data.Ring, &snap)
}
})
res = &pb.TrackSnapShotResponse{
Code: 0,
Message: "success",
Data: data,
}
} else {
err = pkg.ErrNotFound
}
return nil
})
pub.VideoTrack.Ring.Do(func(v *pkg.AVFrame) {
if len(v.Wraps) > 0 {
var snap pb.TrackSnapShot
snap.Sequence = v.Sequence
snap.Timestamp = uint32(v.Timestamp / time.Millisecond)
snap.WriteTime = timestamppb.New(v.WriteTime)
snap.Wrap = make([]*pb.Wrap, len(v.Wraps))
snap.KeyFrame = v.IDR
data.RingDataSize += uint32(v.Wraps[0].GetSize())
for i, wrap := range v.Wraps {
snap.Wrap[i] = &pb.Wrap{
Timestamp: uint32(wrap.GetTimestamp() / time.Millisecond),
Size: uint32(wrap.GetSize()),
Data: wrap.String(),
}
}
data.Ring = append(data.Ring, &snap)
}
})
res = &pb.TrackSnapShotResponse{
Code: 0,
Message: "success",
Data: data,
}
} else {
err = pkg.ErrNotFound
}
return
}
@@ -532,86 +504,65 @@ func (s *Server) StopSubscribe(ctx context.Context, req *pb.RequestWithId) (res
}
func (s *Server) PauseStream(ctx context.Context, req *pb.StreamSnapRequest) (res *pb.SuccessResponse, err error) {
s.Streams.Call(func() error {
if s, ok := s.Streams.Get(req.StreamPath); ok {
s.Pause()
}
return nil
})
if s, ok := s.Streams.SafeGet(req.StreamPath); ok {
s.Pause()
}
return &pb.SuccessResponse{}, err
}
func (s *Server) ResumeStream(ctx context.Context, req *pb.StreamSnapRequest) (res *pb.SuccessResponse, err error) {
s.Streams.Call(func() error {
if s, ok := s.Streams.Get(req.StreamPath); ok {
s.Resume()
}
return nil
})
if s, ok := s.Streams.SafeGet(req.StreamPath); ok {
s.Resume()
}
return &pb.SuccessResponse{}, err
}
func (s *Server) SetStreamSpeed(ctx context.Context, req *pb.SetStreamSpeedRequest) (res *pb.SuccessResponse, err error) {
s.Streams.Call(func() error {
if s, ok := s.Streams.Get(req.StreamPath); ok {
s.Speed = float64(req.Speed)
s.Scale = float64(req.Speed)
s.Info("set stream speed", "speed", req.Speed)
}
return nil
})
if s, ok := s.Streams.SafeGet(req.StreamPath); ok {
s.Speed = float64(req.Speed)
s.Scale = float64(req.Speed)
s.Info("set stream speed", "speed", req.Speed)
}
return &pb.SuccessResponse{}, err
}
func (s *Server) SeekStream(ctx context.Context, req *pb.SeekStreamRequest) (res *pb.SuccessResponse, err error) {
s.Streams.Call(func() error {
if s, ok := s.Streams.Get(req.StreamPath); ok {
s.Seek(time.Unix(int64(req.TimeStamp), 0))
}
return nil
})
if s, ok := s.Streams.SafeGet(req.StreamPath); ok {
s.Seek(time.Unix(int64(req.TimeStamp), 0))
}
return &pb.SuccessResponse{}, err
}
func (s *Server) StopPublish(ctx context.Context, req *pb.StreamSnapRequest) (res *pb.SuccessResponse, err error) {
s.Streams.Call(func() error {
if s, ok := s.Streams.Get(req.StreamPath); ok {
s.Stop(task.ErrStopByUser)
}
return nil
})
if s, ok := s.Streams.SafeGet(req.StreamPath); ok {
s.Stop(task.ErrStopByUser)
}
return &pb.SuccessResponse{}, err
}
// /api/stream/list
func (s *Server) StreamList(_ context.Context, req *pb.StreamListRequest) (res *pb.StreamListResponse, err error) {
recordingMap := make(map[string][]*pb.RecordingDetail)
s.Records.Call(func() error {
for record := range s.Records.Range {
recordingMap[record.StreamPath] = append(recordingMap[record.StreamPath], &pb.RecordingDetail{
FilePath: record.RecConf.FilePath,
Mode: record.Mode,
Fragment: durationpb.New(record.RecConf.Fragment),
Append: record.RecConf.Append,
PluginName: record.Plugin.Meta.Name,
Pointer: uint64(record.GetTaskPointer()),
})
for record := range s.Records.SafeRange {
recordingMap[record.StreamPath] = append(recordingMap[record.StreamPath], &pb.RecordingDetail{
FilePath: record.RecConf.FilePath,
Mode: record.RecConf.Mode,
Fragment: durationpb.New(record.RecConf.Fragment),
Append: record.RecConf.Append,
PluginName: record.Plugin.Meta.Name,
Pointer: uint64(record.GetTaskPointer()),
})
}
var streams []*pb.StreamInfo
for publisher := range s.Streams.SafeRange {
info, err := s.getStreamInfo(publisher)
if err != nil {
continue
}
return nil
})
s.Streams.Call(func() error {
var streams []*pb.StreamInfo
for publisher := range s.Streams.Range {
info, err := s.getStreamInfo(publisher)
if err != nil {
continue
}
info.Data.Recording = recordingMap[info.Data.Path]
streams = append(streams, info.Data)
}
res = &pb.StreamListResponse{Data: streams, Total: int32(s.Streams.Length), PageNum: req.PageNum, PageSize: req.PageSize}
return nil
})
info.Data.Recording = recordingMap[info.Data.Path]
streams = append(streams, info.Data)
}
res = &pb.StreamListResponse{Data: streams, Total: int32(s.Streams.Length), PageNum: req.PageNum, PageSize: req.PageSize}
return
}
@@ -638,24 +589,18 @@ func (s *Server) Api_Summary_SSE(rw http.ResponseWriter, r *http.Request) {
func (s *Server) Api_Stream_Position_SSE(rw http.ResponseWriter, r *http.Request) {
streamPath := r.URL.Query().Get("streamPath")
util.ReturnFetchValue(func() (t time.Time) {
s.Streams.Call(func() error {
if pub, ok := s.Streams.Get(streamPath); ok {
t = pub.GetPosition()
}
return nil
})
if pub, ok := s.Streams.SafeGet(streamPath); ok {
t = pub.GetPosition()
}
return
}, rw, r)
}
// func (s *Server) Api_Vod_Position(rw http.ResponseWriter, r *http.Request) {
// streamPath := r.URL.Query().Get("streamPath")
// s.Streams.Call(func() error {
// if pub, ok := s.Streams.Get(streamPath); ok {
// t = pub.GetPosition()
// }
// return nil
// })
// if pub, ok := s.Streams.SafeGet(streamPath); ok {
// t = pub.GetPosition()
// }
// }
func (s *Server) Summary(context.Context, *emptypb.Empty) (res *pb.SummaryResponse, err error) {
@@ -739,7 +684,7 @@ func (s *Server) GetConfigFile(_ context.Context, req *emptypb.Empty) (res *pb.G
func (s *Server) UpdateConfigFile(_ context.Context, req *pb.UpdateConfigFileRequest) (res *pb.SuccessResponse, err error) {
if s.configFileContent != nil {
s.configFileContent = []byte(req.Content)
os.WriteFile(filepath.Join(ExecDir, s.conf.(string)), s.configFileContent, 0644)
os.WriteFile(s.configFilePath, s.configFileContent, 0644)
res = &pb.SuccessResponse{}
} else {
err = pkg.ErrNotFound
@@ -783,30 +728,7 @@ 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) {
func (s *Server) GetRecordList(ctx context.Context, req *pb.ReqRecordList) (resp *pb.RecordResponseList, err error) {
if s.DB == nil {
err = pkg.ErrNoDB
return
@@ -827,9 +749,6 @@ func (s *Server) GetRecordList(ctx context.Context, req *pb.ReqRecordList) (resp
} else if req.StreamPath != "" {
query = query.Where("stream_path = ?", req.StreamPath)
}
if req.Mode != "" {
query = query.Where("mode = ?", req.Mode)
}
if req.Type != "" {
query = query.Where("type = ?", req.Type)
}
@@ -848,10 +767,10 @@ func (s *Server) GetRecordList(ctx context.Context, req *pb.ReqRecordList) (resp
if err != nil {
return
}
resp = &pb.ResponseList{
TotalCount: uint32(totalCount),
PageNum: req.PageNum,
PageSize: req.PageSize,
resp = &pb.RecordResponseList{
Total: uint32(totalCount),
PageNum: req.PageNum,
PageSize: req.PageSize,
}
for _, recordFile := range result {
resp.Data = append(resp.Data, &pb.RecordFile{
@@ -865,6 +784,69 @@ func (s *Server) GetRecordList(ctx context.Context, req *pb.ReqRecordList) (resp
return
}
func (s *Server) GetEventRecordList(ctx context.Context, req *pb.ReqRecordList) (resp *pb.EventRecordResponseList, err error) {
if s.DB == nil {
err = pkg.ErrNoDB
return
}
if req.PageSize == 0 {
req.PageSize = 10
}
if req.PageNum == 0 {
req.PageNum = 1
}
offset := (req.PageNum - 1) * req.PageSize // 计算偏移量
var totalCount int64 //总条数
var result []*EventRecordStream
query := s.DB.Model(&EventRecordStream{})
if strings.Contains(req.StreamPath, "*") {
query = query.Where("stream_path like ?", strings.ReplaceAll(req.StreamPath, "*", "%"))
} else if req.StreamPath != "" {
query = query.Where("stream_path = ?", req.StreamPath)
}
if req.Type != "" {
query = query.Where("type = ?", req.Type)
}
startTime, endTime, err := util.TimeRangeQueryParse(url.Values{"range": []string{req.Range}, "start": []string{req.Start}, "end": []string{req.End}})
if err == nil {
if !startTime.IsZero() {
query = query.Where("start_time >= ?", startTime)
}
if !endTime.IsZero() {
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
if err != nil {
return
}
resp = &pb.EventRecordResponseList{
Total: uint32(totalCount),
PageNum: req.PageNum,
PageSize: req.PageSize,
}
for _, recordFile := range result {
resp.Data = append(resp.Data, &pb.EventRecordFile{
Id: uint32(recordFile.ID),
StartTime: timestamppb.New(recordFile.StartTime),
EndTime: timestamppb.New(recordFile.EndTime),
FilePath: recordFile.FilePath,
StreamPath: recordFile.StreamPath,
EventLevel: recordFile.EventLevel,
EventId: recordFile.EventId,
EventName: recordFile.EventName,
EventDesc: recordFile.EventDesc,
})
}
return
}
func (s *Server) GetRecordCatalog(ctx context.Context, req *pb.ReqRecordCatalog) (resp *pb.ResponseCatalog, err error) {
if s.DB == nil {
err = pkg.ErrNoDB
@@ -960,3 +942,117 @@ func (s *Server) GetTransformList(ctx context.Context, req *emptypb.Empty) (res
})
return
}
func (s *Server) GetAlarmList(ctx context.Context, req *pb.AlarmListRequest) (res *pb.AlarmListResponse, err error) {
// 初始化响应对象
res = &pb.AlarmListResponse{
Code: 0,
Message: "success",
PageNum: req.PageNum,
PageSize: req.PageSize,
}
// 检查数据库连接是否可用
if s.DB == nil {
res.Code = 500
res.Message = "数据库连接不可用"
return res, nil
}
// 构建查询条件
query := s.DB.Model(&AlarmInfo{})
// 添加时间范围过滤
startTime, endTime, err := util.TimeRangeQueryParse(url.Values{
"range": []string{req.Range},
"start": []string{req.Start},
"end": []string{req.End},
})
if err == nil {
if !startTime.IsZero() {
query = query.Where("created_at >= ?", startTime)
}
if !endTime.IsZero() {
query = query.Where("created_at <= ?", endTime)
}
}
// 添加告警类型过滤
if req.AlarmType != 0 {
query = query.Where("alarm_type = ?", req.AlarmType)
}
// 添加 StreamPath 过滤
if req.StreamPath != "" {
if strings.Contains(req.StreamPath, "*") {
// 支持通配符搜索
query = query.Where("stream_path LIKE ?", strings.ReplaceAll(req.StreamPath, "*", "%"))
} else {
query = query.Where("stream_path = ?", req.StreamPath)
}
}
// 添加 StreamName 过滤
if req.StreamName != "" {
if strings.Contains(req.StreamName, "*") {
// 支持通配符搜索
query = query.Where("stream_name LIKE ?", strings.ReplaceAll(req.StreamName, "*", "%"))
} else {
query = query.Where("stream_name = ?", req.StreamName)
}
}
// 计算总记录数
var total int64
if err = query.Count(&total).Error; err != nil {
res.Code = 500
res.Message = "查询告警信息总数失败: " + err.Error()
return res, nil
}
res.Total = int32(total)
// 如果没有记录,直接返回
if total == 0 {
return res, nil
}
// 处理分页参数
if req.PageNum <= 0 {
req.PageNum = 1
}
if req.PageSize <= 0 {
req.PageSize = 10
}
// 查询分页数据
var alarmInfoList []AlarmInfo
offset := (req.PageNum - 1) * req.PageSize
if err = query.Order("created_at DESC").
Offset(int(offset)).
Limit(int(req.PageSize)).
Find(&alarmInfoList).Error; err != nil {
res.Code = 500
res.Message = "查询告警信息失败: " + err.Error()
return res, nil
}
// 转换为 protobuf 格式
res.Data = make([]*pb.AlarmInfo, len(alarmInfoList))
for i, alarm := range alarmInfoList {
res.Data[i] = &pb.AlarmInfo{
Id: uint32(alarm.ID),
ServerInfo: alarm.ServerInfo,
StreamName: alarm.StreamName,
StreamPath: alarm.StreamPath,
AlarmDesc: alarm.AlarmDesc,
AlarmName: alarm.AlarmName,
AlarmType: int32(alarm.AlarmType),
IsSent: alarm.IsSent,
CreatedAt: timestamppb.New(alarm.CreatedAt),
UpdatedAt: timestamppb.New(alarm.UpdatedAt),
FilePath: alarm.FilePath,
}
}
return res, nil
}

324
api_config.go Normal file
View File

@@ -0,0 +1,324 @@
package m7s
import (
"net/http"
"reflect"
"strconv"
"strings"
"time"
"gopkg.in/yaml.v3"
)
func getIndent(line string) int {
return len(line) - len(strings.TrimLeft(line, " "))
}
func addCommentsToYAML(yamlData []byte) []byte {
lines := strings.Split(string(yamlData), "\n")
var result strings.Builder
var commentBuffer []string
var keyLineBuffer string
var keyLineIndent int
inMultilineValue := false
for _, line := range lines {
trimmedLine := strings.TrimSpace(line)
indent := getIndent(line)
if strings.HasPrefix(trimmedLine, "_description:") {
description := strings.TrimSpace(strings.TrimPrefix(trimmedLine, "_description:"))
commentBuffer = append(commentBuffer, "# "+description)
} else if strings.HasPrefix(trimmedLine, "_enum:") {
enum := strings.TrimSpace(strings.TrimPrefix(trimmedLine, "_enum:"))
commentBuffer = append(commentBuffer, "# 可选值: "+enum)
} else if strings.HasPrefix(trimmedLine, "_value:") {
valueStr := strings.TrimSpace(strings.TrimPrefix(trimmedLine, "_value:"))
if valueStr != "" && valueStr != "{}" && valueStr != "[]" {
// Single line value
result.WriteString(strings.Repeat(" ", keyLineIndent))
result.WriteString(keyLineBuffer)
result.WriteString(": ")
result.WriteString(valueStr)
if len(commentBuffer) > 0 {
result.WriteString(" ")
for j, c := range commentBuffer {
c = strings.TrimSpace(strings.TrimPrefix(c, "#"))
result.WriteString("# " + c)
if j < len(commentBuffer)-1 {
result.WriteString(" ")
}
}
}
result.WriteString("\n")
} else {
// Multi-line value (struct/map)
for _, comment := range commentBuffer {
result.WriteString(strings.Repeat(" ", keyLineIndent))
result.WriteString(comment)
result.WriteString("\n")
}
result.WriteString(strings.Repeat(" ", keyLineIndent))
result.WriteString(keyLineBuffer)
result.WriteString(":")
result.WriteString("\n")
inMultilineValue = true
}
commentBuffer = nil
keyLineBuffer = ""
keyLineIndent = 0
} else if strings.Contains(trimmedLine, ":") {
// This is a key line
if keyLineBuffer != "" { // flush previous key line
result.WriteString(strings.Repeat(" ", keyLineIndent) + keyLineBuffer + ":\n")
}
inMultilineValue = false
keyLineBuffer = strings.TrimSuffix(trimmedLine, ":")
keyLineIndent = indent
} else if inMultilineValue {
// These are the lines of a multiline value
if trimmedLine != "" {
result.WriteString(line + "\n")
}
}
}
if keyLineBuffer != "" {
result.WriteString(strings.Repeat(" ", keyLineIndent) + keyLineBuffer + ":\n")
}
// Final cleanup to remove empty lines and special keys
finalOutput := []string{}
for _, line := range strings.Split(result.String(), "\n") {
trimmed := strings.TrimSpace(line)
if trimmed == "" || strings.HasPrefix(trimmed, "_") {
continue
}
finalOutput = append(finalOutput, line)
}
return []byte(strings.Join(finalOutput, "\n"))
}
func (s *Server) api_Config_YAML_All(rw http.ResponseWriter, r *http.Request) {
query := r.URL.Query()
filterName := query.Get("name")
shouldMergeCommon := query.Get("common") != "false"
configSections := []struct {
name string
data any
}{}
// 1. Get common config if it needs to be merged.
var commonConfig map[string]any
if shouldMergeCommon {
if c, ok := extractStructConfig(reflect.ValueOf(s.Plugin.GetCommonConf())).(map[string]any); ok {
commonConfig = c
}
}
// 2. Process global config.
if filterName == "" || filterName == "global" {
if globalConf, ok := extractStructConfig(reflect.ValueOf(s.ServerConfig)).(map[string]any); ok {
if shouldMergeCommon && commonConfig != nil {
mergedConf := make(map[string]any)
for k, v := range commonConfig {
mergedConf[k] = v
}
for k, v := range globalConf {
mergedConf[k] = v // Global overrides common
}
configSections = append(configSections, struct {
name string
data any
}{"global", mergedConf})
} else {
configSections = append(configSections, struct {
name string
data any
}{"global", globalConf})
}
}
}
// 3. Process plugin configs.
for _, meta := range plugins {
if filterName != "" && meta.Name != filterName {
continue
}
configType := meta.Type
if configType.Kind() == reflect.Ptr {
configType = configType.Elem()
}
if pluginConf, ok := extractStructConfig(reflect.New(configType)).(map[string]any); ok {
pluginConf["enable"] = map[string]any{
"_value": true,
"_description": "在global配置disableall时能启用特定插件",
}
if shouldMergeCommon && commonConfig != nil {
mergedConf := make(map[string]any)
for k, v := range commonConfig {
mergedConf[k] = v
}
for k, v := range pluginConf {
mergedConf[k] = v // Plugin overrides common
}
configSections = append(configSections, struct {
name string
data any
}{meta.Name, mergedConf})
} else {
configSections = append(configSections, struct {
name string
data any
}{meta.Name, pluginConf})
}
}
}
// 4. Serialize each section and combine.
var yamlParts []string
for _, section := range configSections {
if section.data == nil {
continue
}
partMap := map[string]any{section.name: section.data}
partYAML, err := yaml.Marshal(partMap)
if err != nil {
http.Error(rw, err.Error(), http.StatusInternalServerError)
return
}
yamlParts = append(yamlParts, string(partYAML))
}
finalYAML := strings.Join(yamlParts, "")
rw.Header().Set("Content-Type", "text/yaml; charset=utf-8")
rw.Write(addCommentsToYAML([]byte(finalYAML)))
}
func extractStructConfig(v reflect.Value) any {
if v.Kind() == reflect.Ptr {
if v.IsNil() {
return nil
}
v = v.Elem()
}
if v.Kind() != reflect.Struct {
return nil
}
m := make(map[string]any)
for i := 0; i < v.NumField(); i++ {
field := v.Type().Field(i)
if !field.IsExported() {
continue
}
// Filter out Plugin and UnimplementedApiServer
fieldType := field.Type
if fieldType.Kind() == reflect.Ptr {
fieldType = fieldType.Elem()
}
if fieldType.Name() == "Plugin" || fieldType.Name() == "UnimplementedApiServer" {
continue
}
yamlTag := field.Tag.Get("yaml")
if yamlTag == "-" {
continue
}
fieldName := strings.Split(yamlTag, ",")[0]
if fieldName == "" {
fieldName = strings.ToLower(field.Name)
}
m[fieldName] = extractFieldConfig(field, v.Field(i))
}
return m
}
func extractFieldConfig(field reflect.StructField, value reflect.Value) any {
result := make(map[string]any)
description := field.Tag.Get("desc")
enum := field.Tag.Get("enum")
if description != "" {
result["_description"] = description
}
if enum != "" {
result["_enum"] = enum
}
kind := value.Kind()
if kind == reflect.Ptr {
if value.IsNil() {
value = reflect.New(value.Type().Elem())
}
value = value.Elem()
kind = value.Kind()
}
switch kind {
case reflect.Struct:
if dur, ok := value.Interface().(time.Duration); ok {
result["_value"] = extractDurationConfig(field, dur)
} else {
result["_value"] = extractStructConfig(value)
}
case reflect.Map, reflect.Slice:
if value.IsNil() {
result["_value"] = make(map[string]any)
if kind == reflect.Slice {
result["_value"] = make([]any, 0)
}
} else {
result["_value"] = value.Interface()
}
default:
result["_value"] = extractBasicTypeConfig(field, value)
}
if description == "" && enum == "" {
return result["_value"]
}
return result
}
func extractBasicTypeConfig(field reflect.StructField, value reflect.Value) any {
if value.IsZero() {
if defaultValue := field.Tag.Get("default"); defaultValue != "" {
return parseDefaultValue(defaultValue, field.Type)
}
}
return value.Interface()
}
func extractDurationConfig(field reflect.StructField, value time.Duration) any {
if value == 0 {
if defaultValue := field.Tag.Get("default"); defaultValue != "" {
return defaultValue
}
}
return value.String()
}
func parseDefaultValue(defaultValue string, t reflect.Type) any {
switch t.Kind() {
case reflect.String:
return defaultValue
case reflect.Bool:
return defaultValue == "true"
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
if v, err := strconv.ParseInt(defaultValue, 10, 64); err == nil {
return v
}
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
if v, err := strconv.ParseUint(defaultValue, 10, 64); err == nil {
return v
}
case reflect.Float32, reflect.Float64:
if v, err := strconv.ParseFloat(defaultValue, 64); err == nil {
return v
}
}
return defaultValue
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 625 KiB

After

Width:  |  Height:  |  Size: 659 KiB

279
doc/arch/auth.md Normal file
View File

@@ -0,0 +1,279 @@
# Stream Authentication Mechanism
Monibuca V5 provides a comprehensive stream authentication mechanism to control access permissions for publishing and subscribing to streams. The authentication mechanism supports multiple methods, including key-based signature authentication and custom authentication handlers.
## Authentication Principles
### 1. Authentication Flow Sequence Diagrams
#### Publishing Authentication Sequence Diagram
```mermaid
sequenceDiagram
participant Client as Publishing Client
participant Plugin as Plugin
participant AuthHandler as Auth Handler
participant Server as Server
Client->>Plugin: Publishing Request (streamPath, args)
Plugin->>Plugin: Check EnableAuth && Type == PublishTypeServer
alt Authentication Enabled
Plugin->>Plugin: Look for custom auth handler
alt Custom Handler Exists
Plugin->>AuthHandler: onAuthPub(publisher)
AuthHandler->>AuthHandler: Execute custom auth logic
AuthHandler-->>Plugin: Auth result
else Use Key-based Auth
Plugin->>Plugin: Check if conf.Key exists
alt Key Configured
Plugin->>Plugin: auth(streamPath, key, secret, expire)
Plugin->>Plugin: Validate timestamp
Plugin->>Plugin: Validate secret length
Plugin->>Plugin: Calculate MD5 signature
Plugin->>Plugin: Compare signatures
Plugin-->>Plugin: Auth result
end
end
alt Auth Failed
Plugin-->>Client: Auth failed, reject publishing
else Auth Success
Plugin->>Server: Create Publisher and add to stream management
Server-->>Plugin: Publishing successful
Plugin-->>Client: Publishing established successfully
end
else Auth Disabled
Plugin->>Server: Create Publisher directly
Server-->>Plugin: Publishing successful
Plugin-->>Client: Publishing established successfully
end
```
#### Subscribing Authentication Sequence Diagram
```mermaid
sequenceDiagram
participant Client as Subscribing Client
participant Plugin as Plugin
participant AuthHandler as Auth Handler
participant Server as Server
Client->>Plugin: Subscribing Request (streamPath, args)
Plugin->>Plugin: Check EnableAuth && Type == SubscribeTypeServer
alt Authentication Enabled
Plugin->>Plugin: Look for custom auth handler
alt Custom Handler Exists
Plugin->>AuthHandler: onAuthSub(subscriber)
AuthHandler->>AuthHandler: Execute custom auth logic
AuthHandler-->>Plugin: Auth result
else Use Key-based Auth
Plugin->>Plugin: Check if conf.Key exists
alt Key Configured
Plugin->>Plugin: auth(streamPath, key, secret, expire)
Plugin->>Plugin: Validate timestamp
Plugin->>Plugin: Validate secret length
Plugin->>Plugin: Calculate MD5 signature
Plugin->>Plugin: Compare signatures
Plugin-->>Plugin: Auth result
end
end
alt Auth Failed
Plugin-->>Client: Auth failed, reject subscribing
else Auth Success
Plugin->>Server: Create Subscriber and wait for Publisher
Server->>Server: Wait for stream publishing and track ready
Server-->>Plugin: Subscribing ready
Plugin-->>Client: Start streaming data transmission
end
else Auth Disabled
Plugin->>Server: Create Subscriber directly
Server-->>Plugin: Subscribing successful
Plugin-->>Client: Start streaming data transmission
end
```
### 2. Authentication Trigger Points
Authentication is triggered in the following two scenarios:
- **Publishing Authentication**: Triggered when there's a publishing request in the `PublishWithConfig` method
- **Subscribing Authentication**: Triggered when there's a subscribing request in the `SubscribeWithConfig` method
### 3. Authentication Condition Checks
Authentication is only executed when the following conditions are met simultaneously:
```go
if p.config.EnableAuth && publisher.Type == PublishTypeServer
```
- `EnableAuth`: Authentication is enabled in the plugin configuration
- `Type == PublishTypeServer/SubscribeTypeServer`: Only authenticate server-type publishing/subscribing
### 4. Authentication Method Priority
The system executes authentication in the following priority order:
1. **Custom Authentication Handler** (Highest priority)
2. **Key-based Signature Authentication**
3. **No Authentication** (Default pass)
## Custom Authentication Handlers
### Publishing Authentication Handler
```go
onAuthPub := p.Meta.OnAuthPub
if onAuthPub == nil {
onAuthPub = p.Server.Meta.OnAuthPub
}
if onAuthPub != nil {
if err = onAuthPub(publisher).Await(); err != nil {
p.Warn("auth failed", "error", err)
return
}
}
```
Authentication handler lookup order:
1. Plugin-level authentication handler `p.Meta.OnAuthPub`
2. Server-level authentication handler `p.Server.Meta.OnAuthPub`
### Subscribing Authentication Handler
```go
onAuthSub := p.Meta.OnAuthSub
if onAuthSub == nil {
onAuthSub = p.Server.Meta.OnAuthSub
}
if onAuthSub != nil {
if err = onAuthSub(subscriber).Await(); err != nil {
p.Warn("auth failed", "error", err)
return
}
}
```
## Key-based Signature Authentication
When there's no custom authentication handler, if a Key is configured, the system will use MD5-based signature authentication mechanism.
### Authentication Algorithm
```go
func (p *Plugin) auth(streamPath string, key string, secret string, expire string) (err error) {
// 1. Validate expiration time
if unixTime, err := strconv.ParseInt(expire, 16, 64); err != nil || time.Now().Unix() > unixTime {
return fmt.Errorf("auth failed expired")
}
// 2. Validate secret length
if len(secret) != 32 {
return fmt.Errorf("auth failed secret length must be 32")
}
// 3. Calculate the true secret
trueSecret := md5.Sum([]byte(key + streamPath + expire))
// 4. Compare secrets
if secret == hex.EncodeToString(trueSecret[:]) {
return nil
}
return fmt.Errorf("auth failed invalid secret")
}
```
### Signature Calculation Steps
1. **Construct signature string**: `key + streamPath + expire`
2. **MD5 encryption**: Perform MD5 hash on the signature string
3. **Hexadecimal encoding**: Convert MD5 result to 32-character hexadecimal string
4. **Verify signature**: Compare calculation result with client-provided secret
### Parameter Description
| Parameter | Type | Description | Example |
|-----------|------|-------------|---------|
| key | string | Secret key set in configuration file | "mySecretKey" |
| streamPath | string | Stream path | "live/test" |
| expire | string | Expiration timestamp (hexadecimal) | "64a1b2c3" |
| secret | string | Client-calculated signature (32-char hex) | "5d41402abc4b2a76b9719d911017c592" |
### Timestamp Handling
- Expiration time uses hexadecimal Unix timestamp
- System validates if current time exceeds expiration time
- Timestamp parsing failure or expiration will cause authentication failure
## API Key Generation
The system also provides API interfaces for key generation, supporting authentication needs for admin dashboard:
```go
p.handle("/api/secret/{type}/{streamPath...}", http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
// JWT Token validation
authHeader := r.Header.Get("Authorization")
tokenString := strings.TrimPrefix(authHeader, "Bearer ")
_, err := p.Server.ValidateToken(tokenString)
// Generate publishing or subscribing key
streamPath := r.PathValue("streamPath")
t := r.PathValue("type")
expire := r.URL.Query().Get("expire")
if t == "publish" {
secret := md5.Sum([]byte(p.config.Publish.Key + streamPath + expire))
rw.Write([]byte(hex.EncodeToString(secret[:])))
} else if t == "subscribe" {
secret := md5.Sum([]byte(p.config.Subscribe.Key + streamPath + expire))
rw.Write([]byte(hex.EncodeToString(secret[:])))
}
}))
```
## Configuration Examples
### Enable Authentication
```yaml
# Plugin configuration
rtmp:
enableAuth: true
publish:
key: "your-publish-key"
subscribe:
key: "your-subscribe-key"
```
### Publishing URL Example
```
rtmp://localhost/live/test?secret=5d41402abc4b2a76b9719d911017c592&expire=64a1b2c3
```
### Subscribing URL Example
```
http://localhost:8080/flv/live/test.flv?secret=a1b2c3d4e5f6789012345678901234ab&expire=64a1b2c3
```
## Security Considerations
1. **Key Protection**: Keys in configuration files should be properly secured to prevent leakage
2. **Time Window**: Set reasonable expiration times to balance security and usability
3. **HTTPS Transport**: Use HTTPS for transmitting authentication parameters in production
4. **Logging**: Authentication failures are logged as warnings for security auditing
## Error Handling
Common causes of authentication failure:
- `auth failed expired`: Timestamp expired or format error
- `auth failed secret length must be 32`: Incorrect secret length
- `auth failed invalid secret`: Signature verification failed
- `invalid token`: JWT verification failed during API key generation

View File

@@ -26,7 +26,7 @@
### Plugin Development
[plugin/README.md](../plugin/README.md)
[plugin/README.md](../../plugin/README.md)
## Task System

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,279 @@
# 流鉴权机制
Monibuca V5 提供了完善的流鉴权机制,用于控制推流和拉流的访问权限。鉴权机制支持多种方式,包括基于密钥的签名鉴权和自定义鉴权处理器。
## 鉴权原理
### 1. 鉴权流程时序图
#### 推流鉴权时序图
```mermaid
sequenceDiagram
participant Client as 推流客户端
participant Plugin as 插件
participant AuthHandler as 鉴权处理器
participant Server as 服务器
Client->>Plugin: 推流请求 (streamPath, args)
Plugin->>Plugin: 检查 EnableAuth && Type == PublishTypeServer
alt 启用鉴权
Plugin->>Plugin: 查找自定义鉴权处理器
alt 存在自定义处理器
Plugin->>AuthHandler: onAuthPub(publisher)
AuthHandler->>AuthHandler: 执行自定义鉴权逻辑
AuthHandler-->>Plugin: 鉴权结果
else 使用密钥鉴权
Plugin->>Plugin: 检查 conf.Key 是否存在
alt 配置了Key
Plugin->>Plugin: auth(streamPath, key, secret, expire)
Plugin->>Plugin: 验证时间戳
Plugin->>Plugin: 验证secret长度
Plugin->>Plugin: 计算MD5签名
Plugin->>Plugin: 比较签名
Plugin-->>Plugin: 鉴权结果
end
end
alt 鉴权失败
Plugin-->>Client: 鉴权失败,拒绝推流
else 鉴权成功
Plugin->>Server: 创建Publisher并添加到流管理
Server-->>Plugin: 推流成功
Plugin-->>Client: 推流建立成功
end
else 未启用鉴权
Plugin->>Server: 直接创建Publisher
Server-->>Plugin: 推流成功
Plugin-->>Client: 推流建立成功
end
```
#### 拉流鉴权时序图
```mermaid
sequenceDiagram
participant Client as 拉流客户端
participant Plugin as 插件
participant AuthHandler as 鉴权处理器
participant Server as 服务器
Client->>Plugin: 拉流请求 (streamPath, args)
Plugin->>Plugin: 检查 EnableAuth && Type == SubscribeTypeServer
alt 启用鉴权
Plugin->>Plugin: 查找自定义鉴权处理器
alt 存在自定义处理器
Plugin->>AuthHandler: onAuthSub(subscriber)
AuthHandler->>AuthHandler: 执行自定义鉴权逻辑
AuthHandler-->>Plugin: 鉴权结果
else 使用密钥鉴权
Plugin->>Plugin: 检查 conf.Key 是否存在
alt 配置了Key
Plugin->>Plugin: auth(streamPath, key, secret, expire)
Plugin->>Plugin: 验证时间戳
Plugin->>Plugin: 验证secret长度
Plugin->>Plugin: 计算MD5签名
Plugin->>Plugin: 比较签名
Plugin-->>Plugin: 鉴权结果
end
end
alt 鉴权失败
Plugin-->>Client: 鉴权失败,拒绝拉流
else 鉴权成功
Plugin->>Server: 创建Subscriber并等待Publisher
Server->>Server: 等待流发布和轨道就绪
Server-->>Plugin: 拉流准备就绪
Plugin-->>Client: 开始传输流数据
end
else 未启用鉴权
Plugin->>Server: 直接创建Subscriber
Server-->>Plugin: 拉流成功
Plugin-->>Client: 开始传输流数据
end
```
### 2. 鉴权触发时机
鉴权在以下两种情况下触发:
- **推流鉴权**:当有推流请求时,在`PublishWithConfig`方法中触发
- **拉流鉴权**:当有拉流请求时,在`SubscribeWithConfig`方法中触发
### 3. 鉴权条件判断
鉴权只在以下条件同时满足时才会执行:
```go
if p.config.EnableAuth && publisher.Type == PublishTypeServer
```
- `EnableAuth`:插件配置中启用了鉴权
- `Type == PublishTypeServer/SubscribeTypeServer`:只对服务端类型的推流/拉流进行鉴权
### 4. 鉴权方式优先级
系统按以下优先级执行鉴权:
1. **自定义鉴权处理器**(最高优先级)
2. **基于密钥的签名鉴权**
3. **无鉴权**(默认通过)
## 自定义鉴权处理器
### 推流鉴权处理器
```go
onAuthPub := p.Meta.OnAuthPub
if onAuthPub == nil {
onAuthPub = p.Server.Meta.OnAuthPub
}
if onAuthPub != nil {
if err = onAuthPub(publisher).Await(); err != nil {
p.Warn("auth failed", "error", err)
return
}
}
```
鉴权处理器查找顺序:
1. 插件级别的鉴权处理器 `p.Meta.OnAuthPub`
2. 服务器级别的鉴权处理器 `p.Server.Meta.OnAuthPub`
### 拉流鉴权处理器
```go
onAuthSub := p.Meta.OnAuthSub
if onAuthSub == nil {
onAuthSub = p.Server.Meta.OnAuthSub
}
if onAuthSub != nil {
if err = onAuthSub(subscriber).Await(); err != nil {
p.Warn("auth failed", "error", err)
return
}
}
```
## 基于密钥的签名鉴权
当没有自定义鉴权处理器时如果配置了Key系统将使用基于MD5的签名鉴权机制。
### 鉴权算法
```go
func (p *Plugin) auth(streamPath string, key string, secret string, expire string) (err error) {
// 1. 验证过期时间
if unixTime, err := strconv.ParseInt(expire, 16, 64); err != nil || time.Now().Unix() > unixTime {
return fmt.Errorf("auth failed expired")
}
// 2. 验证secret长度
if len(secret) != 32 {
return fmt.Errorf("auth failed secret length must be 32")
}
// 3. 计算真实的secret
trueSecret := md5.Sum([]byte(key + streamPath + expire))
// 4. 比较secret
if secret == hex.EncodeToString(trueSecret[:]) {
return nil
}
return fmt.Errorf("auth failed invalid secret")
}
```
### 签名计算步骤
1. **构造签名字符串**`key + streamPath + expire`
2. **MD5加密**对签名字符串进行MD5哈希
3. **十六进制编码**将MD5结果转换为32位十六进制字符串
4. **验证签名**比较计算结果与客户端提供的secret
### 参数说明
| 参数 | 类型 | 说明 | 示例 |
|------|------|------|------|
| key | string | 密钥,在配置文件中设置 | "mySecretKey" |
| streamPath | string | 流路径 | "live/test" |
| expire | string | 过期时间戳16进制 | "64a1b2c3" |
| secret | string | 客户端计算的签名32位十六进制 | "5d41402abc4b2a76b9719d911017c592" |
### 时间戳处理
- 过期时间使用16进制Unix时间戳
- 系统会验证当前时间是否超过过期时间
- 时间戳解析失败或已过期都会导致鉴权失败
## API密钥生成
系统还提供了API接口用于生成密钥支持管理后台的鉴权需求
```go
p.handle("/api/secret/{type}/{streamPath...}", http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
// JWT Token验证
authHeader := r.Header.Get("Authorization")
tokenString := strings.TrimPrefix(authHeader, "Bearer ")
_, err := p.Server.ValidateToken(tokenString)
// 生成推流或拉流密钥
streamPath := r.PathValue("streamPath")
t := r.PathValue("type")
expire := r.URL.Query().Get("expire")
if t == "publish" {
secret := md5.Sum([]byte(p.config.Publish.Key + streamPath + expire))
rw.Write([]byte(hex.EncodeToString(secret[:])))
} else if t == "subscribe" {
secret := md5.Sum([]byte(p.config.Subscribe.Key + streamPath + expire))
rw.Write([]byte(hex.EncodeToString(secret[:])))
}
}))
```
## 配置示例
### 启用鉴权
```yaml
# 插件配置
rtmp:
enableAuth: true
publish:
key: "your-publish-key"
subscribe:
key: "your-subscribe-key"
```
### 推流URL示例
```
rtmp://localhost/live/test?secret=5d41402abc4b2a76b9719d911017c592&expire=64a1b2c3
```
### 拉流URL示例
```
http://localhost:8080/flv/live/test.flv?secret=a1b2c3d4e5f6789012345678901234ab&expire=64a1b2c3
```
## 安全考虑
1. **密钥保护**配置文件中的key应当妥善保管避免泄露
2. **时间窗口**:合理设置过期时间,平衡安全性和可用性
3. **HTTPS传输**生产环境建议使用HTTPS传输鉴权参数
4. **日志记录**:鉴权失败会记录警告日志,便于安全审计
## 错误处理
鉴权失败的常见原因:
- `auth failed expired`:时间戳已过期或格式错误
- `auth failed secret length must be 32`secret长度不正确
- `auth failed invalid secret`:签名验证失败
- `invalid token`API密钥生成时JWT验证失败

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项目也将持续探索和优化这一技术为用户提供更优质的流媒体服务。

View File

@@ -7,4 +7,9 @@ rtsp:
mp4:
enable: true
pull:
live/test: /Users/dexter/Movies/test.mp4
live/test: /Users/dexter/Movies/test.mp4
rtmp:
enable: true
debug:
enable: true

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 # 查询截图时允许的最大时间差(秒)

13
example/8081/default.yaml Normal file
View File

@@ -0,0 +1,13 @@
global:
# loglevel: debug
http:
listenaddr: :8081
listenaddrtls: :8555
tcp:
listenaddr: :50052
rtsp:
enable: false
rtmp:
tcp: :1936
webrtc:
enable: false

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,35 +4,56 @@ global:
loglevel: debug
admin:
enablelogin: false
subscribe:
subaudio: 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
enable: false # 是否启用GB28181协议
autoinvite: false #建议使用false开启后会自动邀请设备推流
mediaip: 192.168.1.21 #流媒体收流IP,外网情况下使用公网IP,内网情况下使用网卡IP,不要用127.0.0.1
sipip: 192.168.1.21 #SIP通讯IP,不管公网还是内网都使用本机网卡IP,不要用127.0.0.1
sip:
listenaddr:
- udp::5060
# pull:
# live/test: dump/34020000001320000001
onsub:
pull:
.* : $0
^\d{20}/\d{20}$: $0
^gb_\d+/(.+)$: $1
# .* : $0
platforms:
- enable: false #是否启用平台
name: "测试平台" #平台名称
servergbid: "34020000002000000002" #上级平台GBID
servergbdomain: "3402000000" #上级平台GB域
serverip: 192.168.1.106 #上级平台IP
serverport: 5061 #上级平台端口
devicegbid: "34020000002000000001" #本平台设备GBID
deviceip: 192.168.1.106 #本平台设备IP
deviceport: 5060 #本平台设备端口
username: "34020000002000000001" #SIP账号
password: "123456" #SIP密码
expires: 3600 #注册有效期,单位秒
keeptimeout: 60 #注册保持超时时间,单位秒
civilCode: "340200" #行政区划代码
manufacturer: "Monibuca" #设备制造商
model: "GB28181" #设备型号
address: "江苏南京" #设备地址
register_way: 1
platformchannels:
- platformservergbid: "34020000002000000002" #上级平台GBID
channeldbid: "34020000001110000003_34020000001320000005" #通道DBID,格式为设备ID_通道ID
mp4:
# enable: false
publish:
delayclosetimeout: 3s
# publish:
# delayclosetimeout: 3s
# onpub:
# record:
# ^live/.+:
# fragment: 10s
# filepath: record/$0
# type: fmp4
# type: fmp4
# pull:
# live/test: /Users/dexter/Movies/1744963190.mp4
onsub:
pull:
^vod_mp4_\d+/(.+)$: $1
@@ -72,26 +93,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() {

53
go.mod
View File

@@ -1,27 +1,34 @@
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/aws/aws-sdk-go v1.55.7
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/mark3labs/mcp-go v0.27.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,32 +37,34 @@ 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/qpack v0.5.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/valyala/fasthttp v1.61.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
gorm.io/driver/mysql v1.5.7
gorm.io/driver/postgres v1.5.9
gorm.io/gorm v1.25.11
gorm.io/gorm v1.30.0
)
require (
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 +77,17 @@ 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/jmespath/go-jmespath v0.4.0 // 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,37 +95,28 @@ 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/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/spf13/cast v1.7.1 // 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
@@ -130,9 +128,10 @@ require (
github.com/valyala/quicktemplate v1.8.0 // indirect
github.com/vishvananda/netns v0.0.0-20191106174202-0a2b9b5464df // indirect
github.com/wlynxg/anet v0.0.5 // indirect
github.com/yosida95/uritemplate/v3 v3.0.2 // 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 +148,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
)

250
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,12 +17,16 @@ 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=
github.com/asticode/go-astikit v0.30.0/go.mod h1:h4ly7idim1tNhaVkdVBeXQZEE3L0xblP7fCWbgwipF0=
github.com/asticode/go-astits v1.13.0 h1:XOgkaadfZODnyZRR5Y0/DWkA9vrkLLPLeeOvDwfKZ1c=
github.com/asticode/go-astits v1.13.0/go.mod h1:QSHmknZ51pf6KJdHKZHJTLlMegIrhega3LPWz3ND/iI=
github.com/aws/aws-sdk-go v1.55.7 h1:UJrkFq7es5CShfBwlWAC8DA077vp8PyVbQd3lqLiztE=
github.com/aws/aws-sdk-go v1.55.7/go.mod h1:eRwEWoyTWFMVYVQzKMNHWP5/RV4xIUGMQfXQHfHkpNU=
github.com/beevik/etree v1.4.1 h1:PmQJDDYahBGNKDcpdX8uPy1xRCwoCGVUiW669MEirVI=
github.com/beevik/etree v1.4.1/go.mod h1:gPNJNaBGVZ9AwsidazFZyygnd+0pAU38N4D+WemwKNs=
github.com/benburkert/openpgp v0.0.0-20160410205803-c2471f86866c h1:8XZeJrs4+ZYhJeJ2aZxADI2tGADS15AzIF8MQ8XAhT4=
@@ -69,11 +71,11 @@ 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/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
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 +86,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 +99,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 +113,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 +121,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=
@@ -156,10 +141,14 @@ github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg=
github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo=
github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8=
github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U=
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
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=
@@ -179,6 +168,8 @@ github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
github.com/marcboeker/go-duckdb v1.0.5 h1:zIfyrCAJfY9FmXWOZ6jE3DkmWpwK4rlY12zqf9LD2mU=
github.com/marcboeker/go-duckdb v1.0.5/go.mod h1:wm91jO2GNKa6iO9NTcjXIRsW+/ykPoJbQcHSXhdAl28=
github.com/mark3labs/mcp-go v0.27.0 h1:iok9kU4DUIU2/XVLgFS2Q9biIDqstC0jY4EQTK2Erzc=
github.com/mark3labs/mcp-go v0.27.0/go.mod h1:rXqOudj/djTORU/ThxYx8fqEVj/5pvTuuebQ2RC7uk4=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
@@ -205,17 +196,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 +206,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 +256,10 @@ 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/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/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.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 +275,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=
@@ -348,6 +283,8 @@ github.com/shoenig/test v0.6.4 h1:kVTaSd7WLz5WZ2IaoM0RSzRsUD+m8wRR+5qvntpn4LU=
github.com/shoenig/test v0.6.4/go.mod h1:byHiCGXqrVaflBLAMq/srcZIHynQPQgeyvkvXnjqq0k=
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/spf13/cast v1.7.1 h1:cuNEagBQEHWN1FnbGEjCXL2szYEXqfJPbP2HNUaca9Y=
github.com/spf13/cast v1.7.1/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
@@ -355,16 +292,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 +309,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 +327,22 @@ 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 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
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/yosida95/uritemplate/v3 v3.0.2 h1:Ed3Oyj9yrmi9087+NczuL5BwkIc4wvTb5zIM+UJPGz4=
github.com/yosida95/uritemplate/v3 v3.0.2/go.mod h1:ILOh0sOhIJR3+L/8afwt/kE++YT040gmv5BQTMR2HP4=
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 +350,69 @@ 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=
@@ -589,8 +423,8 @@ gorm.io/driver/mysql v1.5.7/go.mod h1:sEtPWMiqiN1N1cMXoXmBbd8C6/l+TESwriotuRRpkD
gorm.io/driver/postgres v1.5.9 h1:DkegyItji119OlcaLjqN11kHoUgZ/j13E0jkJZgD6A8=
gorm.io/driver/postgres v1.5.9/go.mod h1:DX3GReXH+3FPWGrrgffdvCk3DQ1dwDPdmbenSkweRGI=
gorm.io/gorm v1.25.7/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8=
gorm.io/gorm v1.25.11 h1:/Wfyg1B/je1hnDx3sMkX+gAlxrlZpn6X0BXRlwXlvHg=
gorm.io/gorm v1.25.11/go.mod h1:xh7N7RHfYlNc5EmcI/El95gXusucDrQnHXe0+CgWcLQ=
gorm.io/gorm v1.30.0 h1:qbT5aPv1UH8gI99OsRlvDToLxW5zR7FzS9acZDOZcgs=
gorm.io/gorm v1.30.0/go.mod h1:8Z33v652h4//uMA76KjeDH8mJXPm1QNCYrMeatR0DOE=
gotest.tools/v3 v3.0.2/go.mod h1:3SzNCllyD9/Y+b5r9JIKQ474KzkZyqLqEfYqMsX94Bk=
gotest.tools/v3 v3.5.0 h1:Ljk6PdHdOhAb5aDMWXjDLMMhph+BpztA4v1QdqEW2eY=
gotest.tools/v3 v3.5.0/go.mod h1:isy3WKz7GK6uNw/sbHzfKBLvlvXwUyV06n6brMxxopU=

View File

@@ -11,7 +11,7 @@ builds:
tags:
- sqlite
ldflags:
- -s -w -X main.version={{.Tag}}
- -s -w -X m7s.live/v5.Version={{.Tag}}
goos:
- linux
- windows

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.6
// protoc v5.29.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,79 +440,54 @@ func (x *UserInfoResponse) GetData() *UserInfo {
var File_auth_proto protoreflect.FileDescriptor
var file_auth_proto_rawDesc = []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,
0x0a, 0x0c, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1a,
0x0a, 0x08, 0x75, 0x73, 0x65, 0x72, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09,
0x52, 0x08, 0x75, 0x73, 0x65, 0x72, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x1a, 0x0a, 0x08, 0x70, 0x61,
0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x70, 0x61,
0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x22, 0x4e, 0x0a, 0x0c, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x53,
0x75, 0x63, 0x63, 0x65, 0x73, 0x73, 0x12, 0x14, 0x0a, 0x05, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x18,
0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x12, 0x28, 0x0a, 0x08,
0x75, 0x73, 0x65, 0x72, 0x49, 0x6e, 0x66, 0x6f, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0c,
0x2e, 0x70, 0x62, 0x2e, 0x55, 0x73, 0x65, 0x72, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x08, 0x75, 0x73,
0x65, 0x72, 0x49, 0x6e, 0x66, 0x6f, 0x22, 0x63, 0x0a, 0x0d, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x52,
0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 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, 0x24, 0x0a, 0x04, 0x64, 0x61, 0x74, 0x61, 0x18, 0x03, 0x20,
0x01, 0x28, 0x0b, 0x32, 0x10, 0x2e, 0x70, 0x62, 0x2e, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x53, 0x75,
0x63, 0x63, 0x65, 0x73, 0x73, 0x52, 0x04, 0x64, 0x61, 0x74, 0x61, 0x22, 0x25, 0x0a, 0x0d, 0x4c,
0x6f, 0x67, 0x6f, 0x75, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x14, 0x0a, 0x05,
0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x74, 0x6f, 0x6b,
0x65, 0x6e, 0x22, 0x3e, 0x0a, 0x0e, 0x4c, 0x6f, 0x67, 0x6f, 0x75, 0x74, 0x52, 0x65, 0x73, 0x70,
0x6f, 0x6e, 0x73, 0x65, 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, 0x22, 0x27, 0x0a, 0x0f, 0x55, 0x73, 0x65, 0x72, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x65,
0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x14, 0x0a, 0x05, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x01,
0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x22, 0x45, 0x0a, 0x08, 0x55,
0x73, 0x65, 0x72, 0x49, 0x6e, 0x66, 0x6f, 0x12, 0x1a, 0x0a, 0x08, 0x75, 0x73, 0x65, 0x72, 0x6e,
0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x75, 0x73, 0x65, 0x72, 0x6e,
0x61, 0x6d, 0x65, 0x12, 0x1d, 0x0a, 0x0a, 0x65, 0x78, 0x70, 0x69, 0x72, 0x65, 0x73, 0x5f, 0x61,
0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x09, 0x65, 0x78, 0x70, 0x69, 0x72, 0x65, 0x73,
0x41, 0x74, 0x22, 0x62, 0x0a, 0x10, 0x55, 0x73, 0x65, 0x72, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x65,
0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 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, 0x20, 0x0a, 0x04, 0x64, 0x61, 0x74, 0x61, 0x18, 0x03, 0x20, 0x01,
0x28, 0x0b, 0x32, 0x0c, 0x2e, 0x70, 0x62, 0x2e, 0x55, 0x73, 0x65, 0x72, 0x49, 0x6e, 0x66, 0x6f,
0x52, 0x04, 0x64, 0x61, 0x74, 0x61, 0x32, 0xf4, 0x01, 0x0a, 0x04, 0x41, 0x75, 0x74, 0x68, 0x12,
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,
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,
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,
0x65, 0x22, 0x1a, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x14, 0x12, 0x12, 0x2f, 0x61, 0x70, 0x69, 0x2f,
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,
}
const file_auth_proto_rawDesc = "" +
"\n" +
"\n" +
"auth.proto\x12\x02pb\x1a\x1cgoogle/api/annotations.proto\"F\n" +
"\fLoginRequest\x12\x1a\n" +
"\busername\x18\x01 \x01(\tR\busername\x12\x1a\n" +
"\bpassword\x18\x02 \x01(\tR\bpassword\"N\n" +
"\fLoginSuccess\x12\x14\n" +
"\x05token\x18\x01 \x01(\tR\x05token\x12(\n" +
"\buserInfo\x18\x02 \x01(\v2\f.pb.UserInfoR\buserInfo\"c\n" +
"\rLoginResponse\x12\x12\n" +
"\x04code\x18\x01 \x01(\x05R\x04code\x12\x18\n" +
"\amessage\x18\x02 \x01(\tR\amessage\x12$\n" +
"\x04data\x18\x03 \x01(\v2\x10.pb.LoginSuccessR\x04data\"%\n" +
"\rLogoutRequest\x12\x14\n" +
"\x05token\x18\x01 \x01(\tR\x05token\">\n" +
"\x0eLogoutResponse\x12\x12\n" +
"\x04code\x18\x01 \x01(\x05R\x04code\x12\x18\n" +
"\amessage\x18\x02 \x01(\tR\amessage\"'\n" +
"\x0fUserInfoRequest\x12\x14\n" +
"\x05token\x18\x01 \x01(\tR\x05token\"E\n" +
"\bUserInfo\x12\x1a\n" +
"\busername\x18\x01 \x01(\tR\busername\x12\x1d\n" +
"\n" +
"expires_at\x18\x02 \x01(\x03R\texpiresAt\"b\n" +
"\x10UserInfoResponse\x12\x12\n" +
"\x04code\x18\x01 \x01(\x05R\x04code\x12\x18\n" +
"\amessage\x18\x02 \x01(\tR\amessage\x12 \n" +
"\x04data\x18\x03 \x01(\v2\f.pb.UserInfoR\x04data2\xf4\x01\n" +
"\x04Auth\x12H\n" +
"\x05Login\x12\x10.pb.LoginRequest\x1a\x11.pb.LoginResponse\"\x1a\x82\xd3\xe4\x93\x02\x14:\x01*\"\x0f/api/auth/login\x12L\n" +
"\x06Logout\x12\x11.pb.LogoutRequest\x1a\x12.pb.LogoutResponse\"\x1b\x82\xd3\xe4\x93\x02\x15:\x01*\"\x10/api/auth/logout\x12T\n" +
"\vGetUserInfo\x12\x13.pb.UserInfoRequest\x1a\x14.pb.UserInfoResponse\"\x1a\x82\xd3\xe4\x93\x02\x14\x12\x12/api/auth/userinfoB\x10Z\x0em7s.live/v5/pbb\x06proto3"
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 +519,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 +534,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

@@ -10,6 +10,7 @@ package pb
import (
"context"
"errors"
"io"
"net/http"
@@ -24,116 +25,110 @@ import (
)
// Suppress "imported and not used" errors
var _ codes.Code
var _ io.Reader
var _ status.Status
var _ = runtime.String
var _ = utilities.NewDoubleArray
var _ = metadata.Join
var (
_ codes.Code
_ io.Reader
_ status.Status
_ = errors.New
_ = runtime.String
_ = utilities.NewDoubleArray
_ = metadata.Join
)
func request_Auth_Login_0(ctx context.Context, marshaler runtime.Marshaler, client AuthClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
var protoReq LoginRequest
var metadata runtime.ServerMetadata
if err := marshaler.NewDecoder(req.Body).Decode(&protoReq); err != nil && err != io.EOF {
var (
protoReq LoginRequest
metadata runtime.ServerMetadata
)
if err := marshaler.NewDecoder(req.Body).Decode(&protoReq); err != nil && !errors.Is(err, io.EOF) {
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
}
msg, err := client.Login(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))
return msg, metadata, err
}
func local_request_Auth_Login_0(ctx context.Context, marshaler runtime.Marshaler, server AuthServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
var protoReq LoginRequest
var metadata runtime.ServerMetadata
if err := marshaler.NewDecoder(req.Body).Decode(&protoReq); err != nil && err != io.EOF {
var (
protoReq LoginRequest
metadata runtime.ServerMetadata
)
if err := marshaler.NewDecoder(req.Body).Decode(&protoReq); err != nil && !errors.Is(err, io.EOF) {
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
}
msg, err := server.Login(ctx, &protoReq)
return msg, metadata, err
}
func request_Auth_Logout_0(ctx context.Context, marshaler runtime.Marshaler, client AuthClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
var protoReq LogoutRequest
var metadata runtime.ServerMetadata
if err := marshaler.NewDecoder(req.Body).Decode(&protoReq); err != nil && err != io.EOF {
var (
protoReq LogoutRequest
metadata runtime.ServerMetadata
)
if err := marshaler.NewDecoder(req.Body).Decode(&protoReq); err != nil && !errors.Is(err, io.EOF) {
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
}
msg, err := client.Logout(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))
return msg, metadata, err
}
func local_request_Auth_Logout_0(ctx context.Context, marshaler runtime.Marshaler, server AuthServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
var protoReq LogoutRequest
var metadata runtime.ServerMetadata
if err := marshaler.NewDecoder(req.Body).Decode(&protoReq); err != nil && err != io.EOF {
var (
protoReq LogoutRequest
metadata runtime.ServerMetadata
)
if err := marshaler.NewDecoder(req.Body).Decode(&protoReq); err != nil && !errors.Is(err, io.EOF) {
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
}
msg, err := server.Logout(ctx, &protoReq)
return msg, metadata, err
}
var (
filter_Auth_GetUserInfo_0 = &utilities.DoubleArray{Encoding: map[string]int{}, Base: []int(nil), Check: []int(nil)}
)
var filter_Auth_GetUserInfo_0 = &utilities.DoubleArray{Encoding: map[string]int{}, Base: []int(nil), Check: []int(nil)}
func request_Auth_GetUserInfo_0(ctx context.Context, marshaler runtime.Marshaler, client AuthClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
var protoReq UserInfoRequest
var metadata runtime.ServerMetadata
var (
protoReq UserInfoRequest
metadata runtime.ServerMetadata
)
io.Copy(io.Discard, req.Body)
if err := req.ParseForm(); err != nil {
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
}
if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_Auth_GetUserInfo_0); err != nil {
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
}
msg, err := client.GetUserInfo(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))
return msg, metadata, err
}
func local_request_Auth_GetUserInfo_0(ctx context.Context, marshaler runtime.Marshaler, server AuthServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
var protoReq UserInfoRequest
var metadata runtime.ServerMetadata
var (
protoReq UserInfoRequest
metadata runtime.ServerMetadata
)
if err := req.ParseForm(); err != nil {
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
}
if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_Auth_GetUserInfo_0); err != nil {
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
}
msg, err := server.GetUserInfo(ctx, &protoReq)
return msg, metadata, err
}
// RegisterAuthHandlerServer registers the http handlers for service Auth to "mux".
// 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) {
mux.Handle(http.MethodPost, pattern_Auth_Login_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, "/pb.Auth/Login", runtime.WithHTTPPathPattern("/api/auth/login"))
annotatedContext, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/pb.Auth/Login", runtime.WithHTTPPathPattern("/api/auth/login"))
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
@@ -145,20 +140,15 @@ func RegisterAuthHandlerServer(ctx context.Context, mux *runtime.ServeMux, serve
runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err)
return
}
forward_Auth_Login_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
})
mux.Handle("POST", pattern_Auth_Logout_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
mux.Handle(http.MethodPost, pattern_Auth_Logout_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, "/pb.Auth/Logout", runtime.WithHTTPPathPattern("/api/auth/logout"))
annotatedContext, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/pb.Auth/Logout", runtime.WithHTTPPathPattern("/api/auth/logout"))
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
@@ -170,20 +160,15 @@ func RegisterAuthHandlerServer(ctx context.Context, mux *runtime.ServeMux, serve
runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err)
return
}
forward_Auth_Logout_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
})
mux.Handle("GET", pattern_Auth_GetUserInfo_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
mux.Handle(http.MethodGet, pattern_Auth_GetUserInfo_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, "/pb.Auth/GetUserInfo", runtime.WithHTTPPathPattern("/api/auth/userinfo"))
annotatedContext, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/pb.Auth/GetUserInfo", runtime.WithHTTPPathPattern("/api/auth/userinfo"))
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
@@ -195,9 +180,7 @@ func RegisterAuthHandlerServer(ctx context.Context, mux *runtime.ServeMux, serve
runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err)
return
}
forward_Auth_GetUserInfo_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
})
return nil
@@ -206,25 +189,24 @@ 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)
}
}()
}()
return RegisterAuthHandler(ctx, mux, conn)
}
@@ -238,16 +220,13 @@ 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) {
mux.Handle(http.MethodPost, pattern_Auth_Login_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, "/pb.Auth/Login", runtime.WithHTTPPathPattern("/api/auth/login"))
annotatedContext, err := runtime.AnnotateContext(ctx, mux, req, "/pb.Auth/Login", runtime.WithHTTPPathPattern("/api/auth/login"))
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
@@ -258,18 +237,13 @@ func RegisterAuthHandlerClient(ctx context.Context, mux *runtime.ServeMux, clien
runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err)
return
}
forward_Auth_Login_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
})
mux.Handle("POST", pattern_Auth_Logout_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
mux.Handle(http.MethodPost, pattern_Auth_Logout_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, "/pb.Auth/Logout", runtime.WithHTTPPathPattern("/api/auth/logout"))
annotatedContext, err := runtime.AnnotateContext(ctx, mux, req, "/pb.Auth/Logout", runtime.WithHTTPPathPattern("/api/auth/logout"))
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
@@ -280,18 +254,13 @@ func RegisterAuthHandlerClient(ctx context.Context, mux *runtime.ServeMux, clien
runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err)
return
}
forward_Auth_Logout_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
})
mux.Handle("GET", pattern_Auth_GetUserInfo_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
mux.Handle(http.MethodGet, pattern_Auth_GetUserInfo_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, "/pb.Auth/GetUserInfo", runtime.WithHTTPPathPattern("/api/auth/userinfo"))
annotatedContext, err := runtime.AnnotateContext(ctx, mux, req, "/pb.Auth/GetUserInfo", runtime.WithHTTPPathPattern("/api/auth/userinfo"))
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
@@ -302,26 +271,19 @@ func RegisterAuthHandlerClient(ctx context.Context, mux *runtime.ServeMux, clien
runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err)
return
}
forward_Auth_GetUserInfo_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
})
return nil
}
var (
pattern_Auth_Login_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2}, []string{"api", "auth", "login"}, ""))
pattern_Auth_Logout_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2}, []string{"api", "auth", "logout"}, ""))
pattern_Auth_Login_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2}, []string{"api", "auth", "login"}, ""))
pattern_Auth_Logout_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2}, []string{"api", "auth", "logout"}, ""))
pattern_Auth_GetUserInfo_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2}, []string{"api", "auth", "userinfo"}, ""))
)
var (
forward_Auth_Login_0 = runtime.ForwardResponseMessage
forward_Auth_Logout_0 = runtime.ForwardResponseMessage
forward_Auth_Login_0 = runtime.ForwardResponseMessage
forward_Auth_Logout_0 = runtime.ForwardResponseMessage
forward_Auth_GetUserInfo_0 = runtime.ForwardResponseMessage
)

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.29.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

File diff suppressed because it is too large Load Diff

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: "*"
}
};
@@ -229,11 +224,16 @@ service api {
get: "/api/transform/list"
};
}
rpc GetRecordList (ReqRecordList) returns (ResponseList) {
rpc GetRecordList (ReqRecordList) returns (RecordResponseList) {
option (google.api.http) = {
get: "/api/record/{type}/list/{streamPath=**}"
};
}
rpc GetEventRecordList (ReqRecordList) returns (EventRecordResponseList) {
option (google.api.http) = {
get: "/api/record/{type}/event/list/{streamPath=**}"
};
}
rpc GetRecordCatalog (ReqRecordCatalog) returns (ResponseCatalog) {
option (google.api.http) = {
get: "/api/record/{type}/catalog"
@@ -245,6 +245,11 @@ service api {
body: "*"
};
}
rpc GetAlarmList (AlarmListRequest) returns (AlarmListResponse) {
option (google.api.http) = {
get: "/api/alarm/list"
};
}
}
message DisabledPluginsResponse {
@@ -669,8 +674,8 @@ message ReqRecordList {
string end = 4;
uint32 pageNum = 5;
uint32 pageSize = 6;
string mode = 7;
string type = 8;
string type = 7;
string eventLevel = 8;
}
message RecordFile {
@@ -681,15 +686,36 @@ message RecordFile {
google.protobuf.Timestamp endTime = 5;
}
message ResponseList {
message EventRecordFile {
uint32 id = 1;
string filePath = 2;
string streamPath = 3;
google.protobuf.Timestamp startTime = 4;
google.protobuf.Timestamp endTime = 5;
string eventId = 6;
string eventLevel = 7;
string eventName = 8;
string eventDesc = 9;
}
message RecordResponseList {
int32 code = 1;
string message = 2;
uint32 totalCount = 3;
uint32 total = 3;
uint32 pageNum = 4;
uint32 pageSize = 5;
repeated RecordFile data = 6;
}
message EventRecordResponseList {
int32 code = 1;
string message = 2;
uint32 total = 3;
uint32 pageNum = 4;
uint32 pageSize = 5;
repeated EventRecordFile data = 6;
}
message Catalog {
string streamPath = 1;
uint32 count = 2;
@@ -720,4 +746,38 @@ message ResponseDelete {
message ReqRecordCatalog {
string type = 1;
}
message AlarmInfo {
uint32 id = 1;
string serverInfo = 2;
string streamName = 3;
string streamPath = 4;
string alarmDesc = 5;
string alarmName = 6;
int32 alarmType = 7;
bool isSent = 8;
string filePath = 9;
google.protobuf.Timestamp createdAt = 10;
google.protobuf.Timestamp updatedAt = 11;
}
message AlarmListRequest {
int32 pageNum = 1;
int32 pageSize = 2;
string range = 3;
string start = 4;
string end = 5;
int32 alarmType = 6;
string streamPath = 7;
string streamName = 8;
}
message AlarmListResponse {
int32 code = 1;
string message = 2;
int32 total = 3;
int32 pageNum = 4;
int32 pageSize = 5;
repeated AlarmInfo data = 6;
}

File diff suppressed because it is too large Load Diff

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

@@ -65,8 +65,6 @@ type (
}
)
var _ IAVFrame = (*AnnexB)(nil)
func (frame *AVFrame) Clone() {
}

74
pkg/avframe_convert.go Normal file
View File

@@ -0,0 +1,74 @@
package pkg
import (
"reflect"
"m7s.live/v5/pkg/codec"
"m7s.live/v5/pkg/util"
)
type AVFrameConvert[T IAVFrame] struct {
FromTrack, ToTrack *AVTrack
lastFromCodecCtx codec.ICodecCtx
}
func NewAVFrameConvert[T IAVFrame](fromTrack *AVTrack, toTrack *AVTrack) *AVFrameConvert[T] {
ret := &AVFrameConvert[T]{}
ret.FromTrack = fromTrack
ret.ToTrack = toTrack
if ret.FromTrack == nil {
ret.FromTrack = &AVTrack{
RingWriter: &RingWriter{
Ring: util.NewRing[AVFrame](1),
},
}
}
if ret.ToTrack == nil {
ret.ToTrack = &AVTrack{
RingWriter: &RingWriter{
Ring: util.NewRing[AVFrame](1),
},
}
var to T
ret.ToTrack.FrameType = reflect.TypeOf(to).Elem()
}
return ret
}
func (c *AVFrameConvert[T]) ConvertFromAVFrame(avFrame *AVFrame) (to T, err error) {
to = reflect.New(c.ToTrack.FrameType).Interface().(T)
if c.ToTrack.ICodecCtx == nil {
if c.ToTrack.ICodecCtx, c.ToTrack.SequenceFrame, err = to.ConvertCtx(c.FromTrack.ICodecCtx); err != nil {
return
}
}
if err = avFrame.Demux(c.FromTrack.ICodecCtx); err != nil {
return
}
to.SetAllocator(avFrame.Wraps[0].GetAllocator())
to.Mux(c.ToTrack.ICodecCtx, avFrame)
return
}
func (c *AVFrameConvert[T]) Convert(frame IAVFrame) (to T, err error) {
to = reflect.New(c.ToTrack.FrameType).Interface().(T)
// Not From Publisher
if c.FromTrack.LastValue == nil {
err = frame.Parse(c.FromTrack)
if err != nil {
return
}
}
if c.ToTrack.ICodecCtx == nil || c.lastFromCodecCtx != c.FromTrack.ICodecCtx {
if c.ToTrack.ICodecCtx, c.ToTrack.SequenceFrame, err = to.ConvertCtx(c.FromTrack.ICodecCtx); err != nil {
return
}
}
c.lastFromCodecCtx = c.FromTrack.ICodecCtx
if c.FromTrack.Value.Raw, err = frame.Demux(c.FromTrack.ICodecCtx); err != nil {
return
}
to.SetAllocator(frame.GetAllocator())
to.Mux(c.ToTrack.ICodecCtx, &c.FromTrack.Value)
return
}

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,13 @@ func (h265 *H265Ctx) GetBase() ICodecCtx {
func (h265 *H265Ctx) GetRecord() []byte {
return h265.Record
}
func (h265 *H265Ctx) String() string {
// 根据 HEVC 标准格式hvc1.profile.compatibility.level.constraints
profile := h265.RecordInfo.AVCProfileIndication
compatibility := h265.RecordInfo.ProfileCompatibility
level := h265.RecordInfo.AVCLevelIndication
// 简单实现,使用可用字段模拟 HEVC 格式
return fmt.Sprintf("hvc1.%d.%X.L%d.00", profile, compatibility, level)
}

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 { // 读取环境变量
@@ -201,6 +208,9 @@ func (config *Config) ParseUserFile(conf map[string]any) {
}
config.File = conf
for k, v := range conf {
k = strings.ReplaceAll(k, "-", "")
k = strings.ReplaceAll(k, "_", "")
k = strings.ToLower(k)
if config.Has(k) {
if prop := config.Get(k); prop.props != nil {
if v != nil {

View File

@@ -1,6 +1,6 @@
package config
type DB struct {
DBType string `default:"sqlite" desc:"数据库类型"`
DSN string `default:"m7s.db" desc:"数据库文件路径"`
DBType string `default:"sqlite" desc:"数据库类型"`
}

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

@@ -40,6 +40,8 @@ type TCP struct {
KeyFile string `desc:"私钥文件"`
ListenNum int `desc:"同时并行监听数量0为CPU核心数量"` //同时并行监听数量0为CPU核心数量
NoDelay bool `desc:"是否禁用Nagle算法"` //是否禁用Nagle算法
WriteBuffer int `desc:"写缓冲区大小"` //写缓冲区大小
ReadBuffer int `desc:"读缓冲区大小"` //读缓冲区大小
KeepAlive bool `desc:"是否启用KeepAlive"` //是否启用KeepAlive
AutoListen bool `default:"true" desc:"是否自动监听"`
}
@@ -125,7 +127,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
}
@@ -141,6 +143,18 @@ func (task *ListenTCPWork) listen(handler TCPHandler) {
if !task.NoDelay {
tcpConn.SetNoDelay(false)
}
if task.WriteBuffer > 0 {
if err := tcpConn.SetWriteBuffer(task.WriteBuffer); err != nil {
task.Error("failed to set write buffer", "error", err)
continue
}
}
if task.ReadBuffer > 0 {
if err := tcpConn.SetReadBuffer(task.ReadBuffer); err != nil {
task.Error("failed to set read buffer", "error", err)
continue
}
}
tempDelay = 0
subTask := handler(tcpConn)
task.AddTask(subTask)

View File

@@ -16,16 +16,50 @@ const (
RelayModeRelay = "relay"
RelayModeMix = "mix"
HookOnPublish HookType = "publish"
HookOnSubscribe HookType = "subscribe"
HookOnPublishEnd HookType = "publish_end"
HookOnSubscribeEnd HookType = "subscribe_end"
RecordModeAuto RecordMode = "auto"
RecordModeEvent RecordMode = "event"
HookOnServerKeepAlive HookType = "server_keep_alive"
HookOnPublishStart HookType = "publish_start"
HookOnPublishEnd HookType = "publish_end"
HookOnSubscribeStart HookType = "subscribe_start"
HookOnSubscribeEnd HookType = "subscribe_end"
HookOnPullStart HookType = "pull_start"
HookOnPullEnd HookType = "pull_end"
HookOnPushStart HookType = "push_start"
HookOnPushEnd HookType = "push_end"
HookOnRecordStart HookType = "record_start"
HookOnRecordEnd HookType = "record_end"
HookOnTransformStart HookType = "transform_start"
HookOnTransformEnd HookType = "transform_end"
HookOnSystemStart HookType = "system_start"
HookDefault HookType = "default"
EventLevelLow EventLevel = "low"
EventLevelHigh EventLevel = "high"
AlarmStorageException = 0x10010 // 存储异常
AlarmStorageExceptionRecover = 0x10011 // 存储异常恢复
AlarmPullOffline = 0x10012 // 拉流异常,触发一次报警。
AlarmPullRecover = 0x10013 // 拉流恢复
AlarmDiskSpaceFull = 0x10014 // 磁盘空间满,磁盘占有率,超出最大磁盘空间使用率,触发报警。
AlarmStartupRunning = 0x10015 // 启动运行
AlarmPublishOffline = 0x10016 // 发布者异常,触发一次报警。
AlarmPublishRecover = 0x10017 // 发布者恢复
AlarmSubscribeOffline = 0x10018 // 订阅者异常,触发一次报警。
AlarmSubscribeRecover = 0x10019 // 订阅者恢复
AlarmPushOffline = 0x10020 // 推流异常,触发一次报警。
AlarmPushRecover = 0x10021 // 推流恢复
AlarmTransformOffline = 0x10022 // 转换异常,触发一次报警。
AlarmTransformRecover = 0x10023 // 转换恢复
AlarmKeepAliveOnline = 0x10024 // 保活正常,触发一次报警。
)
type (
HookType string
Publish struct {
EventLevel = string
RecordMode = string
HookType string
Publish struct {
MaxCount int `default:"0" desc:"最大发布者数量"` // 最大发布者数量
PubAudio bool `default:"true" desc:"是否发布音频"`
PubVideo bool `default:"true" desc:"是否发布视频"`
@@ -36,9 +70,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:混合转发"` // 转发模式
@@ -54,18 +88,21 @@ type (
SyncMode int `default:"1" desc:"同步模式" enum:"0:采用时间戳同步,1:采用写入时间同步"` // 0采用时间戳同步1采用写入时间同步
IFrameOnly bool `desc:"只要关键帧"` // 只要关键帧
WaitTimeout time.Duration `default:"10s" desc:"等待流超时时间"` // 等待流超时
WriteBufferSize int `desc:"写缓冲大小"` // 写缓冲大小
Key string `desc:"订阅鉴权key"` // 订阅鉴权key
SubType string `desc:"订阅类型"` // 订阅类型
WaitTrack string `default:"video" desc:"等待轨道" enum:"audio:等待音频,video:等待视频,all:等待全部"`
WriteBufferSize int `desc:"写缓冲大小"` // 写缓冲大小
Key string `desc:"订阅鉴权key"` // 订阅鉴权key
SubType string `desc:"订阅类型"` // 订阅类型
}
HTTPValues map[string][]string
Pull struct {
URL string `desc:"拉流地址"`
Loop int `desc:"拉流循环次数,-1:无限循环"` // 拉流循环次数,-1 表示无限循环
MaxRetry int `default:"-1" desc:"断开后自动重试次数,0:不重试,-1:无限重试"` // 断开后自动重拉,0 表示不自动重拉,-1 表示无限重拉高于0 的数代表最大重拉次数
RetryInterval time.Duration `default:"5s" desc:"重试间隔"` // 重试间隔
Proxy string `desc:"代理地址"` // 代理地址
Header HTTPValues
Args HTTPValues `gorm:"-:all"` // 拉流参数
Args HTTPValues `gorm:"-:all"` // 拉流参数
TestMode int `desc:"测试模式,0:关闭,1:只拉流不发布"` // 测试模式
}
Push struct {
URL string `desc:"推送地址"` // 推送地址
@@ -74,11 +111,21 @@ type (
Proxy string `desc:"代理地址"` // 代理地址
Header HTTPValues
}
RecordEvent struct {
EventId string
BeforeDuration uint32 `json:"beforeDuration" desc:"事件前缓存时长" gorm:"comment:事件前缓存时长;default:30000"`
AfterDuration uint32 `json:"afterDuration" desc:"事件后缓存时长" gorm:"comment:事件后缓存时长;default:30000"`
EventDesc string `json:"eventDesc" desc:"事件描述" gorm:"type:varchar(255);comment:事件描述"`
EventLevel EventLevel `json:"eventLevel" desc:"事件级别" gorm:"type:varchar(255);comment:事件级别,high表示重要事件无法删除且表示无需自动删除,low表示非重要事件,达到自动删除时间后,自动删除;default:'low'"`
EventName string `json:"eventName" desc:"事件名称" gorm:"type:varchar(255);comment:事件名称"`
}
Record struct {
Type string `desc:"录制类型"` // 录制类型 mp4、flv、hls、hlsv7
FilePath string `desc:"录制文件路径"` // 录制文件路径
Fragment time.Duration `desc:"分片时长"` // 分片时长
Append bool `desc:"是否追加录制"` // 是否追加录制
Mode RecordMode `json:"mode" desc:"事件类型,auto=连续录像模式event=事件录像模式" gorm:"type:varchar(255);comment:事件类型,auto=连续录像模式event=事件录像模式;default:'auto'"`
Type string `desc:"录制类型"` // 录制类型 mp4、flv、hls、hlsv7
FilePath string `desc:"录制文件路径"` // 录制文件路径
Fragment time.Duration `desc:"分片时长"` // 分片时长
Append bool `desc:"是否追加录制"` // 是否追加录制
Event *RecordEvent `json:"event" desc:"事件录像配置" gorm:"-"` // 事件录像配置
}
TransfromOutput struct {
Target string `desc:"转码目标"` // 转码目标
@@ -99,13 +146,14 @@ type (
Transform map[Regexp]Transform
}
Webhook struct {
URL string `yaml:"url" json:"url"` // Webhook 地址
Method string `yaml:"method" json:"method" default:"POST"` // HTTP 方法
Headers map[string]string `yaml:"headers" json:"headers"` // 自定义请求头
TimeoutSeconds int `yaml:"timeout" json:"timeout" default:"5"` // 超时时间(秒)
RetryTimes int `yaml:"retry" json:"retry" default:"3"` // 重试次数
RetryInterval time.Duration `yaml:"retryInterval" json:"retryInterval" default:"1s"` // 重试间隔
Interval int `yaml:"interval" json:"interval" default:"60"` // 保活间隔(秒)
URL string // Webhook 地址
Method string `default:"POST"` // HTTP 方法
Headers map[string]string // 自定义请求头
TimeoutSeconds int `default:"5"` // 超时时间(秒)
RetryTimes int `default:"3"` // 重试次数
RetryInterval time.Duration `default:"1s"` // 重试间隔
Interval int `default:"60"` // 保活间隔(秒)
SaveAlarm bool `default:"false"` // 是否保存告警到数据库
}
Common struct {
PublicIP string
@@ -164,3 +212,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

@@ -9,14 +9,11 @@ import (
// User represents a user in the system
type User struct {
ID uint `gorm:"primarykey"`
CreatedAt time.Time
UpdatedAt time.Time
DeletedAt gorm.DeletedAt `gorm:"index"`
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
gorm.Model
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 `gorm:"type:timestamp;default:CURRENT_TIMESTAMP"`
}
// BeforeCreate hook to hash password before saving

View File

@@ -9,10 +9,12 @@ var (
ErrRecordExists = errors.New("record exists")
ErrKick = errors.New("kick")
ErrDiscard = errors.New("discard")
ErrPublishMaxCount = errors.New("publish max count exceeded")
ErrPublishTimeout = errors.New("publish timeout")
ErrPublishIdleTimeout = errors.New("publish idle timeout")
ErrPublishDelayCloseTimeout = errors.New("publish delay close timeout")
ErrPushRemoteURLExist = errors.New("push remote url exist")
ErrSubscribeMaxCount = errors.New("subscribe max count exceeded")
ErrSubscribeTimeout = errors.New("subscribe timeout")
ErrRestart = errors.New("restart")
ErrInterrupt = errors.New("interrupt")

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

@@ -2,13 +2,14 @@ package pkg
import (
"fmt"
"io"
"time"
"github.com/deepch/vdk/codec/aacparser"
"github.com/deepch/vdk/codec/h264parser"
"github.com/deepch/vdk/codec/h265parser"
"io"
"m7s.live/v5/pkg/codec"
"m7s.live/v5/pkg/util"
"time"
)
var _ IAVFrame = (*RawAudio)(nil)
@@ -104,6 +105,8 @@ type H26xFrame struct {
}
func (h *H26xFrame) Parse(track *AVTrack) (err error) {
var hasVideoFrame bool
switch h.FourCC {
case codec.FourCC_H264:
var ctx *codec.H264Ctx
@@ -127,6 +130,9 @@ func (h *H26xFrame) Parse(track *AVTrack) (err error) {
}
case codec.NALU_IDR_Picture:
track.Value.IDR = true
hasVideoFrame = true
case codec.NALU_Non_IDR_Picture:
hasVideoFrame = true
}
}
case codec.FourCC_H265:
@@ -155,9 +161,18 @@ func (h *H26xFrame) Parse(track *AVTrack) (err error) {
h265parser.NAL_UNIT_CODED_SLICE_IDR_N_LP,
h265parser.NAL_UNIT_CODED_SLICE_CRA:
track.Value.IDR = true
hasVideoFrame = true
case 0, 1, 2, 3, 4, 5, 6, 7, 8, 9:
hasVideoFrame = true
}
}
}
// Return ErrSkip if no video frames are present (only metadata NALUs)
if !hasVideoFrame {
return ErrSkip
}
return
}

157
pkg/raw_test.go Normal file
View File

@@ -0,0 +1,157 @@
package pkg
import (
"testing"
"m7s.live/v5/pkg/codec"
"m7s.live/v5/pkg/util"
)
func TestH26xFrame_Parse_VideoFrameDetection(t *testing.T) {
// Test H264 IDR Picture (should not skip)
t.Run("H264_IDR_Picture", func(t *testing.T) {
frame := &H26xFrame{
FourCC: codec.FourCC_H264,
Nalus: []util.Memory{
util.NewMemory([]byte{0x65}), // IDR Picture NALU type
},
}
track := &AVTrack{}
err := frame.Parse(track)
if err == ErrSkip {
t.Error("Expected H264 IDR frame to not be skipped, but got ErrSkip")
}
if !track.Value.IDR {
t.Error("Expected IDR flag to be set for H264 IDR frame")
}
})
// Test H264 Non-IDR Picture (should not skip)
t.Run("H264_Non_IDR_Picture", func(t *testing.T) {
frame := &H26xFrame{
FourCC: codec.FourCC_H264,
Nalus: []util.Memory{
util.NewMemory([]byte{0x21}), // Non-IDR Picture NALU type
},
}
track := &AVTrack{}
err := frame.Parse(track)
if err == ErrSkip {
t.Error("Expected H264 Non-IDR frame to not be skipped, but got ErrSkip")
}
})
// Test H264 metadata only (should skip)
t.Run("H264_SPS_Only", func(t *testing.T) {
frame := &H26xFrame{
FourCC: codec.FourCC_H264,
Nalus: []util.Memory{
util.NewMemory([]byte{0x67}), // SPS NALU type
},
}
track := &AVTrack{}
err := frame.Parse(track)
if err != ErrSkip {
t.Errorf("Expected H264 SPS-only frame to be skipped, but got: %v", err)
}
})
// Test H264 PPS only (should skip)
t.Run("H264_PPS_Only", func(t *testing.T) {
frame := &H26xFrame{
FourCC: codec.FourCC_H264,
Nalus: []util.Memory{
util.NewMemory([]byte{0x68}), // PPS NALU type
},
}
track := &AVTrack{}
err := frame.Parse(track)
if err != ErrSkip {
t.Errorf("Expected H264 PPS-only frame to be skipped, but got: %v", err)
}
})
// Test H265 IDR slice (should not skip)
t.Run("H265_IDR_Slice", func(t *testing.T) {
frame := &H26xFrame{
FourCC: codec.FourCC_H265,
Nalus: []util.Memory{
util.NewMemory([]byte{0x4E, 0x01}), // IDR_W_RADL slice type (19 << 1 = 38 = 0x26, so first byte should be 0x4C, but let's use a simpler approach)
// Using NAL_UNIT_CODED_SLICE_IDR_W_RADL which should be type 19
},
}
track := &AVTrack{}
// Let's use the correct byte pattern for H265 IDR slice
// NAL_UNIT_CODED_SLICE_IDR_W_RADL = 19
// H265 header: (type << 1) | layer_id_bit
idrSliceByte := byte(19 << 1) // 19 * 2 = 38 = 0x26
frame.Nalus[0] = util.NewMemory([]byte{idrSliceByte})
err := frame.Parse(track)
if err == ErrSkip {
t.Error("Expected H265 IDR slice to not be skipped, but got ErrSkip")
}
if !track.Value.IDR {
t.Error("Expected IDR flag to be set for H265 IDR slice")
}
})
// Test H265 metadata only (should skip)
t.Run("H265_VPS_Only", func(t *testing.T) {
frame := &H26xFrame{
FourCC: codec.FourCC_H265,
Nalus: []util.Memory{
util.NewMemory([]byte{0x40, 0x01}), // VPS NALU type (32 << 1 = 64 = 0x40)
},
}
track := &AVTrack{}
err := frame.Parse(track)
if err != ErrSkip {
t.Errorf("Expected H265 VPS-only frame to be skipped, but got: %v", err)
}
})
// Test mixed H264 frame with SPS and IDR (should not skip)
t.Run("H264_Mixed_SPS_And_IDR", func(t *testing.T) {
frame := &H26xFrame{
FourCC: codec.FourCC_H264,
Nalus: []util.Memory{
util.NewMemory([]byte{0x67}), // SPS NALU type
util.NewMemory([]byte{0x65}), // IDR Picture NALU type
},
}
track := &AVTrack{}
err := frame.Parse(track)
if err == ErrSkip {
t.Error("Expected H264 mixed SPS+IDR frame to not be skipped, but got ErrSkip")
}
if !track.Value.IDR {
t.Error("Expected IDR flag to be set for H264 mixed frame with IDR")
}
})
// Test mixed H265 frame with VPS and IDR (should not skip)
t.Run("H265_Mixed_VPS_And_IDR", func(t *testing.T) {
frame := &H26xFrame{
FourCC: codec.FourCC_H265,
Nalus: []util.Memory{
util.NewMemory([]byte{0x40, 0x01}), // VPS NALU type (32 << 1)
util.NewMemory([]byte{0x4C, 0x01}), // IDR_W_RADL slice type (19 << 1)
},
}
track := &AVTrack{}
// Fix the IDR slice byte for H265
idrSliceByte := byte(19 << 1) // NAL_UNIT_CODED_SLICE_IDR_W_RADL = 19
frame.Nalus[1] = util.NewMemory([]byte{idrSliceByte, 0x01})
err := frame.Parse(track)
if err == ErrSkip {
t.Error("Expected H265 mixed VPS+IDR frame to not be skipped, but got ErrSkip")
}
if !track.Value.IDR {
t.Error("Expected IDR flag to be set for H265 mixed frame with IDR")
}
})
}

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

@@ -32,14 +32,15 @@ func GetNextTaskID() uint32 {
// Job include tasks
type Job struct {
Task
cases []reflect.SelectCase
addSub chan ITask
children []ITask
lazyRun sync.Once
eventLoopLock sync.Mutex
childrenDisposed chan struct{}
childDisposeListeners []func(ITask)
blocked ITask
cases []reflect.SelectCase
addSub chan ITask
children []ITask
lazyRun sync.Once
eventLoopLock sync.Mutex
childrenDisposed chan struct{}
descendantsDisposeListeners []func(ITask)
descendantsStartListeners []func(ITask)
blocked ITask
}
func (*Job) GetTaskType() TaskType {
@@ -55,19 +56,26 @@ func (mt *Job) Blocked() ITask {
}
func (mt *Job) waitChildrenDispose() {
if blocked := mt.blocked; blocked != nil {
blocked := mt.blocked
defer func() {
// 忽略由于在任务关闭过程中可能存在竞态条件,当父任务关闭时子任务可能已经被释放。
if err := recover(); err != nil {
mt.Debug("waitChildrenDispose panic", "err", err)
}
mt.addSub <- nil
<-mt.childrenDisposed
}()
if blocked != nil {
blocked.Stop(mt.StopReason())
}
mt.addSub <- nil
<-mt.childrenDisposed
}
func (mt *Job) OnChildDispose(listener func(ITask)) {
mt.childDisposeListeners = append(mt.childDisposeListeners, listener)
func (mt *Job) OnDescendantsDispose(listener func(ITask)) {
mt.descendantsDisposeListeners = append(mt.descendantsDisposeListeners, listener)
}
func (mt *Job) onDescendantsDispose(descendants ITask) {
for _, listener := range mt.childDisposeListeners {
for _, listener := range mt.descendantsDisposeListeners {
listener(descendants)
}
if mt.parent != nil {
@@ -76,11 +84,28 @@ func (mt *Job) onDescendantsDispose(descendants ITask) {
}
func (mt *Job) onChildDispose(child ITask) {
if child.getParent() == mt {
if child.GetTaskType() != TASK_TYPE_CALL || child.GetOwnerType() != "CallBack" {
mt.onDescendantsDispose(child)
}
child.dispose()
if child.GetTaskType() != TASK_TYPE_CALL || child.GetOwnerType() != "CallBack" {
mt.onDescendantsDispose(child)
}
child.dispose()
}
func (mt *Job) OnDescendantsStart(listener func(ITask)) {
mt.descendantsStartListeners = append(mt.descendantsStartListeners, listener)
}
func (mt *Job) onDescendantsStart(descendants ITask) {
for _, listener := range mt.descendantsStartListeners {
listener(descendants)
}
if mt.parent != nil {
mt.parent.onDescendantsStart(descendants)
}
}
func (mt *Job) onChildStart(child ITask) {
if child.GetTaskType() != TASK_TYPE_CALL || child.GetOwnerType() != "CallBack" {
mt.onDescendantsStart(child)
}
}
@@ -157,9 +182,7 @@ func (mt *Job) AddTask(t ITask, opt ...any) (task *Task) {
return
}
if len(mt.addSub) > 10 {
if mt.Logger != nil {
mt.Warn("task wait list too many", "count", len(mt.addSub), "taskId", task.ID, "taskType", task.GetTaskType(), "ownerType", task.GetOwnerType(), "parent", mt.GetOwnerType())
}
mt.Warn("task wait list too many", "count", len(mt.addSub), "taskId", task.ID, "taskType", task.GetTaskType(), "ownerType", task.GetOwnerType(), "parent", mt.GetOwnerType())
}
mt.addSub <- t
return
@@ -182,9 +205,7 @@ func (mt *Job) run() {
defer func() {
err := recover()
if err != nil {
if mt.Logger != nil {
mt.Logger.Error("job panic", "err", err, "stack", string(debug.Stack()))
}
mt.Error("job panic", "err", err, "stack", string(debug.Stack()))
if !ThrowPanic {
mt.Stop(errors.Join(err.(error), ErrPanic))
} else {
@@ -203,31 +224,42 @@ func (mt *Job) run() {
mt.blocked = nil
if chosen, rev, ok := reflect.Select(mt.cases); chosen == 0 {
if rev.IsNil() {
mt.Debug("job addSub channel closed, exiting", "taskId", mt.GetTaskID())
return
}
if mt.blocked = rev.Interface().(ITask); mt.blocked.getParent() != mt || mt.blocked.start() {
if mt.blocked = rev.Interface().(ITask); mt.blocked.start() {
mt.children = append(mt.children, mt.blocked)
mt.cases = append(mt.cases, reflect.SelectCase{Dir: reflect.SelectRecv, Chan: reflect.ValueOf(mt.blocked.GetSignal())})
mt.onChildStart(mt.blocked)
}
} else {
taskIndex := chosen - 1
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())
mt.onChildStart(mt.blocked)
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

@@ -24,15 +24,49 @@ func (m *Manager[K, T]) Add(ctx T, opt ...any) *Task {
ctx.Stop(ErrExist)
return
}
if m.Logger != nil {
m.Logger.Debug("add", "key", ctx.GetKey(), "count", m.Length)
}
m.Debug("add", "key", ctx.GetKey(), "count", m.Length)
})
ctx.OnDispose(func() {
m.Remove(ctx)
if m.Logger != nil {
m.Logger.Debug("remove", "key", ctx.GetKey(), "count", m.Length)
}
m.Debug("remove", "key", ctx.GetKey(), "count", m.Length)
})
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)
}
}
// SafeFind 用于不同协程获取元素,防止并发请求
func (m *Manager[K, T]) SafeFind(f func(T) bool) (item T, ok bool) {
if m.L == nil {
m.Call(func() error {
item, ok = m.Collection.Find(f)
return nil
})
} else {
item, ok = m.Collection.Find(f)
}
return
}

View File

@@ -7,6 +7,7 @@ import (
"log/slog"
"maps"
"reflect"
"runtime"
"runtime/debug"
"strings"
"sync"
@@ -53,7 +54,6 @@ type (
ITask interface {
context.Context
keepalive() bool
getParent() *Job
GetParent() ITask
GetTask() *Task
GetTaskID() uint32
@@ -77,13 +77,16 @@ type (
OnDispose(func())
GetState() TaskState
GetLevel() byte
WaitStopped() error
WaitStarted() error
}
IJob interface {
ITask
getJob() *Job
AddTask(ITask, ...any) *Task
RangeSubTask(func(yield ITask) bool)
OnChildDispose(func(ITask))
OnDescendantsDispose(func(ITask))
OnDescendantsStart(func(ITask))
Blocked() ITask
Call(func() error, ...any)
Post(func() error, ...any) *Task
@@ -115,7 +118,7 @@ type (
ID uint32
StartTime time.Time
StartReason string
*slog.Logger
Logger *slog.Logger
context.Context
context.CancelCauseFunc
handler ITask
@@ -176,10 +179,6 @@ func (task *Task) GetTaskPointer() uintptr {
return uintptr(unsafe.Pointer(task))
}
func (task *Task) getParent() *Job {
return task.parent
}
func (task *Task) GetKey() uint32 {
return task.ID
}
@@ -200,7 +199,11 @@ func (task *Task) WaitStopped() (err error) {
}
func (task *Task) Trace(msg string, fields ...any) {
task.Log(task.Context, TraceLevel, msg, fields...)
if task.Logger == nil {
slog.Default().Log(task.Context, TraceLevel, msg, fields...)
return
}
task.Logger.Log(task.Context, TraceLevel, msg, fields...)
}
func (task *Task) IsStopped() bool {
@@ -227,8 +230,9 @@ func (task *Task) Stop(err error) {
panic("task stop with nil error")
}
if task.CancelCauseFunc != nil {
if tt := task.handler.GetTaskType(); task.Logger != nil && tt != TASK_TYPE_CALL {
task.Debug("task stop", "reason", err, "elapsed", time.Since(task.StartTime), "taskId", task.ID, "taskType", tt, "ownerType", task.GetOwnerType())
if tt := task.handler.GetTaskType(); tt != TASK_TYPE_CALL {
_, file, line, _ := runtime.Caller(1)
task.Debug("task stop", "caller", fmt.Sprintf("%s:%d", strings.TrimPrefix(file, sourceFilePathPrefix), line), "reason", err, "elapsed", time.Since(task.StartTime), "taskId", task.ID, "taskType", tt, "ownerType", task.GetOwnerType())
}
task.CancelCauseFunc(err)
}
@@ -266,12 +270,10 @@ func (task *Task) checkRetry(err error) bool {
if task.retry.MaxRetry < 0 || task.retry.RetryCount < task.retry.MaxRetry {
task.retry.RetryCount++
task.SetDescription("retryCount", task.retry.RetryCount)
if task.Logger != nil {
if task.retry.MaxRetry < 0 {
task.Warn(fmt.Sprintf("retry %d/∞", task.retry.RetryCount), "taskId", task.ID)
} else {
task.Warn(fmt.Sprintf("retry %d/%d", task.retry.RetryCount, task.retry.MaxRetry), "taskId", task.ID)
}
if task.retry.MaxRetry < 0 {
task.Warn(fmt.Sprintf("retry %d/∞", task.retry.RetryCount), "taskId", task.ID)
} else {
task.Warn(fmt.Sprintf("retry %d/%d", task.retry.RetryCount, task.retry.MaxRetry), "taskId", task.ID)
}
if delta := time.Since(task.StartTime); delta < task.retry.RetryInterval {
time.Sleep(task.retry.RetryInterval - delta)
@@ -279,9 +281,7 @@ func (task *Task) checkRetry(err error) bool {
return true
} else {
if task.retry.MaxRetry > 0 {
if task.Logger != nil {
task.Warn(fmt.Sprintf("max retry %d failed", task.retry.MaxRetry))
}
task.Warn(fmt.Sprintf("max retry %d failed", task.retry.MaxRetry))
return false
}
}
@@ -294,15 +294,13 @@ func (task *Task) start() bool {
defer func() {
if r := recover(); r != nil {
err = errors.New(fmt.Sprint(r))
if task.Logger != nil {
task.Error("panic", "error", err, "stack", string(debug.Stack()))
}
task.Error("panic", "error", err, "stack", string(debug.Stack()))
}
}()
}
for {
task.StartTime = time.Now()
if tt := task.handler.GetTaskType(); task.Logger != nil && tt != TASK_TYPE_CALL {
if tt := task.handler.GetTaskType(); tt != TASK_TYPE_CALL {
task.Debug("task start", "taskId", task.ID, "taskType", tt, "ownerType", task.GetOwnerType(), "reason", task.StartReason)
}
task.state = TASK_STATE_STARTING
@@ -324,6 +322,7 @@ func (task *Task) start() bool {
task.ResetRetryCount()
if runHandler, ok := task.handler.(TaskBlock); ok {
task.state = TASK_STATE_RUNNING
task.Debug("task run", "taskId", task.ID, "taskType", task.GetTaskType(), "ownerType", task.GetOwnerType())
err = runHandler.Run()
if err == nil {
err = ErrTaskComplete
@@ -334,6 +333,7 @@ func (task *Task) start() bool {
if err == nil {
if goHandler, ok := task.handler.(TaskGo); ok {
task.state = TASK_STATE_GOING
task.Debug("task go", "taskId", task.ID, "taskType", task.GetTaskType(), "ownerType", task.GetOwnerType())
go task.run(goHandler.Go)
}
return true
@@ -380,19 +380,17 @@ func (task *Task) SetDescriptions(value Description) {
func (task *Task) dispose() {
taskType, ownerType := task.handler.GetTaskType(), task.GetOwnerType()
if task.state < TASK_STATE_STARTED {
if task.Logger != nil && taskType != TASK_TYPE_CALL {
if taskType != TASK_TYPE_CALL {
task.Debug("task dispose canceled", "taskId", task.ID, "taskType", taskType, "ownerType", ownerType, "state", task.state)
}
return
}
reason := task.StopReason()
task.state = TASK_STATE_DISPOSING
if task.Logger != nil {
if taskType != TASK_TYPE_CALL {
yargs := []any{"reason", reason, "taskId", task.ID, "taskType", taskType, "ownerType", ownerType}
task.Debug("task dispose", yargs...)
defer task.Debug("task disposed", yargs...)
}
if taskType != TASK_TYPE_CALL {
yargs := []any{"reason", reason, "taskId", task.ID, "taskType", taskType, "ownerType", ownerType}
task.Debug("task dispose", yargs...)
defer task.Debug("task disposed", yargs...)
}
befores := len(task.beforeDisposeListeners)
for i, listener := range task.beforeDisposeListeners {
@@ -427,15 +425,17 @@ func (task *Task) ResetRetryCount() {
task.retry.RetryCount = 0
}
func (task *Task) GetRetryCount() int {
return task.retry.RetryCount
}
func (task *Task) run(handler func() error) {
var err error
defer func() {
if !ThrowPanic {
if r := recover(); r != nil {
err = errors.New(fmt.Sprint(r))
if task.Logger != nil {
task.Error("panic", "error", err, "stack", string(debug.Stack()))
}
task.Error("panic", "error", err, "stack", string(debug.Stack()))
}
}
if err == nil {
@@ -446,3 +446,39 @@ func (task *Task) run(handler func() error) {
}()
err = handler()
}
func (task *Task) Debug(msg string, args ...any) {
if task.Logger == nil {
slog.Default().Debug(msg, args...)
return
}
task.Logger.Debug(msg, args...)
}
func (task *Task) Info(msg string, args ...any) {
if task.Logger == nil {
slog.Default().Info(msg, args...)
return
}
task.Logger.Info(msg, args...)
}
func (task *Task) Warn(msg string, args ...any) {
if task.Logger == nil {
slog.Default().Warn(msg, args...)
return
}
task.Logger.Warn(msg, args...)
}
func (task *Task) Error(msg string, args ...any) {
if task.Logger == nil {
slog.Default().Error(msg, args...)
return
}
task.Logger.Error(msg, args...)
}
func (task *Task) TraceEnabled() bool {
return task.Logger.Enabled(task.Context, TraceLevel)
}

View File

@@ -142,6 +142,26 @@ func Test_Hooks(t *testing.T) {
root.AddTask(&task).WaitStopped()
}
type startFailTask struct {
Task
}
func (task *startFailTask) Start() error {
return errors.New("start failed")
}
func (task *startFailTask) Dispose() {
task.Logger.Info("Dispose")
}
func Test_StartFail(t *testing.T) {
var task startFailTask
root.AddTask(&task)
if err := task.WaitStarted(); err == nil {
t.Errorf("expected start to fail")
}
}
//
//type DemoTask struct {
// Task

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 {
@@ -94,9 +168,9 @@ func (t *AVTrack) Ready(err error) {
} else {
switch ctx := t.ICodecCtx.(type) {
case IVideoCodecCtx:
t.Info("ready", "info", t.ICodecCtx.GetInfo(), "width", ctx.Width(), "height", ctx.Height())
t.Info("ready", "codec", t.ICodecCtx.FourCC(), "info", t.ICodecCtx.GetInfo(), "width", ctx.Width(), "height", ctx.Height())
case IAudioCodecCtx:
t.Info("ready", "info", t.ICodecCtx.GetInfo(), "channels", ctx.GetChannels(), "sample_rate", ctx.GetSampleRate())
t.Info("ready", "codec", t.ICodecCtx.FourCC(), "info", t.ICodecCtx.GetInfo(), "channels", ctx.GetChannels(), "sample_rate", ctx.GetSampleRate())
}
}
t.ready.Fulfill(err)
@@ -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

@@ -2,33 +2,55 @@ package util
import (
"errors"
"sync"
"unsafe"
)
type Buddy struct {
size int
longests []int
size int
longests [BuddySize>>(MinPowerOf2-1) - 1]int
memoryPool [BuddySize]byte
poolStart int64
lock sync.Mutex // 保护 longests 数组的并发访问
}
var (
InValidParameterErr = errors.New("buddy: invalid parameter")
NotFoundErr = errors.New("buddy: can't find block")
buddyPool = sync.Pool{
New: func() interface{} {
return NewBuddy()
},
}
)
// GetBuddy 从池中获取一个 Buddy 实例
func GetBuddy() *Buddy {
buddy := buddyPool.Get().(*Buddy)
return buddy
}
// PutBuddy 将 Buddy 实例放回池中
func PutBuddy(b *Buddy) {
buddyPool.Put(b)
}
// NewBuddy creates a buddy instance.
// If the parameter isn't valid, return the nil and error as well
func NewBuddy(size int) *Buddy {
if !isPowerOf2(size) {
size = fixSize(size)
func NewBuddy() *Buddy {
size := BuddySize >> MinPowerOf2
ret := &Buddy{
size: size,
}
nodeCount := 2*size - 1
longests := make([]int, nodeCount)
for nodeSize, i := 2*size, 0; i < nodeCount; i++ {
for nodeSize, i := 2*size, 0; i < len(ret.longests); i++ {
if isPowerOf2(i + 1) {
nodeSize /= 2
}
longests[i] = nodeSize
ret.longests[i] = nodeSize
}
return &Buddy{size, longests}
ret.poolStart = int64(uintptr(unsafe.Pointer(&ret.memoryPool[0])))
return ret
}
// Alloc find a unused block according to the size
@@ -42,6 +64,8 @@ func (b *Buddy) Alloc(size int) (offset int, err error) {
if !isPowerOf2(size) {
size = fixSize(size)
}
b.lock.Lock()
defer b.lock.Unlock()
if size > b.longests[0] {
err = NotFoundErr
return
@@ -70,6 +94,8 @@ func (b *Buddy) Free(offset int) error {
if offset < 0 || offset >= b.size {
return InValidParameterErr
}
b.lock.Lock()
defer b.lock.Unlock()
nodeSize := 1
index := offset + b.size - 1
for ; b.longests[index] != 0; index = parent(index) {

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

@@ -3,11 +3,9 @@
package util
import (
"container/list"
"fmt"
"io"
"slices"
"sync"
"unsafe"
)
@@ -58,53 +56,59 @@ func (r *RecyclableMemory) Recycle() {
}
}
var (
memoryPool [BuddySize]byte
buddy = NewBuddy(BuddySize >> MinPowerOf2)
lock sync.Mutex
poolStart = int64(uintptr(unsafe.Pointer(&memoryPool[0])))
blockPool = list.New()
//EnableCheckSize bool = false
)
type MemoryAllocator struct {
allocator *Allocator
start int64
memory []byte
Size int
buddy *Buddy
}
// createMemoryAllocator 创建并初始化 MemoryAllocator
func createMemoryAllocator(size int, buddy *Buddy, offset int) *MemoryAllocator {
ret := &MemoryAllocator{
allocator: NewAllocator(size),
buddy: buddy,
Size: size,
memory: buddy.memoryPool[offset : offset+size],
start: buddy.poolStart + int64(offset),
}
ret.allocator.Init(size)
return ret
}
func GetMemoryAllocator(size int) (ret *MemoryAllocator) {
lock.Lock()
offset, err := buddy.Alloc(size >> MinPowerOf2)
if blockPool.Len() > 0 {
ret = blockPool.Remove(blockPool.Front()).(*MemoryAllocator)
} else {
ret = &MemoryAllocator{
allocator: NewAllocator(size),
if size < BuddySize {
requiredSize := size >> MinPowerOf2
// 循环尝试从池中获取可用的 buddy
for {
buddy := GetBuddy()
offset, err := buddy.Alloc(requiredSize)
PutBuddy(buddy)
if err == nil {
// 分配成功,使用这个 buddy
return createMemoryAllocator(size, buddy, offset<<MinPowerOf2)
}
}
}
lock.Unlock()
ret.Size = size
ret.allocator.Init(size)
if err != nil {
ret.memory = make([]byte, size)
ret.start = int64(uintptr(unsafe.Pointer(&ret.memory[0])))
return
// 池中的 buddy 都无法分配或大小不够,使用系统内存
memory := make([]byte, size)
start := int64(uintptr(unsafe.Pointer(&memory[0])))
return &MemoryAllocator{
allocator: NewAllocator(size),
Size: size,
memory: memory,
start: start,
}
offset = offset << MinPowerOf2
ret.memory = memoryPool[offset : offset+size]
ret.start = poolStart + int64(offset)
return
}
func (ma *MemoryAllocator) Recycle() {
ma.allocator.Recycle()
lock.Lock()
blockPool.PushBack(ma)
_ = buddy.Free(int((poolStart - ma.start) >> MinPowerOf2))
if ma.buddy != nil {
_ = ma.buddy.Free(int((ma.buddy.poolStart - ma.start) >> MinPowerOf2))
ma.buddy = nil
}
ma.memory = nil
lock.Unlock()
}
func (ma *MemoryAllocator) Find(size int) (memory []byte) {

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()
}

479
plugin.go
View File

@@ -7,6 +7,7 @@ import (
"encoding/hex"
"encoding/json"
"fmt"
"gopkg.in/yaml.v3"
"net"
"net/http"
"net/url"
@@ -25,8 +26,8 @@ import (
gatewayRuntime "github.com/grpc-ecosystem/grpc-gateway/v2/runtime"
myip "github.com/husanpao/ip"
"google.golang.org/grpc"
"gopkg.in/yaml.v3"
"gorm.io/gorm"
. "m7s.live/v5/pkg"
"m7s.live/v5/pkg/config"
"m7s.live/v5/pkg/db"
@@ -43,13 +44,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 +91,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 +118,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)
@@ -137,20 +134,9 @@ func (plugin *PluginMeta) Init(s *Server, userConfig map[string]any) (p *Plugin)
finalConfig, _ := yaml.Marshal(p.Config.GetMap())
p.Logger.Handler().(*MultiLogHandler).SetLevel(ParseLevel(p.config.LogLevel))
p.Debug("config", "detail", string(finalConfig))
if s.DisableAll {
p.Disabled = true
}
if userConfig["enable"] == false {
p.Disabled = true
} else if userConfig["enable"] == true {
p.Disabled = false
}
if p.Disabled {
if userConfig["enable"] == false || (s.DisableAll && userConfig["enable"] != true) {
p.disable("config")
p.Warn("plugin disabled")
return
} else {
p.assign()
}
p.Info("init", "version", plugin.Version)
var err error
@@ -158,7 +144,7 @@ func (plugin *PluginMeta) Init(s *Server, userConfig map[string]any) (p *Plugin)
p.DB = s.DB
} else if p.config.DSN != "" {
if factory, ok := db.Factory[p.config.DBType]; ok {
s.DB, err = gorm.Open(factory(p.config.DSN), &gorm.Config{})
p.DB, err = gorm.Open(factory(p.config.DSN), &gorm.Config{})
if err != nil {
s.Error("failed to connect database", "error", err, "dsn", s.config.DSN, "type", s.config.DBType)
p.disable(fmt.Sprintf("database %v", err))
@@ -166,47 +152,65 @@ 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
}
if err = p.DB.AutoMigrate(&EventRecordStream{}); err != nil {
p.disable(fmt.Sprintf("auto migrate event record stream failed %v", err))
return
}
}
s.AddTask(instance)
if err := s.AddTask(instance).WaitStarted(); err != nil {
p.disable(instance.StopReason().Error())
return
}
var handlers map[string]http.HandlerFunc
if v, ok := instance.(IRegisterHandler); ok {
handlers = v.RegisterHandler()
}
p.registerHandler(handlers)
s.Plugins.Add(p)
return
}
// 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,36 +273,23 @@ 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.Warn("plugin disabled")
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
s.AddTask(&webHookQueueTask)
if err = p.listen(); err != nil {
return
}
if err = p.handler.OnInit(); err != nil {
return
}
if p.Meta.ServiceDesc != nil && s.grpcServer != nil {
s.grpcServer.RegisterService(p.Meta.ServiceDesc, p.handler)
if p.Meta.RegisterGRPCHandler != nil {
@@ -310,15 +301,6 @@ func (p *Plugin) Start() (err error) {
}
}
}
s.Plugins.Add(p)
if err = p.listen(); err != nil {
p.disable(fmt.Sprintf("listen %v", err))
return
}
if err = p.handler.OnInit(); err != nil {
p.disable(fmt.Sprintf("init %v", err))
return
}
if p.config.Hook != nil {
if hook, ok := p.config.Hook[config.HookOnServerKeepAlive]; ok && hook.Interval > 0 {
p.AddTask(&ServerKeepAliveTask{plugin: p})
@@ -337,12 +319,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 {
@@ -399,31 +381,100 @@ func (p *Plugin) OnStop() {
}
type WebHookQueueTask struct {
task.Work
}
var webHookQueueTask WebHookQueueTask
type WebHookTask struct {
task.Task
plugin *Plugin
hookType config.HookType
conf *config.Webhook
conf config.Webhook
data any
jsonData []byte
alarm AlarmInfo
}
func (t *WebHookTask) Start() error {
if t.conf == nil || t.conf.URL == "" {
if t.conf.URL == "" {
return task.ErrTaskComplete
}
var err error
t.jsonData, err = json.Marshal(t.data)
if err != nil {
return fmt.Errorf("marshal webhook data: %w", err)
// 处理AlarmInfo数据
if t.data != nil {
// 获取主机名和IP地址
hostname, err := os.Hostname()
if err != nil {
hostname = "unknown"
}
// 获取本机IP地址
var ipAddr string
addrs, err := net.InterfaceAddrs()
if err == nil {
for _, addr := range addrs {
if ipnet, ok := addr.(*net.IPNet); ok && !ipnet.IP.IsLoopback() {
if ipnet.IP.To4() != nil {
ipAddr = ipnet.IP.String()
break
}
}
}
}
if ipAddr == "" {
ipAddr = "unknown"
}
// 直接使用t.data作为AlarmInfo
alarmInfo, ok := t.data.(AlarmInfo)
if !ok {
return fmt.Errorf("data is not of type AlarmInfo")
}
// 更新服务器信息
if alarmInfo.ServerInfo == "" {
alarmInfo.ServerInfo = fmt.Sprintf("%s (%s)", hostname, ipAddr)
}
// 确保时间戳已设置
if alarmInfo.CreatedAt.IsZero() {
alarmInfo.CreatedAt = time.Now()
}
if alarmInfo.UpdatedAt.IsZero() {
alarmInfo.UpdatedAt = time.Now()
}
// 将AlarmInfo序列化为JSON
jsonData, err := json.Marshal(alarmInfo)
if err != nil {
return fmt.Errorf("marshal AlarmInfo to json: %w", err)
}
t.jsonData = jsonData
t.alarm = alarmInfo
}
t.SetRetry(t.conf.RetryTimes, t.conf.RetryInterval)
return nil
}
func (t *WebHookTask) Run() error {
func (t *WebHookTask) Go() error {
// 检查是否需要保存告警到数据库
var dbID uint
if t.conf.SaveAlarm && t.plugin.DB != nil {
// 默认 IsSent 为 false
t.alarm.IsSent = false
if err := t.plugin.DB.Create(&t.alarm).Error; err != nil {
t.plugin.Error("保存告警到数据库失败", "error", err)
} else {
dbID = t.alarm.ID
t.plugin.Info(""+
"", "id", dbID)
}
}
req, err := http.NewRequest(t.conf.Method, t.conf.URL, bytes.NewBuffer(t.jsonData))
if err != nil {
return err
@@ -440,41 +491,51 @@ func (t *WebHookTask) Run() error {
resp, err := client.Do(req)
if err != nil {
t.plugin.Error("webhook request failed", "error", err)
t.plugin.Error("webhook请求失败", "error", err)
return err
}
defer resp.Body.Close()
// 如果发送成功且已保存到数据库则更新IsSent字段为true
if resp.StatusCode >= 200 && resp.StatusCode < 300 && t.conf.SaveAlarm && t.plugin.DB != nil && dbID > 0 {
t.alarm.IsSent = true
if err := t.plugin.DB.Model(&AlarmInfo{}).Where("id = ?", dbID).Update("is_sent", true).Error; err != nil {
t.plugin.Error("更新告警发送状态失败", "error", err)
} else {
t.plugin.Info("告警发送状态已更新", "id", dbID, "is_sent", true)
}
return task.ErrTaskComplete
}
if resp.StatusCode >= 200 && resp.StatusCode < 300 {
return task.ErrTaskComplete
}
err = fmt.Errorf("webhook request failed with status: %d", resp.StatusCode)
t.plugin.Error("webhook response error", "status", resp.StatusCode)
err = fmt.Errorf("webhook请求失败,状态码:%d", resp.StatusCode)
t.plugin.Error("webhook响应错误", "状态码", resp.StatusCode)
return err
}
func (p *Plugin) SendWebhook(hookType config.HookType, conf config.Webhook, data any) *task.Task {
func (p *Plugin) SendWebhook(conf config.Webhook, data any) *task.Task {
webhookTask := &WebHookTask{
plugin: p,
hookType: hookType,
conf: &conf,
data: data,
plugin: p,
conf: conf,
data: data,
}
return p.AddTask(webhookTask)
return webHookQueueTask.AddTask(webhookTask)
}
// 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 +547,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 +592,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)
@@ -556,6 +617,7 @@ func (p *Plugin) OnSubscribe(streamPath string, args url.Values) {
// }
//}
}
func (p *Plugin) PublishWithConfig(ctx context.Context, streamPath string, conf config.Publish) (publisher *Publisher, err error) {
publisher = createPublisher(p, streamPath, conf)
if p.config.EnableAuth && publisher.Type == PublishTypeServer {
@@ -577,10 +639,25 @@ func (p *Plugin) PublishWithConfig(ctx context.Context, streamPath string, conf
}
err = p.Server.Streams.AddTask(publisher, ctx).WaitStarted()
if err == nil {
publisher.OnDispose(func() {
p.sendPublishEndWebhook(publisher)
})
p.sendPublishWebhook(publisher)
if sender, webhook := p.getHookSender(config.HookOnPublishEnd); sender != nil {
publisher.OnDispose(func() {
alarmInfo := AlarmInfo{
AlarmName: string(config.HookOnPublishEnd),
AlarmDesc: publisher.StopReason().Error(),
AlarmType: config.AlarmPublishOffline,
StreamPath: publisher.StreamPath,
}
sender(webhook, alarmInfo)
})
}
if sender, webhook := p.getHookSender(config.HookOnPublishStart); sender != nil {
alarmInfo := AlarmInfo{
AlarmName: string(config.HookOnPublishStart),
AlarmType: config.AlarmPublishRecover,
StreamPath: publisher.StreamPath,
}
sender(webhook, alarmInfo)
}
}
return
}
@@ -612,16 +689,33 @@ func (p *Plugin) SubscribeWithConfig(ctx context.Context, streamPath string, con
if err == nil {
select {
case <-subscriber.waitPublishDone:
err = subscriber.Publisher.WaitTrack()
waitAudio := conf.WaitTrack == "all" || strings.Contains(conf.WaitTrack, "audio")
waitVideo := conf.WaitTrack == "all" || strings.Contains(conf.WaitTrack, "video")
err = subscriber.Publisher.WaitTrack(waitAudio, waitVideo)
case <-subscriber.Done():
err = subscriber.StopReason()
}
}
if err == nil {
subscriber.OnDispose(func() {
p.sendSubscribeEndWebhook(subscriber)
})
p.sendSubscribeWebhook(subscriber)
if sender, webhook := p.getHookSender(config.HookOnSubscribeEnd); sender != nil {
subscriber.OnDispose(func() {
alarmInfo := AlarmInfo{
AlarmName: string(config.HookOnSubscribeEnd),
AlarmDesc: subscriber.StopReason().Error(),
AlarmType: config.AlarmSubscribeOffline,
StreamPath: subscriber.StreamPath,
}
sender(webhook, alarmInfo)
})
}
if sender, webhook := p.getHookSender(config.HookOnSubscribeStart); sender != nil {
alarmInfo := AlarmInfo{
AlarmName: string(config.HookOnSubscribeStart),
AlarmType: config.AlarmSubscribeRecover,
StreamPath: subscriber.StreamPath,
}
sender(webhook, alarmInfo)
}
}
return
}
@@ -631,7 +725,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 +733,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)
}
@@ -691,10 +785,11 @@ func (p *Plugin) registerHandler(handlers map[string]http.HandlerFunc) {
streamPath := r.PathValue("streamPath")
t := r.PathValue("type")
expire := r.URL.Query().Get("expire")
if t == "publish" {
switch t {
case "publish":
secret := md5.Sum([]byte(p.config.Publish.Key + streamPath + expire))
rw.Write([]byte(hex.EncodeToString(secret[:])))
} else if t == "subscribe" {
case "subscribe":
secret := md5.Sum([]byte(p.config.Subscribe.Key + streamPath + expire))
rw.Write([]byte(hex.EncodeToString(secret[:])))
}
@@ -732,121 +827,27 @@ 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
func (p *Plugin) getHookSender(hookType config.HookType) (sender func(webhook config.Webhook, data any) *task.Task, conf config.Webhook) {
if p.config.Hook != nil {
if _, ok := p.config.Hook[hookType]; ok {
sender = p.SendWebhook
conf = p.config.Hook[hookType]
} else if _, ok := p.config.Hook[config.HookDefault]; ok {
sender = p.SendWebhook
conf = p.config.Hook[config.HookDefault]
} else if p.Server.config.Hook != nil {
if _, ok := p.Server.config.Hook[hookType]; ok {
conf = p.config.Hook[hookType]
sender = p.Server.SendWebhook
} else if _, ok := p.Server.config.Hook[config.HookDefault]; ok {
sender = p.Server.SendWebhook
conf = p.config.Hook[config.HookDefault]
}
}
}
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
}
webhookData := map[string]interface{}{
"event": "publish",
"streamPath": pub.StreamPath,
"args": pub.Args,
"publishId": pub.ID,
"remoteAddr": pub.RemoteAddr,
"type": pub.Type,
"pluginName": p.Meta.Name,
"timestamp": time.Now().Unix(),
}
p.SendWebhook(config.HookOnPublish, p.config.Hook[config.HookOnPublish], webhookData)
if p.Server.config.Hook == nil {
return
}
p.Server.SendWebhook(config.HookOnPublish, p.Server.config.Hook[config.HookOnPublish], webhookData)
}
func (p *Plugin) sendPublishEndWebhook(pub *Publisher) {
if p.config.Hook == nil {
return
}
webhookData := map[string]interface{}{
"event": "publish_end",
"streamPath": pub.StreamPath,
"publishId": pub.ID,
"reason": pub.StopReason().Error(),
"timestamp": time.Now().Unix(),
}
p.SendWebhook(config.HookOnPublishEnd, p.config.Hook[config.HookOnPublishEnd], webhookData)
}
func (p *Plugin) sendSubscribeWebhook(sub *Subscriber) {
if p.config.Hook == nil {
return
}
webhookData := map[string]interface{}{
"event": "subscribe",
"streamPath": sub.StreamPath,
"publishId": sub.Publisher.ID,
"subscriberId": sub.ID,
"remoteAddr": sub.RemoteAddr,
"type": sub.Type,
"args": sub.Args,
"timestamp": time.Now().Unix(),
}
p.SendWebhook(config.HookOnSubscribe, p.config.Hook[config.HookOnSubscribe], webhookData)
}
func (p *Plugin) sendSubscribeEndWebhook(sub *Subscriber) {
if p.config.Hook == nil {
return
}
webhookData := map[string]interface{}{
"event": "subscribe_end",
"streamPath": sub.StreamPath,
"subscriberId": sub.ID,
"reason": sub.StopReason().Error(),
"timestamp": time.Now().Unix(),
}
if sub.Publisher != nil {
webhookData["publishId"] = sub.Publisher.ID
}
p.SendWebhook(config.HookOnSubscribeEnd, p.config.Hook[config.HookOnSubscribeEnd], webhookData)
}
func (p *Plugin) sendServerKeepAliveWebhook() {
if p.config.Hook == nil {
return
}
s := p.Server
webhookData := map[string]interface{}{
"event": "server_keep_alive",
"timestamp": time.Now().Unix(),
"streams": s.Streams.Length,
"subscribers": s.Subscribers.Length,
"publisherCount": s.Streams.Length,
"subscriberCount": s.Subscribers.Length,
"uptime": time.Since(s.StartTime).Seconds(),
}
p.SendWebhook(config.HookOnServerKeepAlive, p.config.Hook[config.HookOnServerKeepAlive], webhookData)
}
type ServerKeepAliveTask struct {
task.TickTask
plugin *Plugin
@@ -857,5 +858,25 @@ func (t *ServerKeepAliveTask) GetTickInterval() time.Duration {
}
func (t *ServerKeepAliveTask) Tick(now any) {
t.plugin.sendServerKeepAliveWebhook()
sender, webhook := t.plugin.getHookSender(config.HookOnServerKeepAlive)
if sender == nil {
return
}
//s := t.plugin.Server
alarmInfo := AlarmInfo{
AlarmName: string(config.HookOnServerKeepAlive),
AlarmType: config.AlarmKeepAliveOnline,
StreamPath: "",
}
sender(webhook, alarmInfo)
//webhookData := map[string]interface{}{
// "event": config.HookOnServerKeepAlive,
// "timestamp": time.Now().Unix(),
// "streams": s.Streams.Length,
// "subscribers": s.Subscribers.Length,
// "publisherCount": s.Streams.Length,
// "subscriberCount": s.Subscribers.Length,
// "uptime": time.Since(s.StartTime).Seconds(),
//}
//sender(webhook, webhookData)
}

View File

@@ -9,8 +9,8 @@
### Install gRPC
```shell
$ go install google.golang.org/protobuf/cmd/protoc-gen-go@v1.28
$ go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@v1.2
$ go install google.golang.org/protobuf/cmd/protoc-gen-go@latest
$ go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest
```
### Install gRPC-Gateway

View File

@@ -8,8 +8,8 @@
- Cursor
### 安装gRPC
```shell
$ go install google.golang.org/protobuf/cmd/protoc-gen-go@v1.28
$ go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@v1.2
$ go install google.golang.org/protobuf/cmd/protoc-gen-go@latest
$ go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest
```
### 安装gRPC-Gateway

990
plugin/crontab/api.go Normal file
View File

@@ -0,0 +1,990 @@
package plugin_crontab
import (
"context"
"fmt"
"sort"
"strings"
"time"
"google.golang.org/protobuf/types/known/timestamppb"
cronpb "m7s.live/v5/plugin/crontab/pb"
"m7s.live/v5/plugin/crontab/pkg"
)
func (ct *CrontabPlugin) List(ctx context.Context, req *cronpb.ReqPlanList) (*cronpb.PlanResponseList, error) {
if req.PageNum < 1 {
req.PageNum = 1
}
if req.PageSize < 1 {
req.PageSize = 10
}
// 从内存中获取所有计划
plans := ct.recordPlans.Items
total := len(plans)
// 计算分页
start := int(req.PageNum-1) * int(req.PageSize)
end := start + int(req.PageSize)
if start >= total {
start = total
}
if end > total {
end = total
}
// 获取当前页的数据
pagePlans := plans[start:end]
data := make([]*cronpb.Plan, 0, len(pagePlans))
for _, plan := range pagePlans {
data = append(data, &cronpb.Plan{
Id: uint32(plan.ID),
Name: plan.Name,
Enable: plan.Enable,
CreateTime: timestamppb.New(plan.CreatedAt),
UpdateTime: timestamppb.New(plan.UpdatedAt),
Plan: plan.Plan,
})
}
return &cronpb.PlanResponseList{
Code: 0,
Message: "success",
TotalCount: uint32(total),
PageNum: req.PageNum,
PageSize: req.PageSize,
Data: data,
}, nil
}
func (ct *CrontabPlugin) Add(ctx context.Context, req *cronpb.Plan) (*cronpb.Response, error) {
// 参数验证
if strings.TrimSpace(req.Name) == "" {
return &cronpb.Response{
Code: 400,
Message: "name is required",
}, nil
}
if strings.TrimSpace(req.Plan) == "" {
return &cronpb.Response{
Code: 400,
Message: "plan is required",
}, nil
}
// 检查名称是否已存在
var count int64
if err := ct.DB.Model(&pkg.RecordPlan{}).Where("name = ?", req.Name).Count(&count).Error; err != nil {
return &cronpb.Response{
Code: 500,
Message: err.Error(),
}, nil
}
if count > 0 {
return &cronpb.Response{
Code: 400,
Message: "name already exists",
}, nil
}
plan := &pkg.RecordPlan{
Name: req.Name,
Plan: req.Plan,
Enable: req.Enable,
}
if err := ct.DB.Create(plan).Error; err != nil {
return &cronpb.Response{
Code: 500,
Message: err.Error(),
}, nil
}
// 添加到内存中
ct.recordPlans.Add(plan)
return &cronpb.Response{
Code: 0,
Message: "success",
}, nil
}
func (ct *CrontabPlugin) Update(ctx context.Context, req *cronpb.Plan) (*cronpb.Response, error) {
if req.Id == 0 {
return &cronpb.Response{
Code: 400,
Message: "id is required",
}, nil
}
// 参数验证
if strings.TrimSpace(req.Name) == "" {
return &cronpb.Response{
Code: 400,
Message: "name is required",
}, nil
}
if strings.TrimSpace(req.Plan) == "" {
return &cronpb.Response{
Code: 400,
Message: "plan is required",
}, nil
}
// 检查记录是否存在
var existingPlan pkg.RecordPlan
if err := ct.DB.First(&existingPlan, req.Id).Error; err != nil {
return &cronpb.Response{
Code: 404,
Message: "record not found",
}, nil
}
// 检查新名称是否与其他记录冲突
var count int64
if err := ct.DB.Model(&pkg.RecordPlan{}).Where("name = ? AND id != ?", req.Name, req.Id).Count(&count).Error; err != nil {
return &cronpb.Response{
Code: 500,
Message: err.Error(),
}, nil
}
if count > 0 {
return &cronpb.Response{
Code: 400,
Message: "name already exists",
}, nil
}
// 处理 enable 状态变更
enableChanged := existingPlan.Enable != req.Enable
// 更新记录
updates := map[string]interface{}{
"name": req.Name,
"plan": req.Plan,
"enable": req.Enable,
}
if err := ct.DB.Model(&existingPlan).Updates(updates).Error; err != nil {
return &cronpb.Response{
Code: 500,
Message: err.Error(),
}, nil
}
// 更新内存中的记录
existingPlan.Name = req.Name
existingPlan.Plan = req.Plan
existingPlan.Enable = req.Enable
ct.recordPlans.Set(&existingPlan)
// 处理 enable 状态变更后的操作
if enableChanged {
if req.Enable {
// 从 false 变为 true需要创建并启动新的定时任务
var streams []pkg.RecordPlanStream
model := &pkg.RecordPlanStream{PlanID: existingPlan.ID}
if err := ct.DB.Model(model).Where(model).Find(&streams).Error; err != nil {
ct.Error("query record plan streams error: %v", err)
} else {
// 为每个流创建定时任务
for _, stream := range streams {
crontab := &Crontab{
ctp: ct,
RecordPlan: &existingPlan,
RecordPlanStream: &stream,
}
crontab.OnStart(func() {
ct.crontabs.Set(crontab)
})
ct.AddTask(crontab)
}
}
} else {
// 从 true 变为 false需要停止相关的定时任务
ct.crontabs.Range(func(crontab *Crontab) bool {
if crontab.RecordPlan.ID == existingPlan.ID {
crontab.Stop(nil)
}
return true
})
}
}
return &cronpb.Response{
Code: 0,
Message: "success",
}, nil
}
func (ct *CrontabPlugin) Remove(ctx context.Context, req *cronpb.DeleteRequest) (*cronpb.Response, error) {
if req.Id == 0 {
return &cronpb.Response{
Code: 400,
Message: "id is required",
}, nil
}
// 检查记录是否存在
var existingPlan pkg.RecordPlan
if err := ct.DB.First(&existingPlan, req.Id).Error; err != nil {
return &cronpb.Response{
Code: 404,
Message: "record not found",
}, nil
}
// 先停止所有相关的定时任务
ct.crontabs.Range(func(crontab *Crontab) bool {
if crontab.RecordPlan.ID == existingPlan.ID {
crontab.Stop(nil)
}
return true
})
// 执行软删除
if err := ct.DB.Delete(&existingPlan).Error; err != nil {
return &cronpb.Response{
Code: 500,
Message: err.Error(),
}, nil
}
// 从内存中移除
ct.recordPlans.RemoveByKey(existingPlan.ID)
return &cronpb.Response{
Code: 0,
Message: "success",
}, nil
}
func (ct *CrontabPlugin) ListRecordPlanStreams(ctx context.Context, req *cronpb.ReqPlanStreamList) (*cronpb.RecordPlanStreamResponseList, error) {
if req.PageNum < 1 {
req.PageNum = 1
}
if req.PageSize < 1 {
req.PageSize = 10
}
var total int64
var streams []pkg.RecordPlanStream
model := &pkg.RecordPlanStream{}
// 构建查询条件
query := ct.DB.Model(model).
Scopes(
pkg.ScopeRecordPlanID(uint(req.PlanId)),
pkg.ScopeStreamPathLike(req.StreamPath),
pkg.ScopeOrderByCreatedAtDesc(),
)
result := query.Count(&total)
if result.Error != nil {
return &cronpb.RecordPlanStreamResponseList{
Code: 500,
Message: result.Error.Error(),
}, nil
}
offset := (req.PageNum - 1) * req.PageSize
result = query.Offset(int(offset)).Limit(int(req.PageSize)).Find(&streams)
if result.Error != nil {
return &cronpb.RecordPlanStreamResponseList{
Code: 500,
Message: result.Error.Error(),
}, nil
}
data := make([]*cronpb.PlanStream, 0, len(streams))
for _, stream := range streams {
data = append(data, &cronpb.PlanStream{
PlanId: uint32(stream.PlanID),
StreamPath: stream.StreamPath,
Fragment: stream.Fragment,
FilePath: stream.FilePath,
CreatedAt: timestamppb.New(stream.CreatedAt),
UpdatedAt: timestamppb.New(stream.UpdatedAt),
Enable: stream.Enable,
})
}
return &cronpb.RecordPlanStreamResponseList{
Code: 0,
Message: "success",
TotalCount: uint32(total),
PageNum: req.PageNum,
PageSize: req.PageSize,
Data: data,
}, nil
}
func (ct *CrontabPlugin) AddRecordPlanStream(ctx context.Context, req *cronpb.PlanStream) (*cronpb.Response, error) {
if req.PlanId == 0 {
return &cronpb.Response{
Code: 400,
Message: "record_plan_id is required",
}, nil
}
if strings.TrimSpace(req.StreamPath) == "" {
return &cronpb.Response{
Code: 400,
Message: "stream_path is required",
}, nil
}
// 从内存中获取录制计划
plan, ok := ct.recordPlans.Get(uint(req.PlanId))
if !ok {
return &cronpb.Response{
Code: 404,
Message: "record plan not found",
}, nil
}
// 检查是否已存在相同的记录
var count int64
searchModel := pkg.RecordPlanStream{
PlanID: uint(req.PlanId),
StreamPath: req.StreamPath,
}
if err := ct.DB.Model(&searchModel).Where(&searchModel).Count(&count).Error; err != nil {
return &cronpb.Response{
Code: 500,
Message: err.Error(),
}, nil
}
if count > 0 {
return &cronpb.Response{
Code: 400,
Message: "record already exists",
}, nil
}
stream := &pkg.RecordPlanStream{
PlanID: uint(req.PlanId),
StreamPath: req.StreamPath,
Fragment: req.Fragment,
FilePath: req.FilePath,
Enable: req.Enable,
RecordType: req.RecordType,
}
if err := ct.DB.Create(stream).Error; err != nil {
return &cronpb.Response{
Code: 500,
Message: err.Error(),
}, nil
}
// 如果计划是启用状态,创建并启动定时任务
if plan.Enable {
crontab := &Crontab{
ctp: ct,
RecordPlan: plan,
RecordPlanStream: stream,
}
crontab.OnStart(func() {
ct.crontabs.Set(crontab)
})
ct.AddTask(crontab)
}
return &cronpb.Response{
Code: 0,
Message: "success",
}, nil
}
func (ct *CrontabPlugin) UpdateRecordPlanStream(ctx context.Context, req *cronpb.PlanStream) (*cronpb.Response, error) {
if req.PlanId == 0 {
return &cronpb.Response{
Code: 400,
Message: "record_plan_id is required",
}, nil
}
if strings.TrimSpace(req.StreamPath) == "" {
return &cronpb.Response{
Code: 400,
Message: "stream_path is required",
}, nil
}
// 检查记录是否存在
var existingStream pkg.RecordPlanStream
searchModel := pkg.RecordPlanStream{
PlanID: uint(req.PlanId),
StreamPath: req.StreamPath,
}
if err := ct.DB.Where(&searchModel).First(&existingStream).Error; err != nil {
return &cronpb.Response{
Code: 404,
Message: "record not found",
}, nil
}
// 更新记录
existingStream.Fragment = req.Fragment
existingStream.FilePath = req.FilePath
existingStream.Enable = req.Enable
existingStream.RecordType = req.RecordType
if err := ct.DB.Save(&existingStream).Error; err != nil {
return &cronpb.Response{
Code: 500,
Message: err.Error(),
}, nil
}
// 停止当前流相关的所有任务
ct.crontabs.Range(func(crontab *Crontab) bool {
if crontab.RecordPlanStream.StreamPath == req.StreamPath {
crontab.Stop(nil)
}
return true
})
// 查询所有关联此流的记录
var streams []pkg.RecordPlanStream
if err := ct.DB.Where("stream_path = ?", req.StreamPath).Find(&streams).Error; err != nil {
ct.Error("query record plan streams error: %v", err)
return &cronpb.Response{
Code: 500,
Message: err.Error(),
}, nil
}
// 为每个启用的计划创建新的定时任务
for _, stream := range streams {
// 从内存中获取对应的计划
plan, ok := ct.recordPlans.Get(stream.PlanID)
if !ok {
ct.Error("record plan not found in memory: %d", stream.PlanID)
continue
}
// 如果计划是启用状态,创建并启动定时任务
if plan.Enable && stream.Enable {
crontab := &Crontab{
ctp: ct,
RecordPlan: plan,
RecordPlanStream: &stream,
}
crontab.OnStart(func() {
ct.crontabs.Set(crontab)
})
ct.AddTask(crontab)
}
}
return &cronpb.Response{
Code: 0,
Message: "success",
}, nil
}
func (ct *CrontabPlugin) RemoveRecordPlanStream(ctx context.Context, req *cronpb.DeletePlanStreamRequest) (*cronpb.Response, error) {
if req.PlanId == 0 {
return &cronpb.Response{
Code: 400,
Message: "record_plan_id is required",
}, nil
}
if strings.TrimSpace(req.StreamPath) == "" {
return &cronpb.Response{
Code: 400,
Message: "stream_path is required",
}, nil
}
// 检查记录是否存在
var existingStream pkg.RecordPlanStream
searchModel := pkg.RecordPlanStream{
PlanID: uint(req.PlanId),
StreamPath: req.StreamPath,
}
if err := ct.DB.Where(&searchModel).First(&existingStream).Error; err != nil {
return &cronpb.Response{
Code: 404,
Message: "record not found",
}, nil
}
// 停止所有相关的定时任务
ct.crontabs.Range(func(crontab *Crontab) bool {
if crontab.RecordPlanStream.StreamPath == req.StreamPath && crontab.RecordPlan.ID == uint(req.PlanId) {
crontab.Stop(nil)
}
return true
})
// 执行删除
if err := ct.DB.Delete(&existingStream).Error; err != nil {
return &cronpb.Response{
Code: 500,
Message: err.Error(),
}, nil
}
return &cronpb.Response{
Code: 0,
Message: "success",
}, nil
}
// 获取周几的名称0=周日1=周一,...6=周六)
func getWeekdayName(weekday int) string {
weekdays := []string{"周日", "周一", "周二", "周三", "周四", "周五", "周六"}
return weekdays[weekday]
}
// 获取周几的索引0=周日1=周一,...6=周六)
func getWeekdayIndex(weekdayName string) int {
weekdays := map[string]int{
"周日": 0, "周一": 1, "周二": 2, "周三": 3, "周四": 4, "周五": 5, "周六": 6,
}
return weekdays[weekdayName]
}
// 获取下一个指定周几的日期
func getNextDateForWeekday(now time.Time, targetWeekday int, location *time.Location) time.Time {
nowWeekday := int(now.Weekday())
daysToAdd := 0
if targetWeekday >= nowWeekday {
daysToAdd = targetWeekday - nowWeekday
} else {
daysToAdd = 7 - (nowWeekday - targetWeekday)
}
// 如果是同一天但当前时间已经过了最后的时间段,则推到下一周
if daysToAdd == 0 {
// 这里简化处理直接加7天到下周同一天
daysToAdd = 7
}
return now.AddDate(0, 0, daysToAdd)
}
// 计算计划中的所有时间段
func calculateTimeSlots(plan string, now time.Time, location *time.Location) ([]*cronpb.TimeSlotInfo, error) {
if len(plan) != 168 {
return nil, fmt.Errorf("invalid plan format: length should be 168")
}
var slots []*cronpb.TimeSlotInfo
// 按周几遍历0=周日1=周一,...6=周六)
for weekday := 0; weekday < 7; weekday++ {
dayOffset := weekday * 24
var startHour int = -1
// 遍历这一天的每个小时
for hour := 0; hour <= 24; hour++ {
// 如果到了一天的结尾或者当前小时状态为0
isEndOfDay := hour == 24
isHourOff := !isEndOfDay && plan[dayOffset+hour] == '0'
if isEndOfDay || isHourOff {
// 如果之前有开始的时间段,现在结束了
if startHour != -1 {
// 计算下一个该周几的日期
targetDate := getNextDateForWeekday(now, weekday, location)
// 创建时间段
startTime := time.Date(targetDate.Year(), targetDate.Month(), targetDate.Day(), startHour, 0, 0, 0, location)
endTime := time.Date(targetDate.Year(), targetDate.Month(), targetDate.Day(), hour, 0, 0, 0, location)
// 转换为 UTC 时间
startTs := timestamppb.New(startTime.UTC())
endTs := timestamppb.New(endTime.UTC())
slots = append(slots, &cronpb.TimeSlotInfo{
Start: startTs,
End: endTs,
Weekday: getWeekdayName(weekday),
TimeRange: fmt.Sprintf("%02d:00-%02d:00", startHour, hour),
})
startHour = -1
}
} else if plan[dayOffset+hour] == '1' && startHour == -1 {
// 找到新的开始时间
startHour = hour
}
}
}
// 按时间排序
sort.Slice(slots, func(i, j int) bool {
// 先按周几排序
weekdayI := getWeekdayIndex(slots[i].Weekday)
weekdayJ := getWeekdayIndex(slots[j].Weekday)
if weekdayI != weekdayJ {
return weekdayI < weekdayJ
}
// 同一天按开始时间排序
return slots[i].Start.AsTime().Hour() < slots[j].Start.AsTime().Hour()
})
return slots, nil
}
// 获取下一个时间段
func getNextTimeSlotFromNow(plan string, now time.Time, location *time.Location) (*cronpb.TimeSlotInfo, error) {
if len(plan) != 168 {
return nil, fmt.Errorf("invalid plan format: length should be 168")
}
// 将当前时间转换为本地时间
localNow := now.In(location)
currentWeekday := int(localNow.Weekday())
currentHour := localNow.Hour()
// 检查是否在整点边界附近(前后30秒)
isNearHourBoundary := localNow.Minute() == 59 && localNow.Second() >= 30 || localNow.Minute() == 0 && localNow.Second() <= 30
// 首先检查当前时间是否在某个时间段内
dayOffset := currentWeekday * 24
if currentHour < 24 && plan[dayOffset+currentHour] == '1' {
// 找到当前小时所在的完整时间段
startHour := currentHour
// 向前查找时间段的开始
for h := currentHour - 1; h >= 0; h-- {
if plan[dayOffset+h] == '1' {
startHour = h
} else {
break
}
}
// 向后查找时间段的结束
endHour := currentHour + 1
for h := endHour; h < 24; h++ {
if plan[dayOffset+h] == '1' {
endHour = h + 1
} else {
break
}
}
// 检查是否已经接近当前时间段的结束
isNearEndOfTimeSlot := currentHour == endHour-1 && localNow.Minute() >= 59 && localNow.Second() >= 30
// 如果我们靠近时间段结束且在小时边界附近,我们跳过此时间段,找下一个
if isNearEndOfTimeSlot && isNearHourBoundary {
// 继续查找下一个时间段
} else {
// 创建时间段
startTime := time.Date(localNow.Year(), localNow.Month(), localNow.Day(), startHour, 0, 0, 0, location)
endTime := time.Date(localNow.Year(), localNow.Month(), localNow.Day(), endHour, 0, 0, 0, location)
// 如果当前时间已经接近或超过了结束时间,调整结束时间
if localNow.After(endTime.Add(-30*time.Second)) || localNow.Equal(endTime) {
// 继续查找下一个时间段
} else {
// 返回当前时间段
return &cronpb.TimeSlotInfo{
Start: timestamppb.New(startTime.UTC()),
End: timestamppb.New(endTime.UTC()),
Weekday: getWeekdayName(currentWeekday),
TimeRange: fmt.Sprintf("%02d:00-%02d:00", startHour, endHour),
}, nil
}
}
}
// 查找下一个时间段
// 先查找当天剩余时间
for h := currentHour + 1; h < 24; h++ {
if plan[dayOffset+h] == '1' {
// 找到开始小时
startHour := h
// 查找结束小时
endHour := h + 1
for j := h + 1; j < 24; j++ {
if plan[dayOffset+j] == '1' {
endHour = j + 1
} else {
break
}
}
// 创建时间段
startTime := time.Date(localNow.Year(), localNow.Month(), localNow.Day(), startHour, 0, 0, 0, location)
endTime := time.Date(localNow.Year(), localNow.Month(), localNow.Day(), endHour, 0, 0, 0, location)
return &cronpb.TimeSlotInfo{
Start: timestamppb.New(startTime.UTC()),
End: timestamppb.New(endTime.UTC()),
Weekday: getWeekdayName(currentWeekday),
TimeRange: fmt.Sprintf("%02d:00-%02d:00", startHour, endHour),
}, nil
}
}
// 如果当天没有找到,则查找后续日期
for d := 1; d <= 7; d++ {
nextDay := (currentWeekday + d) % 7
dayOffset := nextDay * 24
for h := 0; h < 24; h++ {
if plan[dayOffset+h] == '1' {
// 找到开始小时
startHour := h
// 查找结束小时
endHour := h + 1
for j := h + 1; j < 24; j++ {
if plan[dayOffset+j] == '1' {
endHour = j + 1
} else {
break
}
}
// 计算日期
nextDate := localNow.AddDate(0, 0, d)
// 创建时间段
startTime := time.Date(nextDate.Year(), nextDate.Month(), nextDate.Day(), startHour, 0, 0, 0, location)
endTime := time.Date(nextDate.Year(), nextDate.Month(), nextDate.Day(), endHour, 0, 0, 0, location)
return &cronpb.TimeSlotInfo{
Start: timestamppb.New(startTime.UTC()),
End: timestamppb.New(endTime.UTC()),
Weekday: getWeekdayName(nextDay),
TimeRange: fmt.Sprintf("%02d:00-%02d:00", startHour, endHour),
}, nil
}
}
}
return nil, nil
}
func (ct *CrontabPlugin) ParsePlanTime(ctx context.Context, req *cronpb.ParsePlanRequest) (*cronpb.ParsePlanResponse, error) {
if len(req.Plan) != 168 {
return &cronpb.ParsePlanResponse{
Code: 400,
Message: "invalid plan format: length should be 168",
}, nil
}
// 检查字符串格式是否正确只包含0和1
for i, c := range req.Plan {
if c != '0' && c != '1' {
return &cronpb.ParsePlanResponse{
Code: 400,
Message: fmt.Sprintf("invalid character at position %d: %c (should be 0 or 1)", i, c),
}, nil
}
}
// 获取所有时间段
slots, err := calculateTimeSlots(req.Plan, time.Now(), time.Local)
if err != nil {
return &cronpb.ParsePlanResponse{
Code: 500,
Message: err.Error(),
}, nil
}
// 获取下一个时间段
nextSlot, err := getNextTimeSlotFromNow(req.Plan, time.Now(), time.Local)
if err != nil {
return &cronpb.ParsePlanResponse{
Code: 500,
Message: err.Error(),
}, nil
}
return &cronpb.ParsePlanResponse{
Code: 0,
Message: "success",
Slots: slots,
NextSlot: nextSlot,
}, nil
}
// 辅助函数:构建任务状态信息
func buildCrontabTaskInfo(crontab *Crontab, now time.Time) *cronpb.CrontabTaskInfo {
// 基础任务信息
taskInfo := &cronpb.CrontabTaskInfo{
PlanId: uint32(crontab.RecordPlan.ID),
PlanName: crontab.RecordPlan.Name,
StreamPath: crontab.StreamPath,
FilePath: crontab.FilePath,
Fragment: crontab.Fragment,
}
// 获取完整计划时间段列表
if crontab.RecordPlan != nil && crontab.RecordPlan.Plan != "" {
planSlots, err := calculateTimeSlots(crontab.RecordPlan.Plan, now, time.Local)
if err == nil && planSlots != nil && len(planSlots) > 0 {
taskInfo.PlanSlots = planSlots
}
}
return taskInfo
}
// GetCrontabStatus 获取当前Crontab任务状态
func (ct *CrontabPlugin) GetCrontabStatus(ctx context.Context, req *cronpb.CrontabStatusRequest) (*cronpb.CrontabStatusResponse, error) {
response := &cronpb.CrontabStatusResponse{
Code: 0,
Message: "success",
RunningTasks: []*cronpb.CrontabTaskInfo{},
NextTasks: []*cronpb.CrontabTaskInfo{},
TotalRunning: 0,
TotalPlanned: 0,
}
// 获取当前正在运行的任务
runningTasks := make([]*cronpb.CrontabTaskInfo, 0)
nextTasks := make([]*cronpb.CrontabTaskInfo, 0)
// 如果只指定了流路径但未找到对应的任务,也返回该流的计划信息
streamPathFound := false
// 遍历所有Crontab任务
ct.crontabs.Range(func(crontab *Crontab) bool {
// 如果指定了stream_path过滤条件且不匹配则跳过
if req.StreamPath != "" && crontab.StreamPath != req.StreamPath {
return true // 继续遍历
}
// 标记已找到指定的流
if req.StreamPath != "" {
streamPathFound = true
}
now := time.Now()
// 构建基本任务信息
taskInfo := buildCrontabTaskInfo(crontab, now)
// 检查是否正在录制
if crontab.recording && crontab.currentSlot != nil {
// 当前正在录制
taskInfo.IsRecording = true
// 设置时间信息
taskInfo.StartTime = timestamppb.New(crontab.currentSlot.Start)
taskInfo.EndTime = timestamppb.New(crontab.currentSlot.End)
// 计算已运行时间和剩余时间
elapsedDuration := now.Sub(crontab.currentSlot.Start)
remainingDuration := crontab.currentSlot.End.Sub(now)
taskInfo.ElapsedSeconds = uint32(elapsedDuration.Seconds())
taskInfo.RemainingSeconds = uint32(remainingDuration.Seconds())
// 设置时间范围和周几
startHour := crontab.currentSlot.Start.Hour()
endHour := crontab.currentSlot.End.Hour()
taskInfo.TimeRange = fmt.Sprintf("%02d:00-%02d:00", startHour, endHour)
taskInfo.Weekday = getWeekdayName(int(crontab.currentSlot.Start.Weekday()))
// 添加到正在运行的任务列表
runningTasks = append(runningTasks, taskInfo)
} else {
// 获取下一个时间段
nextSlot := crontab.getNextTimeSlot()
if nextSlot != nil {
// 设置下一个任务的信息
taskInfo.IsRecording = false
// 设置时间信息
taskInfo.StartTime = timestamppb.New(nextSlot.Start)
taskInfo.EndTime = timestamppb.New(nextSlot.End)
// 计算等待时间
waitingDuration := nextSlot.Start.Sub(now)
taskInfo.RemainingSeconds = uint32(waitingDuration.Seconds())
// 设置时间范围和周几
startHour := nextSlot.Start.Hour()
endHour := nextSlot.End.Hour()
taskInfo.TimeRange = fmt.Sprintf("%02d:00-%02d:00", startHour, endHour)
taskInfo.Weekday = getWeekdayName(int(nextSlot.Start.Weekday()))
// 添加到计划任务列表
nextTasks = append(nextTasks, taskInfo)
}
}
return true // 继续遍历
})
// 如果指定了流路径但未找到对应的任务,查询数据库获取该流的计划信息
if req.StreamPath != "" && !streamPathFound {
// 查询与该流相关的所有计划
var streams []pkg.RecordPlanStream
if err := ct.DB.Where("stream_path = ?", req.StreamPath).Find(&streams).Error; err == nil && len(streams) > 0 {
for _, stream := range streams {
// 获取对应的计划
var plan pkg.RecordPlan
if err := ct.DB.First(&plan, stream.PlanID).Error; err == nil && plan.Enable && stream.Enable {
now := time.Now()
// 构建任务信息
taskInfo := &cronpb.CrontabTaskInfo{
PlanId: uint32(plan.ID),
PlanName: plan.Name,
StreamPath: stream.StreamPath,
FilePath: stream.FilePath,
Fragment: stream.Fragment,
IsRecording: false,
}
// 获取完整计划时间段列表
planSlots, err := calculateTimeSlots(plan.Plan, now, time.Local)
if err == nil && planSlots != nil && len(planSlots) > 0 {
taskInfo.PlanSlots = planSlots
}
// 获取下一个时间段
nextSlot, err := getNextTimeSlotFromNow(plan.Plan, now, time.Local)
if err == nil && nextSlot != nil {
// 设置时间信息
taskInfo.StartTime = nextSlot.Start
taskInfo.EndTime = nextSlot.End
taskInfo.TimeRange = nextSlot.TimeRange
taskInfo.Weekday = nextSlot.Weekday
// 计算等待时间
waitingDuration := nextSlot.Start.AsTime().Sub(now)
taskInfo.RemainingSeconds = uint32(waitingDuration.Seconds())
// 添加到计划任务列表
nextTasks = append(nextTasks, taskInfo)
}
}
}
}
}
// 按开始时间排序下一个任务列表
sort.Slice(nextTasks, func(i, j int) bool {
return nextTasks[i].StartTime.AsTime().Before(nextTasks[j].StartTime.AsTime())
})
// 设置响应结果
response.RunningTasks = runningTasks
response.NextTasks = nextTasks
response.TotalRunning = uint32(len(runningTasks))
response.TotalPlanned = uint32(len(nextTasks))
return response, nil
}

244
plugin/crontab/api_test.go Normal file
View File

@@ -0,0 +1,244 @@
package plugin_crontab
import (
"testing"
"time"
"github.com/stretchr/testify/assert"
)
func TestCalculateTimeSlots(t *testing.T) {
// 测试案例:周五的凌晨和上午有开启时间段
// 字符串中1的索引是120(0点),122(2点),123(3点),125(5点),130(10点),135(15点)
// 000000000000000000000000 - 周日(0-23小时) - 全0
// 000000000000000000000000 - 周一(24-47小时) - 全0
// 000000000000000000000000 - 周二(48-71小时) - 全0
// 000000000000000000000000 - 周三(72-95小时) - 全0
// 000000000000000000000000 - 周四(96-119小时) - 全0
// 101101000010000100000000 - 周五(120-143小时) - 0,2,3,5,10,15点开启
// 000000000000000000000000 - 周六(144-167小时) - 全0
planStr := "000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000101101000010000100000000000000000000000000000000"
now := time.Date(2023, 5, 1, 12, 0, 0, 0, time.Local) // 周一中午
slots, err := calculateTimeSlots(planStr, now, time.Local)
assert.NoError(t, err)
assert.Equal(t, 5, len(slots), "应该有5个时间段")
// 检查结果中的时间段(按实际解析结果排序)
assert.Equal(t, "周五", slots[0].Weekday)
assert.Equal(t, "10:00-11:00", slots[0].TimeRange)
assert.Equal(t, "周五", slots[1].Weekday)
assert.Equal(t, "15:00-16:00", slots[1].TimeRange)
assert.Equal(t, "周五", slots[2].Weekday)
assert.Equal(t, "00:00-01:00", slots[2].TimeRange)
assert.Equal(t, "周五", slots[3].Weekday)
assert.Equal(t, "02:00-04:00", slots[3].TimeRange)
assert.Equal(t, "周五", slots[4].Weekday)
assert.Equal(t, "05:00-06:00", slots[4].TimeRange)
// 打印出所有时间段,便于调试
for i, slot := range slots {
t.Logf("时间段 %d: %s %s", i, slot.Weekday, slot.TimeRange)
}
}
func TestGetNextTimeSlotFromNow(t *testing.T) {
// 测试案例:周五的凌晨和上午有开启时间段
planStr := "000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000101101000010000100000000000000000000000000000000"
// 测试1: 当前是周一下一个时间段应该是周五凌晨0点
now1 := time.Date(2023, 5, 1, 12, 0, 0, 0, time.Local) // 周一中午
nextSlot1, err := getNextTimeSlotFromNow(planStr, now1, time.Local)
assert.NoError(t, err)
assert.NotNil(t, nextSlot1)
assert.Equal(t, "周五", nextSlot1.Weekday)
assert.Equal(t, "00:00-01:00", nextSlot1.TimeRange)
// 测试2: 当前是周五凌晨1点下一个时间段应该是周五凌晨2点
now2 := time.Date(2023, 5, 5, 1, 30, 0, 0, time.Local) // 周五凌晨1:30
nextSlot2, err := getNextTimeSlotFromNow(planStr, now2, time.Local)
assert.NoError(t, err)
assert.NotNil(t, nextSlot2)
assert.Equal(t, "周五", nextSlot2.Weekday)
assert.Equal(t, "02:00-04:00", nextSlot2.TimeRange)
// 测试3: 当前是周五凌晨3点此时正在一个时间段内
now3 := time.Date(2023, 5, 5, 3, 0, 0, 0, time.Local) // 周五凌晨3:00
nextSlot3, err := getNextTimeSlotFromNow(planStr, now3, time.Local)
assert.NoError(t, err)
assert.NotNil(t, nextSlot3)
assert.Equal(t, "周五", nextSlot3.Weekday)
assert.Equal(t, "02:00-04:00", nextSlot3.TimeRange)
}
func TestParsePlanFromString(t *testing.T) {
// 测试用户提供的案例字符串的第36-41位表示周一的时间段
// 这个案例中对应周一的12点、14-15点、17点和22点开启
planStr := "000000000000000000000000000000000000101101000010000100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"
now := time.Now()
slots, err := calculateTimeSlots(planStr, now, time.Local)
assert.NoError(t, err)
// 验证解析结果
var foundMondaySlots bool
for _, slot := range slots {
if slot.Weekday == "周一" {
foundMondaySlots = true
t.Logf("找到周一时间段: %s", slot.TimeRange)
}
}
assert.True(t, foundMondaySlots, "应该找到周一的时间段")
// 预期的周一时间段
var mondaySlots []string
for _, slot := range slots {
if slot.Weekday == "周一" {
mondaySlots = append(mondaySlots, slot.TimeRange)
}
}
// 检查是否包含预期的时间段
expectedSlots := []string{
"12:00-13:00",
"14:00-16:00",
"17:00-18:00",
"22:00-23:00",
}
for _, expected := range expectedSlots {
found := false
for _, actual := range mondaySlots {
if expected == actual {
found = true
break
}
}
assert.True(t, found, "应该找到周一时间段:"+expected)
}
// 获取下一个时间段
nextSlot, err := getNextTimeSlotFromNow(planStr, now, time.Local)
assert.NoError(t, err)
if nextSlot != nil {
t.Logf("下一个时间段: %s %s", nextSlot.Weekday, nextSlot.TimeRange)
} else {
t.Log("没有找到下一个时间段")
}
}
// 手动计算字符串长度的辅助函数
func TestCountStringLength(t *testing.T) {
str1 := "000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000101101000010000100000000000000000000000000000000"
assert.Equal(t, 168, len(str1), "第一个测试字符串长度应为168")
str2 := "000000000000000000000000000000000000101101000010000100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"
assert.Equal(t, 168, len(str2), "第二个测试字符串长度应为168")
}
// 测试用户提供的具体字符串
func TestUserProvidedPlanString(t *testing.T) {
// 用户提供的测试字符串
planStr := "000000000000000000000000000000000000101101000010000100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"
// 验证字符串长度
assert.Equal(t, 168, len(planStr), "字符串长度应为168")
// 解析时间段
now := time.Now()
slots, err := calculateTimeSlots(planStr, now, time.Local)
assert.NoError(t, err)
// 打印所有时间段
t.Log("所有时间段:")
for i, slot := range slots {
t.Logf("%d: %s %s", i, slot.Weekday, slot.TimeRange)
}
// 获取下一个时间段
nextSlot, err := getNextTimeSlotFromNow(planStr, now, time.Local)
assert.NoError(t, err)
if nextSlot != nil {
t.Logf("下一个执行时间段: %s %s", nextSlot.Weekday, nextSlot.TimeRange)
t.Logf("开始时间: %s", nextSlot.Start.AsTime().In(time.Local).Format("2006-01-02 15:04:05"))
t.Logf("结束时间: %s", nextSlot.End.AsTime().In(time.Local).Format("2006-01-02 15:04:05"))
} else {
t.Log("没有找到下一个时间段")
}
// 验证周一的时间段
var mondaySlots []string
for _, slot := range slots {
if slot.Weekday == "周一" {
mondaySlots = append(mondaySlots, slot.TimeRange)
}
}
// 预期周一应该有这些时间段
expectedMondaySlots := []string{
"12:00-13:00",
"14:00-16:00",
"17:00-18:00",
"22:00-23:00",
}
assert.Equal(t, len(expectedMondaySlots), len(mondaySlots), "周一时间段数量不匹配")
for i, expected := range expectedMondaySlots {
if i < len(mondaySlots) {
t.Logf("期望周一时间段 %s, 实际是 %s", expected, mondaySlots[i])
}
}
}
// 测试用户提供的第二个字符串
func TestUserProvidedPlanString2(t *testing.T) {
// 用户提供的第二个测试字符串
planStr := "000000000000000000000000000000000000000000000000000000000000001011010100001000000000000000000000000100000000000000000000000010000000000000000000000001000000000000000000"
// 验证字符串长度
assert.Equal(t, 168, len(planStr), "字符串长度应为168")
// 解析时间段
now := time.Now()
slots, err := calculateTimeSlots(planStr, now, time.Local)
assert.NoError(t, err)
// 打印所有时间段并按周几分组
weekdaySlots := make(map[string][]string)
for _, slot := range slots {
weekdaySlots[slot.Weekday] = append(weekdaySlots[slot.Weekday], slot.TimeRange)
}
t.Log("所有时间段(按周几分组):")
weekdays := []string{"周日", "周一", "周二", "周三", "周四", "周五", "周六"}
for _, weekday := range weekdays {
if timeRanges, ok := weekdaySlots[weekday]; ok {
t.Logf("%s: %v", weekday, timeRanges)
}
}
// 打印所有时间段的详细信息
t.Log("\n所有时间段详细信息:")
for i, slot := range slots {
t.Logf("%d: %s %s", i, slot.Weekday, slot.TimeRange)
}
// 获取下一个时间段
nextSlot, err := getNextTimeSlotFromNow(planStr, now, time.Local)
assert.NoError(t, err)
if nextSlot != nil {
t.Logf("\n下一个执行时间段: %s %s", nextSlot.Weekday, nextSlot.TimeRange)
t.Logf("开始时间: %s", nextSlot.Start.AsTime().In(time.Local).Format("2006-01-02 15:04:05"))
t.Logf("结束时间: %s", nextSlot.End.AsTime().In(time.Local).Format("2006-01-02 15:04:05"))
} else {
t.Log("没有找到下一个时间段")
}
}

422
plugin/crontab/crontab.go Normal file
View File

@@ -0,0 +1,422 @@
package plugin_crontab
import (
"bytes"
"encoding/json"
"fmt"
"net/http"
"strconv"
"time"
"m7s.live/v5/pkg/task"
"m7s.live/v5/plugin/crontab/pkg"
)
// 计划时间段
type TimeSlot struct {
Start time.Time // 开始时间
End time.Time // 结束时间
}
// Crontab 定时任务调度器
type Crontab struct {
task.Job
ctp *CrontabPlugin
*pkg.RecordPlan
*pkg.RecordPlanStream
stop chan struct{}
running bool
location *time.Location
timer *time.Timer
currentSlot *TimeSlot // 当前执行的时间段
recording bool // 是否正在录制
}
func (cron *Crontab) GetKey() string {
return strconv.Itoa(int(cron.PlanID)) + "_" + cron.StreamPath
}
// 初始化
func (cron *Crontab) Start() (err error) {
cron.Info("crontab plugin start")
if cron.running {
return // 已经运行中,不重复启动
}
// 初始化必要字段
if cron.stop == nil {
cron.stop = make(chan struct{})
}
if cron.location == nil {
cron.location = time.Local
}
cron.running = true
return nil
}
// 阻塞运行
func (cron *Crontab) Run() (err error) {
cron.Info("crontab plugin is running")
// 初始化必要字段
if cron.stop == nil {
cron.stop = make(chan struct{})
}
if cron.location == nil {
cron.location = time.Local
}
cron.Info("调度器启动")
for {
// 获取当前时间
now := time.Now().In(cron.location)
// 首先检查是否需要立即执行操作(如停止录制)
if cron.recording && cron.currentSlot != nil &&
(now.Equal(cron.currentSlot.End) || now.After(cron.currentSlot.End)) {
cron.stopRecording()
continue
}
// 确定下一个事件
var nextEvent time.Time
var isStartEvent bool
if cron.recording {
// 如果正在录制,下一个事件是结束时间
nextEvent = cron.currentSlot.End
isStartEvent = false
} else {
// 如果没有录制,计算下一个开始时间
nextSlot := cron.getNextTimeSlot()
if nextSlot == nil {
// 无法确定下次执行时间,使用默认间隔
cron.timer = time.NewTimer(1 * time.Hour)
cron.Info("无有效计划等待1小时后重试")
// 等待定时器或停止信号
select {
case <-cron.timer.C:
continue // 继续循环
case <-cron.stop:
// 停止调度器
if cron.timer != nil {
cron.timer.Stop()
}
cron.Info("调度器停止")
return
}
}
cron.currentSlot = nextSlot
nextEvent = nextSlot.Start
isStartEvent = true
// 如果已过开始时间,立即开始录制
if now.Equal(nextEvent) || now.After(nextEvent) {
cron.startRecording()
continue
}
}
// 计算等待时间
waitDuration := nextEvent.Sub(now)
// 如果等待时间为负,立即执行
if waitDuration <= 0 {
if isStartEvent {
cron.startRecording()
} else {
cron.stopRecording()
}
continue
}
// 设置定时器
timer := time.NewTimer(waitDuration)
if isStartEvent {
cron.Info("下次开始时间: ", nextEvent, "等待时间:", waitDuration)
} else {
cron.Info("下次结束时间: ", nextEvent, " 等待时间:", waitDuration)
}
// 等待定时器或停止信号
select {
case now = <-timer.C:
// 更新当前时间为定时器触发时间
now = now.In(cron.location)
// 执行任务
if isStartEvent {
cron.startRecording()
} else {
cron.stopRecording()
}
case <-cron.stop:
// 停止调度器
timer.Stop()
cron.Info("调度器停止")
return
}
}
}
// 停止
func (cron *Crontab) Dispose() (err error) {
if cron.running {
cron.stop <- struct{}{}
cron.running = false
if cron.timer != nil {
cron.timer.Stop()
}
// 如果还在录制,停止录制
if cron.recording {
cron.stopRecording()
}
}
return
}
// 获取下一个时间段
func (cron *Crontab) getNextTimeSlot() *TimeSlot {
if cron.RecordPlan == nil || !cron.RecordPlan.Enable || cron.RecordPlan.Plan == "" {
return nil // 无有效计划
}
plan := cron.RecordPlan.Plan
if len(plan) != 168 {
cron.Error("无效的计划格式: %s, 长度应为168", plan)
return nil
}
// 使用当地时间
now := time.Now().In(cron.location)
cron.Debug("当前本地时间: %v, 星期%d, 小时%d", now.Format("2006-01-02 15:04:05"), now.Weekday(), now.Hour())
// 当前小时
currentWeekday := int(now.Weekday())
currentHour := now.Hour()
// 检查是否在整点边界附近(前后30秒)
isNearHourBoundary := now.Minute() == 59 && now.Second() >= 30 || now.Minute() == 0 && now.Second() <= 30
// 首先检查当前时间是否在某个时间段内
dayOffset := currentWeekday * 24
if currentHour < 24 && plan[dayOffset+currentHour] == '1' {
// 找到当前小时所在的完整时间段
startHour := currentHour
// 向前查找时间段的开始
for h := currentHour - 1; h >= 0; h-- {
if plan[dayOffset+h] == '1' {
startHour = h
} else {
break
}
}
// 向后查找时间段的结束
endHour := currentHour + 1
for h := endHour; h < 24; h++ {
if plan[dayOffset+h] == '1' {
endHour = h + 1
} else {
break
}
}
// 检查我们是否已经接近当前时间段的结束
isNearEndOfTimeSlot := currentHour == endHour-1 && now.Minute() == 59 && now.Second() >= 30
// 如果我们靠近时间段结束且在小时边界附近,我们跳过此时间段,找下一个
if isNearEndOfTimeSlot && isNearHourBoundary {
cron.Debug("接近当前时间段结束,准备查找下一个时间段")
} else {
// 创建时间段
startTime := time.Date(now.Year(), now.Month(), now.Day(), startHour, 0, 0, 0, cron.location)
endTime := time.Date(now.Year(), now.Month(), now.Day(), endHour, 0, 0, 0, cron.location)
// 如果当前时间已经接近或超过了结束时间,调整结束时间
if now.After(endTime.Add(-30*time.Second)) || now.Equal(endTime) {
cron.Debug("当前时间已接近或超过结束时间,尝试查找下一个时间段")
} else {
cron.Debug("当前已在有效时间段内: 开始=%v, 结束=%v",
startTime.Format("2006-01-02 15:04:05"), endTime.Format("2006-01-02 15:04:05"))
return &TimeSlot{
Start: startTime,
End: endTime,
}
}
}
}
// 查找下一个时间段
// 先查找当天剩余时间
for h := currentHour + 1; h < 24; h++ {
if plan[dayOffset+h] == '1' {
// 找到开始小时
startHour := h
// 查找结束小时
endHour := h + 1
for j := h + 1; j < 24; j++ {
if plan[dayOffset+j] == '1' {
endHour = j + 1
} else {
break
}
}
// 创建时间段
startTime := time.Date(now.Year(), now.Month(), now.Day(), startHour, 0, 0, 0, cron.location)
endTime := time.Date(now.Year(), now.Month(), now.Day(), endHour, 0, 0, 0, cron.location)
cron.Debug("找到今天的有效时间段: 开始=%v, 结束=%v",
startTime.Format("2006-01-02 15:04:05"), endTime.Format("2006-01-02 15:04:05"))
return &TimeSlot{
Start: startTime,
End: endTime,
}
}
}
// 如果当天没有找到,则查找后续日期
for d := 1; d <= 7; d++ {
nextDay := (currentWeekday + d) % 7
dayOffset := nextDay * 24
for h := 0; h < 24; h++ {
if plan[dayOffset+h] == '1' {
// 找到开始小时
startHour := h
// 查找结束小时
endHour := h + 1
for j := h + 1; j < 24; j++ {
if plan[dayOffset+j] == '1' {
endHour = j + 1
} else {
break
}
}
// 计算日期
nextDate := now.AddDate(0, 0, d)
// 创建时间段
startTime := time.Date(nextDate.Year(), nextDate.Month(), nextDate.Day(), startHour, 0, 0, 0, cron.location)
endTime := time.Date(nextDate.Year(), nextDate.Month(), nextDate.Day(), endHour, 0, 0, 0, cron.location)
cron.Debug("找到未来有效时间段: 开始=%v, 结束=%v",
startTime.Format("2006-01-02 15:04:05"), endTime.Format("2006-01-02 15:04:05"))
return &TimeSlot{
Start: startTime,
End: endTime,
}
}
}
}
cron.Debug("未找到有效的时间段")
return nil
}
// 开始录制
func (cron *Crontab) startRecording() {
if cron.recording {
return // 已经在录制了
}
now := time.Now().In(cron.location)
cron.Info("开始录制任务: %s, 时间: %v, 计划结束时间: %v",
cron.RecordPlan.Name, now, cron.currentSlot.End)
// 构造请求体
reqBody := map[string]string{
"fragment": cron.Fragment,
"filePath": cron.FilePath,
}
jsonBody, err := json.Marshal(reqBody)
if err != nil {
cron.Error("构造请求体失败: %v", err)
return
}
// 获取 HTTP 地址
addr := cron.ctp.Plugin.GetCommonConf().HTTP.ListenAddr
if addr == "" {
addr = ":8080" // 使用默认端口
}
if addr[0] == ':' {
addr = "localhost" + addr
}
// 发送开始录制请求
resp, err := http.Post(fmt.Sprintf("http://%s/mp4/api/start/%s", addr, cron.StreamPath), "application/json", bytes.NewBuffer(jsonBody))
if err != nil {
cron.Error("开始录制失败: %v", err)
return
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
cron.Error("开始录制失败HTTP状态码: %d", resp.StatusCode)
return
}
cron.recording = true
}
// 停止录制
func (cron *Crontab) stopRecording() {
if !cron.recording {
return // 没有在录制
}
// 立即记录当前时间并重置状态,避免重复调用
now := time.Now().In(cron.location)
cron.Info("停止录制任务: %s, 时间: %v", cron.RecordPlan.Name, now)
// 先重置状态,避免循环中重复检测到停止条件
wasRecording := cron.recording
cron.recording = false
savedSlot := cron.currentSlot
cron.currentSlot = nil
// 获取 HTTP 地址
addr := cron.ctp.Plugin.GetCommonConf().HTTP.ListenAddr
if addr == "" {
addr = ":8080" // 使用默认端口
}
if addr[0] == ':' {
addr = "localhost" + addr
}
// 发送停止录制请求
resp, err := http.Post(fmt.Sprintf("http://%s/mp4/api/stop/%s", addr, cron.StreamPath), "application/json", nil)
if err != nil {
cron.Error("停止录制失败: %v", err)
// 如果请求失败,恢复状态以便下次重试
if wasRecording {
cron.recording = true
cron.currentSlot = savedSlot
}
return
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
cron.Error("停止录制失败HTTP状态码: %d", resp.StatusCode)
// 如果请求失败,恢复状态以便下次重试
if wasRecording {
cron.recording = true
cron.currentSlot = savedSlot
}
}
}

71
plugin/crontab/index.go Normal file
View File

@@ -0,0 +1,71 @@
package plugin_crontab
import (
"fmt"
"m7s.live/v5/pkg/util"
"m7s.live/v5"
"m7s.live/v5/plugin/crontab/pb"
"m7s.live/v5/plugin/crontab/pkg"
)
type CrontabPlugin struct {
m7s.Plugin
pb.UnimplementedApiServer
crontabs util.Collection[string, *Crontab]
recordPlans util.Collection[uint, *pkg.RecordPlan]
}
var _ = m7s.InstallPlugin[CrontabPlugin](m7s.PluginMeta{
ServiceDesc: &pb.Api_ServiceDesc,
RegisterGRPCHandler: pb.RegisterApiHandler,
})
func (ct *CrontabPlugin) OnInit() (err error) {
if ct.DB == nil {
ct.Error("DB is nil")
} else {
err = ct.DB.AutoMigrate(&pkg.RecordPlan{}, &pkg.RecordPlanStream{})
if err != nil {
return fmt.Errorf("auto migrate tables error: %v", err)
}
ct.Info("init database success")
// 查询所有录制计划
var plans []pkg.RecordPlan
if err = ct.DB.Find(&plans).Error; err != nil {
return fmt.Errorf("query record plans error: %v", err)
}
// 遍历所有计划
for _, plan := range plans {
// 将计划存入 recordPlans 集合
ct.recordPlans.Add(&plan)
// 如果计划已启用,查询对应的流信息并创建定时任务
if plan.Enable {
var streams []pkg.RecordPlanStream
model := &pkg.RecordPlanStream{PlanID: plan.ID}
if err = ct.DB.Model(model).Where(model).Find(&streams).Error; err != nil {
ct.Error("query record plan streams error: %v", err)
continue
}
// 为每个流创建定时任务
for _, stream := range streams {
crontab := &Crontab{
ctp: ct,
RecordPlan: &plan,
RecordPlanStream: &stream,
}
crontab.OnStart(func() {
ct.crontabs.Set(crontab)
})
ct.AddTask(crontab)
}
}
}
}
return
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,831 @@
// Code generated by protoc-gen-grpc-gateway. DO NOT EDIT.
// source: crontab.proto
/*
Package pb is a reverse proxy.
It translates gRPC into RESTful JSON APIs.
*/
package pb
import (
"context"
"errors"
"io"
"net/http"
"github.com/grpc-ecosystem/grpc-gateway/v2/runtime"
"github.com/grpc-ecosystem/grpc-gateway/v2/utilities"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/grpclog"
"google.golang.org/grpc/metadata"
"google.golang.org/grpc/status"
"google.golang.org/protobuf/proto"
)
// Suppress "imported and not used" errors
var (
_ codes.Code
_ io.Reader
_ status.Status
_ = errors.New
_ = runtime.String
_ = utilities.NewDoubleArray
_ = metadata.Join
)
var filter_Api_List_0 = &utilities.DoubleArray{Encoding: map[string]int{}, Base: []int(nil), Check: []int(nil)}
func request_Api_List_0(ctx context.Context, marshaler runtime.Marshaler, client ApiClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
var (
protoReq ReqPlanList
metadata runtime.ServerMetadata
)
io.Copy(io.Discard, req.Body)
if err := req.ParseForm(); err != nil {
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
}
if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_Api_List_0); err != nil {
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
}
msg, err := client.List(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))
return msg, metadata, err
}
func local_request_Api_List_0(ctx context.Context, marshaler runtime.Marshaler, server ApiServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
var (
protoReq ReqPlanList
metadata runtime.ServerMetadata
)
if err := req.ParseForm(); err != nil {
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
}
if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_Api_List_0); err != nil {
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
}
msg, err := server.List(ctx, &protoReq)
return msg, metadata, err
}
func request_Api_Add_0(ctx context.Context, marshaler runtime.Marshaler, client ApiClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
var (
protoReq Plan
metadata runtime.ServerMetadata
)
if err := marshaler.NewDecoder(req.Body).Decode(&protoReq); err != nil && !errors.Is(err, io.EOF) {
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
}
msg, err := client.Add(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))
return msg, metadata, err
}
func local_request_Api_Add_0(ctx context.Context, marshaler runtime.Marshaler, server ApiServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
var (
protoReq Plan
metadata runtime.ServerMetadata
)
if err := marshaler.NewDecoder(req.Body).Decode(&protoReq); err != nil && !errors.Is(err, io.EOF) {
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
}
msg, err := server.Add(ctx, &protoReq)
return msg, metadata, err
}
func request_Api_Update_0(ctx context.Context, marshaler runtime.Marshaler, client ApiClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
var (
protoReq Plan
metadata runtime.ServerMetadata
err error
)
if err := marshaler.NewDecoder(req.Body).Decode(&protoReq); err != nil && !errors.Is(err, io.EOF) {
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
}
val, ok := pathParams["id"]
if !ok {
return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "id")
}
protoReq.Id, err = runtime.Uint32(val)
if err != nil {
return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "id", err)
}
msg, err := client.Update(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))
return msg, metadata, err
}
func local_request_Api_Update_0(ctx context.Context, marshaler runtime.Marshaler, server ApiServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
var (
protoReq Plan
metadata runtime.ServerMetadata
err error
)
if err := marshaler.NewDecoder(req.Body).Decode(&protoReq); err != nil && !errors.Is(err, io.EOF) {
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
}
val, ok := pathParams["id"]
if !ok {
return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "id")
}
protoReq.Id, err = runtime.Uint32(val)
if err != nil {
return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "id", err)
}
msg, err := server.Update(ctx, &protoReq)
return msg, metadata, err
}
func request_Api_Remove_0(ctx context.Context, marshaler runtime.Marshaler, client ApiClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
var (
protoReq DeleteRequest
metadata runtime.ServerMetadata
err error
)
if err := marshaler.NewDecoder(req.Body).Decode(&protoReq); err != nil && !errors.Is(err, io.EOF) {
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
}
val, ok := pathParams["id"]
if !ok {
return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "id")
}
protoReq.Id, err = runtime.Uint32(val)
if err != nil {
return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "id", err)
}
msg, err := client.Remove(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))
return msg, metadata, err
}
func local_request_Api_Remove_0(ctx context.Context, marshaler runtime.Marshaler, server ApiServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
var (
protoReq DeleteRequest
metadata runtime.ServerMetadata
err error
)
if err := marshaler.NewDecoder(req.Body).Decode(&protoReq); err != nil && !errors.Is(err, io.EOF) {
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
}
val, ok := pathParams["id"]
if !ok {
return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "id")
}
protoReq.Id, err = runtime.Uint32(val)
if err != nil {
return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "id", err)
}
msg, err := server.Remove(ctx, &protoReq)
return msg, metadata, err
}
var filter_Api_ListRecordPlanStreams_0 = &utilities.DoubleArray{Encoding: map[string]int{}, Base: []int(nil), Check: []int(nil)}
func request_Api_ListRecordPlanStreams_0(ctx context.Context, marshaler runtime.Marshaler, client ApiClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
var (
protoReq ReqPlanStreamList
metadata runtime.ServerMetadata
)
io.Copy(io.Discard, req.Body)
if err := req.ParseForm(); err != nil {
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
}
if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_Api_ListRecordPlanStreams_0); err != nil {
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
}
msg, err := client.ListRecordPlanStreams(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))
return msg, metadata, err
}
func local_request_Api_ListRecordPlanStreams_0(ctx context.Context, marshaler runtime.Marshaler, server ApiServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
var (
protoReq ReqPlanStreamList
metadata runtime.ServerMetadata
)
if err := req.ParseForm(); err != nil {
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
}
if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_Api_ListRecordPlanStreams_0); err != nil {
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
}
msg, err := server.ListRecordPlanStreams(ctx, &protoReq)
return msg, metadata, err
}
func request_Api_AddRecordPlanStream_0(ctx context.Context, marshaler runtime.Marshaler, client ApiClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
var (
protoReq PlanStream
metadata runtime.ServerMetadata
)
if err := marshaler.NewDecoder(req.Body).Decode(&protoReq); err != nil && !errors.Is(err, io.EOF) {
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
}
msg, err := client.AddRecordPlanStream(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))
return msg, metadata, err
}
func local_request_Api_AddRecordPlanStream_0(ctx context.Context, marshaler runtime.Marshaler, server ApiServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
var (
protoReq PlanStream
metadata runtime.ServerMetadata
)
if err := marshaler.NewDecoder(req.Body).Decode(&protoReq); err != nil && !errors.Is(err, io.EOF) {
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
}
msg, err := server.AddRecordPlanStream(ctx, &protoReq)
return msg, metadata, err
}
func request_Api_UpdateRecordPlanStream_0(ctx context.Context, marshaler runtime.Marshaler, client ApiClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
var (
protoReq PlanStream
metadata runtime.ServerMetadata
)
if err := marshaler.NewDecoder(req.Body).Decode(&protoReq); err != nil && !errors.Is(err, io.EOF) {
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
}
msg, err := client.UpdateRecordPlanStream(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))
return msg, metadata, err
}
func local_request_Api_UpdateRecordPlanStream_0(ctx context.Context, marshaler runtime.Marshaler, server ApiServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
var (
protoReq PlanStream
metadata runtime.ServerMetadata
)
if err := marshaler.NewDecoder(req.Body).Decode(&protoReq); err != nil && !errors.Is(err, io.EOF) {
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
}
msg, err := server.UpdateRecordPlanStream(ctx, &protoReq)
return msg, metadata, err
}
func request_Api_RemoveRecordPlanStream_0(ctx context.Context, marshaler runtime.Marshaler, client ApiClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
var (
protoReq DeletePlanStreamRequest
metadata runtime.ServerMetadata
err error
)
if err := marshaler.NewDecoder(req.Body).Decode(&protoReq); err != nil && !errors.Is(err, io.EOF) {
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
}
val, ok := pathParams["planId"]
if !ok {
return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "planId")
}
protoReq.PlanId, err = runtime.Uint32(val)
if err != nil {
return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "planId", err)
}
val, ok = pathParams["streamPath"]
if !ok {
return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "streamPath")
}
protoReq.StreamPath, err = runtime.String(val)
if err != nil {
return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "streamPath", err)
}
msg, err := client.RemoveRecordPlanStream(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))
return msg, metadata, err
}
func local_request_Api_RemoveRecordPlanStream_0(ctx context.Context, marshaler runtime.Marshaler, server ApiServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
var (
protoReq DeletePlanStreamRequest
metadata runtime.ServerMetadata
err error
)
if err := marshaler.NewDecoder(req.Body).Decode(&protoReq); err != nil && !errors.Is(err, io.EOF) {
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
}
val, ok := pathParams["planId"]
if !ok {
return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "planId")
}
protoReq.PlanId, err = runtime.Uint32(val)
if err != nil {
return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "planId", err)
}
val, ok = pathParams["streamPath"]
if !ok {
return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "streamPath")
}
protoReq.StreamPath, err = runtime.String(val)
if err != nil {
return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "streamPath", err)
}
msg, err := server.RemoveRecordPlanStream(ctx, &protoReq)
return msg, metadata, err
}
func request_Api_ParsePlanTime_0(ctx context.Context, marshaler runtime.Marshaler, client ApiClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
var (
protoReq ParsePlanRequest
metadata runtime.ServerMetadata
err error
)
io.Copy(io.Discard, req.Body)
val, ok := pathParams["plan"]
if !ok {
return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "plan")
}
protoReq.Plan, err = runtime.String(val)
if err != nil {
return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "plan", err)
}
msg, err := client.ParsePlanTime(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))
return msg, metadata, err
}
func local_request_Api_ParsePlanTime_0(ctx context.Context, marshaler runtime.Marshaler, server ApiServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
var (
protoReq ParsePlanRequest
metadata runtime.ServerMetadata
err error
)
val, ok := pathParams["plan"]
if !ok {
return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "plan")
}
protoReq.Plan, err = runtime.String(val)
if err != nil {
return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "plan", err)
}
msg, err := server.ParsePlanTime(ctx, &protoReq)
return msg, metadata, err
}
var filter_Api_GetCrontabStatus_0 = &utilities.DoubleArray{Encoding: map[string]int{}, Base: []int(nil), Check: []int(nil)}
func request_Api_GetCrontabStatus_0(ctx context.Context, marshaler runtime.Marshaler, client ApiClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
var (
protoReq CrontabStatusRequest
metadata runtime.ServerMetadata
)
io.Copy(io.Discard, req.Body)
if err := req.ParseForm(); err != nil {
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
}
if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_Api_GetCrontabStatus_0); err != nil {
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
}
msg, err := client.GetCrontabStatus(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))
return msg, metadata, err
}
func local_request_Api_GetCrontabStatus_0(ctx context.Context, marshaler runtime.Marshaler, server ApiServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
var (
protoReq CrontabStatusRequest
metadata runtime.ServerMetadata
)
if err := req.ParseForm(); err != nil {
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
}
if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_Api_GetCrontabStatus_0); err != nil {
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
}
msg, err := server.GetCrontabStatus(ctx, &protoReq)
return msg, metadata, err
}
// RegisterApiHandlerServer registers the http handlers for service Api to "mux".
// UnaryRPC :call ApiServer directly.
// StreamingRPC :currently unsupported pending https://github.com/grpc/grpc-go/issues/906.
// 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(http.MethodGet, pattern_Api_List_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
ctx, cancel := context.WithCancel(req.Context())
defer cancel()
var stream runtime.ServerTransportStream
ctx = grpc.NewContextWithServerTransportStream(ctx, &stream)
inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
annotatedContext, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/crontab.Api/List", runtime.WithHTTPPathPattern("/plan/api/list"))
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
resp, md, err := local_request_Api_List_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_List_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
})
mux.Handle(http.MethodPost, pattern_Api_Add_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
ctx, cancel := context.WithCancel(req.Context())
defer cancel()
var stream runtime.ServerTransportStream
ctx = grpc.NewContextWithServerTransportStream(ctx, &stream)
inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
annotatedContext, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/crontab.Api/Add", runtime.WithHTTPPathPattern("/plan/api/add"))
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
resp, md, err := local_request_Api_Add_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_Add_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
})
mux.Handle(http.MethodPost, pattern_Api_Update_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
ctx, cancel := context.WithCancel(req.Context())
defer cancel()
var stream runtime.ServerTransportStream
ctx = grpc.NewContextWithServerTransportStream(ctx, &stream)
inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
annotatedContext, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/crontab.Api/Update", runtime.WithHTTPPathPattern("/plan/api/update/{id}"))
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
resp, md, err := local_request_Api_Update_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_Update_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
})
mux.Handle(http.MethodPost, pattern_Api_Remove_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
ctx, cancel := context.WithCancel(req.Context())
defer cancel()
var stream runtime.ServerTransportStream
ctx = grpc.NewContextWithServerTransportStream(ctx, &stream)
inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
annotatedContext, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/crontab.Api/Remove", runtime.WithHTTPPathPattern("/plan/api/remove/{id}"))
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
resp, md, err := local_request_Api_Remove_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_Remove_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
})
mux.Handle(http.MethodGet, pattern_Api_ListRecordPlanStreams_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
ctx, cancel := context.WithCancel(req.Context())
defer cancel()
var stream runtime.ServerTransportStream
ctx = grpc.NewContextWithServerTransportStream(ctx, &stream)
inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
annotatedContext, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/crontab.Api/ListRecordPlanStreams", runtime.WithHTTPPathPattern("/planstream/api/list"))
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
resp, md, err := local_request_Api_ListRecordPlanStreams_0(annotatedContext, inboundMarshaler, server, req, pathParams)
md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer())
annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md)
if err != nil {
runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err)
return
}
forward_Api_ListRecordPlanStreams_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
})
mux.Handle(http.MethodPost, pattern_Api_AddRecordPlanStream_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
ctx, cancel := context.WithCancel(req.Context())
defer cancel()
var stream runtime.ServerTransportStream
ctx = grpc.NewContextWithServerTransportStream(ctx, &stream)
inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
annotatedContext, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/crontab.Api/AddRecordPlanStream", runtime.WithHTTPPathPattern("/planstream/api/add"))
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
resp, md, err := local_request_Api_AddRecordPlanStream_0(annotatedContext, inboundMarshaler, server, req, pathParams)
md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer())
annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md)
if err != nil {
runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err)
return
}
forward_Api_AddRecordPlanStream_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
})
mux.Handle(http.MethodPost, pattern_Api_UpdateRecordPlanStream_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
ctx, cancel := context.WithCancel(req.Context())
defer cancel()
var stream runtime.ServerTransportStream
ctx = grpc.NewContextWithServerTransportStream(ctx, &stream)
inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
annotatedContext, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/crontab.Api/UpdateRecordPlanStream", runtime.WithHTTPPathPattern("/planstream/api/update"))
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
resp, md, err := local_request_Api_UpdateRecordPlanStream_0(annotatedContext, inboundMarshaler, server, req, pathParams)
md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer())
annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md)
if err != nil {
runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err)
return
}
forward_Api_UpdateRecordPlanStream_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
})
mux.Handle(http.MethodPost, pattern_Api_RemoveRecordPlanStream_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
ctx, cancel := context.WithCancel(req.Context())
defer cancel()
var stream runtime.ServerTransportStream
ctx = grpc.NewContextWithServerTransportStream(ctx, &stream)
inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
annotatedContext, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/crontab.Api/RemoveRecordPlanStream", runtime.WithHTTPPathPattern("/planstream/api/remove/{planId}/{streamPath=**}"))
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
resp, md, err := local_request_Api_RemoveRecordPlanStream_0(annotatedContext, inboundMarshaler, server, req, pathParams)
md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer())
annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md)
if err != nil {
runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err)
return
}
forward_Api_RemoveRecordPlanStream_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
})
mux.Handle(http.MethodGet, pattern_Api_ParsePlanTime_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
ctx, cancel := context.WithCancel(req.Context())
defer cancel()
var stream runtime.ServerTransportStream
ctx = grpc.NewContextWithServerTransportStream(ctx, &stream)
inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
annotatedContext, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/crontab.Api/ParsePlanTime", runtime.WithHTTPPathPattern("/plan/api/parse/{plan}"))
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
resp, md, err := local_request_Api_ParsePlanTime_0(annotatedContext, inboundMarshaler, server, req, pathParams)
md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer())
annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md)
if err != nil {
runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err)
return
}
forward_Api_ParsePlanTime_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
})
mux.Handle(http.MethodGet, pattern_Api_GetCrontabStatus_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
ctx, cancel := context.WithCancel(req.Context())
defer cancel()
var stream runtime.ServerTransportStream
ctx = grpc.NewContextWithServerTransportStream(ctx, &stream)
inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
annotatedContext, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/crontab.Api/GetCrontabStatus", runtime.WithHTTPPathPattern("/crontab/api/status"))
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
resp, md, err := local_request_Api_GetCrontabStatus_0(annotatedContext, inboundMarshaler, server, req, pathParams)
md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer())
annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md)
if err != nil {
runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err)
return
}
forward_Api_GetCrontabStatus_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
})
return nil
}
// 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.NewClient(endpoint, opts...)
if err != nil {
return err
}
defer func() {
if err != nil {
if cerr := conn.Close(); cerr != nil {
grpclog.Errorf("Failed to close conn to %s: %v", endpoint, cerr)
}
return
}
go func() {
<-ctx.Done()
if cerr := conn.Close(); cerr != nil {
grpclog.Errorf("Failed to close conn to %s: %v", endpoint, cerr)
}
}()
}()
return RegisterApiHandler(ctx, mux, conn)
}
// RegisterApiHandler registers the http handlers for service Api to "mux".
// The handlers forward requests to the grpc endpoint over "conn".
func RegisterApiHandler(ctx context.Context, mux *runtime.ServeMux, conn *grpc.ClientConn) error {
return RegisterApiHandlerClient(ctx, mux, NewApiClient(conn))
}
// RegisterApiHandlerClient registers the http handlers for service Api
// 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. This client ignores the HTTP middlewares.
func RegisterApiHandlerClient(ctx context.Context, mux *runtime.ServeMux, client ApiClient) error {
mux.Handle(http.MethodGet, pattern_Api_List_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
ctx, cancel := context.WithCancel(req.Context())
defer cancel()
inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
annotatedContext, err := runtime.AnnotateContext(ctx, mux, req, "/crontab.Api/List", runtime.WithHTTPPathPattern("/plan/api/list"))
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
resp, md, err := request_Api_List_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_List_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
})
mux.Handle(http.MethodPost, pattern_Api_Add_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
ctx, cancel := context.WithCancel(req.Context())
defer cancel()
inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
annotatedContext, err := runtime.AnnotateContext(ctx, mux, req, "/crontab.Api/Add", runtime.WithHTTPPathPattern("/plan/api/add"))
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
resp, md, err := request_Api_Add_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_Add_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
})
mux.Handle(http.MethodPost, pattern_Api_Update_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
ctx, cancel := context.WithCancel(req.Context())
defer cancel()
inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
annotatedContext, err := runtime.AnnotateContext(ctx, mux, req, "/crontab.Api/Update", runtime.WithHTTPPathPattern("/plan/api/update/{id}"))
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
resp, md, err := request_Api_Update_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_Update_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
})
mux.Handle(http.MethodPost, pattern_Api_Remove_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
ctx, cancel := context.WithCancel(req.Context())
defer cancel()
inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
annotatedContext, err := runtime.AnnotateContext(ctx, mux, req, "/crontab.Api/Remove", runtime.WithHTTPPathPattern("/plan/api/remove/{id}"))
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
resp, md, err := request_Api_Remove_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_Remove_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
})
mux.Handle(http.MethodGet, pattern_Api_ListRecordPlanStreams_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
ctx, cancel := context.WithCancel(req.Context())
defer cancel()
inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
annotatedContext, err := runtime.AnnotateContext(ctx, mux, req, "/crontab.Api/ListRecordPlanStreams", runtime.WithHTTPPathPattern("/planstream/api/list"))
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
resp, md, err := request_Api_ListRecordPlanStreams_0(annotatedContext, inboundMarshaler, client, req, pathParams)
annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md)
if err != nil {
runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err)
return
}
forward_Api_ListRecordPlanStreams_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
})
mux.Handle(http.MethodPost, pattern_Api_AddRecordPlanStream_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
ctx, cancel := context.WithCancel(req.Context())
defer cancel()
inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
annotatedContext, err := runtime.AnnotateContext(ctx, mux, req, "/crontab.Api/AddRecordPlanStream", runtime.WithHTTPPathPattern("/planstream/api/add"))
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
resp, md, err := request_Api_AddRecordPlanStream_0(annotatedContext, inboundMarshaler, client, req, pathParams)
annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md)
if err != nil {
runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err)
return
}
forward_Api_AddRecordPlanStream_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
})
mux.Handle(http.MethodPost, pattern_Api_UpdateRecordPlanStream_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
ctx, cancel := context.WithCancel(req.Context())
defer cancel()
inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
annotatedContext, err := runtime.AnnotateContext(ctx, mux, req, "/crontab.Api/UpdateRecordPlanStream", runtime.WithHTTPPathPattern("/planstream/api/update"))
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
resp, md, err := request_Api_UpdateRecordPlanStream_0(annotatedContext, inboundMarshaler, client, req, pathParams)
annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md)
if err != nil {
runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err)
return
}
forward_Api_UpdateRecordPlanStream_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
})
mux.Handle(http.MethodPost, pattern_Api_RemoveRecordPlanStream_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
ctx, cancel := context.WithCancel(req.Context())
defer cancel()
inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
annotatedContext, err := runtime.AnnotateContext(ctx, mux, req, "/crontab.Api/RemoveRecordPlanStream", runtime.WithHTTPPathPattern("/planstream/api/remove/{planId}/{streamPath=**}"))
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
resp, md, err := request_Api_RemoveRecordPlanStream_0(annotatedContext, inboundMarshaler, client, req, pathParams)
annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md)
if err != nil {
runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err)
return
}
forward_Api_RemoveRecordPlanStream_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
})
mux.Handle(http.MethodGet, pattern_Api_ParsePlanTime_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
ctx, cancel := context.WithCancel(req.Context())
defer cancel()
inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
annotatedContext, err := runtime.AnnotateContext(ctx, mux, req, "/crontab.Api/ParsePlanTime", runtime.WithHTTPPathPattern("/plan/api/parse/{plan}"))
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
resp, md, err := request_Api_ParsePlanTime_0(annotatedContext, inboundMarshaler, client, req, pathParams)
annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md)
if err != nil {
runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err)
return
}
forward_Api_ParsePlanTime_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
})
mux.Handle(http.MethodGet, pattern_Api_GetCrontabStatus_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
ctx, cancel := context.WithCancel(req.Context())
defer cancel()
inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
annotatedContext, err := runtime.AnnotateContext(ctx, mux, req, "/crontab.Api/GetCrontabStatus", runtime.WithHTTPPathPattern("/crontab/api/status"))
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
resp, md, err := request_Api_GetCrontabStatus_0(annotatedContext, inboundMarshaler, client, req, pathParams)
annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md)
if err != nil {
runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err)
return
}
forward_Api_GetCrontabStatus_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
})
return nil
}
var (
pattern_Api_List_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2}, []string{"plan", "api", "list"}, ""))
pattern_Api_Add_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2}, []string{"plan", "api", "add"}, ""))
pattern_Api_Update_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 1, 0, 4, 1, 5, 3}, []string{"plan", "api", "update", "id"}, ""))
pattern_Api_Remove_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 1, 0, 4, 1, 5, 3}, []string{"plan", "api", "remove", "id"}, ""))
pattern_Api_ListRecordPlanStreams_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2}, []string{"planstream", "api", "list"}, ""))
pattern_Api_AddRecordPlanStream_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2}, []string{"planstream", "api", "add"}, ""))
pattern_Api_UpdateRecordPlanStream_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2}, []string{"planstream", "api", "update"}, ""))
pattern_Api_RemoveRecordPlanStream_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 1, 0, 4, 1, 5, 3, 3, 0, 4, 1, 5, 4}, []string{"planstream", "api", "remove", "planId", "streamPath"}, ""))
pattern_Api_ParsePlanTime_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 1, 0, 4, 1, 5, 0}, []string{"plan", "api", "parse"}, ""))
pattern_Api_GetCrontabStatus_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2}, []string{"crontab", "api", "status"}, ""))
)
var (
forward_Api_List_0 = runtime.ForwardResponseMessage
forward_Api_Add_0 = runtime.ForwardResponseMessage
forward_Api_Update_0 = runtime.ForwardResponseMessage
forward_Api_Remove_0 = runtime.ForwardResponseMessage
forward_Api_ListRecordPlanStreams_0 = runtime.ForwardResponseMessage
forward_Api_AddRecordPlanStream_0 = runtime.ForwardResponseMessage
forward_Api_UpdateRecordPlanStream_0 = runtime.ForwardResponseMessage
forward_Api_RemoveRecordPlanStream_0 = runtime.ForwardResponseMessage
forward_Api_ParsePlanTime_0 = runtime.ForwardResponseMessage
forward_Api_GetCrontabStatus_0 = runtime.ForwardResponseMessage
)

View File

@@ -0,0 +1,190 @@
syntax = "proto3";
import "google/api/annotations.proto";
import "google/protobuf/timestamp.proto";
package crontab;
option go_package="m7s.live/v5/plugin/crontab/pb";
service api {
rpc List (ReqPlanList) returns (PlanResponseList) {
option (google.api.http) = {
get: "/plan/api/list"
};
}
rpc Add (Plan) returns (Response) {
option (google.api.http) = {
post: "/plan/api/add"
body: "*"
};
}
rpc Update (Plan) returns (Response) {
option (google.api.http) = {
post: "/plan/api/update/{id}"
body: "*"
};
}
rpc Remove (DeleteRequest) returns (Response) {
option (google.api.http) = {
post: "/plan/api/remove/{id}"
body: "*"
};
}
// RecordPlanStream 相关接口
rpc ListRecordPlanStreams (ReqPlanStreamList) returns (RecordPlanStreamResponseList) {
option (google.api.http) = {
get: "/planstream/api/list"
};
}
rpc AddRecordPlanStream (PlanStream) returns (Response) {
option (google.api.http) = {
post: "/planstream/api/add"
body: "*"
};
}
rpc UpdateRecordPlanStream (PlanStream) returns (Response) {
option (google.api.http) = {
post: "/planstream/api/update"
body: "*"
};
}
rpc RemoveRecordPlanStream (DeletePlanStreamRequest) returns (Response) {
option (google.api.http) = {
post: "/planstream/api/remove/{planId}/{streamPath=**}"
body: "*"
};
}
// 解析计划字符串,返回时间段信息
rpc ParsePlanTime (ParsePlanRequest) returns (ParsePlanResponse) {
option (google.api.http) = {
get: "/plan/api/parse/{plan}"
};
}
// 获取当前Crontab任务状态
rpc GetCrontabStatus (CrontabStatusRequest) returns (CrontabStatusResponse) {
option (google.api.http) = {
get: "/crontab/api/status"
};
}
}
message PlanResponseList {
int32 code = 1;
string message = 2;
uint32 totalCount = 3;
uint32 pageNum = 4;
uint32 pageSize = 5;
repeated Plan data = 6;
}
message Plan {
uint32 id = 1;
string name = 2;
bool enable = 3;
google.protobuf.Timestamp createTime = 4;
google.protobuf.Timestamp updateTime = 5;
string plan = 6;
}
message ReqPlanList {
uint32 pageNum = 1;
uint32 pageSize = 2;
}
message DeleteRequest {
uint32 id = 1;
}
message Response {
int32 code = 1;
string message = 2;
}
// RecordPlanStream 相关消息定义
message PlanStream {
uint32 planId = 1;
string stream_path = 2;
string fragment = 3;
string filePath = 4;
string record_type = 5; // 录制类型,例如 "mp4", "flv"
google.protobuf.Timestamp created_at = 6;
google.protobuf.Timestamp updated_at = 7;
bool enable = 8; // 是否启用该录制流
}
message ReqPlanStreamList {
uint32 pageNum = 1;
uint32 pageSize = 2;
uint32 planId = 3; // 可选的按录制计划ID筛选
string stream_path = 4; // 可选的按流路径筛选
}
message RecordPlanStreamResponseList {
int32 code = 1;
string message = 2;
uint32 totalCount = 3;
uint32 pageNum = 4;
uint32 pageSize = 5;
repeated PlanStream data = 6;
}
message DeletePlanStreamRequest {
uint32 planId = 1;
string streamPath = 2;
}
// 解析计划请求
message ParsePlanRequest {
string plan = 1; // 168位的0/1字符串表示一周的每个小时是否录制
}
// 时间段信息
message TimeSlotInfo {
google.protobuf.Timestamp start = 1; // 开始时间
google.protobuf.Timestamp end = 2; // 结束时间
string weekday = 3; // 周几(例如:周一)
string time_range = 4; // 时间范围例如09:00-10:00
}
// 解析计划响应
message ParsePlanResponse {
int32 code = 1; // 响应码
string message = 2; // 响应消息
repeated TimeSlotInfo slots = 3; // 所有计划的时间段
TimeSlotInfo next_slot = 4; // 从当前时间开始的下一个时间段
}
// 新增的消息定义
// 获取Crontab状态请求
message CrontabStatusRequest {
// 可以为空,表示获取所有任务
string stream_path = 1; // 可选,按流路径过滤
}
// 任务信息
message CrontabTaskInfo {
uint32 plan_id = 1; // 计划ID
string plan_name = 2; // 计划名称
string stream_path = 3; // 流路径
bool is_recording = 4; // 是否正在录制
google.protobuf.Timestamp start_time = 5; // 当前/下一个任务开始时间
google.protobuf.Timestamp end_time = 6; // 当前/下一个任务结束时间
string time_range = 7; // 时间范围例如09:00-10:00
string weekday = 8; // 周几(例如:周一)
string file_path = 9; // 文件保存路径
string fragment = 10; // 分片设置
uint32 elapsed_seconds = 11; // 已运行时间(秒,仅对正在运行的任务有效)
uint32 remaining_seconds = 12; // 剩余时间(秒)
repeated TimeSlotInfo plan_slots = 13; // 完整的计划时间段列表
}
// 获取Crontab状态响应
message CrontabStatusResponse {
int32 code = 1; // 响应码
string message = 2; // 响应消息
repeated CrontabTaskInfo running_tasks = 3; // 当前正在执行的任务列表
repeated CrontabTaskInfo next_tasks = 4; // 下一个计划执行的任务列表
uint32 total_running = 5; // 正在运行的任务总数
uint32 total_planned = 6; // 计划中的任务总数
}

View File

@@ -0,0 +1,469 @@
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
// versions:
// - protoc-gen-go-grpc v1.5.1
// - protoc v5.29.3
// source: crontab.proto
package pb
import (
context "context"
grpc "google.golang.org/grpc"
codes "google.golang.org/grpc/codes"
status "google.golang.org/grpc/status"
)
// 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.64.0 or later.
const _ = grpc.SupportPackageIsVersion9
const (
Api_List_FullMethodName = "/crontab.api/List"
Api_Add_FullMethodName = "/crontab.api/Add"
Api_Update_FullMethodName = "/crontab.api/Update"
Api_Remove_FullMethodName = "/crontab.api/Remove"
Api_ListRecordPlanStreams_FullMethodName = "/crontab.api/ListRecordPlanStreams"
Api_AddRecordPlanStream_FullMethodName = "/crontab.api/AddRecordPlanStream"
Api_UpdateRecordPlanStream_FullMethodName = "/crontab.api/UpdateRecordPlanStream"
Api_RemoveRecordPlanStream_FullMethodName = "/crontab.api/RemoveRecordPlanStream"
Api_ParsePlanTime_FullMethodName = "/crontab.api/ParsePlanTime"
Api_GetCrontabStatus_FullMethodName = "/crontab.api/GetCrontabStatus"
)
// ApiClient is the client API for Api service.
//
// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream.
type ApiClient interface {
List(ctx context.Context, in *ReqPlanList, opts ...grpc.CallOption) (*PlanResponseList, error)
Add(ctx context.Context, in *Plan, opts ...grpc.CallOption) (*Response, error)
Update(ctx context.Context, in *Plan, opts ...grpc.CallOption) (*Response, error)
Remove(ctx context.Context, in *DeleteRequest, opts ...grpc.CallOption) (*Response, error)
// RecordPlanStream 相关接口
ListRecordPlanStreams(ctx context.Context, in *ReqPlanStreamList, opts ...grpc.CallOption) (*RecordPlanStreamResponseList, error)
AddRecordPlanStream(ctx context.Context, in *PlanStream, opts ...grpc.CallOption) (*Response, error)
UpdateRecordPlanStream(ctx context.Context, in *PlanStream, opts ...grpc.CallOption) (*Response, error)
RemoveRecordPlanStream(ctx context.Context, in *DeletePlanStreamRequest, opts ...grpc.CallOption) (*Response, error)
// 解析计划字符串,返回时间段信息
ParsePlanTime(ctx context.Context, in *ParsePlanRequest, opts ...grpc.CallOption) (*ParsePlanResponse, error)
// 获取当前Crontab任务状态
GetCrontabStatus(ctx context.Context, in *CrontabStatusRequest, opts ...grpc.CallOption) (*CrontabStatusResponse, error)
}
type apiClient struct {
cc grpc.ClientConnInterface
}
func NewApiClient(cc grpc.ClientConnInterface) ApiClient {
return &apiClient{cc}
}
func (c *apiClient) List(ctx context.Context, in *ReqPlanList, opts ...grpc.CallOption) (*PlanResponseList, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(PlanResponseList)
err := c.cc.Invoke(ctx, Api_List_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *apiClient) Add(ctx context.Context, in *Plan, opts ...grpc.CallOption) (*Response, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(Response)
err := c.cc.Invoke(ctx, Api_Add_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *apiClient) Update(ctx context.Context, in *Plan, opts ...grpc.CallOption) (*Response, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(Response)
err := c.cc.Invoke(ctx, Api_Update_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *apiClient) Remove(ctx context.Context, in *DeleteRequest, opts ...grpc.CallOption) (*Response, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(Response)
err := c.cc.Invoke(ctx, Api_Remove_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *apiClient) ListRecordPlanStreams(ctx context.Context, in *ReqPlanStreamList, opts ...grpc.CallOption) (*RecordPlanStreamResponseList, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(RecordPlanStreamResponseList)
err := c.cc.Invoke(ctx, Api_ListRecordPlanStreams_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *apiClient) AddRecordPlanStream(ctx context.Context, in *PlanStream, opts ...grpc.CallOption) (*Response, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(Response)
err := c.cc.Invoke(ctx, Api_AddRecordPlanStream_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *apiClient) UpdateRecordPlanStream(ctx context.Context, in *PlanStream, opts ...grpc.CallOption) (*Response, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(Response)
err := c.cc.Invoke(ctx, Api_UpdateRecordPlanStream_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *apiClient) RemoveRecordPlanStream(ctx context.Context, in *DeletePlanStreamRequest, opts ...grpc.CallOption) (*Response, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(Response)
err := c.cc.Invoke(ctx, Api_RemoveRecordPlanStream_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *apiClient) ParsePlanTime(ctx context.Context, in *ParsePlanRequest, opts ...grpc.CallOption) (*ParsePlanResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(ParsePlanResponse)
err := c.cc.Invoke(ctx, Api_ParsePlanTime_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *apiClient) GetCrontabStatus(ctx context.Context, in *CrontabStatusRequest, opts ...grpc.CallOption) (*CrontabStatusResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(CrontabStatusResponse)
err := c.cc.Invoke(ctx, Api_GetCrontabStatus_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
// ApiServer is the server API for Api service.
// All implementations must embed UnimplementedApiServer
// for forward compatibility.
type ApiServer interface {
List(context.Context, *ReqPlanList) (*PlanResponseList, error)
Add(context.Context, *Plan) (*Response, error)
Update(context.Context, *Plan) (*Response, error)
Remove(context.Context, *DeleteRequest) (*Response, error)
// RecordPlanStream 相关接口
ListRecordPlanStreams(context.Context, *ReqPlanStreamList) (*RecordPlanStreamResponseList, error)
AddRecordPlanStream(context.Context, *PlanStream) (*Response, error)
UpdateRecordPlanStream(context.Context, *PlanStream) (*Response, error)
RemoveRecordPlanStream(context.Context, *DeletePlanStreamRequest) (*Response, error)
// 解析计划字符串,返回时间段信息
ParsePlanTime(context.Context, *ParsePlanRequest) (*ParsePlanResponse, error)
// 获取当前Crontab任务状态
GetCrontabStatus(context.Context, *CrontabStatusRequest) (*CrontabStatusResponse, error)
mustEmbedUnimplementedApiServer()
}
// 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, *ReqPlanList) (*PlanResponseList, error) {
return nil, status.Errorf(codes.Unimplemented, "method List not implemented")
}
func (UnimplementedApiServer) Add(context.Context, *Plan) (*Response, error) {
return nil, status.Errorf(codes.Unimplemented, "method Add not implemented")
}
func (UnimplementedApiServer) Update(context.Context, *Plan) (*Response, error) {
return nil, status.Errorf(codes.Unimplemented, "method Update not implemented")
}
func (UnimplementedApiServer) Remove(context.Context, *DeleteRequest) (*Response, error) {
return nil, status.Errorf(codes.Unimplemented, "method Remove not implemented")
}
func (UnimplementedApiServer) ListRecordPlanStreams(context.Context, *ReqPlanStreamList) (*RecordPlanStreamResponseList, error) {
return nil, status.Errorf(codes.Unimplemented, "method ListRecordPlanStreams not implemented")
}
func (UnimplementedApiServer) AddRecordPlanStream(context.Context, *PlanStream) (*Response, error) {
return nil, status.Errorf(codes.Unimplemented, "method AddRecordPlanStream not implemented")
}
func (UnimplementedApiServer) UpdateRecordPlanStream(context.Context, *PlanStream) (*Response, error) {
return nil, status.Errorf(codes.Unimplemented, "method UpdateRecordPlanStream not implemented")
}
func (UnimplementedApiServer) RemoveRecordPlanStream(context.Context, *DeletePlanStreamRequest) (*Response, error) {
return nil, status.Errorf(codes.Unimplemented, "method RemoveRecordPlanStream not implemented")
}
func (UnimplementedApiServer) ParsePlanTime(context.Context, *ParsePlanRequest) (*ParsePlanResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method ParsePlanTime not implemented")
}
func (UnimplementedApiServer) GetCrontabStatus(context.Context, *CrontabStatusRequest) (*CrontabStatusResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method GetCrontabStatus not implemented")
}
func (UnimplementedApiServer) mustEmbedUnimplementedApiServer() {}
func (UnimplementedApiServer) testEmbeddedByValue() {}
// 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
// result in compilation errors.
type UnsafeApiServer interface {
mustEmbedUnimplementedApiServer()
}
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)
}
func _Api_List_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(ReqPlanList)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(ApiServer).List(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: Api_List_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(ApiServer).List(ctx, req.(*ReqPlanList))
}
return interceptor(ctx, in, info, handler)
}
func _Api_Add_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(Plan)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(ApiServer).Add(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: Api_Add_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(ApiServer).Add(ctx, req.(*Plan))
}
return interceptor(ctx, in, info, handler)
}
func _Api_Update_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(Plan)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(ApiServer).Update(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: Api_Update_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(ApiServer).Update(ctx, req.(*Plan))
}
return interceptor(ctx, in, info, handler)
}
func _Api_Remove_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(DeleteRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(ApiServer).Remove(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: Api_Remove_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(ApiServer).Remove(ctx, req.(*DeleteRequest))
}
return interceptor(ctx, in, info, handler)
}
func _Api_ListRecordPlanStreams_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(ReqPlanStreamList)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(ApiServer).ListRecordPlanStreams(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: Api_ListRecordPlanStreams_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(ApiServer).ListRecordPlanStreams(ctx, req.(*ReqPlanStreamList))
}
return interceptor(ctx, in, info, handler)
}
func _Api_AddRecordPlanStream_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(PlanStream)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(ApiServer).AddRecordPlanStream(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: Api_AddRecordPlanStream_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(ApiServer).AddRecordPlanStream(ctx, req.(*PlanStream))
}
return interceptor(ctx, in, info, handler)
}
func _Api_UpdateRecordPlanStream_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(PlanStream)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(ApiServer).UpdateRecordPlanStream(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: Api_UpdateRecordPlanStream_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(ApiServer).UpdateRecordPlanStream(ctx, req.(*PlanStream))
}
return interceptor(ctx, in, info, handler)
}
func _Api_RemoveRecordPlanStream_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(DeletePlanStreamRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(ApiServer).RemoveRecordPlanStream(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: Api_RemoveRecordPlanStream_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(ApiServer).RemoveRecordPlanStream(ctx, req.(*DeletePlanStreamRequest))
}
return interceptor(ctx, in, info, handler)
}
func _Api_ParsePlanTime_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(ParsePlanRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(ApiServer).ParsePlanTime(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: Api_ParsePlanTime_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(ApiServer).ParsePlanTime(ctx, req.(*ParsePlanRequest))
}
return interceptor(ctx, in, info, handler)
}
func _Api_GetCrontabStatus_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(CrontabStatusRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(ApiServer).GetCrontabStatus(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: Api_GetCrontabStatus_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(ApiServer).GetCrontabStatus(ctx, req.(*CrontabStatusRequest))
}
return interceptor(ctx, in, info, handler)
}
// Api_ServiceDesc is the grpc.ServiceDesc for Api service.
// It's only intended for direct use with grpc.RegisterService,
// and not to be introspected or modified (even as a copy)
var Api_ServiceDesc = grpc.ServiceDesc{
ServiceName: "crontab.api",
HandlerType: (*ApiServer)(nil),
Methods: []grpc.MethodDesc{
{
MethodName: "List",
Handler: _Api_List_Handler,
},
{
MethodName: "Add",
Handler: _Api_Add_Handler,
},
{
MethodName: "Update",
Handler: _Api_Update_Handler,
},
{
MethodName: "Remove",
Handler: _Api_Remove_Handler,
},
{
MethodName: "ListRecordPlanStreams",
Handler: _Api_ListRecordPlanStreams_Handler,
},
{
MethodName: "AddRecordPlanStream",
Handler: _Api_AddRecordPlanStream_Handler,
},
{
MethodName: "UpdateRecordPlanStream",
Handler: _Api_UpdateRecordPlanStream_Handler,
},
{
MethodName: "RemoveRecordPlanStream",
Handler: _Api_RemoveRecordPlanStream_Handler,
},
{
MethodName: "ParsePlanTime",
Handler: _Api_ParsePlanTime_Handler,
},
{
MethodName: "GetCrontabStatus",
Handler: _Api_GetCrontabStatus_Handler,
},
},
Streams: []grpc.StreamDesc{},
Metadata: "crontab.proto",
}

View File

@@ -0,0 +1,17 @@
package pkg
import (
"gorm.io/gorm"
)
// RecordPlan 录制计划模型
type RecordPlan struct {
gorm.Model
Name string `json:"name" gorm:"default:''"`
Plan string `json:"plan" gorm:"type:text"`
Enable bool `json:"enable" gorm:"default:false"` // 是否启用
}
func (r *RecordPlan) GetKey() uint {
return r.ID
}

View File

@@ -0,0 +1,51 @@
package pkg
import (
"gorm.io/gorm"
"time"
)
// RecordPlanStream 录制计划流信息模型
type RecordPlanStream struct {
PlanID uint `json:"plan_id" gorm:"primaryKey;type:bigint;not null"` // 录制计划ID
StreamPath string `json:"stream_path" gorm:"primaryKey;type:varchar(255)"`
Fragment string `json:"fragment" gorm:"type:text"`
FilePath string `json:"file_path" gorm:"type:varchar(255)"`
CreatedAt time.Time
UpdatedAt time.Time
DeletedAt gorm.DeletedAt `gorm:"index"`
Enable bool `json:"enable" gorm:"default:false"` // 是否启用
RecordType string `json:"record_type" gorm:"type:varchar(255)"`
}
// TableName 设置表名
func (RecordPlanStream) TableName() string {
return "record_plans_streams"
}
// ScopeStreamPathLike 模糊查询 StreamPath
func ScopeStreamPathLike(streamPath string) func(db *gorm.DB) *gorm.DB {
return func(db *gorm.DB) *gorm.DB {
if streamPath != "" {
return db.Where("record_plans_streams.stream_path LIKE ?", "%"+streamPath+"%")
}
return db
}
}
// ScopeOrderByCreatedAtDesc 按创建时间倒序
func ScopeOrderByCreatedAtDesc() func(db *gorm.DB) *gorm.DB {
return func(db *gorm.DB) *gorm.DB {
return db.Order("record_plans_streams.created_at DESC")
}
}
// ScopeRecordPlanID 按录制计划ID查询
func ScopeRecordPlanID(recordPlanID uint) func(db *gorm.DB) *gorm.DB {
return func(db *gorm.DB) *gorm.DB {
if recordPlanID > 0 {
return db.Where(&RecordPlanStream{PlanID: recordPlanID})
}
return db
}
}

View File

@@ -15,6 +15,7 @@ import (
"github.com/gorilla/websocket"
"github.com/shirou/gopsutil/v4/cpu"
"github.com/shirou/gopsutil/v4/process"
"m7s.live/v5/pkg/task"
)
//go:embed static/*
@@ -40,8 +41,17 @@ type consumer struct {
}
type server struct {
task.TickTask
consumers []consumer
consumersMutex sync.RWMutex
data DataStorage
lastPause uint32
dataMutex sync.RWMutex
lastConsumerID uint
upgrader websocket.Upgrader
prevSysTime float64
prevUserTime float64
myProcess *process.Process
}
type SimplePair struct {
@@ -75,99 +85,91 @@ const (
maxCount int = 86400
)
var (
data DataStorage
lastPause uint32
mutex sync.RWMutex
lastConsumerID uint
s server
upgrader = websocket.Upgrader{
func (s *server) Start() error {
var err error
s.myProcess, err = process.NewProcess(int32(os.Getpid()))
if err != nil {
log.Printf("Failed to get process: %v", err)
}
// 初始化 WebSocket upgrader
s.upgrader = websocket.Upgrader{
ReadBufferSize: 1024,
WriteBufferSize: 1024,
}
prevSysTime float64
prevUserTime float64
myProcess *process.Process
)
func init() {
myProcess, _ = process.NewProcess(int32(os.Getpid()))
// preallocate arrays in data, helps save on reallocations caused by append()
// when maxCount is large
data.BytesAllocated = make([]SimplePair, 0, maxCount)
data.GcPauses = make([]SimplePair, 0, maxCount)
data.CPUUsage = make([]CPUPair, 0, maxCount)
data.Pprof = make([]PprofPair, 0, maxCount)
go s.gatherData()
s.data.BytesAllocated = make([]SimplePair, 0, maxCount)
s.data.GcPauses = make([]SimplePair, 0, maxCount)
s.data.CPUUsage = make([]CPUPair, 0, maxCount)
s.data.Pprof = make([]PprofPair, 0, maxCount)
return s.TickTask.Start()
}
func (s *server) gatherData() {
timer := time.Tick(time.Second)
func (s *server) GetTickInterval() time.Duration {
return time.Second
}
for now := range timer {
nowUnix := now.Unix()
func (s *server) Tick(any) {
now := time.Now()
nowUnix := now.Unix()
var ms runtime.MemStats
runtime.ReadMemStats(&ms)
var ms runtime.MemStats
runtime.ReadMemStats(&ms)
u := update{
Ts: nowUnix * 1000,
Block: pprof.Lookup("block").Count(),
Goroutine: pprof.Lookup("goroutine").Count(),
Heap: pprof.Lookup("heap").Count(),
Mutex: pprof.Lookup("mutex").Count(),
Threadcreate: pprof.Lookup("threadcreate").Count(),
}
data.Pprof = append(data.Pprof, PprofPair{
uint64(nowUnix) * 1000,
u.Block,
u.Goroutine,
u.Heap,
u.Mutex,
u.Threadcreate,
})
cpuTimes, err := myProcess.Times()
if err != nil {
cpuTimes = &cpu.TimesStat{}
}
if prevUserTime != 0 {
u.CPUUser = cpuTimes.User - prevUserTime
u.CPUSys = cpuTimes.System - prevSysTime
data.CPUUsage = append(data.CPUUsage, CPUPair{uint64(nowUnix) * 1000, u.CPUUser, u.CPUSys})
}
prevUserTime = cpuTimes.User
prevSysTime = cpuTimes.System
mutex.Lock()
bytesAllocated := ms.Alloc
u.BytesAllocated = bytesAllocated
data.BytesAllocated = append(data.BytesAllocated, SimplePair{uint64(nowUnix) * 1000, bytesAllocated})
if lastPause == 0 || lastPause != ms.NumGC {
gcPause := ms.PauseNs[(ms.NumGC+255)%256]
u.GcPause = gcPause
data.GcPauses = append(data.GcPauses, SimplePair{uint64(nowUnix) * 1000, gcPause})
lastPause = ms.NumGC
}
if len(data.BytesAllocated) > maxCount {
data.BytesAllocated = data.BytesAllocated[len(data.BytesAllocated)-maxCount:]
}
if len(data.GcPauses) > maxCount {
data.GcPauses = data.GcPauses[len(data.GcPauses)-maxCount:]
}
mutex.Unlock()
s.sendToConsumers(u)
u := update{
Ts: nowUnix * 1000,
Block: pprof.Lookup("block").Count(),
Goroutine: pprof.Lookup("goroutine").Count(),
Heap: pprof.Lookup("heap").Count(),
Mutex: pprof.Lookup("mutex").Count(),
Threadcreate: pprof.Lookup("threadcreate").Count(),
}
s.data.Pprof = append(s.data.Pprof, PprofPair{
uint64(nowUnix) * 1000,
u.Block,
u.Goroutine,
u.Heap,
u.Mutex,
u.Threadcreate,
})
cpuTimes, err := s.myProcess.Times()
if err != nil {
cpuTimes = &cpu.TimesStat{}
}
if s.prevUserTime != 0 {
u.CPUUser = cpuTimes.User - s.prevUserTime
u.CPUSys = cpuTimes.System - s.prevSysTime
s.data.CPUUsage = append(s.data.CPUUsage, CPUPair{uint64(nowUnix) * 1000, u.CPUUser, u.CPUSys})
}
s.prevUserTime = cpuTimes.User
s.prevSysTime = cpuTimes.System
s.dataMutex.Lock()
bytesAllocated := ms.Alloc
u.BytesAllocated = bytesAllocated
s.data.BytesAllocated = append(s.data.BytesAllocated, SimplePair{uint64(nowUnix) * 1000, bytesAllocated})
if s.lastPause == 0 || s.lastPause != ms.NumGC {
gcPause := ms.PauseNs[(ms.NumGC+255)%256]
u.GcPause = gcPause
s.data.GcPauses = append(s.data.GcPauses, SimplePair{uint64(nowUnix) * 1000, gcPause})
s.lastPause = ms.NumGC
}
if len(s.data.BytesAllocated) > maxCount {
s.data.BytesAllocated = s.data.BytesAllocated[len(s.data.BytesAllocated)-maxCount:]
}
if len(s.data.GcPauses) > maxCount {
s.data.GcPauses = s.data.GcPauses[len(s.data.GcPauses)-maxCount:]
}
s.dataMutex.Unlock()
s.sendToConsumers(u)
}
func (s *server) sendToConsumers(u update) {
@@ -203,10 +205,10 @@ func (s *server) addConsumer() consumer {
s.consumersMutex.Lock()
defer s.consumersMutex.Unlock()
lastConsumerID++
s.lastConsumerID++
c := consumer{
id: lastConsumerID,
id: s.lastConsumerID,
c: make(chan update),
}
@@ -221,7 +223,7 @@ func (s *server) dataFeedHandler(w http.ResponseWriter, r *http.Request) {
lastPong time.Time
)
conn, err := upgrader.Upgrade(w, r, nil)
conn, err := s.upgrader.Upgrade(w, r, nil)
if err != nil {
log.Println(err)
return
@@ -268,9 +270,9 @@ func (s *server) dataFeedHandler(w http.ResponseWriter, r *http.Request) {
}
}
func dataHandler(w http.ResponseWriter, r *http.Request) {
mutex.RLock()
defer mutex.RUnlock()
func (s *server) dataHandler(w http.ResponseWriter, r *http.Request) {
s.dataMutex.RLock()
defer s.dataMutex.RUnlock()
if e := r.ParseForm(); e != nil {
log.Print("error parsing form")
@@ -284,7 +286,7 @@ func dataHandler(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
encoder := json.NewEncoder(w)
encoder.Encode(data)
encoder.Encode(s.data)
fmt.Fprint(w, ")")
}

219
plugin/debug/envcheck.go Normal file
View File

@@ -0,0 +1,219 @@
package plugin_debug
import (
"encoding/json"
"fmt"
"io"
"net"
"net/http"
"net/url"
"time"
"google.golang.org/protobuf/types/known/timestamppb"
"gopkg.in/yaml.v3"
"m7s.live/v5/pb"
"m7s.live/v5/pkg/util"
)
type EnvCheckResult struct {
Message string `json:"message"`
Type string `json:"type"` // info, success, error, complete
}
// 自定义系统信息响应结构体,用于 JSON 解析
type SysInfoResponseJSON struct {
Code int32 `json:"code"`
Message string `json:"message"`
Data struct {
StartTime string `json:"startTime"`
LocalIP string `json:"localIP"`
PublicIP string `json:"publicIP"`
Version string `json:"version"`
GoVersion string `json:"goVersion"`
OS string `json:"os"`
Arch string `json:"arch"`
CPUs int32 `json:"cpus"`
Plugins []struct {
Name string `json:"name"`
PushAddr []string `json:"pushAddr"`
PlayAddr []string `json:"playAddr"`
Description map[string]string `json:"description"`
} `json:"plugins"`
} `json:"data"`
}
// 插件配置响应结构体
type PluginConfigResponse struct {
Code int32 `json:"code"`
Message string `json:"message"`
Data struct {
File string `json:"file"`
Modified string `json:"modified"`
Merged string `json:"merged"`
} `json:"data"`
}
// TCP 配置结构体
type TCPConfig struct {
ListenAddr string `yaml:"listenaddr"`
ListenAddrTLS string `yaml:"listenaddrtls"`
}
// 插件配置结构体
type PluginConfig struct {
TCP TCPConfig `yaml:"tcp"`
}
func (p *DebugPlugin) EnvCheck(w http.ResponseWriter, r *http.Request) {
// Get target URL from query parameter
targetURL := r.URL.Query().Get("target")
if targetURL == "" {
r.URL.Path = "/static/envcheck.html"
staticFSHandler.ServeHTTP(w, r)
return
}
// Create SSE connection
util.NewSSE(w, r.Context(), func(sse *util.SSE) {
// Function to send SSE messages
sendMessage := func(message string, msgType string) {
result := EnvCheckResult{
Message: message,
Type: msgType,
}
sse.WriteJSON(result)
}
// Parse target URL
_, err := url.Parse(targetURL)
if err != nil {
sendMessage(fmt.Sprintf("Invalid URL: %v", err), "error")
return
}
// Check if we can connect to the target server
sendMessage(fmt.Sprintf("Checking connection to %s...", targetURL), "info")
// Get system info from target server
resp, err := http.Get(fmt.Sprintf("%s/api/sysinfo", targetURL))
if err != nil {
sendMessage(fmt.Sprintf("Failed to connect to target server: %v", err), "error")
return
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
sendMessage(fmt.Sprintf("Target server returned status code: %d", resp.StatusCode), "error")
return
}
// Read and parse system info
body, err := io.ReadAll(resp.Body)
if err != nil {
sendMessage(fmt.Sprintf("Failed to read response: %v", err), "error")
return
}
var sysInfoJSON SysInfoResponseJSON
if err := json.Unmarshal(body, &sysInfoJSON); err != nil {
sendMessage(fmt.Sprintf("Failed to parse system info: %v", err), "error")
return
}
// Convert JSON response to protobuf response
sysInfo := &pb.SysInfoResponse{
Code: sysInfoJSON.Code,
Message: sysInfoJSON.Message,
Data: &pb.SysInfoData{
LocalIP: sysInfoJSON.Data.LocalIP,
PublicIP: sysInfoJSON.Data.PublicIP,
Version: sysInfoJSON.Data.Version,
GoVersion: sysInfoJSON.Data.GoVersion,
Os: sysInfoJSON.Data.OS,
Arch: sysInfoJSON.Data.Arch,
Cpus: sysInfoJSON.Data.CPUs,
},
}
// Parse start time
if startTime, err := time.Parse(time.RFC3339, sysInfoJSON.Data.StartTime); err == nil {
sysInfo.Data.StartTime = timestamppb.New(startTime)
}
// Convert plugins
for _, pluginJSON := range sysInfoJSON.Data.Plugins {
plugin := &pb.PluginInfo{
Name: pluginJSON.Name,
PushAddr: pluginJSON.PushAddr,
PlayAddr: pluginJSON.PlayAddr,
Description: pluginJSON.Description,
}
sysInfo.Data.Plugins = append(sysInfo.Data.Plugins, plugin)
}
// Check each plugin's configuration
for _, plugin := range sysInfo.Data.Plugins {
// Get plugin configuration
configResp, err := http.Get(fmt.Sprintf("%s/api/config/get/%s", targetURL, plugin.Name))
if err != nil {
sendMessage(fmt.Sprintf("Failed to get configuration for plugin %s: %v", plugin.Name, err), "error")
continue
}
defer configResp.Body.Close()
if configResp.StatusCode != http.StatusOK {
sendMessage(fmt.Sprintf("Failed to get configuration for plugin %s: status code %d", plugin.Name, configResp.StatusCode), "error")
continue
}
var configRespJSON PluginConfigResponse
if err := json.NewDecoder(configResp.Body).Decode(&configRespJSON); err != nil {
sendMessage(fmt.Sprintf("Failed to parse configuration for plugin %s: %v", plugin.Name, err), "error")
continue
}
// Parse YAML configuration
var config PluginConfig
if err := yaml.Unmarshal([]byte(configRespJSON.Data.Merged), &config); err != nil {
sendMessage(fmt.Sprintf("Failed to parse YAML configuration for plugin %s: %v", plugin.Name, err), "error")
continue
}
// Check TCP configuration
if config.TCP.ListenAddr != "" {
host, port, err := net.SplitHostPort(config.TCP.ListenAddr)
if err != nil {
sendMessage(fmt.Sprintf("Invalid listenaddr format for plugin %s: %v", plugin.Name, err), "error")
} else {
sendMessage(fmt.Sprintf("Checking TCP listenaddr %s for plugin %s...", config.TCP.ListenAddr, plugin.Name), "info")
// Try to establish TCP connection
conn, err := net.DialTimeout("tcp", fmt.Sprintf("%s:%s", host, port), 5*time.Second)
if err != nil {
sendMessage(fmt.Sprintf("TCP listenaddr %s for plugin %s is not accessible: %v", config.TCP.ListenAddr, plugin.Name, err), "error")
} else {
conn.Close()
sendMessage(fmt.Sprintf("TCP listenaddr %s for plugin %s is accessible", config.TCP.ListenAddr, plugin.Name), "success")
}
}
}
if config.TCP.ListenAddrTLS != "" {
host, port, err := net.SplitHostPort(config.TCP.ListenAddrTLS)
if err != nil {
sendMessage(fmt.Sprintf("Invalid listenaddrtls format for plugin %s: %v", plugin.Name, err), "error")
} else {
sendMessage(fmt.Sprintf("Checking TCP TLS listenaddr %s for plugin %s...", config.TCP.ListenAddrTLS, plugin.Name), "info")
// Try to establish TCP connection
conn, err := net.DialTimeout("tcp", fmt.Sprintf("%s:%s", host, port), 5*time.Second)
if err != nil {
sendMessage(fmt.Sprintf("TCP TLS listenaddr %s for plugin %s is not accessible: %v", config.TCP.ListenAddrTLS, plugin.Name, err), "error")
} else {
conn.Close()
sendMessage(fmt.Sprintf("TCP TLS listenaddr %s for plugin %s is accessible", config.TCP.ListenAddrTLS, plugin.Name), "success")
}
}
}
}
sendMessage("Environment check completed", "complete")
})
}

View File

@@ -7,9 +7,11 @@ import (
"net/http"
"net/http/pprof"
"os"
"os/exec" // 新增导入
"runtime"
runtimePPROF "runtime/pprof"
"sort"
"strconv"
"strings"
"sync"
"time"
@@ -32,13 +34,13 @@ type DebugPlugin struct {
m7s.Plugin
ProfileDuration time.Duration `default:"10s" desc:"profile持续时间"`
Profile string `desc:"采集profile存储文件"`
ChartPeriod time.Duration `default:"1s" desc:"图表更新周期"`
Grfout string `default:"grf.out" desc:"grf输出文件"`
EnableChart bool `default:"true" desc:"是否启用图表功能"`
// 添加缓存字段
cpuProfileData *profile.Profile // 缓存 CPU Profile 数据
cpuProfileOnce sync.Once // 确保只采集一次
cpuProfileLock sync.Mutex // 保护缓存数据
chartServer server
}
type WriteToFile struct {
@@ -70,6 +72,10 @@ func (p *DebugPlugin) OnInit() error {
p.Info("cpu profile done")
}()
}
if p.EnableChart {
p.AddTask(&p.chartServer)
}
return nil
}
@@ -98,11 +104,11 @@ func (p *DebugPlugin) Charts_(w http.ResponseWriter, r *http.Request) {
}
func (p *DebugPlugin) Charts_data(w http.ResponseWriter, r *http.Request) {
dataHandler(w, r)
p.chartServer.dataHandler(w, r)
}
func (p *DebugPlugin) Charts_datafeed(w http.ResponseWriter, r *http.Request) {
s.dataFeedHandler(w, r)
p.chartServer.dataFeedHandler(w, r)
}
func (p *DebugPlugin) Grf(w http.ResponseWriter, r *http.Request) {
@@ -193,7 +199,7 @@ func (p *DebugPlugin) GetHeap(ctx context.Context, empty *emptypb.Empty) (*pb.He
obj.Size += size
totalSize += size
// 构建引<EFBFBD><EFBFBD><EFBFBD>关系
// 构建引关系
for i := 1; i < len(sample.Location); i++ {
loc := sample.Location[i]
if len(loc.Line) == 0 || loc.Line[0].Function == nil {
@@ -443,3 +449,42 @@ func (p *DebugPlugin) GetHeapGraph(ctx context.Context, empty *emptypb.Empty) (*
Data: dot,
}, nil
}
func (p *DebugPlugin) API_TcpDump(rw http.ResponseWriter, r *http.Request) {
query := r.URL.Query()
args := []string{"-W", "1"}
if query.Get("interface") != "" {
args = append(args, "-i", query.Get("interface"))
}
if query.Get("filter") != "" {
args = append(args, query.Get("filter"))
}
if query.Get("extra_args") != "" {
args = append(args, strings.Fields(query.Get("extra_args"))...)
}
if query.Get("duration") == "" {
http.Error(rw, "duration is required", http.StatusBadRequest)
return
}
rw.Header().Set("Content-Type", "text/plain")
rw.Header().Set("Cache-Control", "no-cache")
rw.Header().Set("Content-Disposition", "attachment; filename=tcpdump.txt")
cmd := exec.CommandContext(p, "tcpdump", args...)
p.Info("starting tcpdump", "args", strings.Join(cmd.Args, " "))
cmd.Stdout = rw
cmd.Stderr = os.Stderr // 将错误输出重定向到标准错误
err := cmd.Start()
if err != nil {
http.Error(rw, fmt.Sprintf("failed to start tcpdump: %v", err), http.StatusInternalServerError)
return
}
duration, err := strconv.Atoi(query.Get("duration"))
if err != nil {
http.Error(rw, "invalid duration", http.StatusBadRequest)
return
}
<-time.After(time.Duration(duration) * time.Second)
if err := cmd.Process.Kill(); err != nil {
p.Error("failed to kill tcpdump process", "error", err)
}
}

View File

@@ -1,7 +1,7 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.36.0
// protoc v5.29.1
// protoc-gen-go v1.36.6
// protoc v5.29.3
// source: debug.proto
package pb
@@ -14,6 +14,7 @@ import (
_ "google.golang.org/protobuf/types/known/timestamppb"
reflect "reflect"
sync "sync"
unsafe "unsafe"
)
const (
@@ -1007,176 +1008,107 @@ func (x *RuntimeStats) GetBlockingTimeNs() uint64 {
var File_debug_proto protoreflect.FileDescriptor
var file_debug_proto_rawDesc = []byte{
0x0a, 0x0b, 0x64, 0x65, 0x62, 0x75, 0x67, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x05, 0x64,
0x65, 0x62, 0x75, 0x67, 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,
0x22, 0x42, 0x0a, 0x0a, 0x43, 0x70, 0x75, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x18,
0x0a, 0x07, 0x72, 0x65, 0x66, 0x72, 0x65, 0x73, 0x68, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52,
0x07, 0x72, 0x65, 0x66, 0x72, 0x65, 0x73, 0x68, 0x12, 0x1a, 0x0a, 0x08, 0x64, 0x75, 0x72, 0x61,
0x74, 0x69, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x08, 0x64, 0x75, 0x72, 0x61,
0x74, 0x69, 0x6f, 0x6e, 0x22, 0x94, 0x01, 0x0a, 0x0a, 0x48, 0x65, 0x61, 0x70, 0x4f, 0x62, 0x6a,
0x65, 0x63, 0x74, 0x12, 0x12, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28,
0x09, 0x52, 0x04, 0x74, 0x79, 0x70, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x63, 0x6f, 0x75, 0x6e, 0x74,
0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x05, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x12, 0x0a,
0x04, 0x73, 0x69, 0x7a, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x03, 0x52, 0x04, 0x73, 0x69, 0x7a,
0x65, 0x12, 0x1a, 0x0a, 0x08, 0x73, 0x69, 0x7a, 0x65, 0x50, 0x65, 0x72, 0x63, 0x18, 0x04, 0x20,
0x01, 0x28, 0x01, 0x52, 0x08, 0x73, 0x69, 0x7a, 0x65, 0x50, 0x65, 0x72, 0x63, 0x12, 0x18, 0x0a,
0x07, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07,
0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x12, 0x12, 0x0a, 0x04, 0x72, 0x65, 0x66, 0x73, 0x18,
0x06, 0x20, 0x03, 0x28, 0x09, 0x52, 0x04, 0x72, 0x65, 0x66, 0x73, 0x22, 0xc7, 0x02, 0x0a, 0x09,
0x48, 0x65, 0x61, 0x70, 0x53, 0x74, 0x61, 0x74, 0x73, 0x12, 0x14, 0x0a, 0x05, 0x61, 0x6c, 0x6c,
0x6f, 0x63, 0x18, 0x01, 0x20, 0x01, 0x28, 0x04, 0x52, 0x05, 0x61, 0x6c, 0x6c, 0x6f, 0x63, 0x12,
0x1e, 0x0a, 0x0a, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x41, 0x6c, 0x6c, 0x6f, 0x63, 0x18, 0x02, 0x20,
0x01, 0x28, 0x04, 0x52, 0x0a, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x41, 0x6c, 0x6c, 0x6f, 0x63, 0x12,
0x10, 0x0a, 0x03, 0x73, 0x79, 0x73, 0x18, 0x03, 0x20, 0x01, 0x28, 0x04, 0x52, 0x03, 0x73, 0x79,
0x73, 0x12, 0x14, 0x0a, 0x05, 0x6e, 0x75, 0x6d, 0x47, 0x43, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0d,
0x52, 0x05, 0x6e, 0x75, 0x6d, 0x47, 0x43, 0x12, 0x1c, 0x0a, 0x09, 0x68, 0x65, 0x61, 0x70, 0x41,
0x6c, 0x6c, 0x6f, 0x63, 0x18, 0x05, 0x20, 0x01, 0x28, 0x04, 0x52, 0x09, 0x68, 0x65, 0x61, 0x70,
0x41, 0x6c, 0x6c, 0x6f, 0x63, 0x12, 0x18, 0x0a, 0x07, 0x68, 0x65, 0x61, 0x70, 0x53, 0x79, 0x73,
0x18, 0x06, 0x20, 0x01, 0x28, 0x04, 0x52, 0x07, 0x68, 0x65, 0x61, 0x70, 0x53, 0x79, 0x73, 0x12,
0x1a, 0x0a, 0x08, 0x68, 0x65, 0x61, 0x70, 0x49, 0x64, 0x6c, 0x65, 0x18, 0x07, 0x20, 0x01, 0x28,
0x04, 0x52, 0x08, 0x68, 0x65, 0x61, 0x70, 0x49, 0x64, 0x6c, 0x65, 0x12, 0x1c, 0x0a, 0x09, 0x68,
0x65, 0x61, 0x70, 0x49, 0x6e, 0x75, 0x73, 0x65, 0x18, 0x08, 0x20, 0x01, 0x28, 0x04, 0x52, 0x09,
0x68, 0x65, 0x61, 0x70, 0x49, 0x6e, 0x75, 0x73, 0x65, 0x12, 0x22, 0x0a, 0x0c, 0x68, 0x65, 0x61,
0x70, 0x52, 0x65, 0x6c, 0x65, 0x61, 0x73, 0x65, 0x64, 0x18, 0x09, 0x20, 0x01, 0x28, 0x04, 0x52,
0x0c, 0x68, 0x65, 0x61, 0x70, 0x52, 0x65, 0x6c, 0x65, 0x61, 0x73, 0x65, 0x64, 0x12, 0x20, 0x0a,
0x0b, 0x68, 0x65, 0x61, 0x70, 0x4f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x73, 0x18, 0x0a, 0x20, 0x01,
0x28, 0x04, 0x52, 0x0b, 0x68, 0x65, 0x61, 0x70, 0x4f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x73, 0x12,
0x24, 0x0a, 0x0d, 0x67, 0x63, 0x43, 0x50, 0x55, 0x46, 0x72, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e,
0x18, 0x0b, 0x20, 0x01, 0x28, 0x01, 0x52, 0x0d, 0x67, 0x63, 0x43, 0x50, 0x55, 0x46, 0x72, 0x61,
0x63, 0x74, 0x69, 0x6f, 0x6e, 0x22, 0x86, 0x01, 0x0a, 0x08, 0x48, 0x65, 0x61, 0x70, 0x44, 0x61,
0x74, 0x61, 0x12, 0x26, 0x0a, 0x05, 0x73, 0x74, 0x61, 0x74, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28,
0x0b, 0x32, 0x10, 0x2e, 0x64, 0x65, 0x62, 0x75, 0x67, 0x2e, 0x48, 0x65, 0x61, 0x70, 0x53, 0x74,
0x61, 0x74, 0x73, 0x52, 0x05, 0x73, 0x74, 0x61, 0x74, 0x73, 0x12, 0x2b, 0x0a, 0x07, 0x6f, 0x62,
0x6a, 0x65, 0x63, 0x74, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x11, 0x2e, 0x64, 0x65,
0x62, 0x75, 0x67, 0x2e, 0x48, 0x65, 0x61, 0x70, 0x4f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x52, 0x07,
0x6f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x73, 0x12, 0x25, 0x0a, 0x05, 0x65, 0x64, 0x67, 0x65, 0x73,
0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x0f, 0x2e, 0x64, 0x65, 0x62, 0x75, 0x67, 0x2e, 0x48,
0x65, 0x61, 0x70, 0x45, 0x64, 0x67, 0x65, 0x52, 0x05, 0x65, 0x64, 0x67, 0x65, 0x73, 0x22, 0x4c,
0x0a, 0x08, 0x48, 0x65, 0x61, 0x70, 0x45, 0x64, 0x67, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x66, 0x72,
0x6f, 0x6d, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x66, 0x72, 0x6f, 0x6d, 0x12, 0x0e,
0x0a, 0x02, 0x74, 0x6f, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x74, 0x6f, 0x12, 0x1c,
0x0a, 0x09, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x4e, 0x61, 0x6d, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28,
0x09, 0x52, 0x09, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x4e, 0x61, 0x6d, 0x65, 0x22, 0x61, 0x0a, 0x0c,
0x48, 0x65, 0x61, 0x70, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x12, 0x0a, 0x04,
0x63, 0x6f, 0x64, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0d, 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, 0x23, 0x0a, 0x04, 0x64, 0x61,
0x74, 0x61, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0f, 0x2e, 0x64, 0x65, 0x62, 0x75, 0x67,
0x2e, 0x48, 0x65, 0x61, 0x70, 0x44, 0x61, 0x74, 0x61, 0x52, 0x04, 0x64, 0x61, 0x74, 0x61, 0x22,
0x55, 0x0a, 0x11, 0x48, 0x65, 0x61, 0x70, 0x47, 0x72, 0x61, 0x70, 0x68, 0x52, 0x65, 0x73, 0x70,
0x6f, 0x6e, 0x73, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x63, 0x6f, 0x64, 0x65, 0x18, 0x01, 0x20, 0x01,
0x28, 0x0d, 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, 0x09,
0x52, 0x04, 0x64, 0x61, 0x74, 0x61, 0x22, 0x54, 0x0a, 0x10, 0x43, 0x70, 0x75, 0x47, 0x72, 0x61,
0x70, 0x68, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x63, 0x6f,
0x64, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0d, 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, 0x09, 0x52, 0x04, 0x64, 0x61, 0x74, 0x61, 0x22, 0x5f, 0x0a, 0x0b,
0x43, 0x70, 0x75, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x63,
0x6f, 0x64, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0d, 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, 0x22, 0x0a, 0x04, 0x64, 0x61, 0x74,
0x61, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0e, 0x2e, 0x64, 0x65, 0x62, 0x75, 0x67, 0x2e,
0x43, 0x70, 0x75, 0x44, 0x61, 0x74, 0x61, 0x52, 0x04, 0x64, 0x61, 0x74, 0x61, 0x22, 0xc5, 0x02,
0x0a, 0x07, 0x43, 0x70, 0x75, 0x44, 0x61, 0x74, 0x61, 0x12, 0x29, 0x0a, 0x11, 0x74, 0x6f, 0x74,
0x61, 0x6c, 0x5f, 0x63, 0x70, 0x75, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x5f, 0x6e, 0x73, 0x18, 0x01,
0x20, 0x01, 0x28, 0x04, 0x52, 0x0e, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x43, 0x70, 0x75, 0x54, 0x69,
0x6d, 0x65, 0x4e, 0x73, 0x12, 0x30, 0x0a, 0x14, 0x73, 0x61, 0x6d, 0x70, 0x6c, 0x69, 0x6e, 0x67,
0x5f, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x76, 0x61, 0x6c, 0x5f, 0x6e, 0x73, 0x18, 0x02, 0x20, 0x01,
0x28, 0x04, 0x52, 0x12, 0x73, 0x61, 0x6d, 0x70, 0x6c, 0x69, 0x6e, 0x67, 0x49, 0x6e, 0x74, 0x65,
0x72, 0x76, 0x61, 0x6c, 0x4e, 0x73, 0x12, 0x34, 0x0a, 0x09, 0x66, 0x75, 0x6e, 0x63, 0x74, 0x69,
0x6f, 0x6e, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x64, 0x65, 0x62, 0x75,
0x67, 0x2e, 0x46, 0x75, 0x6e, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x50, 0x72, 0x6f, 0x66, 0x69, 0x6c,
0x65, 0x52, 0x09, 0x66, 0x75, 0x6e, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x37, 0x0a, 0x0a,
0x67, 0x6f, 0x72, 0x6f, 0x75, 0x74, 0x69, 0x6e, 0x65, 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, 0x0b,
0x32, 0x17, 0x2e, 0x64, 0x65, 0x62, 0x75, 0x67, 0x2e, 0x47, 0x6f, 0x72, 0x6f, 0x75, 0x74, 0x69,
0x6e, 0x65, 0x50, 0x72, 0x6f, 0x66, 0x69, 0x6c, 0x65, 0x52, 0x0a, 0x67, 0x6f, 0x72, 0x6f, 0x75,
0x74, 0x69, 0x6e, 0x65, 0x73, 0x12, 0x34, 0x0a, 0x0c, 0x73, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x5f,
0x63, 0x61, 0x6c, 0x6c, 0x73, 0x18, 0x05, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x11, 0x2e, 0x64, 0x65,
0x62, 0x75, 0x67, 0x2e, 0x53, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x43, 0x61, 0x6c, 0x6c, 0x52, 0x0b,
0x73, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x43, 0x61, 0x6c, 0x6c, 0x73, 0x12, 0x38, 0x0a, 0x0d, 0x72,
0x75, 0x6e, 0x74, 0x69, 0x6d, 0x65, 0x5f, 0x73, 0x74, 0x61, 0x74, 0x73, 0x18, 0x06, 0x20, 0x01,
0x28, 0x0b, 0x32, 0x13, 0x2e, 0x64, 0x65, 0x62, 0x75, 0x67, 0x2e, 0x52, 0x75, 0x6e, 0x74, 0x69,
0x6d, 0x65, 0x53, 0x74, 0x61, 0x74, 0x73, 0x52, 0x0c, 0x72, 0x75, 0x6e, 0x74, 0x69, 0x6d, 0x65,
0x53, 0x74, 0x61, 0x74, 0x73, 0x22, 0xbf, 0x01, 0x0a, 0x0f, 0x46, 0x75, 0x6e, 0x63, 0x74, 0x69,
0x6f, 0x6e, 0x50, 0x72, 0x6f, 0x66, 0x69, 0x6c, 0x65, 0x12, 0x23, 0x0a, 0x0d, 0x66, 0x75, 0x6e,
0x63, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09,
0x52, 0x0c, 0x66, 0x75, 0x6e, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x1e,
0x0a, 0x0b, 0x63, 0x70, 0x75, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x5f, 0x6e, 0x73, 0x18, 0x02, 0x20,
0x01, 0x28, 0x04, 0x52, 0x09, 0x63, 0x70, 0x75, 0x54, 0x69, 0x6d, 0x65, 0x4e, 0x73, 0x12, 0x29,
0x0a, 0x10, 0x69, 0x6e, 0x76, 0x6f, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x63, 0x6f, 0x75,
0x6e, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x04, 0x52, 0x0f, 0x69, 0x6e, 0x76, 0x6f, 0x63, 0x61,
0x74, 0x69, 0x6f, 0x6e, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x1d, 0x0a, 0x0a, 0x63, 0x61, 0x6c,
0x6c, 0x5f, 0x73, 0x74, 0x61, 0x63, 0x6b, 0x18, 0x04, 0x20, 0x03, 0x28, 0x09, 0x52, 0x09, 0x63,
0x61, 0x6c, 0x6c, 0x53, 0x74, 0x61, 0x63, 0x6b, 0x12, 0x1d, 0x0a, 0x0a, 0x69, 0x73, 0x5f, 0x69,
0x6e, 0x6c, 0x69, 0x6e, 0x65, 0x64, 0x18, 0x05, 0x20, 0x01, 0x28, 0x08, 0x52, 0x09, 0x69, 0x73,
0x49, 0x6e, 0x6c, 0x69, 0x6e, 0x65, 0x64, 0x22, 0x77, 0x0a, 0x10, 0x47, 0x6f, 0x72, 0x6f, 0x75,
0x74, 0x69, 0x6e, 0x65, 0x50, 0x72, 0x6f, 0x66, 0x69, 0x6c, 0x65, 0x12, 0x0e, 0x0a, 0x02, 0x69,
0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x04, 0x52, 0x02, 0x69, 0x64, 0x12, 0x14, 0x0a, 0x05, 0x73,
0x74, 0x61, 0x74, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x73, 0x74, 0x61, 0x74,
0x65, 0x12, 0x1e, 0x0a, 0x0b, 0x63, 0x70, 0x75, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x5f, 0x6e, 0x73,
0x18, 0x03, 0x20, 0x01, 0x28, 0x04, 0x52, 0x09, 0x63, 0x70, 0x75, 0x54, 0x69, 0x6d, 0x65, 0x4e,
0x73, 0x12, 0x1d, 0x0a, 0x0a, 0x63, 0x61, 0x6c, 0x6c, 0x5f, 0x73, 0x74, 0x61, 0x63, 0x6b, 0x18,
0x04, 0x20, 0x03, 0x28, 0x09, 0x52, 0x09, 0x63, 0x61, 0x6c, 0x6c, 0x53, 0x74, 0x61, 0x63, 0x6b,
0x22, 0x56, 0x0a, 0x0a, 0x53, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x43, 0x61, 0x6c, 0x6c, 0x12, 0x12,
0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61,
0x6d, 0x65, 0x12, 0x1e, 0x0a, 0x0b, 0x63, 0x70, 0x75, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x5f, 0x6e,
0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x04, 0x52, 0x09, 0x63, 0x70, 0x75, 0x54, 0x69, 0x6d, 0x65,
0x4e, 0x73, 0x12, 0x14, 0x0a, 0x05, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28,
0x04, 0x52, 0x05, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x22, 0xa4, 0x01, 0x0a, 0x0c, 0x52, 0x75, 0x6e,
0x74, 0x69, 0x6d, 0x65, 0x53, 0x74, 0x61, 0x74, 0x73, 0x12, 0x26, 0x0a, 0x0f, 0x67, 0x63, 0x5f,
0x63, 0x70, 0x75, 0x5f, 0x66, 0x72, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x01, 0x20, 0x01,
0x28, 0x01, 0x52, 0x0d, 0x67, 0x63, 0x43, 0x70, 0x75, 0x46, 0x72, 0x61, 0x63, 0x74, 0x69, 0x6f,
0x6e, 0x12, 0x19, 0x0a, 0x08, 0x67, 0x63, 0x5f, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x02, 0x20,
0x01, 0x28, 0x04, 0x52, 0x07, 0x67, 0x63, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x27, 0x0a, 0x10,
0x67, 0x63, 0x5f, 0x70, 0x61, 0x75, 0x73, 0x65, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x5f, 0x6e, 0x73,
0x18, 0x03, 0x20, 0x01, 0x28, 0x04, 0x52, 0x0d, 0x67, 0x63, 0x50, 0x61, 0x75, 0x73, 0x65, 0x54,
0x69, 0x6d, 0x65, 0x4e, 0x73, 0x12, 0x28, 0x0a, 0x10, 0x62, 0x6c, 0x6f, 0x63, 0x6b, 0x69, 0x6e,
0x67, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x5f, 0x6e, 0x73, 0x18, 0x04, 0x20, 0x01, 0x28, 0x04, 0x52,
0x0e, 0x62, 0x6c, 0x6f, 0x63, 0x6b, 0x69, 0x6e, 0x67, 0x54, 0x69, 0x6d, 0x65, 0x4e, 0x73, 0x32,
0xd9, 0x02, 0x0a, 0x03, 0x61, 0x70, 0x69, 0x12, 0x4f, 0x0a, 0x07, 0x47, 0x65, 0x74, 0x48, 0x65,
0x61, 0x70, 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, 0x13, 0x2e, 0x64, 0x65, 0x62,
0x75, 0x67, 0x2e, 0x48, 0x65, 0x61, 0x70, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22,
0x17, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x11, 0x12, 0x0f, 0x2f, 0x64, 0x65, 0x62, 0x75, 0x67, 0x2f,
0x61, 0x70, 0x69, 0x2f, 0x68, 0x65, 0x61, 0x70, 0x12, 0x5f, 0x0a, 0x0c, 0x47, 0x65, 0x74, 0x48,
0x65, 0x61, 0x70, 0x47, 0x72, 0x61, 0x70, 0x68, 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, 0x18, 0x2e, 0x64, 0x65, 0x62, 0x75, 0x67, 0x2e, 0x48, 0x65, 0x61, 0x70, 0x47, 0x72, 0x61,
0x70, 0x68, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x1d, 0x82, 0xd3, 0xe4, 0x93,
0x02, 0x17, 0x12, 0x15, 0x2f, 0x64, 0x65, 0x62, 0x75, 0x67, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x68,
0x65, 0x61, 0x70, 0x2f, 0x67, 0x72, 0x61, 0x70, 0x68, 0x12, 0x57, 0x0a, 0x0b, 0x47, 0x65, 0x74,
0x43, 0x70, 0x75, 0x47, 0x72, 0x61, 0x70, 0x68, 0x12, 0x11, 0x2e, 0x64, 0x65, 0x62, 0x75, 0x67,
0x2e, 0x43, 0x70, 0x75, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x17, 0x2e, 0x64, 0x65,
0x62, 0x75, 0x67, 0x2e, 0x43, 0x70, 0x75, 0x47, 0x72, 0x61, 0x70, 0x68, 0x52, 0x65, 0x73, 0x70,
0x6f, 0x6e, 0x73, 0x65, 0x22, 0x1c, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x16, 0x12, 0x14, 0x2f, 0x64,
0x65, 0x62, 0x75, 0x67, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x63, 0x70, 0x75, 0x2f, 0x67, 0x72, 0x61,
0x70, 0x68, 0x12, 0x47, 0x0a, 0x06, 0x47, 0x65, 0x74, 0x43, 0x70, 0x75, 0x12, 0x11, 0x2e, 0x64,
0x65, 0x62, 0x75, 0x67, 0x2e, 0x43, 0x70, 0x75, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a,
0x12, 0x2e, 0x64, 0x65, 0x62, 0x75, 0x67, 0x2e, 0x43, 0x70, 0x75, 0x52, 0x65, 0x73, 0x70, 0x6f,
0x6e, 0x73, 0x65, 0x22, 0x16, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x10, 0x12, 0x0e, 0x2f, 0x64, 0x65,
0x62, 0x75, 0x67, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x63, 0x70, 0x75, 0x42, 0x1d, 0x5a, 0x1b, 0x6d,
0x37, 0x73, 0x2e, 0x6c, 0x69, 0x76, 0x65, 0x2f, 0x76, 0x35, 0x2f, 0x70, 0x6c, 0x75, 0x67, 0x69,
0x6e, 0x2f, 0x64, 0x65, 0x62, 0x75, 0x67, 0x2f, 0x70, 0x62, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74,
0x6f, 0x33,
}
const file_debug_proto_rawDesc = "" +
"\n" +
"\vdebug.proto\x12\x05debug\x1a\x1cgoogle/api/annotations.proto\x1a\x1bgoogle/protobuf/empty.proto\x1a\x1fgoogle/protobuf/timestamp.proto\"B\n" +
"\n" +
"CpuRequest\x12\x18\n" +
"\arefresh\x18\x01 \x01(\bR\arefresh\x12\x1a\n" +
"\bduration\x18\x02 \x01(\rR\bduration\"\x94\x01\n" +
"\n" +
"HeapObject\x12\x12\n" +
"\x04type\x18\x01 \x01(\tR\x04type\x12\x14\n" +
"\x05count\x18\x02 \x01(\x03R\x05count\x12\x12\n" +
"\x04size\x18\x03 \x01(\x03R\x04size\x12\x1a\n" +
"\bsizePerc\x18\x04 \x01(\x01R\bsizePerc\x12\x18\n" +
"\aaddress\x18\x05 \x01(\tR\aaddress\x12\x12\n" +
"\x04refs\x18\x06 \x03(\tR\x04refs\"\xc7\x02\n" +
"\tHeapStats\x12\x14\n" +
"\x05alloc\x18\x01 \x01(\x04R\x05alloc\x12\x1e\n" +
"\n" +
"totalAlloc\x18\x02 \x01(\x04R\n" +
"totalAlloc\x12\x10\n" +
"\x03sys\x18\x03 \x01(\x04R\x03sys\x12\x14\n" +
"\x05numGC\x18\x04 \x01(\rR\x05numGC\x12\x1c\n" +
"\theapAlloc\x18\x05 \x01(\x04R\theapAlloc\x12\x18\n" +
"\aheapSys\x18\x06 \x01(\x04R\aheapSys\x12\x1a\n" +
"\bheapIdle\x18\a \x01(\x04R\bheapIdle\x12\x1c\n" +
"\theapInuse\x18\b \x01(\x04R\theapInuse\x12\"\n" +
"\fheapReleased\x18\t \x01(\x04R\fheapReleased\x12 \n" +
"\vheapObjects\x18\n" +
" \x01(\x04R\vheapObjects\x12$\n" +
"\rgcCPUFraction\x18\v \x01(\x01R\rgcCPUFraction\"\x86\x01\n" +
"\bHeapData\x12&\n" +
"\x05stats\x18\x01 \x01(\v2\x10.debug.HeapStatsR\x05stats\x12+\n" +
"\aobjects\x18\x02 \x03(\v2\x11.debug.HeapObjectR\aobjects\x12%\n" +
"\x05edges\x18\x03 \x03(\v2\x0f.debug.HeapEdgeR\x05edges\"L\n" +
"\bHeapEdge\x12\x12\n" +
"\x04from\x18\x01 \x01(\tR\x04from\x12\x0e\n" +
"\x02to\x18\x02 \x01(\tR\x02to\x12\x1c\n" +
"\tfieldName\x18\x03 \x01(\tR\tfieldName\"a\n" +
"\fHeapResponse\x12\x12\n" +
"\x04code\x18\x01 \x01(\rR\x04code\x12\x18\n" +
"\amessage\x18\x02 \x01(\tR\amessage\x12#\n" +
"\x04data\x18\x03 \x01(\v2\x0f.debug.HeapDataR\x04data\"U\n" +
"\x11HeapGraphResponse\x12\x12\n" +
"\x04code\x18\x01 \x01(\rR\x04code\x12\x18\n" +
"\amessage\x18\x02 \x01(\tR\amessage\x12\x12\n" +
"\x04data\x18\x03 \x01(\tR\x04data\"T\n" +
"\x10CpuGraphResponse\x12\x12\n" +
"\x04code\x18\x01 \x01(\rR\x04code\x12\x18\n" +
"\amessage\x18\x02 \x01(\tR\amessage\x12\x12\n" +
"\x04data\x18\x03 \x01(\tR\x04data\"_\n" +
"\vCpuResponse\x12\x12\n" +
"\x04code\x18\x01 \x01(\rR\x04code\x12\x18\n" +
"\amessage\x18\x02 \x01(\tR\amessage\x12\"\n" +
"\x04data\x18\x03 \x01(\v2\x0e.debug.CpuDataR\x04data\"\xc5\x02\n" +
"\aCpuData\x12)\n" +
"\x11total_cpu_time_ns\x18\x01 \x01(\x04R\x0etotalCpuTimeNs\x120\n" +
"\x14sampling_interval_ns\x18\x02 \x01(\x04R\x12samplingIntervalNs\x124\n" +
"\tfunctions\x18\x03 \x03(\v2\x16.debug.FunctionProfileR\tfunctions\x127\n" +
"\n" +
"goroutines\x18\x04 \x03(\v2\x17.debug.GoroutineProfileR\n" +
"goroutines\x124\n" +
"\fsystem_calls\x18\x05 \x03(\v2\x11.debug.SystemCallR\vsystemCalls\x128\n" +
"\rruntime_stats\x18\x06 \x01(\v2\x13.debug.RuntimeStatsR\fruntimeStats\"\xbf\x01\n" +
"\x0fFunctionProfile\x12#\n" +
"\rfunction_name\x18\x01 \x01(\tR\ffunctionName\x12\x1e\n" +
"\vcpu_time_ns\x18\x02 \x01(\x04R\tcpuTimeNs\x12)\n" +
"\x10invocation_count\x18\x03 \x01(\x04R\x0finvocationCount\x12\x1d\n" +
"\n" +
"call_stack\x18\x04 \x03(\tR\tcallStack\x12\x1d\n" +
"\n" +
"is_inlined\x18\x05 \x01(\bR\tisInlined\"w\n" +
"\x10GoroutineProfile\x12\x0e\n" +
"\x02id\x18\x01 \x01(\x04R\x02id\x12\x14\n" +
"\x05state\x18\x02 \x01(\tR\x05state\x12\x1e\n" +
"\vcpu_time_ns\x18\x03 \x01(\x04R\tcpuTimeNs\x12\x1d\n" +
"\n" +
"call_stack\x18\x04 \x03(\tR\tcallStack\"V\n" +
"\n" +
"SystemCall\x12\x12\n" +
"\x04name\x18\x01 \x01(\tR\x04name\x12\x1e\n" +
"\vcpu_time_ns\x18\x02 \x01(\x04R\tcpuTimeNs\x12\x14\n" +
"\x05count\x18\x03 \x01(\x04R\x05count\"\xa4\x01\n" +
"\fRuntimeStats\x12&\n" +
"\x0fgc_cpu_fraction\x18\x01 \x01(\x01R\rgcCpuFraction\x12\x19\n" +
"\bgc_count\x18\x02 \x01(\x04R\agcCount\x12'\n" +
"\x10gc_pause_time_ns\x18\x03 \x01(\x04R\rgcPauseTimeNs\x12(\n" +
"\x10blocking_time_ns\x18\x04 \x01(\x04R\x0eblockingTimeNs2\xd9\x02\n" +
"\x03api\x12O\n" +
"\aGetHeap\x12\x16.google.protobuf.Empty\x1a\x13.debug.HeapResponse\"\x17\x82\xd3\xe4\x93\x02\x11\x12\x0f/debug/api/heap\x12_\n" +
"\fGetHeapGraph\x12\x16.google.protobuf.Empty\x1a\x18.debug.HeapGraphResponse\"\x1d\x82\xd3\xe4\x93\x02\x17\x12\x15/debug/api/heap/graph\x12W\n" +
"\vGetCpuGraph\x12\x11.debug.CpuRequest\x1a\x17.debug.CpuGraphResponse\"\x1c\x82\xd3\xe4\x93\x02\x16\x12\x14/debug/api/cpu/graph\x12G\n" +
"\x06GetCpu\x12\x11.debug.CpuRequest\x1a\x12.debug.CpuResponse\"\x16\x82\xd3\xe4\x93\x02\x10\x12\x0e/debug/api/cpuB\x1dZ\x1bm7s.live/v5/plugin/debug/pbb\x06proto3"
var (
file_debug_proto_rawDescOnce sync.Once
file_debug_proto_rawDescData = file_debug_proto_rawDesc
file_debug_proto_rawDescData []byte
)
func file_debug_proto_rawDescGZIP() []byte {
file_debug_proto_rawDescOnce.Do(func() {
file_debug_proto_rawDescData = protoimpl.X.CompressGZIP(file_debug_proto_rawDescData)
file_debug_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_debug_proto_rawDesc), len(file_debug_proto_rawDesc)))
})
return file_debug_proto_rawDescData
}
@@ -1233,7 +1165,7 @@ func file_debug_proto_init() {
out := protoimpl.TypeBuilder{
File: protoimpl.DescBuilder{
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: file_debug_proto_rawDesc,
RawDescriptor: unsafe.Slice(unsafe.StringData(file_debug_proto_rawDesc), len(file_debug_proto_rawDesc)),
NumEnums: 0,
NumMessages: 14,
NumExtensions: 0,
@@ -1244,7 +1176,6 @@ func file_debug_proto_init() {
MessageInfos: file_debug_proto_msgTypes,
}.Build()
File_debug_proto = out.File
file_debug_proto_rawDesc = nil
file_debug_proto_goTypes = nil
file_debug_proto_depIdxs = nil
}

View File

@@ -10,7 +10,6 @@ package pb
import (
"context"
"errors"
"io"
"net/http"
@@ -26,129 +25,136 @@ import (
)
// Suppress "imported and not used" errors
var (
_ codes.Code
_ io.Reader
_ status.Status
_ = errors.New
_ = runtime.String
_ = utilities.NewDoubleArray
_ = metadata.Join
)
var _ codes.Code
var _ io.Reader
var _ status.Status
var _ = runtime.String
var _ = utilities.NewDoubleArray
var _ = metadata.Join
func request_Api_GetHeap_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
metadata runtime.ServerMetadata
)
var protoReq emptypb.Empty
var metadata runtime.ServerMetadata
msg, err := client.GetHeap(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))
return msg, metadata, err
}
func local_request_Api_GetHeap_0(ctx context.Context, marshaler runtime.Marshaler, server ApiServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
var (
protoReq emptypb.Empty
metadata runtime.ServerMetadata
)
var protoReq emptypb.Empty
var metadata runtime.ServerMetadata
msg, err := server.GetHeap(ctx, &protoReq)
return msg, metadata, err
}
func request_Api_GetHeapGraph_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
metadata runtime.ServerMetadata
)
var protoReq emptypb.Empty
var metadata runtime.ServerMetadata
msg, err := client.GetHeapGraph(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))
return msg, metadata, err
}
func local_request_Api_GetHeapGraph_0(ctx context.Context, marshaler runtime.Marshaler, server ApiServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
var (
protoReq emptypb.Empty
metadata runtime.ServerMetadata
)
var protoReq emptypb.Empty
var metadata runtime.ServerMetadata
msg, err := server.GetHeapGraph(ctx, &protoReq)
return msg, metadata, err
}
var filter_Api_GetCpuGraph_0 = &utilities.DoubleArray{Encoding: map[string]int{}, Base: []int(nil), Check: []int(nil)}
var (
filter_Api_GetCpuGraph_0 = &utilities.DoubleArray{Encoding: map[string]int{}, Base: []int(nil), Check: []int(nil)}
)
func request_Api_GetCpuGraph_0(ctx context.Context, marshaler runtime.Marshaler, client ApiClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
var (
protoReq CpuRequest
metadata runtime.ServerMetadata
)
var protoReq CpuRequest
var metadata runtime.ServerMetadata
if err := req.ParseForm(); err != nil {
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
}
if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_Api_GetCpuGraph_0); err != nil {
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
}
msg, err := client.GetCpuGraph(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))
return msg, metadata, err
}
func local_request_Api_GetCpuGraph_0(ctx context.Context, marshaler runtime.Marshaler, server ApiServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
var (
protoReq CpuRequest
metadata runtime.ServerMetadata
)
var protoReq CpuRequest
var metadata runtime.ServerMetadata
if err := req.ParseForm(); err != nil {
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
}
if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_Api_GetCpuGraph_0); err != nil {
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
}
msg, err := server.GetCpuGraph(ctx, &protoReq)
return msg, metadata, err
}
var filter_Api_GetCpu_0 = &utilities.DoubleArray{Encoding: map[string]int{}, Base: []int(nil), Check: []int(nil)}
var (
filter_Api_GetCpu_0 = &utilities.DoubleArray{Encoding: map[string]int{}, Base: []int(nil), Check: []int(nil)}
)
func request_Api_GetCpu_0(ctx context.Context, marshaler runtime.Marshaler, client ApiClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
var (
protoReq CpuRequest
metadata runtime.ServerMetadata
)
var protoReq CpuRequest
var metadata runtime.ServerMetadata
if err := req.ParseForm(); err != nil {
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
}
if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_Api_GetCpu_0); err != nil {
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
}
msg, err := client.GetCpu(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))
return msg, metadata, err
}
func local_request_Api_GetCpu_0(ctx context.Context, marshaler runtime.Marshaler, server ApiServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
var (
protoReq CpuRequest
metadata runtime.ServerMetadata
)
var protoReq CpuRequest
var metadata runtime.ServerMetadata
if err := req.ParseForm(); err != nil {
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
}
if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_Api_GetCpu_0); err != nil {
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
}
msg, err := server.GetCpu(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(http.MethodGet, pattern_Api_GetHeap_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
mux.Handle("GET", pattern_Api_GetHeap_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
ctx, cancel := context.WithCancel(req.Context())
defer cancel()
var stream runtime.ServerTransportStream
ctx = grpc.NewContextWithServerTransportStream(ctx, &stream)
inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
annotatedContext, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/debug.Api/GetHeap", runtime.WithHTTPPathPattern("/debug/api/heap"))
var err error
var annotatedContext context.Context
annotatedContext, err = runtime.AnnotateIncomingContext(ctx, mux, req, "/debug.Api/GetHeap", runtime.WithHTTPPathPattern("/debug/api/heap"))
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
@@ -160,15 +166,20 @@ func RegisterApiHandlerServer(ctx context.Context, mux *runtime.ServeMux, server
runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err)
return
}
forward_Api_GetHeap_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
})
mux.Handle(http.MethodGet, pattern_Api_GetHeapGraph_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
mux.Handle("GET", pattern_Api_GetHeapGraph_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
ctx, cancel := context.WithCancel(req.Context())
defer cancel()
var stream runtime.ServerTransportStream
ctx = grpc.NewContextWithServerTransportStream(ctx, &stream)
inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
annotatedContext, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/debug.Api/GetHeapGraph", runtime.WithHTTPPathPattern("/debug/api/heap/graph"))
var err error
var annotatedContext context.Context
annotatedContext, err = runtime.AnnotateIncomingContext(ctx, mux, req, "/debug.Api/GetHeapGraph", runtime.WithHTTPPathPattern("/debug/api/heap/graph"))
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
@@ -180,15 +191,20 @@ func RegisterApiHandlerServer(ctx context.Context, mux *runtime.ServeMux, server
runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err)
return
}
forward_Api_GetHeapGraph_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
})
mux.Handle(http.MethodGet, pattern_Api_GetCpuGraph_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
mux.Handle("GET", pattern_Api_GetCpuGraph_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
ctx, cancel := context.WithCancel(req.Context())
defer cancel()
var stream runtime.ServerTransportStream
ctx = grpc.NewContextWithServerTransportStream(ctx, &stream)
inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
annotatedContext, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/debug.Api/GetCpuGraph", runtime.WithHTTPPathPattern("/debug/api/cpu/graph"))
var err error
var annotatedContext context.Context
annotatedContext, err = runtime.AnnotateIncomingContext(ctx, mux, req, "/debug.Api/GetCpuGraph", runtime.WithHTTPPathPattern("/debug/api/cpu/graph"))
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
@@ -200,15 +216,20 @@ func RegisterApiHandlerServer(ctx context.Context, mux *runtime.ServeMux, server
runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err)
return
}
forward_Api_GetCpuGraph_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
})
mux.Handle(http.MethodGet, pattern_Api_GetCpu_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
mux.Handle("GET", pattern_Api_GetCpu_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
ctx, cancel := context.WithCancel(req.Context())
defer cancel()
var stream runtime.ServerTransportStream
ctx = grpc.NewContextWithServerTransportStream(ctx, &stream)
inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
annotatedContext, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/debug.Api/GetCpu", runtime.WithHTTPPathPattern("/debug/api/cpu"))
var err error
var annotatedContext context.Context
annotatedContext, err = runtime.AnnotateIncomingContext(ctx, mux, req, "/debug.Api/GetCpu", runtime.WithHTTPPathPattern("/debug/api/cpu"))
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
@@ -220,7 +241,9 @@ func RegisterApiHandlerServer(ctx context.Context, mux *runtime.ServeMux, server
runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err)
return
}
forward_Api_GetCpu_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
})
return nil
@@ -229,24 +252,25 @@ 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.NewClient(endpoint, opts...)
conn, err := grpc.DialContext(ctx, endpoint, opts...)
if err != nil {
return err
}
defer func() {
if err != nil {
if cerr := conn.Close(); cerr != nil {
grpclog.Errorf("Failed to close conn to %s: %v", endpoint, cerr)
grpclog.Infof("Failed to close conn to %s: %v", endpoint, cerr)
}
return
}
go func() {
<-ctx.Done()
if cerr := conn.Close(); cerr != nil {
grpclog.Errorf("Failed to close conn to %s: %v", endpoint, cerr)
grpclog.Infof("Failed to close conn to %s: %v", endpoint, cerr)
}
}()
}()
return RegisterApiHandler(ctx, mux, conn)
}
@@ -260,13 +284,16 @@ 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. This client ignores the HTTP middlewares.
// "ApiClient" to call the correct interceptors.
func RegisterApiHandlerClient(ctx context.Context, mux *runtime.ServeMux, client ApiClient) error {
mux.Handle(http.MethodGet, pattern_Api_GetHeap_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
mux.Handle("GET", pattern_Api_GetHeap_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
ctx, cancel := context.WithCancel(req.Context())
defer cancel()
inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
annotatedContext, err := runtime.AnnotateContext(ctx, mux, req, "/debug.Api/GetHeap", runtime.WithHTTPPathPattern("/debug/api/heap"))
var err error
var annotatedContext context.Context
annotatedContext, err = runtime.AnnotateContext(ctx, mux, req, "/debug.Api/GetHeap", runtime.WithHTTPPathPattern("/debug/api/heap"))
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
@@ -277,13 +304,18 @@ func RegisterApiHandlerClient(ctx context.Context, mux *runtime.ServeMux, client
runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err)
return
}
forward_Api_GetHeap_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
})
mux.Handle(http.MethodGet, pattern_Api_GetHeapGraph_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
mux.Handle("GET", pattern_Api_GetHeapGraph_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
ctx, cancel := context.WithCancel(req.Context())
defer cancel()
inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
annotatedContext, err := runtime.AnnotateContext(ctx, mux, req, "/debug.Api/GetHeapGraph", runtime.WithHTTPPathPattern("/debug/api/heap/graph"))
var err error
var annotatedContext context.Context
annotatedContext, err = runtime.AnnotateContext(ctx, mux, req, "/debug.Api/GetHeapGraph", runtime.WithHTTPPathPattern("/debug/api/heap/graph"))
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
@@ -294,13 +326,18 @@ func RegisterApiHandlerClient(ctx context.Context, mux *runtime.ServeMux, client
runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err)
return
}
forward_Api_GetHeapGraph_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
})
mux.Handle(http.MethodGet, pattern_Api_GetCpuGraph_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
mux.Handle("GET", pattern_Api_GetCpuGraph_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
ctx, cancel := context.WithCancel(req.Context())
defer cancel()
inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
annotatedContext, err := runtime.AnnotateContext(ctx, mux, req, "/debug.Api/GetCpuGraph", runtime.WithHTTPPathPattern("/debug/api/cpu/graph"))
var err error
var annotatedContext context.Context
annotatedContext, err = runtime.AnnotateContext(ctx, mux, req, "/debug.Api/GetCpuGraph", runtime.WithHTTPPathPattern("/debug/api/cpu/graph"))
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
@@ -311,13 +348,18 @@ func RegisterApiHandlerClient(ctx context.Context, mux *runtime.ServeMux, client
runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err)
return
}
forward_Api_GetCpuGraph_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
})
mux.Handle(http.MethodGet, pattern_Api_GetCpu_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
mux.Handle("GET", pattern_Api_GetCpu_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
ctx, cancel := context.WithCancel(req.Context())
defer cancel()
inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
annotatedContext, err := runtime.AnnotateContext(ctx, mux, req, "/debug.Api/GetCpu", runtime.WithHTTPPathPattern("/debug/api/cpu"))
var err error
var annotatedContext context.Context
annotatedContext, err = runtime.AnnotateContext(ctx, mux, req, "/debug.Api/GetCpu", runtime.WithHTTPPathPattern("/debug/api/cpu"))
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
@@ -328,21 +370,30 @@ func RegisterApiHandlerClient(ctx context.Context, mux *runtime.ServeMux, client
runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err)
return
}
forward_Api_GetCpu_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
})
return nil
}
var (
pattern_Api_GetHeap_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2}, []string{"debug", "api", "heap"}, ""))
pattern_Api_GetHeap_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2}, []string{"debug", "api", "heap"}, ""))
pattern_Api_GetHeapGraph_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 2, 3}, []string{"debug", "api", "heap", "graph"}, ""))
pattern_Api_GetCpuGraph_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 2, 3}, []string{"debug", "api", "cpu", "graph"}, ""))
pattern_Api_GetCpu_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2}, []string{"debug", "api", "cpu"}, ""))
pattern_Api_GetCpuGraph_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 2, 3}, []string{"debug", "api", "cpu", "graph"}, ""))
pattern_Api_GetCpu_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2}, []string{"debug", "api", "cpu"}, ""))
)
var (
forward_Api_GetHeap_0 = runtime.ForwardResponseMessage
forward_Api_GetHeap_0 = runtime.ForwardResponseMessage
forward_Api_GetHeapGraph_0 = runtime.ForwardResponseMessage
forward_Api_GetCpuGraph_0 = runtime.ForwardResponseMessage
forward_Api_GetCpu_0 = runtime.ForwardResponseMessage
forward_Api_GetCpuGraph_0 = runtime.ForwardResponseMessage
forward_Api_GetCpu_0 = runtime.ForwardResponseMessage
)

View File

@@ -132,4 +132,4 @@ message RuntimeStats {
uint64 gc_count = 2; // 垃圾回收次数
uint64 gc_pause_time_ns = 3; // 垃圾回收暂停时间(纳秒)
uint64 blocking_time_ns = 4; // 阻塞时间(纳秒)
}
}

View File

@@ -1,7 +1,7 @@
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
// versions:
// - protoc-gen-go-grpc v1.5.1
// - protoc v5.29.1
// - protoc v5.29.3
// source: debug.proto
package pb

View File

@@ -0,0 +1,122 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Environment Check</title>
<style>
body {
font-family: Arial, sans-serif;
margin: 20px;
background-color: #f5f5f5;
}
.container {
max-width: 800px;
margin: 0 auto;
background-color: white;
padding: 20px;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.input-group {
margin-bottom: 20px;
}
input[type="text"] {
padding: 8px;
width: 300px;
margin-right: 10px;
}
button {
padding: 8px 16px;
background-color: #4CAF50;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
}
button:hover {
background-color: #45a049;
}
#log {
background-color: #f8f9fa;
border: 1px solid #ddd;
padding: 10px;
height: 400px;
overflow-y: auto;
font-family: monospace;
white-space: pre-wrap;
}
.success {
color: #28a745;
}
.error {
color: #dc3545;
}
.info {
color: #17a2b8;
}
</style>
</head>
<body>
<div class="container">
<h1>Environment Check</h1>
<div class="input-group">
<input type="text" id="targetUrl" placeholder="Enter target URL (e.g., http://192.168.1.100:8080)">
<button onclick="startCheck()">Start Check</button>
</div>
<div id="log"></div>
</div>
<script>
function appendLog(message, type = 'info') {
const log = document.getElementById('log');
const entry = document.createElement('div');
entry.className = type;
entry.textContent = message;
log.appendChild(entry);
log.scrollTop = log.scrollHeight;
}
function startCheck() {
const targetUrl = document.getElementById('targetUrl').value;
if (!targetUrl) {
appendLog('Please enter a target URL', 'error');
return;
}
// Clear previous log
document.getElementById('log').innerHTML = '';
appendLog('Starting environment check...');
// Create SSE connection
const eventSource = new EventSource(`/debug/envcheck?target=${encodeURIComponent(targetUrl)}`);
eventSource.onmessage = function (event) {
const data = JSON.parse(event.data);
appendLog(data.message, data.type);
if (data.type === 'complete') {
eventSource.close();
}
};
eventSource.onerror = function (error) {
appendLog('Connection error occurred', 'error');
eventSource.close();
};
}
</script>
</body>
</html>

View File

@@ -1,18 +1,143 @@
<!DOCTYPE html>
<html>
<head>
<title></title>
<meta charset="utf-8" />
<script src="jquery-2.1.4.min.js"></script>
<script src="moment.min.js"></script>
<script src="plotly-1.51.3.min.js"></script>
</head>
<body>
<div id="container1" style="min-width: 310px; height: 400px; margin: 0 auto"></div>
<div id="container2" style="min-width: 310px; height: 400px; margin: 0 auto"></div>
<div id="container3" style="min-width: 310px; height: 400px; margin: 0 auto"></div>
<div id="container4" style="min-width: 310px; height: 400px; margin: 0 auto"></div>
<script src="main.js"></script>
</body>
</html>
<head>
<title></title>
<meta charset="utf-8" />
<script src="https://code.jquery.com/jquery-2.1.4.min.js"></script>
<script src="https://lf26-cdn-tos.bytecdntp.com/cdn/expire-1-y/moment.js/2.11.0/moment.min.js"></script>
<script src="https://lf26-cdn-tos.bytecdntp.com/cdn/expire-1-y/plotly.js/1.51.3/plotly.min.js"></script>
</head>
<body>
<div id="container1" style="min-width: 310px; height: 400px; margin: 0 auto"></div>
<div id="container2" style="min-width: 310px; height: 400px; margin: 0 auto"></div>
<div id="container3" style="min-width: 310px; height: 400px; margin: 0 auto"></div>
<div id="container4" style="min-width: 310px; height: 400px; margin: 0 auto"></div>
<script>
var chart1;
var chart2;
var chart3;
var chart4;
function stackedArea(traces) {
for (var i = 1; i < traces.length; i++) {
for (var j = 0; j < (Math.min(traces[i]['y'].length, traces[i - 1]['y'].length)); j++) {
traces[i]['y'][j] += traces[i - 1]['y'][j];
}
}
return traces;
}
$(function () {
$.getJSON('/debug/charts/data?callback=?', function (data) {
var pDataChart1 = [{ x: [], y: [], type: "scattergl" }];
for (i = 0; i < data.GcPauses.length; i++) {
var d = moment(data.GcPauses[i].Ts).format('YYYY-MM-DD HH:mm:ss');
pDataChart1[0].x.push(d);
pDataChart1[0].y.push(data.GcPauses[i].Value);
}
chart1 = Plotly.newPlot('container1', pDataChart1, {
title: "GC Pauses",
xaxis: {
type: "date"
},
yaxis: {
title: "Nanoseconds"
}
});
var pDataChart2 = [{ x: [], y: [], type: "scattergl" }];
for (i = 0; i < data.BytesAllocated.length; i++) {
var d = moment(data.BytesAllocated[i].Ts).format('YYYY-MM-DD HH:mm:ss');
pDataChart2[0].x.push(d);
pDataChart2[0].y.push(data.BytesAllocated[i].Value);
}
chart2 = Plotly.newPlot('container2', pDataChart2, {
title: "Memory Allocated",
xaxis: {
type: "date"
},
yaxis: {
title: "Bytes"
}
});
var pDataChart3 = [
{ x: [], y: [], fill: 'tozeroy', name: 'sys', hoverinfo: 'none', type: "scattergl" },
{ x: [], y: [], fill: 'tonexty', name: 'user', hoverinfo: 'none', type: "scattergl" }
];
for (i = 0; i < data.CPUUsage.length; i++) {
var d = moment(data.CPUUsage[i].Ts).format('YYYY-MM-DD HH:mm:ss');
pDataChart3[0].x.push(d);
pDataChart3[1].x.push(d);
pDataChart3[0].y.push(data.CPUUsage[i].Sys);
pDataChart3[1].y.push(data.CPUUsage[i].User);
}
pDataChart3 = stackedArea(pDataChart3);
chart3 = Plotly.newPlot('container3', pDataChart3, {
title: "CPU Usage",
xaxis: {
type: "date"
},
yaxis: {
title: "Seconds"
}
});
var pprofList = ["Block", "Goroutine", "Heap", "Mutex", "Threadcreate"];
var pDataChart4 = [];
for (i = 0; i < pprofList.length; i++) {
pDataChart4.push({ x: [], y: [], name: pprofList[i].toLowerCase() });
}
for (i = 0; i < data.Pprof.length; i++) {
var d = moment(data.Pprof[i].Ts).format('YYYY-MM-DD HH:mm:ss');
for (j = 0; j < pprofList.length; j++) {
pDataChart4[j].x.push(d);
pDataChart4[j].y.push(data.Pprof[i][pprofList[j]]);
}
}
chart4 = Plotly.newPlot('container4', pDataChart4, {
title: "PPROF",
xaxis: {
type: "date",
},
yaxis: {
title: "Count"
}
});
});
function wsurl() {
var l = window.location;
return ((l.protocol === "https:") ? "wss://" : "ws://") + l.hostname + (((l.port != 80) && (l.port != 443)) ? ":" + l.port : "") + "/debug/charts/datafeed";
}
ws = new WebSocket(wsurl());
ws.onopen = function () {
ws.onmessage = function (evt) {
var data = JSON.parse(evt.data);
var d = moment(data.Ts).format('YYYY-MM-DD HH:mm:ss');
if (data.GcPause != 0) {
Plotly.extendTraces('container1', { x: [[d]], y: [[data.GcPause]] }, [0], 86400);
}
Plotly.extendTraces('container2', { x: [[d]], y: [[data.BytesAllocated]] }, [0], 86400);
Plotly.extendTraces('container3', { x: [[d], [d]], y: [[data.CPUSys], [data.CPUUser]] }, [0, 1], 86400);
Plotly.extendTraces('container4', { x: [[d], [d], [d], [d], [d]], y: [[data.Block], [data.Goroutine], [data.Heap], [data.Mutex], [data.Threadcreate]] }, [0, 1, 2, 3, 4], 86400);
};
};
})
</script>
</body>
</html>

File diff suppressed because one or more lines are too long

View File

@@ -1,122 +0,0 @@
var chart1;
var chart2;
var chart3;
var chart4;
function stackedArea(traces) {
for(var i=1; i<traces.length; i++) {
for(var j=0; j<(Math.min(traces[i]['y'].length, traces[i-1]['y'].length)); j++) {
traces[i]['y'][j] += traces[i-1]['y'][j];
}
}
return traces;
}
$(function () {
$.getJSON('/debug/charts/data?callback=?', function (data) {
var pDataChart1 = [{x: [], y: [], type: "scattergl"}];
for (i = 0; i < data.GcPauses.length; i++) {
var d = moment(data.GcPauses[i].Ts).format('YYYY-MM-DD HH:mm:ss');
pDataChart1[0].x.push(d);
pDataChart1[0].y.push(data.GcPauses[i].Value);
}
chart1 = Plotly.newPlot('container1', pDataChart1, {
title: "GC Pauses",
xaxis: {
type: "date"
},
yaxis: {
title: "Nanoseconds"
}
});
var pDataChart2 = [{x: [], y: [], type: "scattergl"}];
for (i = 0; i < data.BytesAllocated.length; i++) {
var d = moment(data.BytesAllocated[i].Ts).format('YYYY-MM-DD HH:mm:ss');
pDataChart2[0].x.push(d);
pDataChart2[0].y.push(data.BytesAllocated[i].Value);
}
chart2 = Plotly.newPlot('container2', pDataChart2, {
title: "Memory Allocated",
xaxis: {
type: "date"
},
yaxis: {
title: "Bytes"
}
});
var pDataChart3 = [
{x: [], y: [], fill: 'tozeroy', name: 'sys', hoverinfo: 'none', type: "scattergl"},
{x: [], y: [], fill: 'tonexty', name: 'user', hoverinfo: 'none', type: "scattergl"}
];
for (i = 0; i < data.CPUUsage.length; i++) {
var d = moment(data.CPUUsage[i].Ts).format('YYYY-MM-DD HH:mm:ss');
pDataChart3[0].x.push(d);
pDataChart3[1].x.push(d);
pDataChart3[0].y.push(data.CPUUsage[i].Sys);
pDataChart3[1].y.push(data.CPUUsage[i].User);
}
pDataChart3 = stackedArea(pDataChart3);
chart3 = Plotly.newPlot('container3', pDataChart3, {
title: "CPU Usage",
xaxis: {
type: "date"
},
yaxis: {
title: "Seconds"
}
});
var pprofList = ["Block", "Goroutine", "Heap", "Mutex", "Threadcreate"];
var pDataChart4 = []
for (i = 0; i < pprofList.length; i++) {
pDataChart4.push({x: [], y: [], name: pprofList[i].toLowerCase()})
}
for (i = 0; i < data.Pprof.length; i++) {
var d = moment(data.Pprof[i].Ts).format('YYYY-MM-DD HH:mm:ss');
for (j = 0; j < pprofList.length; j++) {
pDataChart4[j].x.push(d);
pDataChart4[j].y.push(data.Pprof[i][pprofList[j]])
}
}
chart4 = Plotly.newPlot('container4', pDataChart4, {
title: "PPROF",
xaxis: {
type: "date",
},
yaxis: {
title: "Count"
}
});
});
function wsurl() {
var l = window.location;
return ((l.protocol === "https:") ? "wss://" : "ws://") + l.hostname + (((l.port != 80) && (l.port != 443)) ? ":" + l.port : "") + "/debug/charts/datafeed";
}
ws = new WebSocket(wsurl());
ws.onopen = function () {
ws.onmessage = function (evt) {
var data = JSON.parse(evt.data);
var d = moment(data.Ts).format('YYYY-MM-DD HH:mm:ss');
if (data.GcPause != 0) {
Plotly.extendTraces('container1', {x:[[d]],y:[[data.GcPause]]}, [0], 86400);
}
Plotly.extendTraces('container2', {x:[[d]],y:[[data.BytesAllocated]]}, [0], 86400);
Plotly.extendTraces('container3', {x:[[d], [d]],y:[[data.CPUSys], [data.CPUUser]]}, [0, 1], 86400);
Plotly.extendTraces('container4', {x:[[d], [d], [d], [d], [d]],y:[[data.Block], [data.Goroutine], [data.Heap], [data.Mutex], [data.Threadcreate]]}, [0, 1, 2, 3, 4], 86400);
}
};
})

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -1,27 +1,15 @@
package plugin_flv
import (
"bufio"
"context"
"encoding/binary"
"io"
"io/fs"
"net/http"
"os"
"path/filepath"
"strconv"
"strings"
"time"
"google.golang.org/protobuf/types/known/emptypb"
"m7s.live/v5/pb"
"m7s.live/v5/pkg/util"
flvpb "m7s.live/v5/plugin/flv/pb"
flv "m7s.live/v5/plugin/flv/pkg"
rtmp "m7s.live/v5/plugin/rtmp/pkg"
)
func (p *FLVPlugin) List(ctx context.Context, req *flvpb.ReqRecordList) (resp *pb.ResponseList, err error) {
func (p *FLVPlugin) List(ctx context.Context, req *flvpb.ReqRecordList) (resp *pb.RecordResponseList, err error) {
globalReq := &pb.ReqRecordList{
StreamPath: req.StreamPath,
Range: req.Range,
@@ -29,7 +17,6 @@ func (p *FLVPlugin) List(ctx context.Context, req *flvpb.ReqRecordList) (resp *p
End: req.End,
PageNum: req.PageNum,
PageSize: req.PageSize,
Mode: req.Mode,
Type: "flv",
}
return p.Server.GetRecordList(ctx, globalReq)
@@ -52,248 +39,49 @@ func (p *FLVPlugin) Delete(ctx context.Context, req *flvpb.ReqRecordDelete) (res
}
func (plugin *FLVPlugin) Download_(w http.ResponseWriter, r *http.Request) {
streamPath := strings.TrimSuffix(strings.TrimPrefix(r.URL.Path, "/download/"), ".flv")
singleFile := filepath.Join(plugin.Path, streamPath+".flv")
startTime, endTime, err := util.TimeRangeQueryParse(r.URL.Query())
// 解析请求参数
params, err := plugin.parseRequestParams(r)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
timeRange := endTime.Sub(startTime)
plugin.Info("download", "stream", streamPath, "start", startTime, "end", endTime)
dir := filepath.Join(plugin.Path, streamPath)
if util.Exist(singleFile) {
} else if util.Exist(dir) {
var fileList []fs.FileInfo
var found bool
var startOffsetTime time.Duration
err = filepath.Walk(dir, func(path string, info fs.FileInfo, err error) error {
if info.IsDir() || !strings.HasSuffix(info.Name(), ".flv") {
return nil
}
modTime := info.ModTime()
//tmp, _ := strconv.Atoi(strings.TrimSuffix(info.Name(), ".flv"))
//fileStartTime := time.Unix(tmp, 10)
if !found {
if modTime.After(startTime) {
found = true
//fmt.Println(path, modTime, startTime, found)
} else {
fileList = []fs.FileInfo{info}
startOffsetTime = startTime.Sub(modTime)
//fmt.Println(path, modTime, startTime, found)
return nil
}
}
if modTime.After(endTime) {
return fs.ErrInvalid
}
fileList = append(fileList, info)
return nil
})
if !found {
http.NotFound(w, r)
return
}
plugin.Info("download", "stream", params.streamPath, "start", params.startTime, "end", params.endTime)
w.Header().Set("Content-Type", "video/x-flv")
w.Header().Set("Content-Disposition", "attachment")
var writer io.Writer = w
flvHead := make([]byte, 9+4)
tagHead := make(util.Buffer, 11)
var contentLength uint64
// 从数据库查询录像记录
recordStreams, err := plugin.queryRecordStreams(params)
if err != nil {
plugin.Error("Failed to query record streams", "err", err)
http.Error(w, "Database query failed", http.StatusInternalServerError)
return
}
var amf *rtmp.AMF
var metaData rtmp.EcmaArray
initMetaData := func(reader io.Reader, dataLen uint32) {
data := make([]byte, dataLen+4)
_, err = io.ReadFull(reader, data)
amf = &rtmp.AMF{
Buffer: util.Buffer(data[1+2+len("onMetaData") : len(data)-4]),
}
var obj any
obj, err = amf.Unmarshal()
metaData = obj.(rtmp.EcmaArray)
}
var filepositions []uint64
var times []float64
for pass := 0; pass < 2; pass++ {
offsetTime := startOffsetTime
var offsetTimestamp, lastTimestamp uint32
var init, seqAudioWritten, seqVideoWritten bool
if pass == 1 {
metaData["keyframes"] = map[string]any{
"filepositions": filepositions,
"times": times,
}
amf.Marshals("onMetaData", metaData)
offsetDelta := amf.Len() + 15
offset := offsetDelta + len(flvHead)
contentLength += uint64(offset)
metaData["duration"] = timeRange.Seconds()
metaData["filesize"] = contentLength
for i := range filepositions {
filepositions[i] += uint64(offset)
}
metaData["keyframes"] = map[string]any{
"filepositions": filepositions,
"times": times,
}
amf.Reset()
amf.Marshals("onMetaData", metaData)
plugin.Info("start download", "metaData", metaData)
w.Header().Set("Content-Length", strconv.FormatInt(int64(contentLength), 10))
w.WriteHeader(http.StatusOK)
}
if offsetTime == 0 {
init = true
} else {
offsetTimestamp = -uint32(offsetTime.Milliseconds())
}
for i, info := range fileList {
if r.Context().Err() != nil {
return
}
filePath := filepath.Join(dir, info.Name())
plugin.Debug("read", "file", filePath)
file, err := os.Open(filePath)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
reader := bufio.NewReader(file)
if i == 0 {
_, err = io.ReadFull(reader, flvHead)
if pass == 1 {
// 第一次写入头
_, err = writer.Write(flvHead)
tagHead[0] = flv.FLV_TAG_TYPE_SCRIPT
l := amf.Len()
tagHead[1] = byte(l >> 16)
tagHead[2] = byte(l >> 8)
tagHead[3] = byte(l)
flv.PutFlvTimestamp(tagHead, 0)
writer.Write(tagHead)
writer.Write(amf.Buffer)
l += 11
binary.BigEndian.PutUint32(tagHead[:4], uint32(l))
writer.Write(tagHead[:4])
}
} else {
// 后面的头跳过
_, err = reader.Discard(13)
if !init {
offsetTime = 0
offsetTimestamp = 0
}
}
for err == nil {
_, err = io.ReadFull(reader, tagHead)
if err != nil {
break
}
tmp := tagHead
t := tmp.ReadByte()
dataLen := tmp.ReadUint24()
lastTimestamp = tmp.ReadUint24() | uint32(tmp.ReadByte())<<24
//fmt.Println(lastTimestamp, tagHead)
if init {
if t == flv.FLV_TAG_TYPE_SCRIPT {
if pass == 0 {
initMetaData(reader, dataLen)
} else {
_, err = reader.Discard(int(dataLen) + 4)
}
} else {
lastTimestamp += offsetTimestamp
if lastTimestamp >= uint32(timeRange.Milliseconds()) {
break
}
if pass == 0 {
data := make([]byte, dataLen+4)
_, err = io.ReadFull(reader, data)
frameType := (data[0] >> 4) & 0b0111
idr := frameType == 1 || frameType == 4
if idr {
filepositions = append(filepositions, contentLength)
times = append(times, float64(lastTimestamp)/1000)
}
contentLength += uint64(11 + dataLen + 4)
} else {
//fmt.Println("write", lastTimestamp)
flv.PutFlvTimestamp(tagHead, lastTimestamp)
_, err = writer.Write(tagHead)
_, err = io.CopyN(writer, reader, int64(dataLen+4))
}
}
continue
}
switch t {
case flv.FLV_TAG_TYPE_SCRIPT:
if pass == 0 {
initMetaData(reader, dataLen)
} else {
_, err = reader.Discard(int(dataLen) + 4)
}
case flv.FLV_TAG_TYPE_AUDIO:
if !seqAudioWritten {
if pass == 0 {
contentLength += uint64(11 + dataLen + 4)
_, err = reader.Discard(int(dataLen) + 4)
} else {
flv.PutFlvTimestamp(tagHead, 0)
_, err = writer.Write(tagHead)
_, err = io.CopyN(writer, reader, int64(dataLen+4))
}
seqAudioWritten = true
} else {
_, err = reader.Discard(int(dataLen) + 4)
}
case flv.FLV_TAG_TYPE_VIDEO:
if !seqVideoWritten {
if pass == 0 {
contentLength += uint64(11 + dataLen + 4)
_, err = reader.Discard(int(dataLen) + 4)
} else {
flv.PutFlvTimestamp(tagHead, 0)
_, err = writer.Write(tagHead)
_, err = io.CopyN(writer, reader, int64(dataLen+4))
}
seqVideoWritten = true
} else {
if lastTimestamp >= uint32(offsetTime.Milliseconds()) {
data := make([]byte, dataLen+4)
_, err = io.ReadFull(reader, data)
frameType := (data[0] >> 4) & 0b0111
idr := frameType == 1 || frameType == 4
if idr {
init = true
plugin.Debug("init", "lastTimestamp", lastTimestamp)
if pass == 0 {
filepositions = append(filepositions, contentLength)
times = append(times, float64(lastTimestamp)/1000)
contentLength += uint64(11 + dataLen + 4)
} else {
flv.PutFlvTimestamp(tagHead, 0)
_, err = writer.Write(tagHead)
_, err = writer.Write(data)
}
}
} else {
_, err = reader.Discard(int(dataLen) + 4)
}
}
}
}
offsetTimestamp = lastTimestamp
err = file.Close()
}
}
plugin.Info("end download")
} else {
// 构建文件信息列表
fileInfoList, found := plugin.buildFileInfoList(recordStreams, params.startTime, params.endTime)
if !found || len(fileInfoList) == 0 {
plugin.Warn("No records found", "stream", params.streamPath, "start", params.startTime, "end", params.endTime)
http.NotFound(w, r)
return
}
// 根据记录类型选择处理方式
if plugin.hasOnlyMp4Records(fileInfoList) {
// 过滤MP4文件并转换为FLV
mp4FileList := plugin.filterMp4Files(fileInfoList)
if len(mp4FileList) == 0 {
plugin.Warn("No valid MP4 files after filtering", "stream", params.streamPath)
http.NotFound(w, r)
return
}
plugin.processMp4ToFlv(w, r, mp4FileList, params)
} else {
// 过滤FLV文件并处理
flvFileList := plugin.filterFlvFiles(fileInfoList)
if len(flvFileList) == 0 {
plugin.Warn("No valid FLV files after filtering", "stream", params.streamPath)
http.NotFound(w, r)
return
}
plugin.processFlvFiles(w, r, flvFileList, params)
}
}

504
plugin/flv/download.go Normal file
View File

@@ -0,0 +1,504 @@
package plugin_flv
import (
"bufio"
"encoding/binary"
"fmt"
"io"
"net/http"
"os"
"strconv"
"strings"
"time"
m7s "m7s.live/v5"
"m7s.live/v5/pkg/util"
flv "m7s.live/v5/plugin/flv/pkg"
mp4 "m7s.live/v5/plugin/mp4/pkg"
rtmp "m7s.live/v5/plugin/rtmp/pkg"
)
// requestParams 包含请求解析后的参数
type requestParams struct {
streamPath string
startTime time.Time
endTime time.Time
timeRange time.Duration
}
// fileInfo 包含文件信息
type fileInfo struct {
filePath string
startTime time.Time
endTime time.Time
startOffsetTime time.Duration
recordType string // "flv" 或 "mp4"
}
// parseRequestParams 解析请求参数
func (plugin *FLVPlugin) parseRequestParams(r *http.Request) (*requestParams, error) {
// 从URL路径中提取流路径去除前缀 "/download/" 和后缀 ".flv"
streamPath := strings.TrimSuffix(strings.TrimPrefix(r.URL.Path, "/download/"), ".flv")
// 解析URL查询参数中的时间范围start和end参数
startTime, endTime, err := util.TimeRangeQueryParse(r.URL.Query())
if err != nil {
return nil, err
}
return &requestParams{
streamPath: streamPath,
startTime: startTime,
endTime: endTime,
timeRange: endTime.Sub(startTime),
}, nil
}
// queryRecordStreams 从数据库查询录像记录
func (plugin *FLVPlugin) queryRecordStreams(params *requestParams) ([]m7s.RecordStream, error) {
// 检查数据库是否可用
if plugin.DB == nil {
return nil, fmt.Errorf("database not available")
}
var recordStreams []m7s.RecordStream
// 首先查询FLV记录
query := plugin.DB.Model(&m7s.RecordStream{}).Where("stream_path = ? AND type = ?", params.streamPath, "flv")
// 添加时间范围查询条件
if !params.startTime.IsZero() && !params.endTime.IsZero() {
query = query.Where("(start_time <= ? AND end_time >= ?) OR (start_time >= ? AND start_time <= ?)",
params.endTime, params.startTime, params.startTime, params.endTime)
}
err := query.Order("start_time ASC").Find(&recordStreams).Error
if err != nil {
return nil, err
}
// 如果没有找到FLV记录尝试查询MP4记录
if len(recordStreams) == 0 {
query = plugin.DB.Model(&m7s.RecordStream{}).Where("stream_path = ? AND type IN (?)", params.streamPath, []string{"mp4", "fmp4"})
if !params.startTime.IsZero() && !params.endTime.IsZero() {
query = query.Where("(start_time <= ? AND end_time >= ?) OR (start_time >= ? AND start_time <= ?)",
params.endTime, params.startTime, params.startTime, params.endTime)
}
err = query.Order("start_time ASC").Find(&recordStreams).Error
if err != nil {
return nil, err
}
}
return recordStreams, nil
}
// buildFileInfoList 构建文件信息列表
func (plugin *FLVPlugin) buildFileInfoList(recordStreams []m7s.RecordStream, startTime, endTime time.Time) ([]*fileInfo, bool) {
var fileInfoList []*fileInfo
var found bool
for _, record := range recordStreams {
// 检查文件是否存在
if !util.Exist(record.FilePath) {
plugin.Warn("Record file not found", "filePath", record.FilePath)
continue
}
var startOffsetTime time.Duration
recordStartTime := record.StartTime
recordEndTime := record.EndTime
// 计算文件内的偏移时间
if startTime.After(recordStartTime) {
startOffsetTime = startTime.Sub(recordStartTime)
}
// 检查是否在时间范围内
if recordEndTime.Before(startTime) || recordStartTime.After(endTime) {
continue
}
fileInfoList = append(fileInfoList, &fileInfo{
filePath: record.FilePath,
startTime: recordStartTime,
endTime: recordEndTime,
startOffsetTime: startOffsetTime,
recordType: record.Type,
})
found = true
}
return fileInfoList, found
}
// hasOnlyMp4Records 检查是否只有MP4记录
func (plugin *FLVPlugin) hasOnlyMp4Records(fileInfoList []*fileInfo) bool {
if len(fileInfoList) == 0 {
return false
}
for _, info := range fileInfoList {
if info.recordType == "flv" {
return false
}
}
return true
}
// filterFlvFiles 过滤FLV文件
func (plugin *FLVPlugin) filterFlvFiles(fileInfoList []*fileInfo) []*fileInfo {
var filteredList []*fileInfo
for _, info := range fileInfoList {
if info.recordType == "flv" {
filteredList = append(filteredList, info)
}
}
plugin.Debug("FLV files filtered", "original", len(fileInfoList), "filtered", len(filteredList))
return filteredList
}
// filterMp4Files 过滤MP4文件
func (plugin *FLVPlugin) filterMp4Files(fileInfoList []*fileInfo) []*fileInfo {
var filteredList []*fileInfo
for _, info := range fileInfoList {
if info.recordType == "mp4" || info.recordType == "fmp4" {
filteredList = append(filteredList, info)
}
}
plugin.Debug("MP4 files filtered", "original", len(fileInfoList), "filtered", len(filteredList))
return filteredList
}
// processMp4ToFlv 将MP4记录转换为FLV输出
func (plugin *FLVPlugin) processMp4ToFlv(w http.ResponseWriter, r *http.Request, fileInfoList []*fileInfo, params *requestParams) {
plugin.Info("Converting MP4 records to FLV", "count", len(fileInfoList))
// 设置HTTP响应头
w.Header().Set("Content-Type", "video/x-flv")
w.Header().Set("Content-Disposition", "attachment")
// 创建MP4流列表
var mp4Streams []m7s.RecordStream
for _, info := range fileInfoList {
mp4Streams = append(mp4Streams, m7s.RecordStream{
FilePath: info.filePath,
StartTime: info.startTime,
EndTime: info.endTime,
Type: info.recordType,
})
}
// 创建DemuxerConverterRange进行MP4解复用和转换
demuxer := &mp4.DemuxerConverterRange[*rtmp.RTMPAudio, *rtmp.RTMPVideo]{
DemuxerRange: mp4.DemuxerRange{
StartTime: params.startTime,
EndTime: params.endTime,
Streams: mp4Streams,
Logger: plugin.Logger.With("demuxer", "mp4_flv"),
},
}
// 创建FLV编码器状态
flvWriter := flv.NewFlvWriter(w)
hasWritten := false
ts := int64(0) // 初始化时间戳
tsOffset := int64(0) // 偏移时间戳
// 执行解复用和转换
err := demuxer.Demux(r.Context(),
func(audio *rtmp.RTMPAudio) error {
if !hasWritten {
if err := flvWriter.WriteHeader(demuxer.AudioTrack != nil, demuxer.VideoTrack != nil); err != nil {
return err
}
}
// 计算调整后的时间戳
ts = int64(audio.Timestamp) + tsOffset
timestamp := uint32(ts)
// 写入音频数据帧
return flvWriter.WriteTag(flv.FLV_TAG_TYPE_AUDIO, timestamp, uint32(audio.Size), audio.Buffers...)
}, func(frame *rtmp.RTMPVideo) error {
if !hasWritten {
if err := flvWriter.WriteHeader(demuxer.AudioTrack != nil, demuxer.VideoTrack != nil); err != nil {
return err
}
}
// 计算调整后的时间戳
ts = int64(frame.Timestamp) + tsOffset
timestamp := uint32(ts)
// 写入视频数据帧
return flvWriter.WriteTag(flv.FLV_TAG_TYPE_VIDEO, timestamp, uint32(frame.Size), frame.Buffers...)
})
if err != nil {
plugin.Error("MP4 to FLV conversion failed", "err", err)
if !hasWritten {
http.Error(w, "Conversion failed", http.StatusInternalServerError)
}
return
}
plugin.Info("MP4 to FLV conversion completed")
}
// processFlvFiles 处理原生FLV文件
func (plugin *FLVPlugin) processFlvFiles(w http.ResponseWriter, r *http.Request, fileInfoList []*fileInfo, params *requestParams) {
plugin.Info("Processing FLV files", "count", len(fileInfoList))
// 设置HTTP响应头
w.Header().Set("Content-Type", "video/x-flv")
w.Header().Set("Content-Disposition", "attachment")
var writer io.Writer = w
flvHead := make([]byte, 9+4)
tagHead := make(util.Buffer, 11)
var contentLength uint64
var startOffsetTime time.Duration
// 计算第一个文件的偏移时间
if len(fileInfoList) > 0 {
startOffsetTime = fileInfoList[0].startOffsetTime
}
var amf *rtmp.AMF
var metaData rtmp.EcmaArray
initMetaData := func(reader io.Reader, dataLen uint32) {
data := make([]byte, dataLen+4)
_, err := io.ReadFull(reader, data)
if err != nil {
return
}
amf = &rtmp.AMF{
Buffer: util.Buffer(data[1+2+len("onMetaData") : len(data)-4]),
}
var obj any
obj, err = amf.Unmarshal()
if err == nil {
metaData = obj.(rtmp.EcmaArray)
}
}
var filepositions []uint64
var times []float64
// 两次遍历:第一次计算大小,第二次写入数据
for pass := 0; pass < 2; pass++ {
offsetTime := startOffsetTime
var offsetTimestamp, lastTimestamp uint32
var init, seqAudioWritten, seqVideoWritten bool
if pass == 1 {
// 第二次遍历时,准备写入
metaData["keyframes"] = map[string]any{
"filepositions": filepositions,
"times": times,
}
amf.Marshals("onMetaData", metaData)
offsetDelta := amf.Len() + 15
offset := offsetDelta + len(flvHead)
contentLength += uint64(offset)
metaData["duration"] = params.timeRange.Seconds()
metaData["filesize"] = contentLength
for i := range filepositions {
filepositions[i] += uint64(offset)
}
metaData["keyframes"] = map[string]any{
"filepositions": filepositions,
"times": times,
}
amf.Reset()
amf.Marshals("onMetaData", metaData)
plugin.Info("start download", "metaData", metaData)
w.Header().Set("Content-Length", strconv.FormatInt(int64(contentLength), 10))
w.WriteHeader(http.StatusOK)
}
if offsetTime == 0 {
init = true
} else {
offsetTimestamp = -uint32(offsetTime.Milliseconds())
}
for i, info := range fileInfoList {
if r.Context().Err() != nil {
return
}
plugin.Debug("Processing file", "path", info.filePath)
file, err := os.Open(info.filePath)
if err != nil {
plugin.Error("Failed to open file", "path", info.filePath, "err", err)
if pass == 1 {
http.Error(w, err.Error(), http.StatusInternalServerError)
}
return
}
reader := bufio.NewReader(file)
if i == 0 {
_, err = io.ReadFull(reader, flvHead)
if err != nil {
file.Close()
if pass == 1 {
http.Error(w, err.Error(), http.StatusInternalServerError)
}
return
}
if pass == 1 {
// 第一次写入头
_, err = writer.Write(flvHead)
if err != nil {
file.Close()
return
}
tagHead[0] = flv.FLV_TAG_TYPE_SCRIPT
l := amf.Len()
tagHead[1] = byte(l >> 16)
tagHead[2] = byte(l >> 8)
tagHead[3] = byte(l)
flv.PutFlvTimestamp(tagHead, 0)
writer.Write(tagHead)
writer.Write(amf.Buffer)
l += 11
binary.BigEndian.PutUint32(tagHead[:4], uint32(l))
writer.Write(tagHead[:4])
}
} else {
// 后面的头跳过
_, err = reader.Discard(13)
if err != nil {
file.Close()
continue
}
if !init {
offsetTime = 0
offsetTimestamp = 0
}
}
// 处理FLV标签
for err == nil {
_, err = io.ReadFull(reader, tagHead)
if err != nil {
break
}
tmp := tagHead
t := tmp.ReadByte()
dataLen := tmp.ReadUint24()
lastTimestamp = tmp.ReadUint24() | uint32(tmp.ReadByte())<<24
if init {
if t == flv.FLV_TAG_TYPE_SCRIPT {
if pass == 0 {
initMetaData(reader, dataLen)
} else {
_, err = reader.Discard(int(dataLen) + 4)
}
} else {
lastTimestamp += offsetTimestamp
if lastTimestamp >= uint32(params.timeRange.Milliseconds()) {
break
}
if pass == 0 {
data := make([]byte, dataLen+4)
_, err = io.ReadFull(reader, data)
if err == nil {
frameType := (data[0] >> 4) & 0b0111
idr := frameType == 1 || frameType == 4
if idr {
filepositions = append(filepositions, contentLength)
times = append(times, float64(lastTimestamp)/1000)
}
contentLength += uint64(11 + dataLen + 4)
}
} else {
flv.PutFlvTimestamp(tagHead, lastTimestamp)
_, err = writer.Write(tagHead)
if err == nil {
_, err = io.CopyN(writer, reader, int64(dataLen+4))
}
}
}
continue
}
switch t {
case flv.FLV_TAG_TYPE_SCRIPT:
if pass == 0 {
initMetaData(reader, dataLen)
} else {
_, err = reader.Discard(int(dataLen) + 4)
}
case flv.FLV_TAG_TYPE_AUDIO:
if !seqAudioWritten {
if pass == 0 {
contentLength += uint64(11 + dataLen + 4)
_, err = reader.Discard(int(dataLen) + 4)
} else {
flv.PutFlvTimestamp(tagHead, 0)
_, err = writer.Write(tagHead)
if err == nil {
_, err = io.CopyN(writer, reader, int64(dataLen+4))
}
}
seqAudioWritten = true
} else {
_, err = reader.Discard(int(dataLen) + 4)
}
case flv.FLV_TAG_TYPE_VIDEO:
if !seqVideoWritten {
if pass == 0 {
contentLength += uint64(11 + dataLen + 4)
_, err = reader.Discard(int(dataLen) + 4)
} else {
flv.PutFlvTimestamp(tagHead, 0)
_, err = writer.Write(tagHead)
if err == nil {
_, err = io.CopyN(writer, reader, int64(dataLen+4))
}
}
seqVideoWritten = true
} else {
if lastTimestamp >= uint32(offsetTime.Milliseconds()) {
data := make([]byte, dataLen+4)
_, err = io.ReadFull(reader, data)
if err == nil {
frameType := (data[0] >> 4) & 0b0111
idr := frameType == 1 || frameType == 4
if idr {
init = true
plugin.Debug("init", "lastTimestamp", lastTimestamp)
if pass == 0 {
filepositions = append(filepositions, contentLength)
times = append(times, float64(lastTimestamp)/1000)
contentLength += uint64(11 + dataLen + 4)
} else {
flv.PutFlvTimestamp(tagHead, 0)
_, err = writer.Write(tagHead)
if err == nil {
_, err = writer.Write(data)
}
}
}
}
} else {
_, err = reader.Discard(int(dataLen) + 4)
}
}
}
}
offsetTimestamp = lastTimestamp
file.Close()
}
}
plugin.Info("FLV download completed")
}

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

@@ -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.6
// protoc v5.29.3
// source: flv.proto
package pb
@@ -16,6 +16,7 @@ import (
pb "m7s.live/v5/pb"
reflect "reflect"
sync "sync"
unsafe "unsafe"
)
const (
@@ -26,26 +27,23 @@ 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"`
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_flv_proto_msgTypes[0]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
mi := &file_flv_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_flv_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)
@@ -121,24 +119,21 @@ func (x *ReqRecordList) GetMode() string {
}
type ReqRecordDelete 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"`
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
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"`
sizeCache protoimpl.SizeCache
}
func (x *ReqRecordDelete) Reset() {
*x = ReqRecordDelete{}
if protoimpl.UnsafeEnabled {
mi := &file_flv_proto_msgTypes[1]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
mi := &file_flv_proto_msgTypes[1]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *ReqRecordDelete) String() string {
@@ -149,7 +144,7 @@ func (*ReqRecordDelete) ProtoMessage() {}
func (x *ReqRecordDelete) ProtoReflect() protoreflect.Message {
mi := &file_flv_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)
@@ -201,86 +196,58 @@ func (x *ReqRecordDelete) GetRange() string {
var File_flv_proto protoreflect.FileDescriptor
var file_flv_proto_rawDesc = []byte{
0x0a, 0x09, 0x66, 0x6c, 0x76, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x03, 0x66, 0x6c, 0x76,
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, 0x32, 0x98, 0x02, 0x0a, 0x03, 0x61, 0x70, 0x69,
0x12, 0x57, 0x0a, 0x04, 0x4c, 0x69, 0x73, 0x74, 0x12, 0x12, 0x2e, 0x66, 0x6c, 0x76, 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, 0x66, 0x6c, 0x76,
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,
0x66, 0x6c, 0x76, 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, 0x66, 0x6c, 0x76, 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, 0x66, 0x6c, 0x76, 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, 0x42, 0x1b, 0x5a, 0x19, 0x6d, 0x37, 0x73, 0x2e, 0x6c, 0x69, 0x76, 0x65, 0x2f,
0x76, 0x35, 0x2f, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x2f, 0x66, 0x6c, 0x76, 0x2f, 0x70, 0x62,
0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
}
const file_flv_proto_rawDesc = "" +
"\n" +
"\tflv.proto\x12\x03flv\x1a\x1cgoogle/api/annotations.proto\x1a\x1bgoogle/protobuf/empty.proto\x1a\x1fgoogle/protobuf/timestamp.proto\x1a\x1egoogle/protobuf/duration.proto\x1a\fglobal.proto\"\xb7\x01\n" +
"\rReqRecordList\x12\x1e\n" +
"\n" +
"streamPath\x18\x01 \x01(\tR\n" +
"streamPath\x12\x14\n" +
"\x05range\x18\x02 \x01(\tR\x05range\x12\x14\n" +
"\x05start\x18\x03 \x01(\tR\x05start\x12\x10\n" +
"\x03end\x18\x04 \x01(\tR\x03end\x12\x18\n" +
"\apageNum\x18\x05 \x01(\rR\apageNum\x12\x1a\n" +
"\bpageSize\x18\x06 \x01(\rR\bpageSize\x12\x12\n" +
"\x04mode\x18\a \x01(\tR\x04mode\"\x91\x01\n" +
"\x0fReqRecordDelete\x12\x1e\n" +
"\n" +
"streamPath\x18\x01 \x01(\tR\n" +
"streamPath\x12\x10\n" +
"\x03ids\x18\x02 \x03(\rR\x03ids\x12\x1c\n" +
"\tstartTime\x18\x03 \x01(\tR\tstartTime\x12\x18\n" +
"\aendTime\x18\x04 \x01(\tR\aendTime\x12\x14\n" +
"\x05range\x18\x05 \x01(\tR\x05range2\x9e\x02\n" +
"\x03api\x12]\n" +
"\x04List\x12\x12.flv.ReqRecordList\x1a\x1a.global.RecordResponseList\"%\x82\xd3\xe4\x93\x02\x1f\x12\x1d/flv/api/list/{streamPath=**}\x12T\n" +
"\aCatalog\x12\x16.google.protobuf.Empty\x1a\x17.global.ResponseCatalog\"\x18\x82\xd3\xe4\x93\x02\x12\x12\x10/flv/api/catalog\x12b\n" +
"\x06Delete\x12\x14.flv.ReqRecordDelete\x1a\x16.global.ResponseDelete\"*\x82\xd3\xe4\x93\x02$:\x01*\"\x1f/flv/api/delete/{streamPath=**}B\x1bZ\x19m7s.live/v5/plugin/flv/pbb\x06proto3"
var (
file_flv_proto_rawDescOnce sync.Once
file_flv_proto_rawDescData = file_flv_proto_rawDesc
file_flv_proto_rawDescData []byte
)
func file_flv_proto_rawDescGZIP() []byte {
file_flv_proto_rawDescOnce.Do(func() {
file_flv_proto_rawDescData = protoimpl.X.CompressGZIP(file_flv_proto_rawDescData)
file_flv_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_flv_proto_rawDesc), len(file_flv_proto_rawDesc)))
})
return file_flv_proto_rawDescData
}
var file_flv_proto_msgTypes = make([]protoimpl.MessageInfo, 2)
var file_flv_proto_goTypes = []interface{}{
(*ReqRecordList)(nil), // 0: flv.ReqRecordList
(*ReqRecordDelete)(nil), // 1: flv.ReqRecordDelete
(*emptypb.Empty)(nil), // 2: google.protobuf.Empty
(*pb.ResponseList)(nil), // 3: global.ResponseList
(*pb.ResponseCatalog)(nil), // 4: global.ResponseCatalog
(*pb.ResponseDelete)(nil), // 5: global.ResponseDelete
var file_flv_proto_goTypes = []any{
(*ReqRecordList)(nil), // 0: flv.ReqRecordList
(*ReqRecordDelete)(nil), // 1: flv.ReqRecordDelete
(*emptypb.Empty)(nil), // 2: google.protobuf.Empty
(*pb.RecordResponseList)(nil), // 3: global.RecordResponseList
(*pb.ResponseCatalog)(nil), // 4: global.ResponseCatalog
(*pb.ResponseDelete)(nil), // 5: global.ResponseDelete
}
var file_flv_proto_depIdxs = []int32{
0, // 0: flv.api.List:input_type -> flv.ReqRecordList
2, // 1: flv.api.Catalog:input_type -> google.protobuf.Empty
1, // 2: flv.api.Delete:input_type -> flv.ReqRecordDelete
3, // 3: flv.api.List:output_type -> global.ResponseList
3, // 3: flv.api.List:output_type -> global.RecordResponseList
4, // 4: flv.api.Catalog:output_type -> global.ResponseCatalog
5, // 5: flv.api.Delete:output_type -> global.ResponseDelete
3, // [3:6] is the sub-list for method output_type
@@ -295,37 +262,11 @@ func file_flv_proto_init() {
if File_flv_proto != nil {
return
}
if !protoimpl.UnsafeEnabled {
file_flv_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_flv_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
}
}
}
type x struct{}
out := protoimpl.TypeBuilder{
File: protoimpl.DescBuilder{
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: file_flv_proto_rawDesc,
RawDescriptor: unsafe.Slice(unsafe.StringData(file_flv_proto_rawDesc), len(file_flv_proto_rawDesc)),
NumEnums: 0,
NumMessages: 2,
NumExtensions: 0,
@@ -336,7 +277,6 @@ func file_flv_proto_init() {
MessageInfos: file_flv_proto_msgTypes,
}.Build()
File_flv_proto = out.File
file_flv_proto_rawDesc = nil
file_flv_proto_goTypes = nil
file_flv_proto_depIdxs = nil
}

View File

@@ -8,7 +8,7 @@ package flv;
option go_package="m7s.live/v5/plugin/flv/pb";
service api {
rpc List (ReqRecordList) returns (global.ResponseList) {
rpc List (ReqRecordList) returns (global.RecordResponseList) {
option (google.api.http) = {
get: "/flv/api/list/{streamPath=**}"
};

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.29.3
// source: flv.proto
package pb
@@ -17,14 +17,20 @@ 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 = "/flv.api/List"
Api_Catalog_FullMethodName = "/flv.api/Catalog"
Api_Delete_FullMethodName = "/flv.api/Delete"
)
// ApiClient is the client API for Api service.
//
// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream.
type ApiClient interface {
List(ctx context.Context, in *ReqRecordList, opts ...grpc.CallOption) (*pb.ResponseList, error)
List(ctx context.Context, in *ReqRecordList, opts ...grpc.CallOption) (*pb.RecordResponseList, error)
Catalog(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*pb.ResponseCatalog, error)
Delete(ctx context.Context, in *ReqRecordDelete, opts ...grpc.CallOption) (*pb.ResponseDelete, error)
}
@@ -37,9 +43,10 @@ func NewApiClient(cc grpc.ClientConnInterface) ApiClient {
return &apiClient{cc}
}
func (c *apiClient) List(ctx context.Context, in *ReqRecordList, opts ...grpc.CallOption) (*pb.ResponseList, error) {
out := new(pb.ResponseList)
err := c.cc.Invoke(ctx, "/flv.api/List", in, out, opts...)
func (c *apiClient) List(ctx context.Context, in *ReqRecordList, opts ...grpc.CallOption) (*pb.RecordResponseList, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(pb.RecordResponseList)
err := c.cc.Invoke(ctx, Api_List_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
@@ -47,8 +54,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, "/flv.api/Catalog", in, out, opts...)
err := c.cc.Invoke(ctx, Api_Catalog_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
@@ -56,8 +64,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, "/flv.api/Delete", in, out, opts...)
err := c.cc.Invoke(ctx, Api_Delete_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
@@ -66,19 +75,22 @@ func (c *apiClient) Delete(ctx context.Context, in *ReqRecordDelete, opts ...grp
// 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)
List(context.Context, *ReqRecordList) (*pb.RecordResponseList, error)
Catalog(context.Context, *emptypb.Empty) (*pb.ResponseCatalog, error)
Delete(context.Context, *ReqRecordDelete) (*pb.ResponseDelete, 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) {
func (UnimplementedApiServer) List(context.Context, *ReqRecordList) (*pb.RecordResponseList, error) {
return nil, status.Errorf(codes.Unimplemented, "method List not implemented")
}
func (UnimplementedApiServer) Catalog(context.Context, *emptypb.Empty) (*pb.ResponseCatalog, error) {
@@ -88,6 +100,7 @@ func (UnimplementedApiServer) Delete(context.Context, *ReqRecordDelete) (*pb.Res
return nil, status.Errorf(codes.Unimplemented, "method Delete 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
@@ -97,6 +110,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)
}
@@ -110,7 +130,7 @@ func _Api_List_Handler(srv interface{}, ctx context.Context, dec func(interface{
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: "/flv.api/List",
FullMethod: Api_List_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(ApiServer).List(ctx, req.(*ReqRecordList))
@@ -128,7 +148,7 @@ func _Api_Catalog_Handler(srv interface{}, ctx context.Context, dec func(interfa
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: "/flv.api/Catalog",
FullMethod: Api_Catalog_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(ApiServer).Catalog(ctx, req.(*emptypb.Empty))
@@ -146,7 +166,7 @@ func _Api_Delete_Handler(srv interface{}, ctx context.Context, dec func(interfac
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: "/flv.api/Delete",
FullMethod: Api_Delete_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(ApiServer).Delete(ctx, req.(*ReqRecordDelete))

View File

@@ -2,6 +2,7 @@ package flv
import (
"errors"
"io"
"m7s.live/v5"
"m7s.live/v5/pkg/util"
@@ -15,6 +16,10 @@ type Puller struct {
func (p *Puller) Run() (err error) {
reader := util.NewBufReader(p.ReadCloser)
publisher := p.PullJob.Publisher
if publisher == nil {
io.Copy(io.Discard, p.ReadCloser)
return
}
var hasAudio, hasVideo bool
var absTS uint32
var head util.Memory

View File

@@ -9,6 +9,7 @@ import (
"time"
m7s "m7s.live/v5"
"m7s.live/v5/pkg"
"m7s.live/v5/pkg/config"
"m7s.live/v5/pkg/task"
"m7s.live/v5/pkg/util"
@@ -47,6 +48,9 @@ func (p *RecordReader) Dispose() {
func (p *RecordReader) Run() (err error) {
pullJob := &p.PullJob
publisher := pullJob.Publisher
if publisher == nil {
return pkg.ErrDisabled
}
allocator := util.NewScalableMemoryAllocator(1 << 10)
var tagHeader [11]byte
var ts int64
@@ -60,6 +64,7 @@ func (p *RecordReader) Run() (err error) {
publisher.OnGetPosition = func() time.Time {
return realTime
}
for loop := 0; loop < p.Loop; loop++ {
nextStream:
for i, stream := range p.Streams {
@@ -85,15 +90,15 @@ func (p *RecordReader) Run() (err error) {
err = head.NewReader().ReadByteTo(&flvHead[0], &flvHead[1], &flvHead[2], &version, &flag)
hasAudio := (flag & 0x04) != 0
hasVideo := (flag & 0x01) != 0
if err != nil {
return
}
if !hasAudio {
publisher.NoAudio()
}
if !hasVideo {
publisher.NoVideo()
}
if err != nil {
return
}
if flvHead != [3]byte{'F', 'L', 'V'} {
return errors.New("not flv file")
}
@@ -194,7 +199,7 @@ func (p *RecordReader) Run() (err error) {
}
}
} else {
publisher.Info("script", name, obj)
p.Info("script", name, obj)
}
default:
err = fmt.Errorf("unknown tag type: %d", t)

View File

@@ -5,15 +5,12 @@ import (
"io"
"os"
"path/filepath"
"slices"
"time"
"gorm.io/gorm"
"m7s.live/v5"
"m7s.live/v5/pkg"
"m7s.live/v5/pkg/config"
"m7s.live/v5/pkg/task"
"m7s.live/v5/pkg/util"
rtmp "m7s.live/v5/plugin/rtmp/pkg"
)
@@ -144,7 +141,8 @@ func NewRecorder(conf config.Record) m7s.IRecorder {
type Recorder struct {
m7s.DefaultRecorder
stream m7s.RecordStream
writer *FlvWriter
file *os.File
}
var CustomFileName = func(job *m7s.RecordJob) string {
@@ -155,48 +153,34 @@ var CustomFileName = func(job *m7s.RecordJob) string {
}
func (r *Recorder) createStream(start time.Time) (err error) {
recordJob := &r.RecordJob
sub := recordJob.Subscriber
r.stream = m7s.RecordStream{
StartTime: start,
StreamPath: sub.StreamPath,
FilePath: CustomFileName(&r.RecordJob),
EventId: recordJob.EventId,
EventDesc: recordJob.EventDesc,
EventName: recordJob.EventName,
EventLevel: recordJob.EventLevel,
BeforeDuration: recordJob.BeforeDuration,
AfterDuration: recordJob.AfterDuration,
Mode: recordJob.Mode,
Type: "flv",
}
dir := filepath.Dir(r.stream.FilePath)
if err = os.MkdirAll(dir, 0755); err != nil {
r.RecordJob.RecConf.Type = "flv"
err = r.CreateStream(start, CustomFileName)
if err != nil {
return
}
if sub.Publisher.HasAudioTrack() {
r.stream.AudioCodec = sub.Publisher.AudioTrack.ICodecCtx.FourCC().String()
if r.file, err = os.OpenFile(r.Event.FilePath, os.O_CREATE|os.O_RDWR, 0666); err != nil {
return
}
if sub.Publisher.HasVideoTrack() {
r.stream.VideoCodec = sub.Publisher.VideoTrack.ICodecCtx.FourCC().String()
}
if recordJob.Plugin.DB != nil {
recordJob.Plugin.DB.Save(&r.stream)
_, err = r.file.Write(FLVHead)
r.writer = NewFlvWriter(r.file)
if err != nil {
return
}
return
}
func (r *Recorder) writeTailer(end time.Time) {
if r.stream.EndTime.After(r.stream.StartTime) {
if r.Event.EndTime.After(r.Event.StartTime) {
return
}
r.stream.EndTime = end
r.Event.EndTime = end
if r.RecordJob.Plugin.DB != nil {
r.RecordJob.Plugin.DB.Save(&r.stream)
writeMetaTagQueueTask.AddTask(&eventRecordCheck{
DB: r.RecordJob.Plugin.DB,
streamPath: r.stream.StreamPath,
})
if r.RecordJob.Event != nil {
r.RecordJob.Plugin.DB.Save(&r.Event)
} else {
r.RecordJob.Plugin.DB.Save(&r.Event.RecordStream)
}
writeMetaTagQueueTask.AddTask(m7s.NewEventRecordCheck(r.Event.Type, r.Event.StreamPath, r.RecordJob.Plugin.DB))
}
}
@@ -204,42 +188,7 @@ func (r *Recorder) Dispose() {
r.writeTailer(time.Now())
}
type eventRecordCheck struct {
task.Task
DB *gorm.DB
streamPath string
}
func (t *eventRecordCheck) Run() (err error) {
var eventRecordStreams []m7s.RecordStream
queryRecord := m7s.RecordStream{
EventLevel: m7s.EventLevelHigh,
Mode: m7s.RecordModeEvent,
Type: "flv",
}
t.DB.Where(&queryRecord).Find(&eventRecordStreams, "stream_path=?", t.streamPath) //搜索事件录像,且为重要事件(无法自动删除)
if len(eventRecordStreams) > 0 {
for _, recordStream := range eventRecordStreams {
var unimportantEventRecordStreams []m7s.RecordStream
queryRecord.EventLevel = m7s.EventLevelLow
query := `(start_time BETWEEN ? AND ?)
OR (end_time BETWEEN ? AND ?)
OR (? BETWEEN start_time AND end_time)
OR (? BETWEEN start_time AND end_time) AND stream_path=? `
t.DB.Where(&queryRecord).Where(query, recordStream.StartTime, recordStream.EndTime, recordStream.StartTime, recordStream.EndTime, recordStream.StartTime, recordStream.EndTime, recordStream.StreamPath).Find(&unimportantEventRecordStreams)
if len(unimportantEventRecordStreams) > 0 {
for _, unimportantEventRecordStream := range unimportantEventRecordStreams {
unimportantEventRecordStream.EventLevel = m7s.EventLevelHigh
t.DB.Save(&unimportantEventRecordStream)
}
}
}
}
return
}
func (r *Recorder) Run() (err error) {
var file *os.File
var filepositions []uint64
var times []float64
var offset int64
@@ -247,82 +196,27 @@ func (r *Recorder) Run() (err error) {
ctx := &r.RecordJob
suber := ctx.Subscriber
noFragment := ctx.RecConf.Fragment == 0 || ctx.RecConf.Append
startTime := time.Now()
if ctx.BeforeDuration > 0 {
startTime = startTime.Add(-ctx.BeforeDuration)
}
if err = r.createStream(startTime); err != nil {
return
}
if noFragment {
file, err = os.OpenFile(r.stream.FilePath, os.O_CREATE|os.O_RDWR|util.Conditional(ctx.RecConf.Append, os.O_APPEND, os.O_TRUNC), 0666)
if err != nil {
return
}
defer writeMetaTag(file, suber, filepositions, times, &duration)
}
if ctx.RecConf.Append {
var metaData rtmp.EcmaArray
metaData, err = ReadMetaData(file)
keyframes := metaData["keyframes"].(map[string]any)
filepositions = slices.Collect(func(yield func(uint64) bool) {
for _, v := range keyframes["filepositions"].([]float64) {
yield(uint64(v))
}
})
times = keyframes["times"].([]float64)
if _, err = file.Seek(-4, io.SeekEnd); err != nil {
ctx.Error("seek file failed", "err", err)
_, err = file.Write(FLVHead)
} else {
tmp := make(util.Buffer, 4)
tmp2 := tmp
_, err = file.Read(tmp)
tagSize := tmp.ReadUint32()
tmp = tmp2
_, err = file.Seek(int64(tagSize), io.SeekEnd)
_, err = file.Read(tmp2)
ts := tmp2.ReadUint24() | (uint32(tmp[3]) << 24)
ctx.Info("append flv", "last tagSize", tagSize, "last ts", ts)
suber.StartAudioTS = time.Duration(ts) * time.Millisecond
suber.StartVideoTS = time.Duration(ts) * time.Millisecond
offset, err = file.Seek(0, io.SeekEnd)
}
} else if ctx.RecConf.Fragment == 0 {
_, err = file.Write(FLVHead)
} else {
if file, err = os.OpenFile(r.stream.FilePath, os.O_CREATE|os.O_RDWR, 0666); err != nil {
return
}
_, err = file.Write(FLVHead)
}
writer := NewFlvWriter(file)
checkFragment := func(absTime uint32) {
checkFragment := func(absTime uint32, writeTime time.Time) {
if duration = int64(absTime); time.Duration(duration)*time.Millisecond >= ctx.RecConf.Fragment {
writeMetaTag(file, suber, filepositions, times, &duration)
r.writeTailer(time.Now())
writeMetaTag(r.file, suber, filepositions, times, &duration)
r.writeTailer(writeTime)
filepositions = []uint64{0}
times = []float64{0}
offset = 0
if err = r.createStream(time.Now()); err != nil {
if err = r.createStream(writeTime); err != nil {
return
}
if file, err = os.OpenFile(r.stream.FilePath, os.O_CREATE|os.O_RDWR, 0666); err != nil {
return
}
_, err = file.Write(FLVHead)
writer = NewFlvWriter(file)
if vr := suber.VideoReader; vr != nil {
vr.ResetAbsTime()
seq := vr.Track.SequenceFrame.(*rtmp.RTMPVideo)
err = writer.WriteTag(FLV_TAG_TYPE_VIDEO, 0, uint32(seq.Size), seq.Buffers...)
err = r.writer.WriteTag(FLV_TAG_TYPE_VIDEO, 0, uint32(seq.Size), seq.Buffers...)
offset = int64(seq.Size + 15)
}
if ar := suber.AudioReader; ar != nil {
ar.ResetAbsTime()
if ar.Track.SequenceFrame != nil {
seq := ar.Track.SequenceFrame.(*rtmp.RTMPAudio)
err = writer.WriteTag(FLV_TAG_TYPE_AUDIO, 0, uint32(seq.Size), seq.Buffers...)
err = r.writer.WriteTag(FLV_TAG_TYPE_AUDIO, 0, uint32(seq.Size), seq.Buffers...)
offset += int64(seq.Size + 15)
}
}
@@ -330,21 +224,33 @@ func (r *Recorder) Run() (err error) {
}
return m7s.PlayBlock(ctx.Subscriber, func(audio *rtmp.RTMPAudio) (err error) {
if suber.VideoReader == nil && !noFragment {
checkFragment(suber.AudioReader.AbsTime)
if r.Event.StartTime.IsZero() {
err = r.createStream(suber.AudioReader.Value.WriteTime)
if err != nil {
return err
}
}
err = writer.WriteTag(FLV_TAG_TYPE_AUDIO, suber.AudioReader.AbsTime, uint32(audio.Size), audio.Buffers...)
if suber.VideoReader == nil && !noFragment {
checkFragment(suber.AudioReader.AbsTime, suber.AudioReader.Value.WriteTime)
}
err = r.writer.WriteTag(FLV_TAG_TYPE_AUDIO, suber.AudioReader.AbsTime, uint32(audio.Size), audio.Buffers...)
offset += int64(audio.Size + 15)
return
}, func(video *rtmp.RTMPVideo) (err error) {
if r.Event.StartTime.IsZero() {
err = r.createStream(suber.VideoReader.Value.WriteTime)
if err != nil {
return err
}
}
if suber.VideoReader.Value.IDR {
filepositions = append(filepositions, uint64(offset))
times = append(times, float64(suber.VideoReader.AbsTime)/1000)
if !noFragment {
checkFragment(suber.VideoReader.AbsTime)
checkFragment(suber.VideoReader.AbsTime, suber.VideoReader.Value.WriteTime)
}
}
err = writer.WriteTag(FLV_TAG_TYPE_VIDEO, suber.VideoReader.AbsTime, uint32(video.Size), video.Buffers...)
err = r.writer.WriteTag(FLV_TAG_TYPE_VIDEO, suber.VideoReader.AbsTime, uint32(video.Size), video.Buffers...)
offset += int64(video.Size + 15)
return
})

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