Compare commits

..

283 Commits

Author SHA1 Message Date
langhuihui
479a0a79f6 refactor: frame converter and mp4 track improvements
- Refactor frame converter implementation
- Update mp4 track to use ICodex
- General refactoring and code improvements

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-08-28 13:57:58 +08:00
pggiroro
b6ee2843b0 fix: platform register failed 2025-08-22 15:17:33 +08:00
pggiroro
1a8e2bc816 fix: device offline when device unregister,fix delete device failed 2025-08-21 22:43:08 +08:00
pggiroro
bc0c761aa8 feat: batch snap from mp4 file 2025-08-21 22:43:08 +08:00
langhuihui
cabd0e3088 fix: all config plugin name to lowcase 2025-08-19 17:29:32 +08:00
yangjinxing123
2034f068c0 fix: Deadlock issue caused by device logout (#315)
Co-authored-by: yjx <yjx>
2025-08-15 15:28:23 +08:00
pggiroro
eba62c4054 feat: gb28181 support update channel name,channelid 2025-08-06 18:07:27 +08:00
pggiroro
a070dc64f8 fix: continue delete file when delete file that does not exist 2025-08-06 18:07:27 +08:00
langhuihui
e10dfec816 fix: remove pullproxy have to stop pulljob 2025-08-05 09:41:02 +08:00
pggiroro
96b9cbfc08 fix: gb28181 update use taskManager 2025-08-03 20:35:52 +08:00
pggiroro
2bbee90a9f feat: crontab init sql 2025-08-03 20:35:52 +08:00
pggiroro
272def302a feat: plugin snap support batch snap 2025-08-03 20:35:52 +08:00
pggiroro
04843002bf fix: platform get channel info from memory 2025-07-25 22:19:15 +08:00
pggiroro
e4810e9c55 fix: delete oldest mp4 file 2025-07-23 17:10:02 +08:00
langhuihui
15d830f1eb feat: add custom admin home page 2025-07-21 19:00:13 +08:00
langhuihui
ad32f6f96e feat: update pull or push proxy with optional args 2025-07-20 15:14:14 +08:00
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
langhuihui
180e766a24 feat: vod hlsv7 (fmp4) 2025-02-06 14:47:47 +08:00
langhuihui
de986bde24 feat: add record type 2025-02-05 16:45:05 +08:00
langhuihui
da4b8b4f5a doc: update readme 2025-01-30 21:15:28 +08:00
langhuihui
dc2995daf0 doc: add arch docs 2025-01-30 18:09:11 +08:00
langhuihui
3c2f87d38d chore: skip duplicate seq frame 2025-01-25 17:10:41 +08:00
pg
e845f4fb6c fix: Remove redundant NewPuller. 2025-01-23 20:42:19 +08:00
pg
bea10e2cdb fix: Check the timestamps of the audio packets. If the timestamp remains unchanged for 3 seconds, use the timestamp of the video packet as the timestamp for the video frame instead. 2025-01-23 20:33:35 +08:00
pg
b33a72caab feature: add feature that hls record and vod hls record file 2025-01-22 15:43:10 +08:00
langhuihui
9a0d22fa4e fix: seek no track 2025-01-19 10:32:11 +08:00
langhuihui
eacf91b904 chore: add log 2025-01-16 14:52:10 +08:00
banshan
9c785bdba0 fix: onvif pull stream 2025-01-15 15:46:30 +08:00
468 changed files with 79370 additions and 112380 deletions

View File

@@ -0,0 +1,5 @@
---
description: build pb
alwaysApply: false
---
如果修改了 proto 文件需要编译,请使用 scripts 目录下的脚本来编译

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

5
.gitignore vendored
View File

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

199
CLAUDE.md Normal file
View File

@@ -0,0 +1,199 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Project Overview
Monibuca is a high-performance streaming server framework written in Go. It's designed to be a modular, scalable platform for real-time audio/video streaming with support for multiple protocols including RTMP, RTSP, HLS, WebRTC, GB28181, and more.
## Development Commands
### Building and Running
**Basic Run (with SQLite):**
```bash
cd example/default
go run -tags sqlite main.go
```
**Build Tags:**
- `sqlite` - Enable SQLite database support
- `sqliteCGO` - Enable SQLite with CGO
- `mysql` - Enable MySQL database support
- `postgres` - Enable PostgreSQL database support
- `duckdb` - Enable DuckDB database support
- `disable_rm` - Disable memory pool
- `fasthttp` - Use fasthttp instead of net/http
- `taskpanic` - Enable panics for testing
**Protocol Buffer Generation:**
```bash
# Generate all proto files
sh scripts/protoc.sh
# Generate specific plugin proto
sh scripts/protoc.sh plugin_name
```
**Release Building:**
```bash
# Uses goreleaser configuration
goreleaser build
```
**Testing:**
```bash
go test ./...
```
## Architecture Overview
### Core Components
**Server (`server.go`):** Main server instance that manages plugins, streams, and configurations. Implements the central event loop and lifecycle management.
**Plugin System (`plugin.go`):** Modular architecture where functionality is provided through plugins. Each plugin implements the `IPlugin` interface and can provide:
- Protocol handlers (RTMP, RTSP, etc.)
- Media transformers
- Pull/Push proxies
- Recording capabilities
- Custom HTTP endpoints
**Configuration System (`pkg/config/`):** Hierarchical configuration system with priority order: dynamic modifications > environment variables > config files > default YAML > global config > defaults.
**Task System (`pkg/task/`):** Asynchronous task management with dependency handling, lifecycle management, and graceful shutdown capabilities.
### Key Interfaces
**Publisher:** Handles incoming media streams and manages track information
**Subscriber:** Handles outgoing media streams to clients
**Puller:** Pulls streams from external sources
**Pusher:** Pushes streams to external destinations
**Transformer:** Processes/transcodes media streams
**Recorder:** Records streams to storage
### Stream Processing Flow
1. **Publisher** receives media data and creates tracks
2. **Tracks** handle audio/video data with specific codecs
3. **Subscribers** attach to publishers to receive media
4. **Transformers** can process streams between publishers and subscribers
5. **Plugins** provide protocol-specific implementations
## Plugin Development
### Creating a Plugin
1. Implement the `IPlugin` interface
2. Define plugin metadata using `PluginMeta`
3. Register with `InstallPlugin[YourPluginType](meta)`
4. Optionally implement protocol-specific interfaces:
- `ITCPPlugin` for TCP servers
- `IUDPPlugin` for UDP servers
- `IQUICPlugin` for QUIC servers
- `IRegisterHandler` for HTTP endpoints
### Plugin Lifecycle
1. **Init:** Configuration parsing and initialization
2. **Start:** Network listeners and task registration
3. **Run:** Active operation
4. **Dispose:** Cleanup and shutdown
## Configuration Structure
### Global Configuration
- HTTP/TCP/UDP/QUIC listeners
- Database connections (SQLite, MySQL, PostgreSQL, DuckDB)
- Authentication settings
- Admin interface settings
- Global stream alias mappings
### Plugin Configuration
Each plugin can define its own configuration structure that gets merged with global settings.
## Database Integration
Supports multiple database backends:
- **SQLite:** Default lightweight option
- **MySQL:** Production deployments
- **PostgreSQL:** Production deployments
- **DuckDB:** Analytics use cases
Automatic migration is handled for core models including users, proxies, and stream aliases.
## Protocol Support
### Built-in Plugins
- **RTMP:** Real-time messaging protocol
- **RTSP:** Real-time streaming protocol
- **HLS:** HTTP live streaming
- **WebRTC:** Web real-time communication
- **GB28181:** Chinese surveillance standard
- **FLV:** Flash video format
- **MP4:** MPEG-4 format
- **SRT:** Secure reliable transport
## Authentication & Security
- JWT-based authentication for admin interface
- Stream-level authentication with URL signing
- Role-based access control (admin/user)
- Webhook support for external auth integration
## Development Guidelines
### Code Style
- Follow existing patterns and naming conventions
- Use the task system for async operations
- Implement proper error handling and logging
- Use the configuration system for all settings
### Testing
- Unit tests should be placed alongside source files
- Integration tests can use the example configurations
- Use the mock.py script for protocol testing
### Performance Considerations
- Memory pool is enabled by default (disable with `disable_rm`)
- Zero-copy design for media data where possible
- Lock-free data structures for high concurrency
- Efficient buffer management with ring buffers
## Debugging
### Built-in Debug Plugin
- Performance monitoring and profiling
- Real-time metrics via Prometheus endpoint (`/api/metrics`)
- pprof integration for memory/cpu profiling
### Logging
- Structured logging with zerolog
- Configurable log levels
- Log rotation support
- Fatal crash logging
## Web Admin Interface
- Web-based admin UI served from `admin.zip`
- RESTful API for all operations
- Real-time stream monitoring
- Configuration management
- User management (when auth enabled)
## Common Issues
### Port Conflicts
- Default HTTP port: 8080
- Default gRPC port: 50051
- Check plugin-specific port configurations
### Database Connection
- Ensure proper build tags for database support
- Check DSN configuration strings
- Verify database file permissions
### Plugin Loading
- Plugins are auto-discovered from imports
- Check plugin enable/disable status
- Verify configuration merging

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">
@@ -36,8 +39,11 @@
<li><a href="#build-tags">Build Tags</a></li>
<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>
</ol>
</details>
@@ -110,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>
@@ -133,6 +140,18 @@ Monibuca's functionality can be extended through plugins. For information on cre
<p align="right">(<a href="#readme-top">back to top</a>)</p>
## Architecture
For detailed architecture design documentation, please refer to the [Architecture Documentation](./doc/arch/index.md).
<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>
@@ -38,8 +38,10 @@
<li><a href="#构建选项">构建选项</a></li>
<li><a href="#监控系统">监控系统</a></li>
<li><a href="#插件开发">插件开发</a></li>
<li><a href="#架构文档">架构文档</a></li>
<li><a href="#贡献指南">贡献指南</a></li>
<li><a href="#许可证">许可证</a></li>
<li><a href="#联系方式">联系方式</a></li>
</ol>
</details>
@@ -71,6 +73,7 @@ Monibuca简称 m7s是一款纯 Go 开发的开源流媒体服务器开发
<p align="right">(<a href="#readme-top">返回顶部</a>)</p>
## 快速开始
### 环境要求
@@ -112,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>
@@ -135,6 +139,16 @@ Monibuca 支持通过插件扩展功能。查看[插件开发指南](./plugin/RE
<p align="right">(<a href="#readme-top">返回顶部</a>)</p>
## 架构文档
详细的架构设计文档请查看 [架构文档](./doc_CN/arch/index.md)。
<p align="right">(<a href="#readme-top">返回顶部</a>)</p>
## 第三方插件
- - https://github.com/cuteLittleDevil/m7s-jt1078
## 贡献指南
我们非常欢迎社区贡献,您的参与将使开源社区变得更加精彩!

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

@@ -0,0 +1,151 @@
# Monibuca v5.0.x Release Notes
## v5.0.4 (2025-08-15)
### 新增 / 改进 (Features & Improvements)
- GB28181: 支持更新 channelName / channelIdeba62c4
- 定时任务(crontab): 初始化 SQL 支持2bbee90
- Snap 插件: 支持批量抓图272def3
- 管理后台: 支持自定义首页15d830f
- 推/拉代理: 支持可选参数更新ad32f6f
- 心跳/脉冲: pulse interval 允许为 017faf3f
- 告警上报: 通过 Hook 发送报警baf3640
- 告警信息上报: 通过 Hook 发送 alarminfocad47ae
## 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"
}

View File

@@ -48,7 +48,7 @@ func (s *Server) initStreamAlias() {
func (s *Server) GetStreamAlias(ctx context.Context, req *emptypb.Empty) (res *pb.StreamAliasListResponse, err error) {
res = &pb.StreamAliasListResponse{}
s.Streams.Call(func() error {
s.CallOnStreamTask(func() {
for alias := range s.AliasStreams.Range {
info := &pb.StreamAlias{
StreamPath: alias.StreamPath,
@@ -62,18 +62,17 @@ func (s *Server) GetStreamAlias(ctx context.Context, req *emptypb.Empty) (res *p
}
res.Data = append(res.Data, info)
}
return nil
})
return
}
func (s *Server) SetStreamAlias(ctx context.Context, req *pb.SetStreamAliasRequest) (res *pb.SuccessResponse, err error) {
res = &pb.SuccessResponse{}
s.Streams.Call(func() error {
s.CallOnStreamTask(func() {
if req.StreamPath != "" {
u, err := url.Parse(req.StreamPath)
if err != nil {
return err
return
}
req.StreamPath = strings.TrimPrefix(u.Path, "/")
publisher, canReplace := s.Streams.Get(req.StreamPath)
@@ -159,7 +158,6 @@ func (s *Server) SetStreamAlias(ctx context.Context, req *pb.SetStreamAliasReque
}
}
}
return nil
})
return
}

710
api.go
View File

@@ -7,12 +7,12 @@ import (
"net/http"
"net/url"
"os"
"path/filepath"
"reflect"
"runtime"
"strings"
"time"
"m7s.live/v5/pkg/config"
"m7s.live/v5/pkg/task"
myip "github.com/husanpao/ip"
@@ -26,7 +26,7 @@ import (
"gopkg.in/yaml.v3"
"m7s.live/v5/pb"
"m7s.live/v5/pkg"
"m7s.live/v5/pkg/config"
"m7s.live/v5/pkg/format"
"m7s.live/v5/pkg/util"
)
@@ -79,7 +79,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 +97,13 @@ 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 format.AnnexB
err = pkg.ConvertFrameType(reader.Value.Wraps[0], &annexb)
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) {
@@ -159,6 +150,9 @@ func (s *Server) getStreamInfo(pub *Publisher) (res *pb.StreamInfoResponse, err
}
res.Data.AudioTrack.SampleRate = uint32(t.ICodecCtx.(pkg.IAudioCodecCtx).GetSampleRate())
res.Data.AudioTrack.Channels = uint32(t.ICodecCtx.(pkg.IAudioCodecCtx).GetChannels())
if pub.State == PublisherStateInit {
res.Data.State = int32(PublisherStateTrackAdded)
}
}
}
if t := pub.VideoTrack.AVTrack; t != nil {
@@ -174,6 +168,9 @@ func (s *Server) getStreamInfo(pub *Publisher) (res *pb.StreamInfoResponse, err
}
res.Data.VideoTrack.Width = uint32(t.ICodecCtx.(pkg.IVideoCodecCtx).Width())
res.Data.VideoTrack.Height = uint32(t.ICodecCtx.(pkg.IVideoCodecCtx).Height())
if pub.State == PublisherStateInit {
res.Data.State = int32(PublisherStateTrackAdded)
}
}
}
return
@@ -181,32 +178,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.FilePath,
Mode: record.Mode,
Fragment: durationpb.New(record.Fragment),
Append: record.Append,
PluginName: record.Plugin.Meta.Name,
})
}
s.Records.Range(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
}
@@ -226,11 +218,13 @@ func (s *Server) TaskTree(context.Context, *emptypb.Empty) (res *pb.TaskTreeResp
StartTime: timestamppb.New(t.StartTime),
Description: m.GetDescriptions(),
StartReason: t.StartReason,
Level: uint32(t.GetLevel()),
}
if job, ok := m.(task.IJob); ok {
if blockedTask := job.Blocked(); blockedTask != nil {
res.Blocked = fillData(blockedTask)
}
res.EventLoopRunning = job.EventLoopRunning()
for t := range job.RangeSubTask {
child := fillData(t)
if child == nil {
@@ -264,23 +258,21 @@ 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.Range(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
}
func (s *Server) GetSubscribers(context.Context, *pb.SubscribersRequest) (res *pb.SubscribersResponse, err error) {
s.Streams.Call(func() error {
s.CallOnStreamTask(func() {
var subscribers []*pb.SubscriberSnapShot
for subscriber := range s.Subscribers.Range {
meta, _ := json.Marshal(subscriber.GetDescriptions())
@@ -319,55 +311,52 @@ func (s *Server) GetSubscribers(context.Context, *pb.SubscribersRequest) (res *p
Data: subscribers,
Total: int32(s.Subscribers.Length),
}
return nil
})
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 && v.TryRLock() {
defer v.RUnlock()
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.GetSample().Timestamp / 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 +372,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.GetSample().Timestamp / 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 +405,69 @@ 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.GetSample().Timestamp / 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 && v.TryRLock() {
defer v.RUnlock()
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.GetSample().Timestamp / 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
}
@@ -504,126 +485,135 @@ func (s *Server) Shutdown(ctx context.Context, req *pb.RequestWithId) (res *pb.S
}
func (s *Server) ChangeSubscribe(ctx context.Context, req *pb.ChangeSubscribeRequest) (res *pb.SuccessResponse, err error) {
s.Streams.Call(func() error {
s.CallOnStreamTask(func() {
if subscriber, ok := s.Subscribers.Get(req.Id); ok {
if pub, ok := s.Streams.Get(req.StreamPath); ok {
subscriber.Publisher.RemoveSubscriber(subscriber)
subscriber.StreamPath = req.StreamPath
pub.AddSubscriber(subscriber)
return nil
return
}
}
err = pkg.ErrNotFound
return nil
})
return &pb.SuccessResponse{}, err
}
func (s *Server) StopSubscribe(ctx context.Context, req *pb.RequestWithId) (res *pb.SuccessResponse, err error) {
s.Streams.Call(func() error {
s.CallOnStreamTask(func() {
if subscriber, ok := s.Subscribers.Get(req.Id); ok {
subscriber.Stop(errors.New("stop by api"))
} else {
err = pkg.ErrNotFound
}
return nil
})
return &pb.SuccessResponse{}, err
}
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.FilePath,
Mode: record.Mode,
Fragment: durationpb.New(record.Fragment),
Append: record.Append,
PluginName: record.Plugin.Meta.Name,
Pointer: uint64(record.GetTaskPointer()),
})
for record := range s.Records.Range {
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
}
func (s *Server) WaitList(context.Context, *emptypb.Empty) (res *pb.StreamWaitListResponse, err error) {
s.Streams.Call(func() error {
s.CallOnStreamTask(func() {
res = &pb.StreamWaitListResponse{
List: make(map[string]int32),
}
for subs := range s.Waiting.Range {
res.List[subs.StreamPath] = int32(subs.Length)
}
return nil
})
return
}
func (s *Server) GetSubscriptionProgress(ctx context.Context, req *pb.StreamSnapRequest) (res *pb.SubscriptionProgressResponse, err error) {
s.CallOnStreamTask(func() {
if waitStream, ok := s.Waiting.Get(req.StreamPath); ok {
progress := waitStream.Progress
res = &pb.SubscriptionProgressResponse{
Code: 0,
Message: "success",
Data: &pb.SubscriptionProgressData{
CurrentStep: int32(progress.CurrentStep),
},
}
// Convert steps
for _, step := range progress.Steps {
pbStep := &pb.Step{
Name: step.Name,
Description: step.Description,
Error: step.Error,
}
if !step.StartedAt.IsZero() {
pbStep.StartedAt = timestamppb.New(step.StartedAt)
}
if !step.CompletedAt.IsZero() {
pbStep.CompletedAt = timestamppb.New(step.CompletedAt)
}
res.Data.Steps = append(res.Data.Steps, pbStep)
}
} else {
err = pkg.ErrNotFound
}
})
return
}
@@ -638,24 +628,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) {
@@ -698,10 +682,10 @@ func (s *Server) Summary(context.Context, *emptypb.Empty) (res *pb.SummaryRespon
netWorks = append(netWorks, info)
}
res.StreamCount = int32(s.Streams.Length)
res.PullCount = int32(s.Pulls.Length)
res.PushCount = int32(s.Pushs.Length)
res.PullCount = int32(s.Pulls.Length())
res.PushCount = int32(s.Pushs.Length())
res.SubscribeCount = int32(s.Subscribers.Length)
res.RecordCount = int32(s.Records.Length)
res.RecordCount = int32(s.Records.Length())
res.TransformCount = int32(s.Transforms.Length)
res.NetWork = netWorks
s.lastSummary = res
@@ -739,7 +723,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 +767,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 +788,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 +806,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 +823,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
@@ -938,7 +959,7 @@ func (s *Server) DeleteRecord(ctx context.Context, req *pb.ReqRecordDelete) (res
func (s *Server) GetTransformList(ctx context.Context, req *emptypb.Empty) (res *pb.TransformListResponse, err error) {
res = &pb.TransformListResponse{}
s.Transforms.Call(func() error {
s.Transforms.Call(func() {
for transform := range s.Transforms.Range {
info := &pb.Transform{
StreamPath: transform.StreamPath,
@@ -950,13 +971,126 @@ func (s *Server) GetTransformList(ctx context.Context, req *emptypb.Empty) (res
result, err = yaml.Marshal(transform.TransformJob.Config)
if err != nil {
s.Error("marshal transform config failed", "error", err)
return err
return
}
info.Config = string(result)
}
res.Data = append(res.Data, info)
}
return nil
})
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 != "" && !strings.EqualFold(meta.Name, filterName) {
continue
}
name := strings.ToLower(meta.Name)
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
}{name, mergedConf})
} else {
configSections = append(configSections, struct {
name string
data any
}{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

111
doc/arch/admin.md Normal file
View File

@@ -0,0 +1,111 @@
# Admin Service Mechanism
Monibuca provides powerful administrative service support for system monitoring, configuration management, plugin management, and other administrative functions. This document details the implementation mechanism and usage of the Admin service.
## Service Architecture
### 1. UI Interface
The Admin service provides a Web management interface by loading the `admin.zip` file. This interface has the following features:
- Unified management interface entry point
- Access to all server-provided HTTP interfaces
- Responsive design, supporting various devices
- Modular function organization
### 2. Configuration Management
Admin service configuration is located in the admin node within global configuration, including:
```yaml
admin:
enableLogin: false # Whether to enable login mechanism
filePath: admin.zip # Management interface file path
homePage: home # Management interface homepage
users: # User list (effective only when login is enabled)
- username: admin # Username
password: admin # Password
role: admin # Role, options: admin, user
```
When `enableLogin` is false, all users access as anonymous users.
When login is enabled and no users exist in the database, the system automatically creates a default admin account (username: admin, password: admin).
### 3. Authentication Mechanism
Admin provides dedicated user login verification interfaces for:
- User identity verification
- Access token management (JWT)
- Permission control
- Session management
### 4. Interface Specifications
All Admin APIs must follow these specifications:
- Response format uniformly includes code, message, and data fields
- Successful responses use code = 0
- Error handling uses unified error response format
- Must perform permission verification
## Function Modules
### 1. System Monitoring
- CPU usage monitoring
- Memory usage
- Network bandwidth statistics
- Disk usage
- System uptime
- Online user statistics
### 2. Plugin Management
- Plugin enable/disable
- Plugin configuration modification
- Plugin status viewing
- Plugin version management
- Plugin dependency checking
### 3. Stream Media Management
- Online stream list viewing
- Stream status monitoring
- Stream control (start/stop)
- Stream information statistics
- Recording management
- Transcoding task management
## Security Mechanism
### 1. Authentication Mechanism
- JWT token authentication
- Session timeout control
- IP whitelist control
### 2. Permission Control
- Role-Based Access Control (RBAC)
- Fine-grained permission management
- Operation audit logging
- Sensitive operation confirmation
## Best Practices
1. Security
- Use HTTPS encryption
- Implement strong password policies
- Regular key updates
- Monitor abnormal access
2. Performance Optimization
- Reasonable caching strategy
- Paginated query optimization
- Asynchronous processing of time-consuming operations
3. Maintainability
- Complete operation logs
- Clear error messages
- Hot configuration updates

157
doc/arch/alias.md Normal file
View File

@@ -0,0 +1,157 @@
# Monibuca Stream Alias Technical Implementation Documentation
## 1. Feature Overview
Stream Alias is an important feature in Monibuca that allows creating one or more aliases for existing streams, enabling the same stream to be accessed through different paths. This feature is particularly useful in the following scenarios:
- Creating short aliases for streams with long paths
- Dynamically modifying stream access paths
- Implementing stream redirection functionality
## 2. Core Data Structures
### 2.1 AliasStream Structure
```go
type AliasStream struct {
*Publisher // Inherits from Publisher
AutoRemove bool // Whether to automatically remove
StreamPath string // Original stream path
Alias string // Alias path
}
```
### 2.2 StreamAlias Message Structure
```protobuf
message StreamAlias {
string streamPath = 1; // Original stream path
string alias = 2; // Alias
bool autoRemove = 3; // Whether to automatically remove
uint32 status = 4; // Status
}
```
## 3. Core Functionality Implementation
### 3.1 Alias Creation and Modification
When calling the `SetStreamAlias` API to create or modify an alias, the system:
1. Validates and parses the target stream path
2. Checks if the target stream exists
3. Handles the following scenarios:
- Modifying existing alias: Updates auto-remove flag and stream path
- Creating new alias: Initializes new AliasStream structure
4. Handles subscriber transfer or wakes waiting subscribers
### 3.2 Publisher Startup Alias Handling
When a Publisher starts, the system:
1. Checks for aliases pointing to this Publisher
2. For each matching alias:
- If alias Publisher is empty, sets it to the new Publisher
- If alias already has a Publisher, transfers subscribers to the new Publisher
3. Wakes all subscribers waiting for this stream
### 3.3 Publisher Destruction Alias Handling
Publisher destruction process:
1. Checks if stopped due to being kicked out
2. Removes Publisher from Streams
3. Iterates through all aliases, for those pointing to this Publisher:
- If auto-remove is set, deletes the alias
- Otherwise, retains alias structure
4. Handles related subscribers
### 3.4 Subscriber Handling Mechanism
When a new subscription request arrives:
1. Checks for matching alias
2. If alias exists:
- If alias Publisher exists: adds subscriber
- If Publisher doesn't exist: triggers OnSubscribe event
3. If no alias exists:
- Checks for matching regex alias
- Checks if original stream exists
- Adds subscriber or joins wait list based on conditions
## 4. API Interfaces
### 4.1 Set Alias
```http
POST /api/stream/alias
```
Request body:
```json
{
"streamPath": "original stream path",
"alias": "alias path",
"autoRemove": false
}
```
### 4.2 Get Alias List
```http
GET /api/stream/alias
```
Response body:
```json
{
"code": 0,
"message": "",
"data": [
{
"streamPath": "original stream path",
"alias": "alias path",
"autoRemove": false,
"status": 1
}
]
}
```
## 5. Status Descriptions
Alias status descriptions:
- 0: Initial state
- 1: Alias associated with Publisher
- 2: Original stream with same name exists
## 6. Best Practices
1. Using Auto-Remove (autoRemove)
- Enable auto-remove when temporary stream redirection is needed
- This ensures automatic alias cleanup when original stream ends
2. Alias Naming Recommendations
- Use short, meaningful aliases
- Avoid special characters
- Use standardized path format
3. Performance Considerations
- Alias mechanism uses efficient memory mapping
- Maintains connection state during subscriber transfer
- Supports dynamic modification without service restart
## 7. Important Notes
1. Alias Conflict Handling
- System handles appropriately when created alias conflicts with existing stream path
- Recommended to check for conflicts before creating aliases
2. Subscriber Behavior
- Existing subscribers are transferred to new stream when alias is modified
- Ensure clients can handle stream redirection
3. Resource Management
- Clean up unnecessary aliases promptly
- Use auto-remove feature appropriately
- Monitor alias status to avoid resource leaks

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

245
doc/arch/config.md Normal file
View File

@@ -0,0 +1,245 @@
# Monibuca Configuration Mechanism
Monibuca employs a flexible configuration system that supports multiple configuration methods. Configuration files use the YAML format and can be initialized either through files or by directly passing configuration objects.
## Configuration Loading Process
1. Configuration initialization occurs during Server startup and can be provided through one of three methods:
- YAML configuration file path
- Byte array containing YAML configuration content
- Raw configuration object (RawConfig)
2. Configuration parsing process:
```go
// Supports three configuration input methods
case string: // Configuration file path
case []byte: // YAML configuration content
case RawConfig: // Raw configuration object
```
## Configuration Structure
### Simplified Configuration Syntax
When a configuration item's value is a struct or map type, the system supports a simplified configuration approach: if a simple type value is configured directly, that value will be automatically assigned to the first field of the struct.
For example, given the following struct:
```go
type Config struct {
Port int
Host string
}
```
You can use simplified syntax:
```yaml
plugin: 1935 # equivalent to plugin: { port: 1935 }
```
### Configuration Deserialization Mechanism
Each plugin contains a `config.Config` type field for storing and managing configuration information. The configuration loading priority from highest to lowest is:
1. User configuration (via `ParseUserFile`)
2. Default configuration (via `ParseDefaultYaml`)
3. Global configuration (via `ParseGlobal`)
4. Plugin-specific configuration (via `Parse`)
5. Common configuration (via `Parse`)
Configurations are automatically deserialized into the plugin's public properties. For example:
```go
type MyPlugin struct {
Plugin
Port int `yaml:"port"`
Host string `yaml:"host"`
}
```
Corresponding YAML configuration:
```yaml
myplugin:
port: 8080
host: "localhost"
```
The configuration will automatically deserialize to the `Port` and `Host` fields. You can query configurations using methods provided by `Config`:
- `Has(name string)` - Check if a configuration exists
- `Get(name string)` - Get the value of a configuration
- `GetMap()` - Get a map of all configurations
Additionally, plugin configurations support saving modifications:
```go
func (p *Plugin) SaveConfig() (err error)
```
This saves the modified configuration to `{settingDir}/{pluginName}.yaml`.
### Global Configuration
Global configuration is located under the `global` node in the YAML file and includes these main configuration items:
```yaml
global:
settingDir: ".m7s" # Settings directory
fatalDir: "fatal" # Error log directory
pulseInterval: "5s" # Heartbeat interval
disableAll: false # Whether to disable all plugins
streamAlias: # Stream alias configuration
pattern: "target" # Regex -> target path
location: # HTTP routing rules
pattern: "target" # Regex -> target address
admin: # Admin interface configuration
enableLogin: false # Whether to enable login mechanism
filePath: "admin.zip" # Admin interface file path
homePage: "home" # Admin interface homepage
users: # User list (effective only when login is enabled)
- username: "admin" # Username
password: "admin" # Password
role: "admin" # Role (admin/user)
```
### Database Configuration
If database connection is configured, the system will automatically:
1. Connect to the database
2. Auto-migrate data models
3. Initialize user data (if login is enabled)
4. Initialize proxy configurations
```yaml
global:
db:
dsn: "" # Database connection string
type: "" # Database type
```
### Proxy Configuration
The system supports pull and push proxy configurations:
```yaml
global:
pullProxy: # Pull proxy configuration
- id: 1 # Proxy ID
name: "proxy1" # Proxy name
url: "rtmp://..." # Proxy address
type: "rtmp" # Proxy type
pullOnStart: true # Whether to pull on startup
pushProxy: # Push proxy configuration
- id: 1 # Proxy ID
name: "proxy1" # Proxy name
url: "rtmp://..." # Proxy address
type: "rtmp" # Proxy type
pushOnStart: true # Whether to push on startup
audio: true # Whether to push audio
```
## Plugin Configuration
Each plugin can have its own configuration node, named as the lowercase version of the plugin name:
```yaml
rtmp: # RTMP plugin configuration
port: 1935 # Listen port
rtsp: # RTSP plugin configuration
port: 554 # Listen port
```
## Configuration Priority
The configuration system uses a multi-level priority mechanism, from highest to lowest:
1. URL Query Parameter Configuration - Configurations specified via URL query parameters during publishing or subscribing have the highest priority
```
Example: rtmp://localhost/live/stream?audio=false
```
2. Plugin-Specific Configuration - Configuration items under the plugin's configuration node
```yaml
rtmp:
publish:
audio: true
subscribe:
audio: true
```
3. Global Configuration - Configuration items under the global node
```yaml
global:
publish:
audio: true
subscribe:
audio: true
```
## Common Configuration
There are some common configuration items that can appear in both global and plugin configurations. When plugins use these items, they prioritize values from plugin configuration, falling back to global configuration if not set in plugin configuration.
Main common configurations include:
1. Publish Configuration
```yaml
publish:
audio: true # Whether to include audio
video: true # Whether to include video
bufferLength: 1000 # Buffer length
```
2. Subscribe Configuration
```yaml
subscribe:
audio: true # Whether to subscribe to audio
video: true # Whether to subscribe to video
bufferLength: 1000 # Buffer length
```
3. HTTP Configuration
```yaml
http:
listenAddr: ":8080" # Listen address
```
4. Other Common Configurations
- PublicIP - Public IP
- PublicIPv6 - Public IPv6
- LogLevel - Log level
- EnableAuth - Whether to enable authentication
Usage example:
```yaml
# Global configuration
global:
publish:
audio: true
video: true
subscribe:
audio: true
video: true
# Plugin configuration (higher priority than global)
rtmp:
publish:
audio: false # Overrides global configuration
subscribe:
video: false # Overrides global configuration
# URL query parameters (highest priority)
# rtmp://localhost/live/stream?audio=true&video=false
```
## Hot Configuration Update
Currently, the system supports hot updates for the admin interface file (admin.zip), periodically checking for changes and automatically reloading.
## Configuration Validation
The system performs basic validation of configurations at startup:
1. Checks necessary directory permissions
2. Validates database connections
3. Validates user configurations (if login is enabled)

94
doc/arch/db.md Normal file
View File

@@ -0,0 +1,94 @@
# Database Mechanism
Monibuca provides database support functionality, allowing database configuration and usage in both global settings and plugins.
## Configuration
### Global Configuration
Database can be configured in global settings using these fields:
```yaml
global:
dsn: "database connection string"
dbType: "database type"
```
### Plugin Configuration
Each plugin can have its own database configuration:
```yaml
pluginName:
dsn: "database connection string"
dbType: "database type"
```
## Database Initialization Process
### Global Database Initialization
1. When the server starts, if `dsn` is configured, it attempts to connect to the database
2. After successful connection, the following models are automatically migrated:
- User table
- PullProxy table
- PushProxy table
- StreamAliasDB table
3. If login is enabled (`Admin.EnableLogin = true`), users are created or updated based on the configuration file
4. If no users exist in the database, a default admin account is created:
- Username: admin
- Password: admin
- Role: admin
### Plugin Database Initialization
1. During plugin initialization, the plugin's `dsn` configuration is checked
2. If the plugin's `dsn` matches the global configuration, the global database connection is used
3. If the plugin configures a different `dsn`, a new database connection is created
4. If the plugin implements the Recorder interface, the RecordStream table is automatically migrated
## Database Usage
### Global Database Access
The global database can be accessed through the Server instance:
```go
server.DB
```
### Plugin Database Access
Plugins can access their database through their instance:
```go
plugin.DB
```
## Important Notes
1. Database connection failures will disable related functionality
2. Plugins using independent databases need to manage their own database connections
3. Database migration failures will cause plugins to be disabled
4. It's recommended to reuse the global database connection when possible to avoid creating too many connections
## Built-in Tables
### User Table
Stores user information, including:
- Username: User's name
- Password: User's password
- Role: User's role (admin/user)
### PullProxy Table
Stores pull proxy configurations
### PushProxy Table
Stores push proxy configurations
### StreamAliasDB Table
Stores stream alias configurations
### RecordStream Table
Stores recording-related information (only created when plugin implements Recorder interface)

72
doc/arch/grpc.md Normal file
View File

@@ -0,0 +1,72 @@
# gRPC Service Mechanism
Monibuca provides gRPC service support, allowing plugins to offer services via the gRPC protocol. This document explains the implementation mechanism and usage of gRPC services.
## Service Registration Mechanism
### 1. Service Registration
Plugins need to pass ServiceDesc and Handler when calling `InstallPlugin` to register gRPC services:
```go
// Example: Registering gRPC service in a plugin
type MyPlugin struct {
pb.UnimplementedApiServer
m7s.Plugin
}
var _ = m7s.InstallPlugin[MyPlugin](
m7s.DefaultYaml(`your yaml config here`),
&pb.Api_ServiceDesc, // gRPC service descriptor
pb.RegisterApiHandler, // gRPC gateway handler
// ... other parameters
)
```
### 2. Proto File Specifications
All gRPC services must follow these Proto file specifications:
- Response structs must include code, message, and data fields
- Error handling should return errors directly, without manually setting code and message
- Run `sh scripts/protoc.sh` to generate pb files after modifying global.proto
- Run `sh scripts/protoc.sh {pluginName}` to generate corresponding pb files after modifying plugin-related proto files
## Service Implementation Mechanism
### 1. Server Configuration
gRPC services use port settings from the global TCP configuration:
```yaml
global:
tcp:
listenaddr: :8080 # gRPC service listen address and port
listentls: :8443 # gRPC TLS service listen address and port (if enabled)
```
Configuration items include:
- Listen address and port settings (specified in global TCP configuration)
- TLS/SSL certificate configuration (if enabled)
### 2. Error Handling
Error handling follows these principles:
- Return errors directly, no need to manually set code and message
- The system automatically handles errors and sets response format
## Best Practices
1. Service Definition
- Clear service interface design
- Appropriate method naming
- Complete interface documentation
2. Performance Optimization
- Use streaming for large data
- Set reasonable timeout values
3. Security Considerations
- Enable TLS encryption as needed
- Implement necessary access controls

145
doc/arch/http.md Normal file
View File

@@ -0,0 +1,145 @@
# HTTP Service Mechanism
Monibuca provides comprehensive HTTP service support, including RESTful API, WebSocket, HTTP-FLV, and other protocols. This document details the implementation mechanism and usage of the HTTP service.
## HTTP Configuration
### 1. Configuration Priority
- Plugin HTTP configuration takes precedence over global HTTP configuration
- If a plugin doesn't have HTTP configuration, global HTTP configuration is used
### 2. Configuration Items
```yaml
# Global configuration example
global:
http:
listenaddr: :8080 # Listen address and port
listentlsaddr: :8081 # TLS listen address and port
certfile: "" # SSL certificate file path
keyfile: "" # SSL key file path
cors: true # Whether to allow CORS
username: "" # Basic auth username
password: "" # Basic auth password
# Plugin configuration example (takes precedence over global config)
plugin_name:
http:
listenaddr: :8081
cors: false
username: "admin"
password: "123456"
```
## Service Processing Flow
### 1. Request Processing Order
When the HTTP server receives a request, it processes it in the following order:
1. First attempts to forward to the corresponding gRPC service
2. If no corresponding gRPC service is found, looks for plugin-registered HTTP handlers
3. If nothing is found, returns a 404 error
### 2. Handler Registration Methods
Plugins can register HTTP handlers in two ways:
1. Reflection Registration: The system automatically obtains plugin handling methods through reflection
- Method names must start with uppercase to be reflected (Go language rule)
- Usually use `API_` as method name prefix (recommended but not mandatory)
- Method signature must be `func(w http.ResponseWriter, r *http.Request)`
- URL path auto-generation rules:
- Underscores `_` in method names are converted to slashes `/`
- Example: `API_relay_` method maps to `/API/relay/*` path
- If a method name ends with underscore, it indicates a wildcard path that matches any subsequent path
2. Manual Registration: Plugin implements `IRegisterHandler` interface for manual registration
- Lowercase methods can't be reflected, must be registered manually
- Manual registration can use path parameters (like `:id`)
- More flexible routing rule configuration
Example code:
```go
// Reflection registration example
type YourPlugin struct {
// ...
}
// Uppercase start, can be reflected
// Automatically maps to /API/relay/*
func (p *YourPlugin) API_relay_(w http.ResponseWriter, r *http.Request) {
// Handle wildcard path requests
}
// Lowercase start, can't be reflected, needs manual registration
func (p *YourPlugin) handleUserRequest(w http.ResponseWriter, r *http.Request) {
// Handle parameterized requests
}
// Manual registration example
func (p *YourPlugin) RegisterHandler() {
// Can use path parameters
engine.GET("/api/user/:id", p.handleUserRequest)
}
```
## Middleware Mechanism
### 1. Adding Middleware
Plugins can add global middleware using the `AddMiddleware` method to handle all HTTP requests. Middleware executes in the order it was added.
Example code:
```go
func (p *YourPlugin) Start() {
// Add authentication middleware
p.GetCommonConf().AddMiddleware(func(next http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
// Execute before request handling
if !authenticate(r) {
http.Error(w, "Unauthorized", http.StatusUnauthorized)
return
}
// Call next handler
next(w, r)
// Execute after request handling
}
})
}
```
### 2. Middleware Use Cases
- Authentication and Authorization
- Request Logging
- CORS Handling
- Request Rate Limiting
- Response Header Setting
- Error Handling
- Performance Monitoring
## Special Protocol Support
### 1. HTTP-FLV
- Supports HTTP-FLV live stream distribution
- Automatically generates FLV headers
- Supports GOP caching
- Supports WebSocket-FLV
### 2. HTTP-MP4
- Supports HTTP-MP4 stream distribution
- Supports fMP4 file distribution
### 3. HLS
- Supports HLS protocol
- Supports MPEG-TS encapsulation
### 4. WebSocket
- Supports custom message protocols
- Supports ws-flv
- Supports ws-mp4

57
doc/arch/index.md Normal file
View File

@@ -0,0 +1,57 @@
# Architecture Design
## Directory Structure
[catalog.md](./catalog.md)
## Audio/Video Streaming System
### Relay Mechanism
[relay.md](./relay.md)
### Alias Mechanism
[alias.md](./alias.md)
### Authentication Mechanism
[auth.md](./auth.md)
## Plugin System
### Lifecycle
[plugin.md](./plugin.md)
### Plugin Development
[plugin/README.md](../../plugin/README.md)
## Task System
[task.md](./task.md)
## Configuration Mechanism
[config.md](./config.md)
## Logging System
[log.md](./log.md)
## Database Mechanism
[db.md](./db.md)
## GRPC Service
[grpc.md](./grpc.md)
## HTTP Service
[http.md](./http.md)
## Admin Service
[admin.md](./admin.md)

124
doc/arch/log.md Normal file
View File

@@ -0,0 +1,124 @@
# Logging Mechanism
Monibuca uses Go's standard library `slog` as its logging system, providing structured logging functionality.
## Log Configuration
In the global configuration, you can set the log level through the `LogLevel` field. Supported log levels are:
- trace
- debug
- info
- warn
- error
Configuration example:
```yaml
global:
LogLevel: "debug" # Set log level to debug
```
## Log Format
The default log format includes the following information:
- Timestamp (format: HH:MM:SS.MICROSECONDS)
- Log level
- Log message
- Structured fields
Example output:
```
15:04:05.123456 INFO server started
15:04:05.123456 ERROR failed to connect database dsn="xxx" type="mysql"
```
## Log Handlers
Monibuca uses `console-slog` as the default log handler, which provides:
1. Color output support
2. Microsecond-level timestamps
3. Structured field formatting
### Multiple Handler Support
Monibuca implements a `MultiLogHandler` mechanism, supporting multiple log handlers simultaneously. This provides the following advantages:
1. Can output logs to multiple targets simultaneously (e.g., console, file, log service)
2. Supports dynamic addition and removal of log handlers
3. Each handler can have its own log level settings
4. Supports log grouping and property inheritance
Through the plugin system, various logging methods can be extended, for example:
- LogRotate plugin: Supports log file rotation
- VMLog plugin: Supports storing logs in VictoriaMetrics time-series database
## Using Logs in Plugins
Each plugin inherits the server's log configuration. Plugins can log using the following methods:
```go
plugin.Info("message", "key1", value1, "key2", value2) // Log INFO level
plugin.Debug("message", "key1", value1) // Log DEBUG level
plugin.Warn("message", "key1", value1) // Log WARN level
plugin.Error("message", "key1", value1) // Log ERROR level
```
## Log Initialization Process
1. Create default console log handler at server startup
2. Read log level settings from configuration file
3. Apply log level configuration
4. Set inherited log configuration for each plugin
## Best Practices
1. Use Log Levels Appropriately
- trace: For most detailed tracing information
- debug: For debugging information
- info: For important information during normal operation
- warn: For warning information
- error: For error information
2. Use Structured Fields
- Avoid concatenating variables in messages
- Use key-value pairs to record additional information
3. Error Handling
- Include complete error information when logging errors
- Add relevant context information
Example:
```go
// Recommended
s.Error("failed to connect database", "error", err, "dsn", dsn)
// Not recommended
s.Error("failed to connect database: " + err.Error())
```
## Extending the Logging System
To extend the logging system, you can:
1. Implement custom `slog.Handler` interface
2. Use `LogHandler.Add()` method to add new handlers
3. Provide more complex logging functionality through the plugin system
Example of adding a custom log handler:
```go
type MyLogHandler struct {
slog.Handler
}
// Add handler during plugin initialization
func (p *MyPlugin) Start() error {
handler := &MyLogHandler{}
p.Server.LogHandler.Add(handler)
return nil
}
```

170
doc/arch/plugin.md Normal file
View File

@@ -0,0 +1,170 @@
# Plugin System
Monibuca adopts a plugin-based architecture design, extending functionality through its plugin mechanism. The plugin system is one of Monibuca's core features, allowing developers to add new functionality in a modular way without modifying the core code.
## Plugin Lifecycle
The plugin system has complete lifecycle management, including the following phases:
### 1. Registration Phase
Plugins are registered using the `InstallPlugin` generic function, during which:
- Plugin metadata (PluginMeta) is created, including:
- Plugin name: automatically extracted from the plugin struct name (removing "Plugin" suffix)
- Plugin version: extracted from the caller's file path or package path, defaults to "dev" if not extractable
- Plugin type: obtained through reflection of the plugin struct type
- Optional features are registered:
- Exit handler (OnExitHandler)
- Default configuration (DefaultYaml)
- Puller
- Pusher
- Recorder
- Transformer
- Publish authentication (AuthPublisher)
- Subscribe authentication (AuthSubscriber)
- gRPC service (ServiceDesc)
- gRPC gateway handler (RegisterGRPCHandler)
- Plugin metadata is added to the global plugin list
The registration phase is the first stage in a plugin's lifecycle, providing the plugin system with basic information and functional definitions, preparing for subsequent initialization and startup.
### 2. Initialization Phase (Init)
Plugins are initialized through the `Plugin.Init` method, including these steps:
1. Instance Verification
- Check if the plugin implements the IPlugin interface
- Get plugin instance through reflection
2. Basic Setup
- Set plugin metadata and server reference
- Configure plugin logger
- Set plugin name and version information
3. Environment Check
- Check if plugin is disabled by environment variables ({PLUGIN_NAME}_ENABLE=false)
- Check global disable status (DisableAll)
- Check enable status in user configuration (enable)
4. Configuration Loading
- Parse common configuration
- Load default YAML configuration
- Merge user configuration
- Apply final configuration and log
5. Database Initialization (if needed)
- Check database connection configuration (DSN)
- Establish database connection
- Auto-migrate database tables (for recording functionality)
6. Status Recording
- Record plugin version
- Record user configuration
- Set log level
- Record initialization status
If errors occur during initialization:
- Plugin is marked as disabled
- Disable reason is recorded
- Plugin is added to the disabled plugins list
The initialization phase prepares necessary environment and resources for plugin operation, crucial for ensuring normal plugin operation.
### 3. Startup Phase (Start)
Plugins start through the `Plugin.Start` method, executing these operations in sequence:
1. gRPC Service Registration (if configured)
- Register gRPC service
- Register gRPC gateway handler
- Handle gRPC-related errors
2. Plugin Management
- Add plugin to server's plugin list
- Set plugin status to running
3. Network Listener Initialization
- Start HTTP/HTTPS services
- Start TCP/TLS services (if implementing ITCPPlugin interface)
- Start UDP services (if implementing IUDPPlugin interface)
- Start QUIC services (if implementing IQUICPlugin interface)
4. Plugin Initialization Callback
- Call plugin's Start method
- Handle initialization errors
5. Timer Task Setup
- Configure server keepalive task (if enabled)
- Set up other timer tasks
If errors occur during startup:
- Error reason is recorded
- Plugin is marked as disabled
- Subsequent startup steps are stopped
The startup phase is crucial for plugins to begin providing services, with all preparations completed and ready for business logic processing.
### 4. Stop Phase (Stop)
The plugin stop phase is implemented through the `Plugin.OnDispose` method and related stop handling logic, including:
1. Service Shutdown
- Stop all network services (HTTP/HTTPS/TCP/UDP/QUIC)
- Close all network connections
- Stop processing new requests
2. Resource Cleanup
- Stop all timer tasks
- Close database connections (if any)
- Clean up temporary files and cache
3. Status Handling
- Update plugin status to stopped
- Remove from server's active plugin list
- Trigger stop event notifications
4. Callback Processing
- Call plugin's custom OnDispose method
- Execute registered stop callback functions
- Handle errors during stop process
5. Connection Handling
- Wait for current request processing to complete
- Gracefully close existing connections
- Reject new connection requests
The stop phase aims to ensure plugins can safely and cleanly stop running without affecting other parts of the system.
### 5. Destroy Phase (Destroy)
The plugin destroy phase is implemented through the `Plugin.Dispose` method, the final phase in a plugin's lifecycle, including:
1. Resource Release
- Call plugin's OnDispose method for stop processing
- Remove from server's plugin list
- Release all allocated system resources
2. Status Cleanup
- Clear all plugin status information
- Reset plugin internal variables
- Clear plugin configuration information
3. Connection Disconnection
- Disconnect all connections with other plugins
- Clean up plugin dependencies
- Remove event listeners
4. Data Cleanup
- Clean up temporary data generated by plugin
- Close and clean up database connections
- Delete unnecessary files
5. Final Processing
- Execute registered destroy callback functions
- Log destruction
- Ensure all resources are properly released
The destroy phase aims to ensure plugins completely clean up all resources, leaving no residual state, preventing memory and resource leaks.

45
doc/arch/relay.md Normal file
View File

@@ -0,0 +1,45 @@
# Core Relay Process
## Publisher
A Publisher is an object that writes audio/video data to the RingBuffer on the server. It exposes WriteVideo and WriteAudio methods.
When writing through WriteVideo and WriteAudio, it creates Tracks, parses data, and generates ICodecCtx. To start publishing, simply call the Plugin's Publish method.
### Accepting Stream Push
Plugins like rtmp, rtsp listen on a port to accept stream pushes.
### Pulling Streams from Remote
- Plugins that implement OnPullProxyAdd method can pull streams from remote sources.
- Plugins that inherit from HTTPFilePuller can pull streams from http or files.
### Pulling from Local Recording Files
Plugins that inherit from RecordFilePuller can pull streams from local recording files.
## Subscriber
A Subscriber is an object that reads audio/video data from the RingBuffer. Subscribing to a stream involves two steps:
1. Call the Plugin's Subscribe method, passing StreamPath and Subscribe configuration.
2. Call the PlayBlock method to start reading data, which blocks until the subscription ends.
The reason for splitting into two steps is that the first step might fail (timeout etc.), or might need some interaction work after the first step succeeds.
The first step will block for some time, waiting for the publisher (if there's no publisher initially) and waiting for the publisher's tracks to be created.
### Accepting Stream Pull
For example, rtmp and rtsp plugins listen on a port to accept playback requests.
### Pushing to Remote
- Plugins that implement OnPushProxyAdd method can push streams to remote destinations.
### Writing to Local Files
Plugins with recording functionality need to subscribe to the stream before writing to local files.
## On-Demand Pull (Publishing)
Triggered by subscribers, when calling plugin's OnSubscribe, it notifies all plugins of a subscription demand, at which point plugins can respond to this demand by publishing a stream. For example, pulling recording streams falls into this category. It's crucial to configure using regular expressions to prevent simultaneous publishing.

101
doc/arch/task.md Normal file
View File

@@ -0,0 +1,101 @@
# Task Mechanism
The task mechanism permeates the entire project, defined in the /pkg/task directory. When designing any logic, you must first consider implementing it using the task mechanism, which ensures observability and panic capture, among other benefits.
## Concept Definitions
### Inheritance
In the task mechanism, all tasks are implemented through inheritance.
While Go doesn't have inheritance, it can be achieved through embedding.
### Macro Task
A macro task, also called a parent task, can contain multiple child tasks and is itself a task.
### Child Task Goroutine
Each macro task starts a goroutine to execute the Start, Run, and Dispose methods of child tasks. Therefore, child tasks sharing the same parent task can avoid concurrent execution issues. This goroutine might not be created immediately, implementing lazy loading.
## Task Definition
Tasks are typically defined by inheriting from `task.Task`, `task.Job`, `task.Work`, `task.ChannelTask`, or `task.TickTask`.
For example:
```go
type MyTask struct {
task.Task
}
```
- `task.Task` is the base class for all tasks, defining basic task properties and methods.
- `task.Job` can contain child tasks and ends when all child tasks complete.
- `task.Work` similar to Job, but continues executing after child tasks complete.
- `task.ChannelTask` custom signal task, implemented by overriding the `GetSignal` method.
- `task.TickTask` timer task, inherits from `task.ChannelTask`, controls timer interval by overriding `GetTickInterval` method.
### Defining Task Start Method
Implement task startup by defining a Start() error method.
The returned error indicates whether the task started successfully. Nil indicates successful startup, otherwise indicates startup failure (special case: returning Complete indicates task completion).
Start typically includes resource creation, such as opening files, establishing network connections, etc.
The Start method is optional; if not defined, startup is considered successful by default.
### Defining Task Execution Process
Implement task execution process by defining a Run() error method.
This method typically executes time-consuming operations and blocks the parent task's child task goroutine.
There's also a non-blocking way to run time-consuming operations by defining a Go() error method.
A nil error return indicates successful execution, otherwise indicates execution failure (special case: returning Complete indicates task completion).
Run and Go are optional; if not defined, the task remains in running state.
### Defining Task Destruction Process
Implement task destruction process by defining a Dispose() method.
This method typically releases resources, such as closing files, network connections, etc.
The Dispose method is optional; if not defined, no action is taken when the task ends.
## Hook Mechanism
Implement hooks through OnStart, OnBeforeDispose, and OnDispose methods.
## Waiting for Task Start and End
Implement waiting for task start and end through WaitStarted() and WaitStopped() methods. This approach blocks the current goroutine.
## Retry Mechanism
Implement retry mechanism by setting Task's RetryCount and RetryInterval. There's a setting method, SetRetry(maxRetry int, retryInterval time.Duration).
### Trigger Conditions
- When Start fails, it retries calling Start until successful.
- When Run or Go fails, it calls Dispose to release resources before calling Start to begin the retry process.
### Termination Conditions
- Retries stop when the retry count is exhausted.
- Retries stop when Start, Run, or Go returns ErrStopByUser, ErrExit, or ErrTaskComplete.
## Starting a Task
Start a task by calling the parent task's AddTask method. Don't directly call a task's Start method. Start must be called by the parent task.
## Task Stopping
Implement task stopping through the Stop(err error) method. err cannot be nil. Don't override the Stop method when defining tasks.
## Task Stop Reason
Check task stop reason by calling the StopReason() method.
## Call Method
Calling a Job's Call method creates a temporary task to execute a function in the child task goroutine, typically used to access resources like maps that need protection from concurrent read/write. Since this function runs in the child task goroutine, it cannot call WaitStarted, WaitStopped, or other goroutine-blocking logic, as this would cause deadlock.

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.

111
doc_CN/arch/admin.md Normal file
View File

@@ -0,0 +1,111 @@
# Admin 服务机制
Monibuca 提供了强大的管理服务支持,用于系统监控、配置管理、插件管理等管理功能。本文档详细说明了 Admin 服务的实现机制和使用方法。
## 服务架构
### 1. UI 界面
Admin 服务通过加载 `admin.zip` 文件来提供 Web 管理界面。该界面具有以下特点:
- 统一的管理界面入口
- 可调用所有服务器提供的 HTTP 接口
- 响应式设计,支持多种设备访问
- 模块化的功能组织
### 2. 配置管理
Admin 服务的配置位于全局配置global中的 admin 节,包括:
```yaml
admin:
enableLogin: false # 是否启用登录机制
filePath: admin.zip # 管理界面文件路径
homePage: home # 管理界面首页
users: # 用户列表(仅在启用登录机制时生效)
- username: admin # 用户名
password: admin # 密码
role: admin # 角色可选值admin、user
```
`enableLogin` 为 false 时,所有用户都以匿名用户身份访问。
当启用登录机制且数据库中没有用户时系统会自动创建一个默认管理员账户用户名admin密码admin
### 3. 认证机制
Admin 提供专门的用户登录验证接口,用于:
- 用户身份验证
- 访问令牌管理JWT
- 权限控制
- 会话管理
### 4. 接口规范
所有的 Admin API 都需要遵循以下规范:
- 响应格式统一包含 code、message、data 字段
- 成功响应使用 code = 0
- 错误处理采用统一的错误响应格式
- 必须进行权限验证
## 功能模块
### 1. 系统监控
- CPU 使用率监控
- 内存使用情况
- 网络带宽统计
- 磁盘使用情况
- 系统运行时间
- 在线用户统计
### 2. 插件管理
- 插件启用/禁用
- 插件配置修改
- 插件状态查看
- 插件版本管理
- 插件依赖检查
### 3. 流媒体管理
- 在线流列表查看
- 流状态监控
- 流控制(开始/停止)
- 流信息统计
- 录制管理
- 转码任务管理
## 安全机制
### 1. 认证机制
- JWT 令牌认证
- 会话超时控制
- IP 白名单控制
### 2. 权限控制
- 基于角色的访问控制RBAC
- 细粒度的权限管理
- 操作审计日志
- 敏感操作确认
## 最佳实践
1. 安全性
- 使用 HTTPS 加密
- 实施强密码策略
- 定期更新密钥
- 监控异常访问
2. 性能优化
- 合理的缓存策略
- 分页查询优化
- 异步处理耗时操作
3. 可维护性
- 完整的操作日志
- 清晰的错误提示
- 配置热更新

279
doc_CN/arch/auth.md Normal file
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验证失败

77
doc_CN/arch/catalog.md Normal file
View File

@@ -0,0 +1,77 @@
# 目录结构说明
```bash
monibuca/
├── api.go # API接口定义
├── plugin.go # 插件系统核心实现
├── publisher.go # 发布者实现
├── subscriber.go # 订阅者实现
├── server.go # 服务器核心实现
├── puller.go # 拉流器实现
├── pusher.go # 推流器实现
├── pull-proxy.go # 拉流代理实现
├── push-proxy.go # 推流代理实现
├── recoder.go # 录制器实现
├── transformer.go # 转码器实现
├── wait-stream.go # 流等待实现
├── prometheus.go # Prometheus监控实现
├── pkg/ # 核心包
│ ├── auth/ # 认证相关
│ ├── codec/ # 编解码实现
│ ├── config/ # 配置相关
│ ├── db/ # 数据库相关
│ ├── task/ # 任务系统
│ ├── util/ # 工具函数
│ ├── filerotate/ # 文件轮转管理
│ ├── log.go # 日志实现
│ ├── raw.go # 原始数据处理
│ ├── error.go # 错误处理
│ ├── track.go # 媒体轨道实现
│ ├── track_test.go # 媒体轨道测试
│ ├── annexb.go # H.264/H.265 Annex-B格式处理
│ ├── av-reader.go # 音视频读取器
│ ├── avframe.go # 音视频帧结构
│ ├── ring-writer.go # 环形缓冲区写入器
│ ├── ring-reader.go # 环形缓冲区读取器
│ ├── adts.go # AAC-ADTS格式处理
│ ├── port.go # 端口管理
│ ├── ring_test.go # 环形缓冲区测试
│ └── event.go # 事件系统
├── plugin/ # 插件目录
│ ├── rtmp/ # RTMP协议插件
│ ├── rtsp/ # RTSP协议插件
│ ├── hls/ # HLS协议插件
│ ├── flv/ # FLV协议插件
│ ├── webrtc/ # WebRTC协议插件
│ ├── gb28181/ # GB28181协议插件
│ ├── onvif/ # ONVIF协议插件
│ ├── mp4/ # MP4相关插件
│ ├── room/ # 房间管理插件
│ ├── monitor/ # 监控插件
│ ├── rtp/ # RTP协议插件
│ ├── srt/ # SRT协议插件
│ ├── sei/ # SEI数据处理插件
│ ├── snap/ # 截图插件
│ ├── crypto/ # 加密插件
│ ├── debug/ # 调试插件
│ ├── cascade/ # 级联插件
│ ├── logrotate/ # 日志轮转插件
│ ├── stress/ # 压力测试插件
│ ├── vmlog/ # 虚拟内存日志插件
│ ├── preview/ # 预览插件
│ └── transcode/ # 转码插件
├── pb/ # Protocol Buffers定义和生成的代码
├── scripts/ # 脚本文件
├── doc/ # 英文文档
├── doc_CN/ # 中文文档
├── example/ # 示例代码
├── test/ # 测试代码
├── website/ # 网站前端代码
├── go.mod # Go模块定义
├── go.sum # Go依赖版本锁定
├── Dockerfile # Docker构建文件
└── README.md # 项目说明文档
```

291
doc_CN/arch/config.md Normal file
View File

@@ -0,0 +1,291 @@
# Monibuca 配置机制
Monibuca 采用灵活的配置机制,支持多种配置方式。配置文件采用 YAML 格式,可以通过文件或者直接传入配置对象的方式进行初始化。
## 配置加载流程
1. 配置初始化发生在 Server 启动阶段,通过以下三种方式之一提供配置:
- YAML 配置文件路径
- YAML 配置内容的字节数组
- 原始配置对象 (RawConfig)
2. 配置解析过程:
```go
// 支持三种配置输入方式
case string: // 配置文件路径
case []byte: // YAML 配置内容
case RawConfig: // 原始配置对象
```
## 配置结构
### 配置简化语法
当配置项的值是一个结构体或 map 类型时,系统支持一种简化的配置方式:如果直接配置一个简单类型的值,该值会被自动赋给结构体的第一个字段。
例如,对于以下结构体:
```go
type Config struct {
Port int
Host string
}
```
可以使用简化语法:
```yaml
plugin: 1935 # 等同于 plugin: { port: 1935 }
```
### 配置反序列化机制
每个插件都包含一个 `config.Config` 类型的字段,用于存储和管理配置信息。配置加载的优先级从高到低是:
1. 用户配置 (通过 `ParseUserFile`)
2. 默认配置 (通过 `ParseDefaultYaml`)
3. 全局配置 (通过 `ParseGlobal`)
4. 插件特定配置 (通过 `Parse`)
5. 通用配置 (通过 `Parse`)
配置会被自动反序列化到插件的公开属性中。例如:
```go
type MyPlugin struct {
Plugin
Port int `yaml:"port"`
Host string `yaml:"host"`
}
```
对应的 YAML 配置:
```yaml
myplugin:
port: 8080
host: "localhost"
```
配置会自动反序列化到 `Port` 和 `Host` 字段中。你可以通过 `Config` 提供的方法来查询配置:
- `Has(name string)` - 检查是否存在某个配置
- `Get(name string)` - 获取某个配置的值
- `GetMap()` - 获取所有配置的 map
此外,插件的配置支持保存修改:
```go
func (p *Plugin) SaveConfig() (err error)
```
这会将修改后的配置保存到 `{settingDir}/{pluginName}.yaml` 文件中。
### 全局配置
全局配置位于 YAML 文件的 `global` 节点下,包含以下主要配置项:
```yaml
global:
settingDir: ".m7s" # 设置文件目录
fatalDir: "fatal" # 错误日志目录
pulseInterval: "5s" # 心跳间隔
disableAll: false # 是否禁用所有插件
streamAlias: # 流别名配置
pattern: "target" # 正则表达式 -> 目标路径
location: # HTTP 路由转发规则
pattern: "target" # 正则表达式 -> 目标地址
admin: # 管理界面配置
enableLogin: false # 是否启用登录机制
filePath: "admin.zip" # 管理界面文件路径
homePage: "home" # 管理界面首页
users: # 用户列表(仅在启用登录时生效)
- username: "admin" # 用户名
password: "admin" # 密码
role: "admin" # 角色(admin/user)
```
### 数据库配置
如果配置了数据库连接,系统会自动进行以下操作:
1. 连接数据库
2. 自动迁移数据模型
3. 初始化用户数据(如果启用了登录机制)
4. 初始化代理配置
```yaml
global:
db:
dsn: "" # 数据库连接字符串
type: "" # 数据库类型
```
### 代理配置
系统支持拉流代理和推流代理配置:
```yaml
global:
pullProxy: # 拉流代理配置
- id: 1 # 代理ID
name: "proxy1" # 代理名称
url: "rtmp://..." # 代理地址
type: "rtmp" # 代理类型
pullOnStart: true # 是否启动时拉流
pushProxy: # 推流代理配置
- id: 1 # 代理ID
name: "proxy1" # 代理名称
url: "rtmp://..." # 代理地址
type: "rtmp" # 代理类型
pushOnStart: true # 是否启动时推流
audio: true # 是否推送音频
```
## 插件配置
每个插件可以有自己的配置节点,节点名为插件名称的小写形式:
```yaml
rtmp: # RTMP插件配置
port: 1935 # 监听端口
rtsp: # RTSP插件配置
port: 554 # 监听端口
```
## 配置优先级
配置系统采用多级优先级机制,从高到低依次为:
1. URL 查询参数配置 - 发布或订阅时通过 URL 查询参数指定的配置具有最高优先级
```
例如rtmp://localhost/live/stream?audio=false
```
2. 插件特定配置 - 在插件配置节点下的配置项
```yaml
rtmp:
publish:
audio: true
subscribe:
audio: true
```
3. 全局配置 - 在 global 节点下的配置项
```yaml
global:
publish:
audio: true
subscribe:
audio: true
```
## 通用配置
系统中存在一些通用配置项,这些配置项可以同时出现在全局配置和插件配置中。当插件使用这些配置项时,会优先使用插件配置中的值,如果插件配置中没有设置,则使用全局配置中的值。
主要的通用配置包括:
1. 发布配置Publish
```yaml
publish:
audio: true # 是否包含音频
video: true # 是否包含视频
bufferLength: 1000 # 缓冲长度
```
2. 订阅配置Subscribe
```yaml
subscribe:
audio: true # 是否订阅音频
video: true # 是否订阅视频
bufferLength: 1000 # 缓冲长度
```
3. HTTP 配置
```yaml
http:
listenAddr: ":8080" # 监听地址
```
4. 其他通用配置
- PublicIP - 公网 IP
- PublicIPv6 - 公网 IPv6
- LogLevel - 日志级别
- EnableAuth - 是否启用认证
使用示例:
```yaml
# 全局配置
global:
publish:
audio: true
video: true
subscribe:
audio: true
video: true
# 插件配置(优先级高于全局配置)
rtmp:
publish:
audio: false # 覆盖全局配置
subscribe:
video: false # 覆盖全局配置
# URL 查询参数(最高优先级)
# rtmp://localhost/live/stream?audio=true&video=false
```
## 配置热更新
目前系统支持管理界面文件admin.zip的热更新会定期检查文件变化并自动重新加载。
## 配置验证
系统在启动时会对配置进行基本验证:
1. 检查必要的目录权限
2. 验证数据库连接
3. 验证用户配置(如果启用登录机制)
## 配置示例
完整的配置文件示例:
```yaml
global:
settingDir: ".m7s"
fatalDir: "fatal"
pulseInterval: "5s"
disableAll: false
streamAlias:
"live/(.*)": "record/$1"
location:
"^/live/(.*)": "/hls/$1"
admin:
enableLogin: true
filePath: "admin.zip"
homePage: "home"
users:
- username: "admin"
password: "admin"
role: "admin"
db:
dsn: "host=localhost user=postgres password=postgres dbname=monibuca port=5432 sslmode=disable TimeZone=Asia/Shanghai"
type: "postgres"
pullProxy:
- id: 1
name: "proxy1"
url: "rtmp://example.com/live/stream"
type: "rtmp"
pullOnStart: true
pushProxy:
- id: 1
name: "proxy1"
url: "rtmp://example.com/live/stream"
type: "rtmp"
pushOnStart: true
audio: true
rtmp:
port: 1935
rtsp:
port: 554
```

94
doc_CN/arch/db.md Normal file
View File

@@ -0,0 +1,94 @@
# 数据库机制
Monibuca 提供了数据库支持功能,可以在全局配置和插件中分别配置和使用数据库。
## 配置说明
### 全局配置
在全局配置中可以通过以下字段配置数据库:
```yaml
global:
dsn: "数据库连接字符串"
dbType: "数据库类型"
```
### 插件配置
每个插件也可以单独配置数据库:
```yaml
pluginName:
dsn: "数据库连接字符串"
dbType: "数据库类型"
```
## 数据库初始化流程
### 全局数据库初始化
1. 服务器启动时,如果配置了 `dsn`,会尝试连接数据库
2. 连接成功后会自动迁移以下模型:
- User 用户表
- PullProxy 拉流代理表
- PushProxy 推流代理表
- StreamAliasDB 流别名表
3. 如果开启了登录功能(`Admin.EnableLogin = true`),会根据配置文件创建或更新用户
4. 如果数据库中没有任何用户,会创建一个默认的管理员账户:
- 用户名: admin
- 密码: admin
- 角色: admin
### 插件数据库初始化
1. 插件初始化时会检查插件配置中的 `dsn`
2. 如果插件配置的 `dsn` 与全局配置相同,则直接使用全局数据库连接
3. 如果插件配置了不同的 `dsn`,则会创建新的数据库连接
4. 如果插件实现了 Recorder 接口,会自动迁移 RecordStream 表
## 数据库使用
### 全局数据库访问
可以通过 Server 实例访问全局数据库:
```go
server.DB
```
### 插件数据库访问
插件可以通过自身实例访问数据库:
```go
plugin.DB
```
## 注意事项
1. 数据库连接失败会导致相应的功能被禁用
2. 插件使用独立数据库时需要自行管理数据库连接
3. 数据库迁移失败会导致插件被禁用
4. 建议在可能的情况下复用全局数据库连接,避免创建过多连接
## 内置数据表
### User 表
用于存储用户信息,包含以下字段:
- Username: 用户名
- Password: 密码
- Role: 角色(admin/user)
### PullProxy 表
用于存储拉流代理配置
### PushProxy 表
用于存储推流代理配置
### StreamAliasDB 表
用于存储流别名配置
### RecordStream 表
用于存储录制相关信息(仅在插件实现 Recorder 接口时创建)

72
doc_CN/arch/grpc.md Normal file
View File

@@ -0,0 +1,72 @@
# GRPC 服务机制
Monibuca 提供了 gRPC 服务支持,允许插件通过 gRPC 协议提供服务。本文档说明了 gRPC 服务的实现机制和使用方法。
## 服务注册机制
### 1. 服务注册
插件注册 gRPC 服务需要在 `InstallPlugin` 时传入 ServiceDesc 和 Handler
```go
// 示例:在插件中注册 gRPC 服务
type MyPlugin struct {
pb.UnimplementedApiServer
m7s.Plugin
}
var _ = m7s.InstallPlugin[MyPlugin](
m7s.DefaultYaml(`your yaml config here`),
&pb.Api_ServiceDesc, // gRPC service descriptor
pb.RegisterApiHandler, // gRPC gateway handler
// ... 其他参数
)
```
### 2. Proto 文件规范
所有的 gRPC 服务都需要遵循以下 Proto 文件规范:
- 响应结构体必须包含 code、message、data 字段
- 错误处理采用直接返回 error 的方式,无需手动设置 code 和 message
- 修改 global.proto 后需要运行 `sh scripts/protoc.sh` 生成 pb 文件
- 修改插件相关的 proto 文件后需要运行 `sh scripts/protoc.sh {pluginName}` 生成对应的 pb 文件
## 服务实现机制
### 1. 服务器配置
gRPC 服务使用全局 TCP 配置中的端口设置:
```yaml
global:
tcp:
listenaddr: :8080 # gRPC 服务监听地址和端口
listentls: :8443 # gRPC TLS 服务监听地址和端口(如果启用)
```
配置项包括:
- 监听地址和端口设置(在全局 TCP 配置中指定)
- TLS/SSL 证书配置(如果启用)
### 2. 错误处理
错误处理遵循以下原则:
- 直接返回 error无需手动设置 code 和 message
- 系统会自动处理错误并设置响应格式
## 最佳实践
1. 服务定义
- 清晰的服务接口设计
- 合理的方法命名
- 完整的接口文档
2. 性能优化
- 使用流式处理大数据
- 合理设置超时时间
3. 安全考虑
- 根据需要启用 TLS 加密
- 实现必要的访问控制

145
doc_CN/arch/http.md Normal file
View File

@@ -0,0 +1,145 @@
# HTTP 服务机制
Monibuca 提供了完整的 HTTP 服务支持,包括 RESTful API、WebSocket、HTTP-FLV 等多种协议支持。本文档详细说明了 HTTP 服务的实现机制和使用方法。
## HTTP 配置
### 1. 配置优先级
- 插件的 HTTP 配置优先于全局 HTTP 配置
- 如果插件没有配置 HTTP则使用全局 HTTP 配置
### 2. 配置项说明
```yaml
# 全局配置示例
global:
http:
listenaddr: :8080 # 监听地址和端口
listentlsaddr: :8081 # 监听tls地址和端口
certfile: "" # SSL证书文件路径
keyfile: "" # SSL密钥文件路径
cors: true # 是否允许跨域
username: "" # Basic认证用户名
password: "" # Basic认证密码
# 插件配置示例(优先于全局配置)
plugin_name:
http:
listenaddr: :8081
cors: false
username: "admin"
password: "123456"
```
## 服务处理流程
### 1. 请求处理顺序
HTTP 服务器接收到请求后,按以下顺序处理:
1. 首先尝试转发到对应的 gRPC 服务
2. 如果没有找到对应的 gRPC 服务,则查找插件注册的 HTTP handler
3. 如果都没有找到,返回 404 错误
### 2. Handler 注册方式
插件可以通过以下两种方式注册 HTTP handler
1. 反射注册:系统自动通过反射获取插件的处理方法
- 方法名必须大写开头才能被反射获取Go 语言规则)
- 通常使用 `API_` 作为方法名前缀(推荐但不强制)
- 方法签名必须为 `func(w http.ResponseWriter, r *http.Request)`
- URL 路径自动生成规则:
- 方法名中的下划线 `_` 会被转换为斜杠 `/`
- 例如:`API_relay_` 方法将映射到 `/API/relay/*` 路径
- 如果方法名以下划线结尾,表示这是一个通配符路径,可以匹配后续任意路径
2. 手动注册:插件实现 `IRegisterHandler` 接口进行手动注册
- 小写开头的方法无法被反射获取,需要通过手动注册方式
- 手动注册可以使用路径参数(如 `:id`
- 更灵活的路由规则配置
示例代码:
```go
// 反射注册示例
type YourPlugin struct {
// ...
}
// 大写开头,可以被反射获取
// 自动映射到 /API/relay/*
func (p *YourPlugin) API_relay_(w http.ResponseWriter, r *http.Request) {
// 处理通配符路径的请求
}
// 小写开头,无法被反射获取,需要手动注册
func (p *YourPlugin) handleUserRequest(w http.ResponseWriter, r *http.Request) {
// 处理带参数的请求
}
// 手动注册示例
func (p *YourPlugin) RegisterHandler() {
// 可以使用路径参数
engine.GET("/api/user/:id", p.handleUserRequest)
}
```
## 中间件机制
### 1. 添加中间件
插件可以通过 `AddMiddleware` 方法添加全局中间件,用于处理所有 HTTP 请求。中间件按照添加顺序依次执行。
示例代码:
```go
func (p *YourPlugin) Start() {
// 添加认证中间件
p.GetCommonConf().AddMiddleware(func(next http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
// 在请求处理前执行
if !authenticate(r) {
http.Error(w, "Unauthorized", http.StatusUnauthorized)
return
}
// 调用下一个处理器
next(w, r)
// 在请求处理后执行
}
})
}
```
### 2. 中间件使用场景
- 认证和授权
- 请求日志记录
- CORS 处理
- 请求限流
- 响应头设置
- 错误处理
- 性能监控
## 特殊协议支持
### 1. HTTP-FLV
- 支持 HTTP-FLV 直播流分发
- 自动生成 FLV 头
- 支持 GOP 缓存
- 支持 WebSocket-FLV
### 2. HTTP-MP4
- 支持 HTTP-MP4 流分发
- 支持 fMP4 文件分发
### 3. HLS
- 支持 HLS 协议
- 支持 MPEG-TS 封装
### 4. WebSocket
- 支持自定义消息协议
- 支持ws-flv
- 支持ws-mp4

57
doc_CN/arch/index.md Normal file
View File

@@ -0,0 +1,57 @@
# 架构设计
## 目录结构
[catalog.md](./catalog.md)
## 音视频流系统
### 转发机制
[relay.md](./relay.md)
### 别名机制
[alias.md](./alias.md)
### 鉴权机制
[auth.md](./auth.md)
## 插件系统
### 生命周期
[plugin.md](./plugin.md)
### 开发插件
[plugin/README_CN.md](../plugin/README_CN.md)
## 任务系统
[task.md](./task.md)
## 配置机制
[config.md](./config.md)
## 日志系统
[log.md](./log.md)
## 数据库机制
[db.md](./db.md)
## GRPC 服务
[grpc.md](./grpc.md)
## HTTP 服务
[http.md](./http.md)
## Admin 服务
[admin.md](./admin.md)

124
doc_CN/arch/log.md Normal file
View File

@@ -0,0 +1,124 @@
# 日志机制
Monibuca 使用 Go 标准库的 `slog` 作为日志系统,提供了结构化的日志记录功能。
## 日志配置
在全局配置中,可以通过 `LogLevel` 字段来设置日志级别。支持的日志级别有:
- trace
- debug
- info
- warn
- error
配置示例:
```yaml
global:
LogLevel: "debug" # 设置日志级别为 debug
```
## 日志格式
默认的日志格式包含以下信息:
- 时间戳 (格式: HH:MM:SS.MICROSECONDS)
- 日志级别
- 日志消息
- 结构化字段
示例输出:
```
15:04:05.123456 INFO server started
15:04:05.123456 ERROR failed to connect database dsn="xxx" type="mysql"
```
## 日志处理器
Monibuca 使用 `console-slog` 作为默认的日志处理器,它提供了:
1. 彩色输出支持
2. 微秒级时间戳
3. 结构化字段格式化
### 多处理器支持
Monibuca 实现了 `MultiLogHandler` 机制,支持同时使用多个日志处理器。这提供了以下优势:
1. 可以同时将日志输出到多个目标(如控制台、文件、日志服务等)
2. 支持动态添加和移除日志处理器
3. 每个处理器可以有自己的日志级别设置
4. 支持日志分组和属性继承
通过插件系统,可以扩展多种日志处理方式,例如:
- LogRotate 插件:支持日志文件轮转
- VMLog 插件:支持将日志存储到 VictoriaMetrics 时序数据库
## 在插件中使用日志
每个插件都会继承服务器的日志配置。插件可以通过以下方式记录日志:
```go
plugin.Info("message", "key1", value1, "key2", value2) // 记录 INFO 级别日志
plugin.Debug("message", "key1", value1) // 记录 DEBUG 级别日志
plugin.Warn("message", "key1", value1) // 记录 WARN 级别日志
plugin.Error("message", "key1", value1) // 记录 ERROR 级别日志
```
## 日志初始化流程
1. 服务器启动时创建默认的控制台日志处理器
2. 从配置文件读取日志级别设置
3. 应用日志级别配置
4. 为每个插件设置继承的日志配置
## 最佳实践
1. 合理使用日志级别
- trace: 用于最详细的追踪信息
- debug: 用于调试信息
- info: 用于正常运行时的重要信息
- warn: 用于警告信息
- error: 用于错误信息
2. 使用结构化字段
- 避免在消息中拼接变量
- 使用 key-value 对记录额外信息
3. 错误处理
- 记录错误时包含完整的错误信息
- 添加相关的上下文信息
示例:
```go
// 推荐
s.Error("failed to connect database", "error", err, "dsn", dsn)
// 不推荐
s.Error("failed to connect database: " + err.Error())
```
## 扩展日志系统
要扩展日志系统,可以通过以下方式:
1. 实现自定义的 `slog.Handler` 接口
2. 使用 `LogHandler.Add()` 方法添加新的处理器
3. 可以通过插件系统提供更复杂的日志功能
例如添加自定义日志处理器:
```go
type MyLogHandler struct {
slog.Handler
}
// 在插件初始化时添加处理器
func (p *MyPlugin) Start() error {
handler := &MyLogHandler{}
p.Server.LogHandler.Add(handler)
return nil
}
```

170
doc_CN/arch/plugin.md Normal file
View File

@@ -0,0 +1,170 @@
# 插件系统
Monibuca 采用插件化架构设计,通过插件机制来扩展功能。插件系统是 Monibuca 的核心特性之一,它允许开发者以模块化的方式添加新功能,而不需要修改核心代码。
## 插件生命周期
插件系统具有完整的生命周期管理,主要包含以下阶段:
### 1. 注册阶段
插件通过 `InstallPlugin` 泛型函数进行注册,在此阶段会:
- 创建插件元数据(PluginMeta),包含:
- 插件名称:自动从插件结构体名称中提取(去除"Plugin"后缀)
- 插件版本:从调用者的文件路径或包路径中提取,如果无法提取则默认为"dev"
- 插件类型:通过反射获取插件结构体类型
- 注册可选功能:
- 退出处理器(OnExitHandler)
- 默认配置(DefaultYaml)
- 拉流器(Puller)
- 推流器(Pusher)
- 录制器(Recorder)
- 转换器(Transformer)
- 发布认证(AuthPublisher)
- 订阅认证(AuthSubscriber)
- gRPC服务(ServiceDesc)
- gRPC网关处理器(RegisterGRPCHandler)
- 将插件元数据添加到全局插件列表中
注册阶段是插件生命周期的第一个阶段,它为插件系统提供了插件的基本信息和功能定义,为后续的初始化和启动做准备。
### 2. 初始化阶段 (Init)
插件通过 `Plugin.Init` 方法进行初始化,此阶段包含以下步骤:
1. 实例化检查
- 检查插件是否实现了 IPlugin 接口
- 通过反射获取插件实例
2. 基础设置
- 设置插件元数据和服务器引用
- 配置插件日志记录器
- 设置插件名称和版本信息
3. 环境检查
- 检查环境变量是否禁用插件(通过 {PLUGIN_NAME}_ENABLE=false)
- 检查全局禁用状态(DisableAll)
- 检查用户配置中的启用状态(enable)
4. 配置加载
- 解析通用配置
- 加载默认YAML配置
- 合并用户配置
- 应用最终配置并记录
5. 数据库初始化(如果需要)
- 检查数据库连接配置(DSN)
- 建立数据库连接
- 自动迁移数据库表结构(针对录制功能)
6. 状态记录
- 记录插件版本
- 记录用户配置
- 设置日志级别
- 记录初始化状态
如果在初始化过程中发生错误:
- 插件将被标记为禁用状态
- 记录禁用原因
- 添加到已禁用插件列表
初始化阶段为插件的运行准备必要的环境和资源,是确保插件正常运行的关键阶段。
### 3. 启动阶段 (Start)
插件通过 `Plugin.Start` 方法启动,此阶段按顺序执行以下操作:
1. gRPC服务注册如果配置
- 注册gRPC服务
- 注册gRPC网关处理器
- 处理gRPC相关错误
2. 插件管理
- 将插件添加到服务器的插件列表中
- 设置插件状态为运行中
3. 网络监听初始化
- HTTP/HTTPS服务启动
- TCP/TLS服务启动如果实现了ITCPPlugin接口
- UDP服务启动如果实现了IUDPPlugin接口
- QUIC服务启动如果实现了IQUICPlugin接口
4. 插件初始化回调
- 调用插件的OnInit方法
- 处理初始化错误
5. 定时任务设置
- 配置服务器保活任务(如果启用)
- 设置其他定时任务
如果在启动过程中发生错误:
- 记录错误原因
- 将插件标记为禁用状态
- 停止后续启动步骤
启动阶段是插件开始提供服务的关键阶段,此时插件完成了所有准备工作,可以开始处理业务逻辑。
### 4. 停止阶段 (Stop)
插件的停止阶段通过 `Plugin.OnDispose` 方法和相关的停止处理逻辑实现,主要包含以下步骤:
1. 停止服务
- 停止所有网络服务HTTP/HTTPS/TCP/UDP/QUIC
- 关闭所有网络连接
- 停止处理新的请求
2. 资源清理
- 停止所有定时任务
- 关闭数据库连接(如果有)
- 清理临时文件和缓存
3. 状态处理
- 更新插件状态为已停止
- 从服务器的活动插件列表中移除
- 触发停止事件通知
4. 回调处理
- 调用插件自定义的OnStop方法
- 执行注册的停止回调函数
- 处理停止过程中的错误
5. 连接处理
- 等待当前请求处理完成
- 优雅关闭现有连接
- 拒绝新的连接请求
停止阶段的主要目标是确保插件能够安全、干净地停止运行,不影响系统的其他部分。
### 5. 销毁阶段 (Destroy)
插件的销毁阶段通过 `Plugin.Dispose` 方法实现,这是插件生命周期的最后阶段,主要包含以下步骤:
1. 资源释放
- 调用插件的OnStop方法进行停止处理
- 从服务器的插件列表中移除
- 释放所有分配的系统资源
2. 状态清理
- 清除插件的所有状态信息
- 重置插件的内部变量
- 清空插件的配置信息
3. 连接断开
- 断开与其他插件的所有连接
- 清理插件间的依赖关系
- 移除事件监听器
4. 数据清理
- 清理插件产生的临时数据
- 关闭并清理数据库连接
- 删除不再需要的文件
5. 最终处理
- 执行注册的销毁回调函数
- 记录销毁日志
- 确保所有资源都被正确释放
销毁阶段的主要目标是确保插件完全清理所有资源,不留下任何残留状态,防止内存泄漏和资源泄露。

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

View File

@@ -10,3 +10,5 @@ cascadeclient:
onsub:
pull:
.*: m7s://$0
flv:
enable: true

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

@@ -9,7 +9,7 @@ transcode:
transform:
^live.+:
input:
mode: rtsp
mode: pipe
output:
- target: rtmp://localhost/trans/$0/small
conf: -loglevel debug -c:a aac -c:v h264 -vf scale=320:240

View File

@@ -29,12 +29,12 @@ func main() {
conf := flag.String("c", "config.yaml", "config file")
flag.Parse()
mp4.CustomFileName = func(job *m7s.RecordJob) string {
if job.Fragment == 0 {
return job.FilePath + ".mp4"
if job.RecConf.Fragment == 0 {
return job.RecConf.FilePath + ".mp4"
}
ss := strings.Split(job.StreamPath, "/")
lastPart := ss[len(ss)-1]
return filepath.Join(job.FilePath, fmt.Sprintf("%s_%s%s", lastPart, time.Now().Local().Format("2006-01-02-15-04-05"), ".mp4"))
return filepath.Join(job.RecConf.FilePath, fmt.Sprintf("%s_%s%s", lastPart, time.Now().Local().Format("2006-01-02-15-04-05"), ".mp4"))
}
// ctx, _ := context.WithDeadline(context.Background(), time.Now().Add(time.Second*100))
m7s.Run(context.Background(), *conf)

View File

@@ -2,29 +2,58 @@ global:
location:
"^/hdl/(.*)": "/flv/$1"
loglevel: debug
enablelogin: false
# db:
# dbtype: mysql
# dsn: root:Monibuca#!4@tcp(sh-cynosdbmysql-grp-kxt43lv6.sql.tencentcdb.com:28520)/lkm7s_v5?parseTime=true
admin:
enablelogin: false
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
# onpub:
# record:
# ^live/.+:
# fragment: 10s
# filepath: record/$0
# type: fmp4
# pull:
# live/test: /Users/dexter/Movies/1744963190.mp4
onsub:
pull:
^vod_mp4_\d+/(.+)$: $1
@@ -41,12 +70,17 @@ flv:
# ^live/.+:
# fragment: 1m
# filepath: record/$0
publish:
delayclosetimeout: 3s
onsub:
pull:
^vod_flv_\d+/(.+)$: $1
# pull:
# live/test: https://livecb.alicdn.com/mediaplatform/afb241b3-408c-42dd-b665-04d22b64f9df.flv?auth_key=1734575216-0-0-c62721303ce751c8e5b2c95a2ec242a0&F=pc&source=34675810_null_live_detail&ali_flv_retain=2
hls:
# onsub:
# pull:
# ^vod_hls_\d+/(.+)$: $1
# pull:
# live/test: https://devstreaming-cdn.apple.com/videos/streaming/examples/bipbop_4x3/gear3/prog_index.m3u8
# onpub:
@@ -59,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() {

View File

@@ -72,7 +72,7 @@ func main() {
mp4.CustomFileName = func(job *m7s.RecordJob) string {
fileDir := strings.ReplaceAll(job.FilePath, job.StreamPath, "")
fileDir := strings.ReplaceAll(job.RecConf.FilePath, job.StreamPath, "")
if err := os.MkdirAll(fileDir, 0755); err != nil {
log.Default().Printf("创建目录失败:%s", err)
return fmt.Sprintf("%s_%s%s", job.StreamPath, time.Now().Local().Format("2006-01-02-15-04-05"), ".mp4")

2
example/test/config.yaml Normal file
View File

@@ -0,0 +1,2 @@
global:
log_level: debug

40
example/test/main.go Normal file
View File

@@ -0,0 +1,40 @@
package main
import (
"context"
"flag"
"fmt"
"m7s.live/v5"
_ "m7s.live/v5/plugin/cascade"
_ "m7s.live/v5/plugin/crypto"
_ "m7s.live/v5/plugin/debug"
_ "m7s.live/v5/plugin/flv"
_ "m7s.live/v5/plugin/gb28181"
_ "m7s.live/v5/plugin/hls"
_ "m7s.live/v5/plugin/logrotate"
_ "m7s.live/v5/plugin/monitor"
_ "m7s.live/v5/plugin/mp4"
_ "m7s.live/v5/plugin/onvif"
_ "m7s.live/v5/plugin/preview"
_ "m7s.live/v5/plugin/rtmp"
_ "m7s.live/v5/plugin/rtp"
_ "m7s.live/v5/plugin/rtsp"
_ "m7s.live/v5/plugin/sei"
_ "m7s.live/v5/plugin/snap"
_ "m7s.live/v5/plugin/srt"
_ "m7s.live/v5/plugin/stress"
_ "m7s.live/v5/plugin/test"
_ "m7s.live/v5/plugin/transcode"
_ "m7s.live/v5/plugin/webrtc"
_ "m7s.live/v5/plugin/webtransport"
)
func main() {
conf := flag.String("c", "config.yaml", "config file")
flag.Parse()
// ctx, _ := context.WithDeadline(context.Background(), time.Now().Add(time.Second*100))
err := m7s.Run(context.Background(), *conf)
fmt.Println(err)
}

83
go.mod
View File

@@ -1,61 +1,70 @@
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/ncruces/go-sqlite3 v0.18.1
github.com/ncruces/go-sqlite3/gormlite v0.18.0
github.com/pion/interceptor v0.1.37
github.com/pion/logging v0.2.2
github.com/mozillazg/go-pinyin v0.20.0
github.com/ncruces/go-sqlite3 v0.27.1
github.com/ncruces/go-sqlite3/gormlite v0.24.0
github.com/pion/interceptor v0.1.40
github.com/pion/logging v0.2.4
github.com/pion/rtcp v1.2.15
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/pion/rtp v1.8.21
github.com/pion/sdp/v3 v3.0.15
github.com/pion/webrtc/v4 v4.1.4
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.27.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,38 +95,29 @@ 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/dtls/v3 v3.0.7 // indirect
github.com/pion/ice/v4 v4.0.10 // 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/sctp v1.8.39 // indirect
github.com/pion/srtp/v3 v3.0.7 // 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/pion/turn/v4 v4.1.1 // 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/tetratelabs/wazero v1.8.0 // indirect
github.com/spf13/cast v1.7.1 // indirect
github.com/tetratelabs/wazero v1.9.0 // indirect
github.com/tklauser/go-sysconf v0.3.12 // indirect
github.com/tklauser/numcpus v0.6.1 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect
@@ -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.16.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.40.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/tools v0.23.0 // indirect
golang.org/x/mod v0.25.0 // indirect
golang.org/x/net v0.41.0
golang.org/x/sys v0.34.0
golang.org/x/tools v0.34.0 // indirect
gopkg.in/yaml.v3 v3.0.1
)

317
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=
@@ -198,24 +189,15 @@ github.com/mozillazg/go-pinyin v0.20.0 h1:BtR3DsxpApHfKReaPO1fCqF4pThRwH9uwvXzm+
github.com/mozillazg/go-pinyin v0.20.0/go.mod h1:iR4EnMMRXkfpFVV5FMi4FNB6wGq9NV6uDWbUuPhP4Yc=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
github.com/ncruces/go-sqlite3 v0.18.1 h1:iN8IMZV5EMxpH88NUac9vId23eTKNFUhP7jgY0EBbNc=
github.com/ncruces/go-sqlite3 v0.18.1/go.mod h1:eEOyZnW1dGTJ+zDpMuzfYamEUBtdFz5zeYhqLBtHxvM=
github.com/ncruces/go-sqlite3/gormlite v0.18.0 h1:KqP9a9wlX/Ba+yG+aeVX4pnNBNdaSO6xHdNDWzPxPnk=
github.com/ncruces/go-sqlite3/gormlite v0.18.0/go.mod h1:RXeT1hknrz3A0tBDL6IfluDHuNkHdJeImn5TBMQg9zc=
github.com/ncruces/go-sqlite3 v0.27.1 h1:suqlM7xhSyDVMV9RgX99MCPqt9mB6YOCzHZuiI36K34=
github.com/ncruces/go-sqlite3 v0.27.1/go.mod h1:gpF5s+92aw2MbDmZK0ZOnCdFlpe11BH20CTspVqri0c=
github.com/ncruces/go-sqlite3/gormlite v0.24.0 h1:81sHeq3CCdhjoqAB650n5wEdRlLO9VBvosArskcN3+c=
github.com/ncruces/go-sqlite3/gormlite v0.24.0/go.mod h1:vXfVWdBfg7qOgqQqHpzUWl9LLswD0h+8mK4oouaV2oc=
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,86 +206,38 @@ 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/dtls/v3 v3.0.7 h1:bItXtTYYhZwkPFk4t1n3Kkf5TDrfj6+4wG+CZR8uI9Q=
github.com/pion/dtls/v3 v3.0.7/go.mod h1:uDlH5VPrgOQIw59irKYkMudSFprY9IEFCqz/eTz16f8=
github.com/pion/ice/v4 v4.0.10 h1:P59w1iauC/wPk9PdY8Vjl4fOFL5B+USq1+xbDcN6gT4=
github.com/pion/ice/v4 v4.0.10/go.mod h1:y3M18aPhIxLlcO/4dn9X8LzLLSma84cx6emMSu14FGw=
github.com/pion/interceptor v0.1.40 h1:e0BjnPcGpr2CFQgKhrQisBU7V3GXK6wrfYrGYaU6Jq4=
github.com/pion/interceptor v0.1.40/go.mod h1:Z6kqH7M/FYirg3frjGJ21VLSRJGBXB/KqaTIrdqnOic=
github.com/pion/logging v0.2.4 h1:tTew+7cmQ+Mc1pTBLKH2puKsOvhm32dROumOZ655zB8=
github.com/pion/logging v0.2.4/go.mod h1:DffhXTKYdNZU+KtJ5pyQDjvOAh/GsNSyv1lbkFbe3so=
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/rtp v1.8.21 h1:3yrOwmZFyUpcIosNcWRpQaU+UXIJ6yxLuJ8Bx0mw37Y=
github.com/pion/rtp v1.8.21/go.mod h1:bAu2UFKScgzyFqvUKmbvzSdPr+NGbZtv6UB2hesqXBk=
github.com/pion/sctp v1.8.39 h1:PJma40vRHa3UTO3C4MyeJDQ+KIobVYRZQZ0Nt7SjQnE=
github.com/pion/sctp v1.8.39/go.mod h1:cNiLdchXra8fHQwmIoqw0MbLLMs+f7uQ+dGMG2gWebE=
github.com/pion/sdp/v3 v3.0.15 h1:F0I1zds+K/+37ZrzdADmx2Q44OFDOPRLhPnNTaUX9hk=
github.com/pion/sdp/v3 v3.0.15/go.mod h1:88GMahN5xnScv1hIMTqLdu/cOcUkj6a9ytbncwMCq2E=
github.com/pion/srtp/v3 v3.0.7 h1:QUElw0A/FUg3MP8/KNMZB3i0m8F9XeMnTum86F7S4bs=
github.com/pion/srtp/v3 v3.0.7/go.mod h1:qvnHeqbhT7kDdB+OGB05KA/P067G3mm7XBfLaLiaNF0=
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/pion/turn/v4 v4.1.1 h1:9UnY2HB99tpDyz3cVVZguSxcqkJ1DsTSZ+8TGruh4fc=
github.com/pion/turn/v4 v4.1.1/go.mod h1:2123tHk1O++vmjI5VSD0awT50NywDAq5A2NNNU4Jjs8=
github.com/pion/webrtc/v4 v4.1.4 h1:/gK1ACGHXQmtyVVbJFQDxNoODg4eSRiFLB7t9r9pg8M=
github.com/pion/webrtc/v4 v4.1.4/go.mod h1:Oab9npu1iZtQRMic3K3toYq5zFPvToe/QBw7dMI2ok4=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/profile v1.4.0/go.mod h1:NWz/XGvpEW1FyYQ7fCx4dqYBLlfTcE+A9FLAkNKqjFE=
@@ -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,35 +283,27 @@ 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=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
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=
github.com/sunfish-shogi/bufseekio v0.0.0-20210207115823-a4185644b365/go.mod h1:dEzdXgvImkQ3WLI+0KQpmEx8T/C/ma9KeS3AfmU899I=
github.com/tetratelabs/wazero v1.8.0 h1:iEKu0d4c2Pd+QSRieYbnQC9yiFlMS9D+Jr0LsRmcF4g=
github.com/tetratelabs/wazero v1.8.0/go.mod h1:yAI0XTsMBhREkM/YDAK/zNou3GoiAce1P6+rp/wQhjs=
github.com/tetratelabs/wazero v1.9.0 h1:IcZ56OuxrtaEz8UYNRHBrUa9bYeX9oVY93KspZZBf/I=
github.com/tetratelabs/wazero v1.9.0/go.mod h1:TSbcXCfFP0L2FGkRPxHphadXPjo1T6W+CseNNY7EkjM=
github.com/tklauser/go-sysconf v0.3.12 h1:0QaGUFOdQaIVdPgfITYzaTegZvdCjmYO52cSFAEVmqU=
github.com/tklauser/go-sysconf v0.3.12/go.mod h1:Ho14jnntGE1fpdOqQEEaiKRpvIavV0hSfmBq8nJbHYI=
github.com/tklauser/numcpus v0.6.1 h1:ng9scYS7az0Bk4OZLvrNXNSAO2Pxr1XXRAPyjhIx+Fk=
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 +320,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.40.0 h1:r4x+VvoG5Fm+eJcxMaY8CQM7Lb0l1lsmjGBQ6s8BfKM=
golang.org/x/crypto v0.40.0/go.mod h1:Qr1vMER5WyS2dfPHAlsOj01wgLbsyWtFn/aY+5+ZdxY=
golang.org/x/exp v0.0.0-20240716175740-e3f259677ff7 h1:wDLEX9a7YQoKdKNQt88rtydkqDxeGaBUTnIYc3iG/mA=
golang.org/x/exp v0.0.0-20240716175740-e3f259677ff7/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY=
golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
@@ -426,159 +343,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/mod v0.25.0 h1:n7a+ZbQKQA/Ysbyb0/6IbB1H/X41mKgbhfv7AfG/44w=
golang.org/x/mod v0.25.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww=
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.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw=
golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA=
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.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw=
golang.org/x/sync v0.16.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.34.0 h1:H5Y5sJ2L2JRdyv7ROF1he/lPdvFsd0mJHFw2ThKHxLA=
golang.org/x/sys v0.34.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.27.0 h1:4fGWRpyh641NLlecmyl4LOe6yDdfaYNrGb2zdfo4JV4=
golang.org/x/text v0.27.0/go.mod h1:1D28KMCvyooCX9hBiosv5Tz/+YLxj0j7XhWjpSUF7CU=
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/tools v0.34.0 h1:qIpSLOxeCYGg9TrcJokLBG4KFA6d795g0xkBkiESGlo=
golang.org/x/tools v0.34.0/go.mod h1:pAP9OwEaY1CAW3HOmg3hLZC5Z0CCmzjAF2UQMSqNARg=
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 +416,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

@@ -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

View File

@@ -1192,66 +1192,6 @@ func local_request_Api_GetFormily_0(ctx context.Context, marshaler runtime.Marsh
}
func request_Api_ModifyConfig_0(ctx context.Context, marshaler runtime.Marshaler, client ApiClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
var protoReq ModifyConfigRequest
var metadata runtime.ServerMetadata
if err := marshaler.NewDecoder(req.Body).Decode(&protoReq.Yaml); err != nil && err != io.EOF {
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
}
var (
val string
ok bool
err error
_ = err
)
val, ok = pathParams["name"]
if !ok {
return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "name")
}
protoReq.Name, err = runtime.String(val)
if err != nil {
return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "name", err)
}
msg, err := client.ModifyConfig(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))
return msg, metadata, err
}
func local_request_Api_ModifyConfig_0(ctx context.Context, marshaler runtime.Marshaler, server ApiServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
var protoReq ModifyConfigRequest
var metadata runtime.ServerMetadata
if err := marshaler.NewDecoder(req.Body).Decode(&protoReq.Yaml); err != nil && err != io.EOF {
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
}
var (
val string
ok bool
err error
_ = err
)
val, ok = pathParams["name"]
if !ok {
return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "name")
}
protoReq.Name, err = runtime.String(val)
if err != nil {
return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "name", err)
}
msg, err := server.ModifyConfig(ctx, &protoReq)
return msg, metadata, err
}
func request_Api_GetPullProxyList_0(ctx context.Context, marshaler runtime.Marshaler, client ApiClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
var protoReq emptypb.Empty
var metadata runtime.ServerMetadata
@@ -1461,7 +1401,7 @@ func local_request_Api_RemovePullProxy_1(ctx context.Context, marshaler runtime.
}
func request_Api_UpdatePullProxy_0(ctx context.Context, marshaler runtime.Marshaler, client ApiClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
var protoReq PullProxyInfo
var protoReq UpdatePullProxyRequest
var metadata runtime.ServerMetadata
if err := marshaler.NewDecoder(req.Body).Decode(&protoReq); err != nil && err != io.EOF {
@@ -1474,7 +1414,7 @@ func request_Api_UpdatePullProxy_0(ctx context.Context, marshaler runtime.Marsha
}
func local_request_Api_UpdatePullProxy_0(ctx context.Context, marshaler runtime.Marshaler, server ApiServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
var protoReq PullProxyInfo
var protoReq UpdatePullProxyRequest
var metadata runtime.ServerMetadata
if err := marshaler.NewDecoder(req.Body).Decode(&protoReq); err != nil && err != io.EOF {
@@ -1487,7 +1427,7 @@ func local_request_Api_UpdatePullProxy_0(ctx context.Context, marshaler runtime.
}
func request_Api_UpdatePullProxy_1(ctx context.Context, marshaler runtime.Marshaler, client ApiClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
var protoReq PullProxyInfo
var protoReq UpdatePullProxyRequest
var metadata runtime.ServerMetadata
if err := marshaler.NewDecoder(req.Body).Decode(&protoReq); err != nil && err != io.EOF {
@@ -1500,7 +1440,7 @@ func request_Api_UpdatePullProxy_1(ctx context.Context, marshaler runtime.Marsha
}
func local_request_Api_UpdatePullProxy_1(ctx context.Context, marshaler runtime.Marshaler, server ApiServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
var protoReq PullProxyInfo
var protoReq UpdatePullProxyRequest
var metadata runtime.ServerMetadata
if err := marshaler.NewDecoder(req.Body).Decode(&protoReq); err != nil && err != io.EOF {
@@ -1617,7 +1557,7 @@ func local_request_Api_RemovePushProxy_0(ctx context.Context, marshaler runtime.
}
func request_Api_UpdatePushProxy_0(ctx context.Context, marshaler runtime.Marshaler, client ApiClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
var protoReq PushProxyInfo
var protoReq UpdatePushProxyRequest
var metadata runtime.ServerMetadata
if err := marshaler.NewDecoder(req.Body).Decode(&protoReq); err != nil && err != io.EOF {
@@ -1630,7 +1570,7 @@ func request_Api_UpdatePushProxy_0(ctx context.Context, marshaler runtime.Marsha
}
func local_request_Api_UpdatePushProxy_0(ctx context.Context, marshaler runtime.Marshaler, server ApiServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
var protoReq PushProxyInfo
var protoReq UpdatePushProxyRequest
var metadata runtime.ServerMetadata
if err := marshaler.NewDecoder(req.Body).Decode(&protoReq); err != nil && err != io.EOF {
@@ -1768,6 +1708,96 @@ func local_request_Api_GetRecordList_0(ctx context.Context, marshaler runtime.Ma
}
var (
filter_Api_GetEventRecordList_0 = &utilities.DoubleArray{Encoding: map[string]int{"type": 0, "streamPath": 1}, Base: []int{1, 1, 2, 0, 0}, Check: []int{0, 1, 1, 2, 3}}
)
func request_Api_GetEventRecordList_0(ctx context.Context, marshaler runtime.Marshaler, client ApiClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
var protoReq ReqRecordList
var metadata runtime.ServerMetadata
var (
val string
ok bool
err error
_ = err
)
val, ok = pathParams["type"]
if !ok {
return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "type")
}
protoReq.Type, err = runtime.String(val)
if err != nil {
return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "type", 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)
}
if err := req.ParseForm(); err != nil {
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
}
if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_Api_GetEventRecordList_0); err != nil {
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
}
msg, err := client.GetEventRecordList(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))
return msg, metadata, err
}
func local_request_Api_GetEventRecordList_0(ctx context.Context, marshaler runtime.Marshaler, server ApiServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
var protoReq ReqRecordList
var metadata runtime.ServerMetadata
var (
val string
ok bool
err error
_ = err
)
val, ok = pathParams["type"]
if !ok {
return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "type")
}
protoReq.Type, err = runtime.String(val)
if err != nil {
return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "type", 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)
}
if err := req.ParseForm(); err != nil {
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
}
if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_Api_GetEventRecordList_0); err != nil {
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
}
msg, err := server.GetEventRecordList(ctx, &protoReq)
return msg, metadata, err
}
func request_Api_GetRecordCatalog_0(ctx context.Context, marshaler runtime.Marshaler, client ApiClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
var protoReq ReqRecordCatalog
var metadata runtime.ServerMetadata
@@ -1900,6 +1930,94 @@ func local_request_Api_DeleteRecord_0(ctx context.Context, marshaler runtime.Mar
}
var (
filter_Api_GetAlarmList_0 = &utilities.DoubleArray{Encoding: map[string]int{}, Base: []int(nil), Check: []int(nil)}
)
func request_Api_GetAlarmList_0(ctx context.Context, marshaler runtime.Marshaler, client ApiClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
var protoReq AlarmListRequest
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_GetAlarmList_0); err != nil {
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
}
msg, err := client.GetAlarmList(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))
return msg, metadata, err
}
func local_request_Api_GetAlarmList_0(ctx context.Context, marshaler runtime.Marshaler, server ApiServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
var protoReq AlarmListRequest
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_GetAlarmList_0); err != nil {
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
}
msg, err := server.GetAlarmList(ctx, &protoReq)
return msg, metadata, err
}
func request_Api_GetSubscriptionProgress_0(ctx context.Context, marshaler runtime.Marshaler, client ApiClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
var protoReq StreamSnapRequest
var metadata runtime.ServerMetadata
var (
val string
ok bool
err error
_ = err
)
val, ok = pathParams["streamPath"]
if !ok {
return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "streamPath")
}
protoReq.StreamPath, err = runtime.String(val)
if err != nil {
return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "streamPath", err)
}
msg, err := client.GetSubscriptionProgress(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))
return msg, metadata, err
}
func local_request_Api_GetSubscriptionProgress_0(ctx context.Context, marshaler runtime.Marshaler, server ApiServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
var protoReq StreamSnapRequest
var metadata runtime.ServerMetadata
var (
val string
ok bool
err error
_ = err
)
val, ok = pathParams["streamPath"]
if !ok {
return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "streamPath")
}
protoReq.StreamPath, err = runtime.String(val)
if err != nil {
return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "streamPath", err)
}
msg, err := server.GetSubscriptionProgress(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.
@@ -2581,31 +2699,6 @@ func RegisterApiHandlerServer(ctx context.Context, mux *runtime.ServeMux, server
})
mux.Handle("POST", pattern_Api_ModifyConfig_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
ctx, cancel := context.WithCancel(req.Context())
defer cancel()
var stream runtime.ServerTransportStream
ctx = grpc.NewContextWithServerTransportStream(ctx, &stream)
inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
var err error
var annotatedContext context.Context
annotatedContext, err = runtime.AnnotateIncomingContext(ctx, mux, req, "/global.Api/ModifyConfig", runtime.WithHTTPPathPattern("/api/config/modify/{name}"))
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
resp, md, err := local_request_Api_ModifyConfig_0(annotatedContext, inboundMarshaler, server, req, pathParams)
md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer())
annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md)
if err != nil {
runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err)
return
}
forward_Api_ModifyConfig_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
})
mux.Handle("GET", pattern_Api_GetPullProxyList_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
ctx, cancel := context.WithCancel(req.Context())
defer cancel()
@@ -2739,7 +2832,7 @@ func RegisterApiHandlerServer(ctx context.Context, mux *runtime.ServeMux, server
inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
var err error
var annotatedContext context.Context
annotatedContext, err = runtime.AnnotateIncomingContext(ctx, mux, req, "/global.Api/RemovePullProxy", runtime.WithHTTPPathPattern("/api/device/add/{id}"))
annotatedContext, err = runtime.AnnotateIncomingContext(ctx, mux, req, "/global.Api/RemovePullProxy", runtime.WithHTTPPathPattern("/api/device/remove/{id}"))
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
@@ -2981,6 +3074,31 @@ func RegisterApiHandlerServer(ctx context.Context, mux *runtime.ServeMux, server
})
mux.Handle("GET", pattern_Api_GetEventRecordList_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
ctx, cancel := context.WithCancel(req.Context())
defer cancel()
var stream runtime.ServerTransportStream
ctx = grpc.NewContextWithServerTransportStream(ctx, &stream)
inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
var err error
var annotatedContext context.Context
annotatedContext, err = runtime.AnnotateIncomingContext(ctx, mux, req, "/global.Api/GetEventRecordList", runtime.WithHTTPPathPattern("/api/record/{type}/event/list/{streamPath=**}"))
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
resp, md, err := local_request_Api_GetEventRecordList_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_GetEventRecordList_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
})
mux.Handle("GET", pattern_Api_GetRecordCatalog_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
ctx, cancel := context.WithCancel(req.Context())
defer cancel()
@@ -3031,6 +3149,56 @@ func RegisterApiHandlerServer(ctx context.Context, mux *runtime.ServeMux, server
})
mux.Handle("GET", pattern_Api_GetAlarmList_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
ctx, cancel := context.WithCancel(req.Context())
defer cancel()
var stream runtime.ServerTransportStream
ctx = grpc.NewContextWithServerTransportStream(ctx, &stream)
inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
var err error
var annotatedContext context.Context
annotatedContext, err = runtime.AnnotateIncomingContext(ctx, mux, req, "/global.Api/GetAlarmList", runtime.WithHTTPPathPattern("/api/alarm/list"))
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
resp, md, err := local_request_Api_GetAlarmList_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_GetAlarmList_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
})
mux.Handle("GET", pattern_Api_GetSubscriptionProgress_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
ctx, cancel := context.WithCancel(req.Context())
defer cancel()
var stream runtime.ServerTransportStream
ctx = grpc.NewContextWithServerTransportStream(ctx, &stream)
inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
var err error
var annotatedContext context.Context
annotatedContext, err = runtime.AnnotateIncomingContext(ctx, mux, req, "/global.Api/GetSubscriptionProgress", runtime.WithHTTPPathPattern("/api/stream/progress/{streamPath=**}"))
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
resp, md, err := local_request_Api_GetSubscriptionProgress_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_GetSubscriptionProgress_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
})
return nil
}
@@ -3666,28 +3834,6 @@ func RegisterApiHandlerClient(ctx context.Context, mux *runtime.ServeMux, client
})
mux.Handle("POST", pattern_Api_ModifyConfig_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
ctx, cancel := context.WithCancel(req.Context())
defer cancel()
inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
var err error
var annotatedContext context.Context
annotatedContext, err = runtime.AnnotateContext(ctx, mux, req, "/global.Api/ModifyConfig", runtime.WithHTTPPathPattern("/api/config/modify/{name}"))
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
resp, md, err := request_Api_ModifyConfig_0(annotatedContext, inboundMarshaler, client, req, pathParams)
annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md)
if err != nil {
runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err)
return
}
forward_Api_ModifyConfig_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
})
mux.Handle("GET", pattern_Api_GetPullProxyList_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
ctx, cancel := context.WithCancel(req.Context())
defer cancel()
@@ -3804,7 +3950,7 @@ func RegisterApiHandlerClient(ctx context.Context, mux *runtime.ServeMux, client
inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
var err error
var annotatedContext context.Context
annotatedContext, err = runtime.AnnotateContext(ctx, mux, req, "/global.Api/RemovePullProxy", runtime.WithHTTPPathPattern("/api/device/add/{id}"))
annotatedContext, err = runtime.AnnotateContext(ctx, mux, req, "/global.Api/RemovePullProxy", runtime.WithHTTPPathPattern("/api/device/remove/{id}"))
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
@@ -4018,6 +4164,28 @@ func RegisterApiHandlerClient(ctx context.Context, mux *runtime.ServeMux, client
})
mux.Handle("GET", pattern_Api_GetEventRecordList_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
ctx, cancel := context.WithCancel(req.Context())
defer cancel()
inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
var err error
var annotatedContext context.Context
annotatedContext, err = runtime.AnnotateContext(ctx, mux, req, "/global.Api/GetEventRecordList", runtime.WithHTTPPathPattern("/api/record/{type}/event/list/{streamPath=**}"))
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
resp, md, err := request_Api_GetEventRecordList_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_GetEventRecordList_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
})
mux.Handle("GET", pattern_Api_GetRecordCatalog_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
ctx, cancel := context.WithCancel(req.Context())
defer cancel()
@@ -4062,6 +4230,50 @@ func RegisterApiHandlerClient(ctx context.Context, mux *runtime.ServeMux, client
})
mux.Handle("GET", pattern_Api_GetAlarmList_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
ctx, cancel := context.WithCancel(req.Context())
defer cancel()
inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
var err error
var annotatedContext context.Context
annotatedContext, err = runtime.AnnotateContext(ctx, mux, req, "/global.Api/GetAlarmList", runtime.WithHTTPPathPattern("/api/alarm/list"))
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
resp, md, err := request_Api_GetAlarmList_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_GetAlarmList_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
})
mux.Handle("GET", pattern_Api_GetSubscriptionProgress_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
ctx, cancel := context.WithCancel(req.Context())
defer cancel()
inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
var err error
var annotatedContext context.Context
annotatedContext, err = runtime.AnnotateContext(ctx, mux, req, "/global.Api/GetSubscriptionProgress", runtime.WithHTTPPathPattern("/api/stream/progress/{streamPath=**}"))
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
resp, md, err := request_Api_GetSubscriptionProgress_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_GetSubscriptionProgress_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
})
return nil
}
@@ -4120,8 +4332,6 @@ var (
pattern_Api_GetFormily_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 1, 0, 4, 1, 5, 3}, []string{"api", "config", "formily", "name"}, ""))
pattern_Api_ModifyConfig_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 1, 0, 4, 1, 5, 3}, []string{"api", "config", "modify", "name"}, ""))
pattern_Api_GetPullProxyList_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 2, 3}, []string{"api", "proxy", "pull", "list"}, ""))
pattern_Api_GetPullProxyList_1 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2}, []string{"api", "device", "list"}, ""))
@@ -4132,7 +4342,7 @@ var (
pattern_Api_RemovePullProxy_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 2, 3, 1, 0, 4, 1, 5, 4}, []string{"api", "proxy", "pull", "remove", "id"}, ""))
pattern_Api_RemovePullProxy_1 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 1, 0, 4, 1, 5, 3}, []string{"api", "device", "add", "id"}, ""))
pattern_Api_RemovePullProxy_1 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 1, 0, 4, 1, 5, 3}, []string{"api", "device", "remove", "id"}, ""))
pattern_Api_UpdatePullProxy_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 2, 3}, []string{"api", "proxy", "pull", "update"}, ""))
@@ -4152,9 +4362,15 @@ var (
pattern_Api_GetRecordList_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 1, 0, 4, 1, 5, 2, 2, 3, 3, 0, 4, 1, 5, 4}, []string{"api", "record", "type", "list", "streamPath"}, ""))
pattern_Api_GetEventRecordList_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 1, 0, 4, 1, 5, 2, 2, 3, 2, 4, 3, 0, 4, 1, 5, 5}, []string{"api", "record", "type", "event", "list", "streamPath"}, ""))
pattern_Api_GetRecordCatalog_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 1, 0, 4, 1, 5, 2, 2, 3}, []string{"api", "record", "type", "catalog"}, ""))
pattern_Api_DeleteRecord_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 1, 0, 4, 1, 5, 2, 2, 3, 3, 0, 4, 1, 5, 4}, []string{"api", "record", "type", "delete", "streamPath"}, ""))
pattern_Api_GetAlarmList_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2}, []string{"api", "alarm", "list"}, ""))
pattern_Api_GetSubscriptionProgress_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 3, 0, 4, 1, 5, 3}, []string{"api", "stream", "progress", "streamPath"}, ""))
)
var (
@@ -4212,8 +4428,6 @@ var (
forward_Api_GetFormily_0 = runtime.ForwardResponseMessage
forward_Api_ModifyConfig_0 = runtime.ForwardResponseMessage
forward_Api_GetPullProxyList_0 = runtime.ForwardResponseMessage
forward_Api_GetPullProxyList_1 = runtime.ForwardResponseMessage
@@ -4244,7 +4458,13 @@ var (
forward_Api_GetRecordList_0 = runtime.ForwardResponseMessage
forward_Api_GetEventRecordList_0 = runtime.ForwardResponseMessage
forward_Api_GetRecordCatalog_0 = runtime.ForwardResponseMessage
forward_Api_DeleteRecord_0 = runtime.ForwardResponseMessage
forward_Api_GetAlarmList_0 = runtime.ForwardResponseMessage
forward_Api_GetSubscriptionProgress_0 = runtime.ForwardResponseMessage
)

View File

@@ -152,12 +152,7 @@ service api {
get: "/api/config/formily/{name}"
};
}
rpc ModifyConfig (ModifyConfigRequest) returns (SuccessResponse) {
option (google.api.http) = {
post: "/api/config/modify/{name}"
body: "yaml"
};
}
rpc GetPullProxyList (google.protobuf.Empty) returns (PullProxyListResponse) {
option (google.api.http) = {
get: "/api/proxy/pull/list"
@@ -181,12 +176,12 @@ service api {
post: "/api/proxy/pull/remove/{id}"
body: "*"
additional_bindings {
post: "/api/device/add/{id}"
post: "/api/device/remove/{id}"
body: "*"
}
};
}
rpc UpdatePullProxy (PullProxyInfo) returns (SuccessResponse) {
rpc UpdatePullProxy (UpdatePullProxyRequest) returns (SuccessResponse) {
option (google.api.http) = {
post: "/api/proxy/pull/update"
body: "*"
@@ -213,7 +208,7 @@ service api {
body: "*"
};
}
rpc UpdatePushProxy (PushProxyInfo) returns (SuccessResponse) {
rpc UpdatePushProxy (UpdatePushProxyRequest) returns (SuccessResponse) {
option (google.api.http) = {
post: "/api/proxy/push/update"
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,16 @@ service api {
body: "*"
};
}
rpc GetAlarmList (AlarmListRequest) returns (AlarmListResponse) {
option (google.api.http) = {
get: "/api/alarm/list"
};
}
rpc GetSubscriptionProgress (StreamSnapRequest) returns (SubscriptionProgressResponse) {
option (google.api.http) = {
get: "/api/stream/progress/{streamPath=**}"
};
}
}
message DisabledPluginsResponse {
@@ -361,6 +371,8 @@ message TaskTreeData {
TaskTreeData blocked = 8;
uint64 pointer = 9;
string startReason = 10;
bool eventLoopRunning = 11;
uint32 level = 12;
}
message TaskTreeResponse {
@@ -566,6 +578,22 @@ message PullProxyInfo {
string streamPath = 16; // 流路径
}
message UpdatePullProxyRequest {
uint32 ID = 1;
optional uint32 parentID = 2; // 父设备ID
optional string name = 3; // 设备名称
optional string type = 4; // 设备类型
optional uint32 status = 5; // 设备状态
optional string pullURL = 6; // 拉流地址
optional bool pullOnStart = 7; // 启动时拉流
optional bool stopOnIdle = 8; // 空闲时停止拉流
optional bool audio = 9; // 是否拉取音频
optional string description = 10; // 设备描述
optional string recordPath = 11; // 录制路径
optional google.protobuf.Duration recordFragment = 12; // 录制片段长度
optional string streamPath = 13; // 流路径
}
message PushProxyInfo {
uint32 ID = 1;
google.protobuf.Timestamp createTime = 2;
@@ -582,6 +610,20 @@ message PushProxyInfo {
string streamPath = 13; // 流路径
}
message UpdatePushProxyRequest {
uint32 ID = 1;
optional uint32 parentID = 2; // 父设备ID
optional string name = 3; // 设备名称
optional string type = 4; // 设备类型
optional uint32 status = 5; // 设备状态
optional string pushURL = 6; // 推流地址
optional bool pushOnStart = 7; // 启动时推流
optional bool audio = 8; // 是否推音频
optional string description = 9; // 设备描述
optional uint32 rtt = 10; // 平均RTT
optional string streamPath = 11; // 流路径
}
message PushProxyListResponse {
int32 code = 1;
string message = 2;
@@ -669,8 +711,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 +723,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 +783,57 @@ 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;
}
message Step {
string name = 1;
string description = 2;
string error = 3;
google.protobuf.Timestamp startedAt = 4;
google.protobuf.Timestamp completedAt = 5;
}
message SubscriptionProgressData {
repeated Step steps = 1;
int32 currentStep = 2;
}
message SubscriptionProgressResponse {
int32 code = 1;
string message = 2;
SubscriptionProgressData data = 3;
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,90 +0,0 @@
package pkg
import (
"bytes"
"fmt"
"io"
"time"
"github.com/deepch/vdk/codec/aacparser"
"m7s.live/v5/pkg/codec"
"m7s.live/v5/pkg/util"
)
var _ IAVFrame = (*ADTS)(nil)
type ADTS struct {
DTS time.Duration
util.RecyclableMemory
}
func (A *ADTS) Parse(track *AVTrack) (err error) {
if track.ICodecCtx == nil {
var ctx = &codec.AACCtx{}
var reader = A.NewReader()
var adts []byte
adts, err = reader.ReadBytes(7)
if err != nil {
return err
}
var hdrlen, framelen, samples int
ctx.Config, hdrlen, framelen, samples, err = aacparser.ParseADTSHeader(adts)
if err != nil {
return err
}
b := &bytes.Buffer{}
aacparser.WriteMPEG4AudioConfig(b, ctx.Config)
ctx.ConfigBytes = b.Bytes()
track.ICodecCtx = ctx
track.Info("ADTS", "hdrlen", hdrlen, "framelen", framelen, "samples", samples)
}
track.Value.Raw, err = A.Demux(track.ICodecCtx)
return
}
func (A *ADTS) ConvertCtx(ctx codec.ICodecCtx) (codec.ICodecCtx, IAVFrame, error) {
return ctx.GetBase(), nil, nil
}
func (A *ADTS) Demux(ctx codec.ICodecCtx) (any, error) {
var reader = A.NewReader()
err := reader.Skip(7)
var mem util.Memory
reader.Range(mem.AppendOne)
return mem, err
}
func (A *ADTS) Mux(ctx codec.ICodecCtx, frame *AVFrame) {
A.InitRecycleIndexes(1)
A.DTS = frame.Timestamp * 90 / time.Millisecond
aacCtx, ok := ctx.GetBase().(*codec.AACCtx)
if !ok {
A.Append(frame.Raw.(util.Memory).Buffers...)
return
}
adts := A.NextN(7)
raw := frame.Raw.(util.Memory)
aacparser.FillADTSHeader(adts, aacCtx.Config, raw.Size/aacCtx.GetSampleSize(), raw.Size)
A.Append(raw.Buffers...)
}
func (A *ADTS) GetTimestamp() time.Duration {
return A.DTS * time.Millisecond / 90
}
func (A *ADTS) GetCTS() time.Duration {
return 0
}
func (A *ADTS) GetSize() int {
return A.Size
}
func (A *ADTS) String() string {
return fmt.Sprintf("ADTS{size:%d}", A.Size)
}
func (A *ADTS) Dump(b byte, writer io.Writer) {
//TODO implement me
panic("implement me")
}

View File

@@ -1,182 +0,0 @@
package pkg
import (
"encoding/binary"
"fmt"
"io"
"time"
"github.com/deepch/vdk/codec/h264parser"
"github.com/deepch/vdk/codec/h265parser"
"m7s.live/v5/pkg/codec"
"m7s.live/v5/pkg/util"
)
var _ IAVFrame = (*AnnexB)(nil)
type AnnexB struct {
Hevc bool
PTS time.Duration
DTS time.Duration
util.RecyclableMemory
}
func (a *AnnexB) Dump(t byte, w io.Writer) {
m := a.GetAllocator().Borrow(4 + a.Size)
binary.BigEndian.PutUint32(m, uint32(a.Size))
a.CopyTo(m[4:])
w.Write(m)
}
// DecodeConfig implements pkg.IAVFrame.
func (a *AnnexB) ConvertCtx(ctx codec.ICodecCtx) (codec.ICodecCtx, IAVFrame, error) {
return ctx.GetBase(), nil, nil
}
// GetSize implements pkg.IAVFrame.
func (a *AnnexB) GetSize() int {
return a.Size
}
func (a *AnnexB) GetTimestamp() time.Duration {
return a.DTS * time.Millisecond / 90
}
func (a *AnnexB) GetCTS() time.Duration {
return (a.PTS - a.DTS) * time.Millisecond / 90
}
// Parse implements pkg.IAVFrame.
func (a *AnnexB) Parse(t *AVTrack) (err error) {
if a.Hevc {
if t.ICodecCtx == nil {
t.ICodecCtx = &codec.H265Ctx{}
}
} else {
if t.ICodecCtx == nil {
t.ICodecCtx = &codec.H264Ctx{}
}
}
if t.Value.Raw, err = a.Demux(t.ICodecCtx); err != nil {
return
}
for _, nalu := range t.Value.Raw.(Nalus) {
if a.Hevc {
ctx := t.ICodecCtx.(*codec.H265Ctx)
switch codec.ParseH265NALUType(nalu.Buffers[0][0]) {
case h265parser.NAL_UNIT_VPS:
ctx.RecordInfo.VPS = [][]byte{nalu.ToBytes()}
case h265parser.NAL_UNIT_SPS:
ctx.RecordInfo.SPS = [][]byte{nalu.ToBytes()}
case h265parser.NAL_UNIT_PPS:
ctx.RecordInfo.PPS = [][]byte{nalu.ToBytes()}
ctx.CodecData, err = h265parser.NewCodecDataFromVPSAndSPSAndPPS(ctx.VPS(), ctx.SPS(), ctx.PPS())
case h265parser.NAL_UNIT_CODED_SLICE_BLA_W_LP,
h265parser.NAL_UNIT_CODED_SLICE_BLA_W_RADL,
h265parser.NAL_UNIT_CODED_SLICE_BLA_N_LP,
h265parser.NAL_UNIT_CODED_SLICE_IDR_W_RADL,
h265parser.NAL_UNIT_CODED_SLICE_IDR_N_LP,
h265parser.NAL_UNIT_CODED_SLICE_CRA:
t.Value.IDR = true
}
} else {
ctx := t.ICodecCtx.(*codec.H264Ctx)
switch codec.ParseH264NALUType(nalu.Buffers[0][0]) {
case codec.NALU_SPS:
ctx.RecordInfo.SPS = [][]byte{nalu.ToBytes()}
if len(ctx.RecordInfo.PPS) > 0 {
ctx.CodecData, err = h264parser.NewCodecDataFromSPSAndPPS(ctx.SPS(), ctx.PPS())
}
case codec.NALU_PPS:
ctx.RecordInfo.PPS = [][]byte{nalu.ToBytes()}
if len(ctx.RecordInfo.SPS) > 0 {
ctx.CodecData, err = h264parser.NewCodecDataFromSPSAndPPS(ctx.SPS(), ctx.PPS())
}
case codec.NALU_IDR_Picture:
t.Value.IDR = true
}
}
}
return
}
// String implements pkg.IAVFrame.
func (a *AnnexB) String() string {
return fmt.Sprintf("%d %d", a.DTS, a.Memory.Size)
}
// Demux implements pkg.IAVFrame.
func (a *AnnexB) Demux(codecCtx codec.ICodecCtx) (ret any, err error) {
var nalus Nalus
var lastFourBytes [4]byte
var b byte
var shallow util.Memory
shallow.Append(a.Buffers...)
reader := shallow.NewReader()
gotNalu := func() {
var nalu util.Memory
for buf := range reader.ClipFront {
nalu.AppendOne(buf)
}
nalus = append(nalus, nalu)
}
for {
b, err = reader.ReadByte()
if err == nil {
copy(lastFourBytes[:], lastFourBytes[1:])
lastFourBytes[3] = b
var startCode = 0
if lastFourBytes == codec.NALU_Delimiter2 {
startCode = 4
} else if [3]byte(lastFourBytes[1:]) == codec.NALU_Delimiter1 {
startCode = 3
}
if startCode > 0 && reader.Offset() >= 3 {
if reader.Offset() == 3 {
startCode = 3
}
reader.Unread(startCode)
if reader.Offset() > 0 {
gotNalu()
}
reader.Skip(startCode)
for range reader.ClipFront {
}
}
} else if err == io.EOF {
if reader.Offset() > 0 {
gotNalu()
}
err = nil
break
}
}
ret = nalus
return
}
func (a *AnnexB) Mux(codecCtx codec.ICodecCtx, frame *AVFrame) {
a.DTS = frame.Timestamp * 90 / time.Millisecond
a.PTS = a.DTS + frame.CTS*90/time.Millisecond
a.InitRecycleIndexes(0)
delimiter2 := codec.NALU_Delimiter2[:]
a.AppendOne(delimiter2)
if frame.IDR {
switch ctx := codecCtx.(type) {
case *codec.H264Ctx:
a.Append(ctx.SPS(), delimiter2, ctx.PPS(), delimiter2)
case *codec.H265Ctx:
a.Append(ctx.SPS(), delimiter2, ctx.PPS(), delimiter2, ctx.VPS(), delimiter2)
}
}
for i, nalu := range frame.Raw.(Nalus) {
if i > 0 {
a.AppendOne(codec.NALU_Delimiter1[:])
}
a.Append(nalu.Buffers...)
}
}

219
pkg/annexb_reader.go Normal file
View File

@@ -0,0 +1,219 @@
package pkg
import (
"fmt"
"m7s.live/v5/pkg/util"
)
// AnnexBReader 专门用于读取 AnnexB 格式数据的读取器
// 模仿 MemoryReader 结构,支持跨切片读取和动态数据管理
type AnnexBReader struct {
util.Memory // 存储数据的多段内存
Length, offset0, offset1 int // 可读长度和当前读取位置
}
// AppendBuffer 追加单个数据缓冲区
func (r *AnnexBReader) AppendBuffer(buf []byte) {
r.PushOne(buf)
r.Length += len(buf)
}
// ClipFront 剔除已读取的数据,释放内存
func (r *AnnexBReader) ClipFront() {
readOffset := r.Size - r.Length
if readOffset == 0 {
return
}
// 剔除已完全读取的缓冲区(不回收内存)
if r.offset0 > 0 {
r.Buffers = r.Buffers[r.offset0:]
r.Size -= readOffset
r.offset0 = 0
}
// 处理部分读取的缓冲区(不回收内存)
if r.offset1 > 0 && len(r.Buffers) > 0 {
buf := r.Buffers[0]
r.Buffers[0] = buf[r.offset1:]
r.Size -= r.offset1
r.offset1 = 0
}
}
// FindStartCode 查找 NALU 起始码,返回起始码位置和长度
func (r *AnnexBReader) FindStartCode() (pos int, startCodeLen int, found bool) {
if r.Length < 3 {
return 0, 0, false
}
// 逐字节检查起始码
for i := 0; i <= r.Length-3; i++ {
// 优先检查 4 字节起始码
if i <= r.Length-4 {
if r.getByteAt(i) == 0x00 && r.getByteAt(i+1) == 0x00 &&
r.getByteAt(i+2) == 0x00 && r.getByteAt(i+3) == 0x01 {
return i, 4, true
}
}
// 检查 3 字节起始码(但要确保不是 4 字节起始码的一部分)
if r.getByteAt(i) == 0x00 && r.getByteAt(i+1) == 0x00 && r.getByteAt(i+2) == 0x01 {
// 确保这不是4字节起始码的一部分
if i == 0 || r.getByteAt(i-1) != 0x00 {
return i, 3, true
}
}
}
return 0, 0, false
}
// getByteAt 获取指定位置的字节,不改变读取位置
func (r *AnnexBReader) getByteAt(pos int) byte {
if pos >= r.Length {
return 0
}
// 计算在哪个缓冲区和缓冲区内的位置
currentPos := 0
bufferIndex := r.offset0
bufferOffset := r.offset1
for bufferIndex < len(r.Buffers) {
buf := r.Buffers[bufferIndex]
available := len(buf) - bufferOffset
if currentPos+available > pos {
// 目标位置在当前缓冲区内
return buf[bufferOffset+(pos-currentPos)]
}
currentPos += available
bufferIndex++
bufferOffset = 0
}
return 0
}
type InvalidDataError struct {
util.Memory
}
func (e InvalidDataError) Error() string {
return fmt.Sprintf("% 02X", e.ToBytes())
}
// ReadNALU 读取一个完整的 NALU
// withStart 用于接收“包含起始码”的内存段
// withoutStart 用于接收“不包含起始码”的内存段
// 允许 withStart 或 withoutStart 为 nil表示调用方不需要该形式的数据
func (r *AnnexBReader) ReadNALU(withStart, withoutStart *util.Memory) error {
r.ClipFront()
// 定位到第一个起始码
firstPos, startCodeLen, found := r.FindStartCode()
if !found {
return nil
}
// 跳过起始码之前的无效数据
if firstPos > 0 {
var invalidData util.Memory
var reader util.MemoryReader
reader.Memory = &r.Memory
reader.RangeN(firstPos, invalidData.PushOne)
return InvalidDataError{invalidData}
}
// 为了查找下一个起始码,需要临时跳过当前起始码再查找
saveOffset0, saveOffset1, saveLength := r.offset0, r.offset1, r.Length
r.forward(startCodeLen)
nextPosAfterStart, _, nextFound := r.FindStartCode()
// 恢复到起始码起点
r.offset0, r.offset1, r.Length = saveOffset0, saveOffset1, saveLength
if !nextFound {
return nil
}
// 依次读取并填充输出,同时推进读取位置到 NALU 末尾(不消耗下一个起始码)
remaining := startCodeLen + nextPosAfterStart
// 需要在 withoutStart 中跳过的前缀(即起始码长度)
skipForWithout := startCodeLen
for remaining > 0 && r.offset0 < len(r.Buffers) {
buf := r.getCurrentBuffer()
readLen := len(buf)
if readLen > remaining {
readLen = remaining
}
segment := buf[:readLen]
if withStart != nil {
withStart.PushOne(segment)
}
if withoutStart != nil {
if skipForWithout >= readLen {
// 本段全部属于起始码,跳过
skipForWithout -= readLen
} else {
// 仅跳过起始码前缀,余下推入 withoutStart
withoutStart.PushOne(segment[skipForWithout:])
skipForWithout = 0
}
}
if readLen == len(buf) {
r.skipCurrentBuffer()
} else {
r.forward(readLen)
}
remaining -= readLen
}
return nil
}
// getCurrentBuffer 获取当前读取位置的缓冲区
func (r *AnnexBReader) getCurrentBuffer() []byte {
if r.offset0 >= len(r.Buffers) {
return nil
}
return r.Buffers[r.offset0][r.offset1:]
}
// forward 向前移动读取位置
func (r *AnnexBReader) forward(n int) {
if n <= 0 || r.Length <= 0 {
return
}
if n > r.Length { // 防御:不允许超出剩余长度
n = r.Length
}
r.Length -= n
for n > 0 && r.offset0 < len(r.Buffers) {
cur := r.Buffers[r.offset0]
remain := len(cur) - r.offset1
if n < remain { // 仍在当前缓冲区内
r.offset1 += n
n = 0
return
}
// 用掉当前缓冲区剩余部分,跳到下一个缓冲区起点
n -= remain
r.offset0++
r.offset1 = 0
}
}
// skipCurrentBuffer 跳过当前缓冲区
func (r *AnnexBReader) skipCurrentBuffer() {
if r.offset0 < len(r.Buffers) {
curBufLen := len(r.Buffers[r.offset0]) - r.offset1
r.Length -= curBufLen
r.offset0++
r.offset1 = 0
}
}

173
pkg/annexb_reader_test.go Normal file
View File

@@ -0,0 +1,173 @@
package pkg
import (
"bytes"
_ "embed"
"math/rand"
"testing"
"m7s.live/v5/pkg/codec"
"m7s.live/v5/pkg/util"
)
func bytesFromMemory(m util.Memory) []byte {
if m.Size == 0 {
return nil
}
out := make([]byte, 0, m.Size)
for _, b := range m.Buffers {
out = append(out, b...)
}
return out
}
func TestAnnexBReader_ReadNALU_Basic(t *testing.T) {
var reader AnnexBReader
// 3 个 NALU分别使用 4 字节、3 字节、4 字节起始码
expected1 := []byte{0x67, 0x42, 0x00, 0x1E}
expected2 := []byte{0x68, 0xCE, 0x3C, 0x80}
expected3 := []byte{0x65, 0x88, 0x84, 0x00}
buf := append([]byte{0x00, 0x00, 0x00, 0x01}, expected1...)
buf = append(buf, append([]byte{0x00, 0x00, 0x01}, expected2...)...)
buf = append(buf, append([]byte{0x00, 0x00, 0x00, 0x01}, expected3...)...)
reader.AppendBuffer(append(buf, codec.NALU_Delimiter2[:]...))
// 读取并校验 3 个 NALU不包含起始码
var n util.Memory
if err := reader.ReadNALU(nil, &n); err != nil {
t.Fatalf("read nalu 1: %v", err)
}
if !bytes.Equal(bytesFromMemory(n), expected1) {
t.Fatalf("nalu1 mismatch")
}
n = util.Memory{}
if err := reader.ReadNALU(nil, &n); err != nil {
t.Fatalf("read nalu 2: %v", err)
}
if !bytes.Equal(bytesFromMemory(n), expected2) {
t.Fatalf("nalu2 mismatch")
}
n = util.Memory{}
if err := reader.ReadNALU(nil, &n); err != nil {
t.Fatalf("read nalu 3: %v", err)
}
if !bytes.Equal(bytesFromMemory(n), expected3) {
t.Fatalf("nalu3 mismatch")
}
// 再读一次应无更多起始码,返回 nil 错误且长度为 0
if err := reader.ReadNALU(nil, &n); err != nil {
t.Fatalf("expected nil error when no more nalu, got: %v", err)
}
if reader.Length != 4 {
t.Fatalf("expected length 0 after reading all, got %d", reader.Length)
}
}
func TestAnnexBReader_AppendBuffer_MultiChunk_Random(t *testing.T) {
var reader AnnexBReader
rng := rand.New(rand.NewSource(1)) // 固定种子,保证可复现
// 生成随机 NALU仅负载部分并构造 AnnexB 数据(随机 3/4 字节起始码)
numNALU := 12
expectedPayloads := make([][]byte, 0, numNALU)
fullStream := make([]byte, 0, 1024)
for i := 0; i < numNALU; i++ {
payloadLen := 1 + rng.Intn(32)
payload := make([]byte, payloadLen)
for j := 0; j < payloadLen; j++ {
payload[j] = byte(rng.Intn(256))
}
expectedPayloads = append(expectedPayloads, payload)
if rng.Intn(2) == 0 {
fullStream = append(fullStream, 0x00, 0x00, 0x01)
} else {
fullStream = append(fullStream, 0x00, 0x00, 0x00, 0x01)
}
fullStream = append(fullStream, payload...)
}
fullStream = append(fullStream, codec.NALU_Delimiter2[:]...) // 结尾加个起始码,方便读取到最后一个 NALU
// 随机切割为多段并 AppendBuffer
for i := 0; i < len(fullStream); {
// 每段长度 1..7 字节(或剩余长度)
maxStep := 7
remain := len(fullStream) - i
step := 1 + rng.Intn(maxStep)
if step > remain {
step = remain
}
reader.AppendBuffer(fullStream[i : i+step])
i += step
}
// 依次读取并校验
for idx, expected := range expectedPayloads {
var n util.Memory
if err := reader.ReadNALU(nil, &n); err != nil {
t.Fatalf("read nalu %d: %v", idx+1, err)
}
got := bytesFromMemory(n)
if !bytes.Equal(got, expected) {
t.Fatalf("nalu %d mismatch: expected %d bytes, got %d bytes", idx+1, len(expected), len(got))
}
}
// 没有更多 NALU
var n util.Memory
if err := reader.ReadNALU(nil, &n); err != nil {
t.Fatalf("expected nil error when no more nalu, got: %v", err)
}
}
// 起始码跨越两个缓冲区的情况测试(例如 00 00 | 00 01
func TestAnnexBReader_StartCodeAcrossBuffers(t *testing.T) {
var reader AnnexBReader
// 构造一个 4 字节起始码被拆成两段的情况,后跟一个短 payload
reader.AppendBuffer([]byte{0x00, 0x00})
reader.AppendBuffer([]byte{0x00})
reader.AppendBuffer([]byte{0x01, 0x11, 0x22, 0x33}) // payload: 11 22 33
reader.AppendBuffer(codec.NALU_Delimiter2[:])
var n util.Memory
if err := reader.ReadNALU(nil, &n); err != nil {
t.Fatalf("read nalu: %v", err)
}
got := bytesFromMemory(n)
expected := []byte{0x11, 0x22, 0x33}
if !bytes.Equal(got, expected) {
t.Fatalf("payload mismatch: expected %v got %v", expected, got)
}
}
//go:embed test.h264
var annexbH264Sample []byte
var clipSizesH264 = [...]int{7823, 7157, 5137, 6268, 5958, 4573, 5661, 5589, 3917, 5207, 5347, 4111, 4755, 5199, 3761, 5014, 4981, 3736, 5075, 4889, 3739, 4701, 4655, 3471, 4086, 4428, 3309, 4388, 28, 8, 63974, 63976, 37544, 4945, 6525, 6974, 4874, 6317, 6141, 4455, 5833, 4105, 5407, 5479, 3741, 5142, 4939, 3745, 4945, 4857, 3518, 4624, 4930, 3649, 4846, 5020, 3293, 4588, 4571, 3430, 4844, 4822, 21223, 8461, 7188, 4882, 6108, 5870, 4432, 5389, 5466, 3726}
func TestAnnexBReader_EmbeddedAnnexB_H265(t *testing.T) {
var reader AnnexBReader
offset := 0
for _, size := range clipSizesH264 {
reader.AppendBuffer(annexbH264Sample[offset : offset+size])
offset += size
var nalu util.Memory
if err := reader.ReadNALU(nil, &nalu); err != nil {
t.Fatalf("read nalu: %v", err)
} else {
t.Logf("read nalu: %d bytes", nalu.Size)
if nalu.Size > 0 {
tryH264Type := codec.ParseH264NALUType(nalu.Buffers[0][0])
t.Logf("tryH264Type: %d", tryH264Type)
}
}
}
}

View File

@@ -172,8 +172,11 @@ 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)
if r.Logger.Enabled(context.TODO(), task.TraceLevel) {
r.Log(context.TODO(), task.TraceLevel, r.Track.FourCC().String(), "ts", r.Value.Timestamp, "delay", r.Delay, "bps", r.BPS)
}
} else {
r.Warn("no codec")
}

View File

@@ -1,8 +1,6 @@
package pkg
import (
"io"
"net"
"sync"
"time"
@@ -27,21 +25,28 @@ type (
}
// Source -> Parse -> Demux -> (ConvertCtx) -> Mux(GetAllocator) -> Recycle
IAVFrame interface {
GetAllocator() *util.ScalableMemoryAllocator
SetAllocator(*util.ScalableMemoryAllocator)
Parse(*AVTrack) error // get codec info, idr
ConvertCtx(codec.ICodecCtx) (codec.ICodecCtx, IAVFrame, error) // convert codec from source stream
Demux(codec.ICodecCtx) (any, error) // demux to raw format
Mux(codec.ICodecCtx, *AVFrame) // mux from raw format
GetTimestamp() time.Duration
GetCTS() time.Duration
GetSample() *Sample
GetSize() int
CheckCodecChange() error
Demux() error // demux to raw format
Mux(*Sample) error // mux from origin format
Recycle()
String() string
Dump(byte, io.Writer)
}
Nalus []util.Memory
ISequenceCodecCtx[T any] interface {
GetSequenceFrame() T
}
BaseSample struct {
Raw IRaw // 裸格式用于转换的中间格式
IDR bool
TS0, Timestamp, CTS time.Duration // 原始 TS、修正 TS、Composition Time Stamp
}
Sample struct {
codec.ICodecCtx
util.RecyclableMemory
*BaseSample
}
Nalus = util.ReuseArray[util.Memory]
AudioData = util.Memory
@@ -49,38 +54,130 @@ type (
AVFrame struct {
DataFrame
IDR bool
Timestamp time.Duration // 绝对时间戳
CTS time.Duration // composition time stamp
Wraps []IAVFrame // 封装格式
*Sample
Wraps []IAVFrame // 封装格式
}
IRaw interface {
util.Resetter
Count() int
}
AVRing = util.Ring[AVFrame]
DataFrame struct {
sync.RWMutex
discard bool
Sequence uint32 // 在一个Track中的序号
WriteTime time.Time // 写入时间,可用于比较两个帧的先后
Raw any // 裸格式
}
)
var _ IAVFrame = (*AnnexB)(nil)
func (sample *Sample) GetSize() int {
return sample.Size
}
func (frame *AVFrame) Clone() {
func (sample *Sample) GetSample() *Sample {
return sample
}
func (sample *Sample) CheckCodecChange() (err error) {
return
}
func (sample *Sample) Demux() error {
return nil
}
func (sample *Sample) Mux(from *Sample) error {
sample.ICodecCtx = from.GetBase()
return nil
}
func ConvertFrameType(from, to IAVFrame) (err error) {
fromSampe, toSample := from.GetSample(), to.GetSample()
if !fromSampe.HasRaw() {
if err = from.Demux(); err != nil {
return
}
}
toSample.SetAllocator(fromSampe.GetAllocator())
toSample.BaseSample = fromSampe.BaseSample
return to.Mux(fromSampe)
}
func (b *BaseSample) HasRaw() bool {
return b.Raw != nil && b.Raw.Count() > 0
}
// 90Hz
func (b *BaseSample) GetDTS() time.Duration {
return b.Timestamp * 90 / time.Millisecond
}
func (b *BaseSample) GetPTS() time.Duration {
return (b.Timestamp + b.CTS) * 90 / time.Millisecond
}
func (b *BaseSample) SetDTS(dts time.Duration) {
b.Timestamp = dts * time.Millisecond / 90
}
func (b *BaseSample) SetPTS(pts time.Duration) {
b.CTS = pts*time.Millisecond/90 - b.Timestamp
}
func (b *BaseSample) SetTS32(ts uint32) {
b.Timestamp = time.Duration(ts) * time.Millisecond
}
func (b *BaseSample) GetTS32() uint32 {
return uint32(b.Timestamp / time.Millisecond)
}
func (b *BaseSample) SetCTS32(ts uint32) {
b.CTS = time.Duration(ts) * time.Millisecond
}
func (b *BaseSample) GetCTS32() uint32 {
return uint32(b.CTS / time.Millisecond)
}
func (b *BaseSample) GetNalus() *util.ReuseArray[util.Memory] {
if b.Raw == nil {
b.Raw = &Nalus{}
}
return b.Raw.(*util.ReuseArray[util.Memory])
}
func (b *BaseSample) GetAudioData() *AudioData {
if b.Raw == nil {
b.Raw = &AudioData{}
}
return b.Raw.(*AudioData)
}
func (b *BaseSample) ParseAVCC(reader *util.MemoryReader, naluSizeLen int) error {
array := b.GetNalus()
for reader.Length > 0 {
l, err := reader.ReadBE(naluSizeLen)
if err != nil {
return err
}
reader.RangeN(int(l), array.GetNextPointer().PushOne)
}
return nil
}
func (frame *AVFrame) Reset() {
frame.Timestamp = 0
frame.IDR = false
frame.CTS = 0
frame.Raw = nil
if len(frame.Wraps) > 0 {
for _, wrap := range frame.Wraps {
wrap.Recycle()
}
frame.Wraps = frame.Wraps[:0]
frame.BaseSample.IDR = false
frame.BaseSample.TS0 = 0
frame.BaseSample.Timestamp = 0
frame.BaseSample.CTS = 0
if frame.Raw != nil {
frame.Raw.Reset()
}
}
}
@@ -89,11 +186,6 @@ func (frame *AVFrame) Discard() {
frame.Reset()
}
func (frame *AVFrame) Demux(codecCtx codec.ICodecCtx) (err error) {
frame.Raw, err = frame.Wraps[0].Demux(codecCtx)
return
}
func (df *DataFrame) StartWrite() (success bool) {
if df.discard {
return
@@ -110,31 +202,6 @@ func (df *DataFrame) Ready() {
df.Unlock()
}
func (nalus *Nalus) H264Type() codec.H264NALUType {
return codec.ParseH264NALUType((*nalus)[0].Buffers[0][0])
}
func (nalus *Nalus) H265Type() codec.H265NALUType {
return codec.ParseH265NALUType((*nalus)[0].Buffers[0][0])
}
func (nalus *Nalus) Append(bytes []byte) {
*nalus = append(*nalus, util.Memory{Buffers: net.Buffers{bytes}, Size: len(bytes)})
}
func (nalus *Nalus) ParseAVCC(reader *util.MemoryReader, naluSizeLen int) error {
for reader.Length > 0 {
l, err := reader.ReadBE(naluSizeLen)
if err != nil {
return err
}
var mem util.Memory
reader.RangeN(int(l), mem.AppendOne)
*nalus = append(*nalus, mem)
}
return nil
}
func (obus *OBUs) ParseAVCC(reader *util.MemoryReader) error {
var obuHeader av1.OBUHeader
startLen := reader.Length
@@ -159,7 +226,15 @@ func (obus *OBUs) ParseAVCC(reader *util.MemoryReader) error {
if err != nil {
return err
}
(*AudioData)(obus).AppendOne(obu)
(*AudioData)(obus).PushOne(obu)
}
return nil
}
func (obus *OBUs) Reset() {
((*util.Memory)(obus)).Reset()
}
func (obus *OBUs) Count() int {
return (*util.Memory)(obus).Count()
}

View File

@@ -2,6 +2,7 @@ package codec
import (
"fmt"
"github.com/deepch/vdk/codec/aacparser"
"github.com/deepch/vdk/codec/opusparser"
)
@@ -26,6 +27,32 @@ type (
}
)
func NewAACCtxFromRecord(record []byte) (ret *AACCtx, err error) {
ret = &AACCtx{}
ret.CodecData, err = aacparser.NewCodecDataFromMPEG4AudioConfigBytes(record)
return
}
func NewPCMACtx() *PCMACtx {
return &PCMACtx{
AudioCtx: AudioCtx{
SampleRate: 90000,
Channels: 1,
SampleSize: 16,
},
}
}
func NewPCMUCtx() *PCMUCtx {
return &PCMUCtx{
AudioCtx: AudioCtx{
SampleRate: 90000,
Channels: 1,
SampleSize: 16,
},
}
}
func (ctx *AudioCtx) GetRecord() []byte {
return []byte{}
}
@@ -58,6 +85,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 +111,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 +137,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"
)
@@ -111,6 +112,12 @@ type (
}
)
func NewH264CtxFromRecord(record []byte) (ret *H264Ctx, err error) {
ret = &H264Ctx{}
ret.CodecData, err = h264parser.NewCodecDataFromAVCDecoderConfRecord(record)
return
}
func (*H264Ctx) FourCC() FourCC {
return FourCC_H264
}
@@ -126,3 +133,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
@@ -21,6 +24,15 @@ type (
}
)
func NewH265CtxFromRecord(record []byte) (ret *H265Ctx, err error) {
ret = &H265Ctx{}
ret.CodecData, err = h265parser.NewCodecDataFromAVCDecoderConfRecord(record)
if err == nil {
ret.RecordInfo.LengthSizeMinusOne = 3
}
return
}
func (ctx *H265Ctx) GetInfo() string {
return fmt.Sprintf("fps: %d, resolution: %s", ctx.FPS(), ctx.Resolution())
}
@@ -36,3 +48,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)
}

25
pkg/codec/h26x.go Normal file
View File

@@ -0,0 +1,25 @@
package codec
type H26XCtx struct {
VPS, SPS, PPS []byte
}
func (ctx *H26XCtx) FourCC() (f FourCC) {
return
}
func (ctx *H26XCtx) GetInfo() string {
return ""
}
func (ctx *H26XCtx) GetBase() ICodecCtx {
return ctx
}
func (ctx *H26XCtx) GetRecord() []byte {
return nil
}
func (ctx *H26XCtx) String() string {
return ""
}

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"
)
@@ -35,6 +36,22 @@ type Config struct {
var (
durationType = reflect.TypeOf(time.Duration(0))
regexpType = reflect.TypeOf(Regexp{})
basicTypes = []reflect.Kind{
reflect.Bool,
reflect.Int,
reflect.Int8,
reflect.Int16,
reflect.Int32,
reflect.Int64,
reflect.Uint,
reflect.Uint8,
reflect.Uint16,
reflect.Uint32,
reflect.Uint64,
reflect.Float32,
reflect.Float64,
reflect.String,
}
)
func (config *Config) Range(f func(key string, value Config)) {
@@ -98,23 +115,29 @@ func (config *Config) Parse(s any, prefix ...string) {
if t.Kind() == reflect.Pointer {
t, v = t.Elem(), v.Elem()
}
isStruct := t.Kind() == reflect.Struct && t != regexpType
if isStruct {
defaults.SetDefaults(v.Addr().Interface())
}
config.Ptr = v
config.Default = v.Interface()
if !v.IsValid() {
fmt.Println("parse to ", prefix, config.name, s, "is not valid")
return
}
if l := len(prefix); l > 0 { // 读取环境变量
name := strings.ToLower(prefix[l-1])
if tag := config.tag.Get("default"); tag != "" {
_, isUnmarshaler := v.Addr().Interface().(yaml.Unmarshaler)
tag := config.tag.Get("default")
if tag != "" && isUnmarshaler {
v.Set(config.assign(name, tag))
config.Default = v.Interface()
}
if envValue := os.Getenv(strings.Join(prefix, "_")); envValue != "" {
v.Set(config.assign(name, envValue))
config.Env = v.Interface()
}
}
if t.Kind() == reflect.Struct && t != regexpType {
config.Default = v.Interface()
if isStruct {
for i, j := 0, t.NumField(); i < j; i++ {
ft, fv := t.Field(i), v.Field(i)
@@ -201,6 +224,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 {
@@ -305,16 +331,18 @@ func (config *Config) GetMap() map[string]any {
var regexPureNumber = regexp.MustCompile(`^\d+$`)
func (config *Config) assign(k string, v any) (target reflect.Value) {
ft := config.Ptr.Type()
func unmarshal(ft reflect.Type, v any) (target reflect.Value) {
source := reflect.ValueOf(v)
for _, t := range basicTypes {
if source.Kind() == t && ft.Kind() == t {
return source
}
}
switch ft {
case durationType:
target = reflect.New(ft).Elem()
if source.Type() == durationType {
target.Set(source)
return source
} else if source.IsZero() || !source.IsValid() {
target.SetInt(0)
} else {
@@ -322,7 +350,7 @@ func (config *Config) assign(k string, v any) (target reflect.Value) {
if d, err := time.ParseDuration(timeStr); err == nil && !regexPureNumber.MatchString(timeStr) {
target.SetInt(int64(d))
} else {
slog.Error("invalid duration value please add unit (s,m,h,d)eg: 100ms, 10s, 4m, 1h", "key", k, "value", source)
slog.Error("invalid duration value please add unit (s,m,h,d)eg: 100ms, 10s, 4m, 1h", "value", timeStr)
os.Exit(1)
}
}
@@ -331,58 +359,69 @@ func (config *Config) assign(k string, v any) (target reflect.Value) {
regexpStr := source.String()
target.Set(reflect.ValueOf(Regexp{regexp.MustCompile(regexpStr)}))
default:
if ft.Kind() == reflect.Map {
target = reflect.MakeMap(ft)
if v != nil {
tmpStruct := reflect.StructOf([]reflect.StructField{
{
Name: "Key",
Type: ft.Key(),
},
})
tmpValue := reflect.New(tmpStruct)
for k, v := range v.(map[string]any) {
_ = yaml.Unmarshal([]byte(fmt.Sprintf("key: %s", k)), tmpValue.Interface())
var value reflect.Value
if ft.Elem().Kind() == reflect.Struct {
value = reflect.New(ft.Elem())
defaults.SetDefaults(value.Interface())
if reflect.TypeOf(v).Kind() != reflect.Map {
value.Elem().Field(0).Set(reflect.ValueOf(v))
} else {
out, _ := yaml.Marshal(v)
_ = yaml.Unmarshal(out, value.Interface())
}
value = value.Elem()
} else {
value = reflect.ValueOf(v)
switch ft.Kind() {
case reflect.Struct:
newStruct := reflect.New(ft)
defaults.SetDefaults(newStruct.Interface())
if value, ok := v.(map[string]any); ok {
for i := 0; i < ft.NumField(); i++ {
key := strings.ToLower(ft.Field(i).Name)
if vv, ok := value[key]; ok {
newStruct.Elem().Field(i).Set(unmarshal(ft.Field(i).Type, vv))
}
target.SetMapIndex(tmpValue.Elem().Field(0), value)
}
} else {
newStruct.Elem().Field(0).Set(unmarshal(ft.Field(0).Type, v))
}
return newStruct.Elem()
case reflect.Map:
if v != nil {
target = reflect.MakeMap(ft)
for k, v := range v.(map[string]any) {
target.SetMapIndex(unmarshal(ft.Key(), k), unmarshal(ft.Elem(), v))
}
}
} else {
tmpStruct := reflect.StructOf([]reflect.StructField{
{
Name: strings.ToUpper(k),
Type: ft,
},
})
tmpValue := reflect.New(tmpStruct)
case reflect.Slice:
if v != nil {
s := v.([]any)
target = reflect.MakeSlice(ft, len(s), len(s))
for i, v := range s {
target.Index(i).Set(unmarshal(ft.Elem(), v))
}
}
default:
if v != nil {
var out []byte
var err error
if vv, ok := v.(string); ok {
out = []byte(fmt.Sprintf("%s: %s", k, vv))
out = []byte(fmt.Sprintf("%s: %s", "value", vv))
} else {
out, _ = yaml.Marshal(map[string]any{k: v})
out, err = yaml.Marshal(map[string]any{"value": v})
if err != nil {
panic(err)
}
}
_ = yaml.Unmarshal(out, tmpValue.Interface())
tmpValue := reflect.New(reflect.StructOf([]reflect.StructField{
{
Name: "Value",
Type: ft,
},
}))
err = yaml.Unmarshal(out, tmpValue.Interface())
if err != nil {
panic(err)
}
return tmpValue.Elem().Field(0)
}
target = tmpValue.Elem().Field(0)
}
}
return
}
func (config *Config) assign(k string, v any) reflect.Value {
return unmarshal(config.Ptr.Type(), v)
}
func Parse(target any, conf map[string]any) {
var c Config
c.Parse(target)

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

@@ -49,6 +49,7 @@ func (task *ListenQuicWork) Start() (err error) {
task.Error("listen quic error", err)
return
}
task.OnStop(task.Listener.Close)
task.Info("listen quic on", task.ListenAddr)
return
}
@@ -63,7 +64,3 @@ func (task *ListenQuicWork) Go() error {
task.AddTask(subTask)
}
}
func (task *ListenQuicWork) Dispose() {
_ = task.Listener.Close()
}

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,51 @@ 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"
RecordModeTest RecordMode = "test"
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 +71,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 `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 +89,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:"拉流地址"`
MaxRetry int `default:"-1" desc:"断开后自动重试次数,0:不重试,-1:无限重试"` // 断开后自动重拉,0 表示不自动重拉,-1 表示无限重拉高于0 的数代表最大重拉次数
RetryInterval time.Duration `default:"5s" desc:"重试间隔"` // 重试间隔
Proxy string `desc:"代理地址"` // 代理地址
Loop int `desc:"拉流循环次数,-1:无限循环"` // 拉流循环次数,-1 表示无限循环
MaxRetry int `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,10 +112,22 @@ 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 {
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:"分片时长"` // 分片时长
RealTime bool `desc:"是否实时录制"` // 是否实时录制
Append bool `desc:"是否追加录制"` // 是否追加录制
Event *RecordEvent `json:"event" desc:"事件录像配置" gorm:"-"` // 事件录像配置
}
TransfromOutput struct {
Target string `desc:"转码目标"` // 转码目标
@@ -98,13 +148,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
@@ -163,3 +214,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

@@ -4,20 +4,24 @@ import "errors"
var (
ErrNotFound = errors.New("not found")
ErrDisposed = errors.New("disposed")
ErrDisabled = errors.New("disabled")
ErrStreamExist = errors.New("stream exist")
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")
ErrUnsupportCodec = errors.New("unsupport codec")
ErrMuted = errors.New("muted")
ErrSkip = errors.New("skip")
ErrNoTrack = errors.New("no track")
ErrNoDB = errors.New("no db")
ErrLost = errors.New("lost")

82
pkg/format/adts.go Normal file
View File

@@ -0,0 +1,82 @@
package format
import (
"bytes"
"fmt"
"github.com/deepch/vdk/codec/aacparser"
"m7s.live/v5/pkg"
"m7s.live/v5/pkg/codec"
)
var _ pkg.IAVFrame = (*Mpeg2Audio)(nil)
type Mpeg2Audio struct {
pkg.Sample
}
func (A *Mpeg2Audio) CheckCodecChange() (err error) {
old := A.ICodecCtx
if old == nil || old.FourCC().Is(codec.FourCC_MP4A) {
var reader = A.NewReader()
var adts []byte
adts, err = reader.ReadBytes(7)
if err != nil {
return
}
var hdrlen, framelen, samples int
var conf aacparser.MPEG4AudioConfig
conf, hdrlen, framelen, samples, err = aacparser.ParseADTSHeader(adts)
if err != nil {
return
}
b := &bytes.Buffer{}
aacparser.WriteMPEG4AudioConfig(b, conf)
if old == nil || !bytes.Equal(b.Bytes(), old.GetRecord()) {
var ctx = &codec.AACCtx{}
ctx.ConfigBytes = b.Bytes()
A.ICodecCtx = ctx
if false {
println("ADTS", "hdrlen", hdrlen, "framelen", framelen, "samples", samples, "config", ctx.Config)
}
// track.Info("ADTS", "hdrlen", hdrlen, "framelen", framelen, "samples", samples)
} else {
}
}
return
}
func (A *Mpeg2Audio) Demux() (err error) {
var reader = A.NewReader()
mem := A.GetAudioData()
if A.ICodecCtx.FourCC().Is(codec.FourCC_MP4A) {
err = reader.Skip(7)
if err != nil {
return
}
}
reader.Range(mem.PushOne)
return
}
func (A *Mpeg2Audio) Mux(frame *pkg.Sample) (err error) {
if A.ICodecCtx == nil {
A.ICodecCtx = frame.GetBase()
}
raw := frame.Raw.(*pkg.AudioData)
aacCtx, ok := A.ICodecCtx.(*codec.AACCtx)
if ok {
A.InitRecycleIndexes(1)
adts := A.NextN(7)
aacparser.FillADTSHeader(adts, aacCtx.Config, raw.Size/aacCtx.GetSampleSize(), raw.Size)
} else {
A.InitRecycleIndexes(0)
}
A.Push(raw.Buffers...)
return
}
func (A *Mpeg2Audio) String() string {
return fmt.Sprintf("ADTS{size:%d}", A.Size)
}

290
pkg/format/annexb.go Normal file
View File

@@ -0,0 +1,290 @@
package format
import (
"bytes"
"fmt"
"io"
"slices"
"github.com/deepch/vdk/codec/h264parser"
"github.com/deepch/vdk/codec/h265parser"
"m7s.live/v5/pkg"
"m7s.live/v5/pkg/codec"
"m7s.live/v5/pkg/util"
)
type AnnexB struct {
pkg.Sample
}
func (a *AnnexB) CheckCodecChange() (err error) {
if !a.HasRaw() || a.ICodecCtx == nil {
err = a.Demux()
if err != nil {
return
}
}
if a.ICodecCtx == nil {
return pkg.ErrSkip
}
var vps, sps, pps []byte
a.IDR = false
for nalu := range a.Raw.(*pkg.Nalus).RangePoint {
if a.FourCC() == codec.FourCC_H265 {
switch codec.ParseH265NALUType(nalu.Buffers[0][0]) {
case h265parser.NAL_UNIT_VPS:
vps = nalu.ToBytes()
case h265parser.NAL_UNIT_SPS:
sps = nalu.ToBytes()
case h265parser.NAL_UNIT_PPS:
pps = nalu.ToBytes()
case h265parser.NAL_UNIT_CODED_SLICE_BLA_W_LP,
h265parser.NAL_UNIT_CODED_SLICE_BLA_W_RADL,
h265parser.NAL_UNIT_CODED_SLICE_BLA_N_LP,
h265parser.NAL_UNIT_CODED_SLICE_IDR_W_RADL,
h265parser.NAL_UNIT_CODED_SLICE_IDR_N_LP,
h265parser.NAL_UNIT_CODED_SLICE_CRA:
a.IDR = true
}
} else {
switch codec.ParseH264NALUType(nalu.Buffers[0][0]) {
case codec.NALU_SPS:
sps = nalu.ToBytes()
case codec.NALU_PPS:
pps = nalu.ToBytes()
case codec.NALU_IDR_Picture:
a.IDR = true
}
}
}
if a.FourCC() == codec.FourCC_H265 {
if vps != nil && sps != nil && pps != nil {
var codecData h265parser.CodecData
codecData, err = h265parser.NewCodecDataFromVPSAndSPSAndPPS(vps, sps, pps)
if err != nil {
return
}
if !bytes.Equal(codecData.Record, a.ICodecCtx.(*codec.H265Ctx).Record) {
a.ICodecCtx = &codec.H265Ctx{
CodecData: codecData,
}
}
}
if a.ICodecCtx.(*codec.H265Ctx).Record == nil {
err = pkg.ErrSkip
}
} else {
if sps != nil && pps != nil {
var codecData h264parser.CodecData
codecData, err = h264parser.NewCodecDataFromSPSAndPPS(sps, pps)
if err != nil {
return
}
if !bytes.Equal(codecData.Record, a.ICodecCtx.(*codec.H264Ctx).Record) {
a.ICodecCtx = &codec.H264Ctx{
CodecData: codecData,
}
}
}
if a.ICodecCtx.(*codec.H264Ctx).Record == nil {
err = pkg.ErrSkip
}
}
return
}
// String implements pkg.IAVFrame.
func (a *AnnexB) String() string {
return fmt.Sprintf("%d %d", a.Timestamp, a.Memory.Size)
}
// Demux implements pkg.IAVFrame.
func (a *AnnexB) Demux() (err error) {
nalus := a.GetNalus()
var lastFourBytes [4]byte
var b byte
var shallow util.Memory
shallow.Push(a.Buffers...)
reader := shallow.NewReader()
gotNalu := func() {
nalu := nalus.GetNextPointer()
for buf := range reader.ClipFront {
nalu.PushOne(buf)
}
if a.ICodecCtx == nil {
naluType := codec.ParseH264NALUType(nalu.Buffers[0][0])
switch naluType {
case codec.NALU_Non_IDR_Picture,
codec.NALU_IDR_Picture,
codec.NALU_SEI,
codec.NALU_SPS,
codec.NALU_PPS,
codec.NALU_Access_Unit_Delimiter:
a.ICodecCtx = &codec.H264Ctx{}
}
}
}
for {
b, err = reader.ReadByte()
if err == nil {
copy(lastFourBytes[:], lastFourBytes[1:])
lastFourBytes[3] = b
var startCode = 0
if lastFourBytes == codec.NALU_Delimiter2 {
startCode = 4
} else if [3]byte(lastFourBytes[1:]) == codec.NALU_Delimiter1 {
startCode = 3
}
if startCode > 0 && reader.Offset() >= 3 {
if reader.Offset() == 3 {
startCode = 3
}
reader.Unread(startCode)
if reader.Offset() > 0 {
gotNalu()
}
reader.Skip(startCode)
for range reader.ClipFront {
}
}
} else if err == io.EOF {
if reader.Offset() > 0 {
gotNalu()
}
err = nil
break
}
}
return
}
func (a *AnnexB) Mux(fromBase *pkg.Sample) (err error) {
if a.ICodecCtx == nil {
a.ICodecCtx = fromBase.GetBase()
}
a.InitRecycleIndexes(0)
delimiter2 := codec.NALU_Delimiter2[:]
a.PushOne(delimiter2)
if fromBase.IDR {
switch ctx := fromBase.GetBase().(type) {
case *codec.H264Ctx:
a.Push(ctx.SPS(), delimiter2, ctx.PPS(), delimiter2)
case *codec.H265Ctx:
a.Push(ctx.SPS(), delimiter2, ctx.PPS(), delimiter2, ctx.VPS(), delimiter2)
}
}
for i, nalu := range *fromBase.Raw.(*pkg.Nalus) {
if i > 0 {
a.PushOne(codec.NALU_Delimiter1[:])
}
a.Push(nalu.Buffers...)
}
return
}
func (a *AnnexB) Parse(reader *pkg.AnnexBReader) (hasFrame bool, err error) {
nalus := a.BaseSample.GetNalus()
for !hasFrame {
nalu := nalus.GetNextPointer()
reader.ReadNALU(&a.Memory, nalu)
if nalu.Size == 0 {
nalus.Reduce()
return
}
tryH264Type := codec.ParseH264NALUType(nalu.Buffers[0][0])
h265Type := codec.ParseH265NALUType(nalu.Buffers[0][0])
if a.ICodecCtx == nil {
a.ICodecCtx = &codec.H26XCtx{}
}
switch ctx := a.ICodecCtx.(type) {
case *codec.H26XCtx:
if tryH264Type == codec.NALU_SPS {
ctx.SPS = nalu.ToBytes()
nalus.Reduce()
a.Recycle()
} else if tryH264Type == codec.NALU_PPS {
ctx.PPS = nalu.ToBytes()
nalus.Reduce()
a.Recycle()
} else if h265Type == h265parser.NAL_UNIT_VPS {
ctx.VPS = nalu.ToBytes()
nalus.Reduce()
a.Recycle()
} else if h265Type == h265parser.NAL_UNIT_SPS {
ctx.SPS = nalu.ToBytes()
nalus.Reduce()
a.Recycle()
} else if h265Type == h265parser.NAL_UNIT_PPS {
ctx.PPS = nalu.ToBytes()
nalus.Reduce()
a.Recycle()
} else {
if ctx.SPS != nil && ctx.PPS != nil && tryH264Type == codec.NALU_IDR_Picture {
var codecData h264parser.CodecData
codecData, err = h264parser.NewCodecDataFromSPSAndPPS(ctx.SPS, ctx.PPS)
if err != nil {
return
}
a.ICodecCtx = &codec.H264Ctx{
CodecData: codecData,
}
*nalus = slices.Insert(*nalus, 0, util.NewMemory(ctx.SPS), util.NewMemory(ctx.PPS))
delimiter2 := codec.NALU_Delimiter2[:]
a.Buffers = slices.Insert(a.Buffers, 0, delimiter2, ctx.SPS, delimiter2, ctx.PPS)
a.Size += 8 + len(ctx.SPS) + len(ctx.PPS)
} else if ctx.VPS != nil && ctx.SPS != nil && ctx.PPS != nil && h265Type == h265parser.NAL_UNIT_CODED_SLICE_IDR_W_RADL {
var codecData h265parser.CodecData
codecData, err = h265parser.NewCodecDataFromVPSAndSPSAndPPS(ctx.VPS, ctx.SPS, ctx.PPS)
if err != nil {
return
}
a.ICodecCtx = &codec.H265Ctx{
CodecData: codecData,
}
*nalus = slices.Insert(*nalus, 0, util.NewMemory(ctx.VPS), util.NewMemory(ctx.SPS), util.NewMemory(ctx.PPS))
delimiter2 := codec.NALU_Delimiter2[:]
a.Buffers = slices.Insert(a.Buffers, 0, delimiter2, ctx.VPS, delimiter2, ctx.SPS, delimiter2, ctx.PPS)
a.Size += 24 + len(ctx.VPS) + len(ctx.SPS) + len(ctx.PPS)
} else {
nalus.Reduce()
a.Recycle()
}
}
case *codec.H264Ctx:
switch tryH264Type {
case codec.NALU_IDR_Picture:
a.IDR = true
hasFrame = true
case codec.NALU_Non_IDR_Picture:
a.IDR = false
hasFrame = true
}
case *codec.H265Ctx:
switch h265Type {
case h265parser.NAL_UNIT_CODED_SLICE_BLA_W_LP,
h265parser.NAL_UNIT_CODED_SLICE_BLA_W_RADL,
h265parser.NAL_UNIT_CODED_SLICE_BLA_N_LP,
h265parser.NAL_UNIT_CODED_SLICE_IDR_W_RADL,
h265parser.NAL_UNIT_CODED_SLICE_IDR_N_LP,
h265parser.NAL_UNIT_CODED_SLICE_CRA:
a.IDR = true
hasFrame = true
case h265parser.NAL_UNIT_CODED_SLICE_TRAIL_N,
h265parser.NAL_UNIT_CODED_SLICE_TRAIL_R,
h265parser.NAL_UNIT_CODED_SLICE_TSA_N,
h265parser.NAL_UNIT_CODED_SLICE_TSA_R,
h265parser.NAL_UNIT_CODED_SLICE_STSA_N,
h265parser.NAL_UNIT_CODED_SLICE_STSA_R,
h265parser.NAL_UNIT_CODED_SLICE_RADL_N,
h265parser.NAL_UNIT_CODED_SLICE_RADL_R,
h265parser.NAL_UNIT_CODED_SLICE_RASL_N,
h265parser.NAL_UNIT_CODED_SLICE_RASL_R:
a.IDR = false
hasFrame = true
}
}
}
return
}

309
pkg/format/ps/mpegps.go Normal file
View File

@@ -0,0 +1,309 @@
package mpegps
import (
"errors"
"fmt"
"io"
"time"
"m7s.live/v5"
"m7s.live/v5/pkg"
"m7s.live/v5/pkg/codec"
"m7s.live/v5/pkg/format"
"m7s.live/v5/pkg/util"
mpegts "m7s.live/v5/pkg/format/ts"
)
const (
StartCodePS = 0x000001ba
StartCodeSYS = 0x000001bb
StartCodeMAP = 0x000001bc
StartCodePadding = 0x000001be
StartCodeVideo = 0x000001e0
StartCodeVideo1 = 0x000001e1
StartCodeVideo2 = 0x000001e2
StartCodeAudio = 0x000001c0
PrivateStreamCode = 0x000001bd
MEPGProgramEndCode = 0x000001b9
)
// PS包头常量
const (
PSPackHeaderSize = 14 // PS pack header basic size
PSSystemHeaderSize = 18 // PS system header basic size
PSMHeaderSize = 12 // PS map header basic size
PESHeaderMinSize = 9 // PES header minimum size
MaxPESPayloadSize = 0xFFEB // 0xFFFF - 14 (to leave room for headers)
)
type MpegPsDemuxer struct {
stAudio, stVideo byte
Publisher *m7s.Publisher
Allocator *util.ScalableMemoryAllocator
writer m7s.PublishWriter[*format.Mpeg2Audio, *format.AnnexB]
}
func (s *MpegPsDemuxer) Feed(reader *util.BufReader) (err error) {
writer := &s.writer
var payload util.Memory
var pesHeader mpegts.MpegPESHeader
var lastVideoPts, lastAudioPts uint64
var annexbReader pkg.AnnexBReader
for {
code, err := reader.ReadBE32(4)
if err != nil {
return err
}
switch code {
case StartCodePS:
var psl byte
if err = reader.Skip(9); err != nil {
return err
}
psl, err = reader.ReadByte()
if err != nil {
return err
}
psl &= 0x07
if err = reader.Skip(int(psl)); err != nil {
return err
}
case StartCodeVideo:
payload, err = s.ReadPayload(reader)
if err != nil {
return err
}
if !s.Publisher.PubVideo {
continue
}
if writer.PublishVideoWriter == nil {
writer.PublishVideoWriter = m7s.NewPublishVideoWriter[*format.AnnexB](s.Publisher, s.Allocator)
switch s.stVideo {
case mpegts.STREAM_TYPE_H264:
writer.VideoFrame.ICodecCtx = &codec.H264Ctx{}
case mpegts.STREAM_TYPE_H265:
writer.VideoFrame.ICodecCtx = &codec.H265Ctx{}
}
}
pes := writer.VideoFrame
reader := payload.NewReader()
pesHeader, err = mpegts.ReadPESHeader(&io.LimitedReader{R: &reader, N: int64(payload.Size)})
if err != nil {
return errors.Join(err, fmt.Errorf("failed to read PES header"))
}
if pesHeader.Pts != 0 && pesHeader.Pts != lastVideoPts {
if pes.Size > 0 {
err = writer.NextVideo()
if err != nil {
return errors.Join(err, fmt.Errorf("failed to get next video frame"))
}
pes = writer.VideoFrame
}
pes.SetDTS(time.Duration(pesHeader.Dts))
pes.SetPTS(time.Duration(pesHeader.Pts))
lastVideoPts = pesHeader.Pts
}
annexb := s.Allocator.Malloc(reader.Length)
reader.Read(annexb)
annexbReader.AppendBuffer(annexb)
_, err = pes.Parse(&annexbReader)
if err != nil {
return errors.Join(err, fmt.Errorf("failed to parse annexb"))
}
case StartCodeAudio:
payload, err = s.ReadPayload(reader)
if err != nil {
return errors.Join(err, fmt.Errorf("failed to read audio payload"))
}
if s.stAudio == 0 || !s.Publisher.PubAudio {
continue
}
if writer.PublishAudioWriter == nil {
writer.PublishAudioWriter = m7s.NewPublishAudioWriter[*format.Mpeg2Audio](s.Publisher, s.Allocator)
switch s.stAudio {
case mpegts.STREAM_TYPE_AAC:
writer.AudioFrame.ICodecCtx = &codec.AACCtx{}
case mpegts.STREAM_TYPE_G711A:
writer.AudioFrame.ICodecCtx = codec.NewPCMACtx()
case mpegts.STREAM_TYPE_G711U:
writer.AudioFrame.ICodecCtx = codec.NewPCMUCtx()
}
}
pes := writer.AudioFrame
reader := payload.NewReader()
pesHeader, err = mpegts.ReadPESHeader(&io.LimitedReader{R: &reader, N: int64(payload.Size)})
if err != nil {
return errors.Join(err, fmt.Errorf("failed to read PES header"))
}
if pesHeader.Pts != 0 && pesHeader.Pts != lastAudioPts {
if pes.Size > 0 {
err = writer.NextAudio()
if err != nil {
return errors.Join(err, fmt.Errorf("failed to get next audio frame"))
}
pes = writer.AudioFrame
}
pes.SetDTS(time.Duration(pesHeader.Pts))
pes.SetPTS(time.Duration(pesHeader.Pts))
lastAudioPts = pesHeader.Pts
}
reader.Range(func(buf []byte) {
copy(pes.NextN(len(buf)), buf)
})
// reader.Range(pes.PushOne)
case StartCodeMAP:
var psm util.Memory
psm, err = s.ReadPayload(reader)
if err != nil {
return errors.Join(err, fmt.Errorf("failed to read program stream map"))
}
err = s.decProgramStreamMap(psm)
if err != nil {
return errors.Join(err, fmt.Errorf("failed to decode program stream map"))
}
default:
payloadlen, err := reader.ReadBE(2)
if err != nil {
return errors.Join(err, fmt.Errorf("failed to read payload length"))
}
reader.Skip(payloadlen)
}
}
}
func (s *MpegPsDemuxer) ReadPayload(reader *util.BufReader) (payload util.Memory, err error) {
payloadlen, err := reader.ReadBE(2)
if err != nil {
return
}
return reader.ReadBytes(payloadlen)
}
func (s *MpegPsDemuxer) decProgramStreamMap(psm util.Memory) (err error) {
var programStreamInfoLen, programStreamMapLen, elementaryStreamInfoLength uint32
var streamType, elementaryStreamID byte
reader := psm.NewReader()
reader.Skip(2)
programStreamInfoLen, err = reader.ReadBE(2)
reader.Skip(int(programStreamInfoLen))
programStreamMapLen, err = reader.ReadBE(2)
for programStreamMapLen > 0 {
streamType, err = reader.ReadByte()
elementaryStreamID, err = reader.ReadByte()
if elementaryStreamID >= 0xe0 && elementaryStreamID <= 0xef {
s.stVideo = streamType
} else if elementaryStreamID >= 0xc0 && elementaryStreamID <= 0xdf {
s.stAudio = streamType
}
elementaryStreamInfoLength, err = reader.ReadBE(2)
reader.Skip(int(elementaryStreamInfoLength))
programStreamMapLen -= 4 + elementaryStreamInfoLength
}
return nil
}
type MpegPSMuxer struct {
*m7s.Subscriber
Packet *util.RecyclableMemory
}
func (muxer *MpegPSMuxer) Mux(onPacket func() error) {
var pesAudio, pesVideo *MpegpsPESFrame
puber := muxer.Publisher
var elementary_stream_map_length uint16
if puber.HasAudioTrack() {
elementary_stream_map_length += 4
pesAudio = &MpegpsPESFrame{}
pesAudio.StreamID = mpegts.STREAM_ID_AUDIO
switch puber.AudioTrack.ICodecCtx.FourCC() {
case codec.FourCC_ALAW:
pesAudio.StreamType = mpegts.STREAM_TYPE_G711A
case codec.FourCC_ULAW:
pesAudio.StreamType = mpegts.STREAM_TYPE_G711U
case codec.FourCC_MP4A:
pesAudio.StreamType = mpegts.STREAM_TYPE_AAC
}
}
if puber.HasVideoTrack() {
elementary_stream_map_length += 4
pesVideo = &MpegpsPESFrame{}
pesVideo.StreamID = mpegts.STREAM_ID_VIDEO
switch puber.VideoTrack.ICodecCtx.FourCC() {
case codec.FourCC_H264:
pesVideo.StreamType = mpegts.STREAM_TYPE_H264
case codec.FourCC_H265:
pesVideo.StreamType = mpegts.STREAM_TYPE_H265
}
}
var outputBuffer util.Buffer = muxer.Packet.NextN(PSPackHeaderSize + PSMHeaderSize + int(elementary_stream_map_length))
outputBuffer.Reset()
MuxPSHeader(&outputBuffer)
// System Header - 定义流的缓冲区信息
// outputBuffer.WriteUint32(StartCodeSYS)
// outputBuffer.WriteByte(0x00) // header_length high
// outputBuffer.WriteByte(0x0C) // header_length low (12 bytes)
// outputBuffer.WriteByte(0x80) // marker + rate_bound[21..15]
// outputBuffer.WriteByte(0x62) // rate_bound[14..8]
// outputBuffer.WriteByte(0x4E) // rate_bound[7..1] + marker
// outputBuffer.WriteByte(0x01) // audio_bound + fixed_flag + CSPS_flag + system_audio_lock_flag + system_video_lock_flag + marker
// outputBuffer.WriteByte(0x01) // video_bound + packet_rate_restriction_flag + reserved
// outputBuffer.WriteByte(frame.StreamId) // stream_id
// outputBuffer.WriteByte(0xC0) // '11' + P-STD_buffer_bound_scale
// outputBuffer.WriteByte(0x20) // P-STD_buffer_size_bound low
// outputBuffer.WriteByte(0x00) // P-STD_buffer_size_bound high
// outputBuffer.WriteByte(0x00)
// outputBuffer.WriteByte(0x00)
// outputBuffer.WriteByte(0x00)
// PSM Header - 程序流映射,定义流类型
outputBuffer.WriteUint32(StartCodeMAP)
outputBuffer.WriteUint16(uint16(PSMHeaderSize) + elementary_stream_map_length - 6) // psm_length
outputBuffer.WriteByte(0xE0) // current_next_indicator + reserved + psm_version
outputBuffer.WriteByte(0xFF) // reserved + marker
outputBuffer.WriteUint16(0) // program_stream_info_length
outputBuffer.WriteUint16(elementary_stream_map_length)
if pesAudio != nil {
outputBuffer.WriteByte(pesAudio.StreamType) // stream_type
outputBuffer.WriteByte(pesAudio.StreamID) // elementary_stream_id
outputBuffer.WriteUint16(0) // elementary_stream_info_length
}
if pesVideo != nil {
outputBuffer.WriteByte(pesVideo.StreamType) // stream_type
outputBuffer.WriteByte(pesVideo.StreamID) // elementary_stream_id
outputBuffer.WriteUint16(0) // elementary_stream_info_length
}
onPacket()
m7s.PlayBlock(muxer.Subscriber, func(audio *format.Mpeg2Audio) error {
pesAudio.Pts = uint64(audio.GetPTS())
pesAudio.WritePESPacket(audio.Memory, muxer.Packet)
return onPacket()
}, func(video *format.AnnexB) error {
pesVideo.Pts = uint64(video.GetPTS())
pesVideo.Dts = uint64(video.GetDTS())
pesVideo.WritePESPacket(video.Memory, muxer.Packet)
return onPacket()
})
}
func MuxPSHeader(outputBuffer *util.Buffer) {
// 写入PS Pack Header - 参考MPEG-2程序流标准
// Pack start code: 0x000001BA
outputBuffer.WriteUint32(StartCodePS)
// SCR字段 (System Clock Reference) - 参考ps-muxer.go的实现
// 系统时钟参考
scr := uint64(time.Now().UnixMilli()) * 90
outputBuffer.WriteByte(0x44 | byte((scr>>30)&0x07)) // '01' + SCR[32..30]
outputBuffer.WriteByte(byte((scr >> 22) & 0xFF)) // SCR[29..22]
outputBuffer.WriteByte(0x04 | byte((scr>>20)&0x03)) // marker + SCR[21..20]
outputBuffer.WriteByte(byte((scr >> 12) & 0xFF)) // SCR[19..12]
outputBuffer.WriteByte(0x04 | byte((scr>>10)&0x03)) // marker + SCR[11..10]
outputBuffer.WriteByte(byte((scr >> 2) & 0xFF)) // SCR[9..2]
outputBuffer.WriteByte(0x04 | byte(scr&0x03)) // marker + SCR[1..0]
outputBuffer.WriteByte(0x01) // SCR_ext + marker
outputBuffer.WriteByte(0x89) // program_mux_rate high
outputBuffer.WriteByte(0xC8) // program_mux_rate low + markers + reserved + stuffing_length(0)
}

View File

@@ -0,0 +1,853 @@
package mpegps
import (
"bytes"
"io"
"testing"
"m7s.live/v5/pkg/util"
)
func min(a, b int) int {
if a < b {
return a
}
return b
}
func TestMpegPSConstants(t *testing.T) {
// Test that PS constants are properly defined
t.Run("Constants", func(t *testing.T) {
if StartCodePS != 0x000001ba {
t.Errorf("Expected StartCodePS %x, got %x", 0x000001ba, StartCodePS)
}
if PSPackHeaderSize != 14 {
t.Errorf("Expected PSPackHeaderSize %d, got %d", 14, PSPackHeaderSize)
}
if MaxPESPayloadSize != 0xFFEB {
t.Errorf("Expected MaxPESPayloadSize %x, got %x", 0xFFEB, MaxPESPayloadSize)
}
})
}
func TestMuxPSHeader(t *testing.T) {
// Test PS header generation
t.Run("PSHeader", func(t *testing.T) {
// Create a buffer for testing - initialize with length 0 to allow appending
buffer := make([]byte, 0, PSPackHeaderSize)
utilBuffer := util.Buffer(buffer)
// Call MuxPSHeader
MuxPSHeader(&utilBuffer)
// Check the buffer length
if len(utilBuffer) != PSPackHeaderSize {
t.Errorf("Expected buffer length %d, got %d", PSPackHeaderSize, len(utilBuffer))
}
// Check PS start code (first 4 bytes should be 0x00 0x00 0x01 0xBA)
expectedStartCode := []byte{0x00, 0x00, 0x01, 0xBA}
if !bytes.Equal(utilBuffer[:4], expectedStartCode) {
t.Errorf("Expected PS start code %x, got %x", expectedStartCode, utilBuffer[:4])
}
t.Logf("PS Header: %x", utilBuffer)
t.Logf("Buffer length: %d", len(utilBuffer))
})
}
func TestMpegpsPESFrame(t *testing.T) {
// Test MpegpsPESFrame basic functionality
t.Run("PESFrame", func(t *testing.T) {
// Create PES frame
pesFrame := &MpegpsPESFrame{
StreamType: 0x1B, // H.264
}
pesFrame.Pts = 90000 // 1 second in 90kHz clock
pesFrame.Dts = 90000
// Test basic properties
if pesFrame.StreamType != 0x1B {
t.Errorf("Expected stream type 0x1B, got %x", pesFrame.StreamType)
}
if pesFrame.Pts != 90000 {
t.Errorf("Expected PTS %d, got %d", 90000, pesFrame.Pts)
}
if pesFrame.Dts != 90000 {
t.Errorf("Expected DTS %d, got %d", 90000, pesFrame.Dts)
}
t.Logf("PES Frame: StreamType=%x, PTS=%d, DTS=%d", pesFrame.StreamType, pesFrame.Pts, pesFrame.Dts)
})
}
func TestReadPayload(t *testing.T) {
// Test ReadPayload functionality
t.Run("ReadPayload", func(t *testing.T) {
// Create test data with payload length and payload
testData := []byte{
0x00, 0x05, // Payload length = 5 bytes
0x01, 0x02, 0x03, 0x04, 0x05, // Payload data
}
demuxer := &MpegPsDemuxer{}
reader := util.NewBufReader(bytes.NewReader(testData))
payload, err := demuxer.ReadPayload(reader)
if err != nil {
t.Fatalf("ReadPayload failed: %v", err)
}
if payload.Size != 5 {
t.Errorf("Expected payload size 5, got %d", payload.Size)
}
expectedPayload := []byte{0x01, 0x02, 0x03, 0x04, 0x05}
if !bytes.Equal(payload.ToBytes(), expectedPayload) {
t.Errorf("Expected payload %x, got %x", expectedPayload, payload.ToBytes())
}
t.Logf("ReadPayload successful: %x", payload.ToBytes())
})
}
func TestMpegPSMuxerBasic(t *testing.T) {
// Test MpegPSMuxer basic functionality
t.Run("MuxBasic", func(t *testing.T) {
// Test basic PS header generation without PlayBlock
// This focuses on testing the header generation logic
var outputBuffer util.Buffer = make([]byte, 0, 1024)
outputBuffer.Reset()
// Test PS header generation
MuxPSHeader(&outputBuffer)
// Add stuffing bytes as expected by the demuxer
// The demuxer expects: 9 bytes + 1 stuffing length byte + stuffing bytes
stuffingLength := byte(0x00) // No stuffing bytes
outputBuffer.WriteByte(stuffingLength)
// Verify PS header contains expected start code
if len(outputBuffer) != PSPackHeaderSize+1 {
t.Errorf("Expected PS header size %d, got %d", PSPackHeaderSize+1, len(outputBuffer))
}
// Check for PS start code
if !bytes.Contains(outputBuffer, []byte{0x00, 0x00, 0x01, 0xBA}) {
t.Error("PS header does not contain PS start code")
}
t.Logf("PS Header: %x", outputBuffer)
t.Logf("PS Header size: %d bytes", len(outputBuffer))
// Test PSM header generation
var pesAudio, pesVideo *MpegpsPESFrame
var elementary_stream_map_length uint16
// Simulate audio stream
hasAudio := true
if hasAudio {
elementary_stream_map_length += 4
pesAudio = &MpegpsPESFrame{}
pesAudio.StreamID = 0xC0 // MPEG audio
pesAudio.StreamType = 0x0F // AAC
}
// Simulate video stream
hasVideo := true
if hasVideo {
elementary_stream_map_length += 4
pesVideo = &MpegpsPESFrame{}
pesVideo.StreamID = 0xE0 // MPEG video
pesVideo.StreamType = 0x1B // H.264
}
// Create PSM header with proper payload length
psmData := make([]byte, 0, PSMHeaderSize+int(elementary_stream_map_length))
psmBuffer := util.Buffer(psmData)
psmBuffer.Reset()
// Write PSM start code
psmBuffer.WriteUint32(StartCodeMAP)
psmLength := uint16(PSMHeaderSize + int(elementary_stream_map_length) - 6)
psmBuffer.WriteUint16(psmLength) // psm_length
psmBuffer.WriteByte(0xE0) // current_next_indicator + reserved + psm_version
psmBuffer.WriteByte(0xFF) // reserved + marker
psmBuffer.WriteUint16(0) // program_stream_info_length
psmBuffer.WriteUint16(elementary_stream_map_length)
if pesAudio != nil {
psmBuffer.WriteByte(pesAudio.StreamType) // stream_type
psmBuffer.WriteByte(pesAudio.StreamID) // elementary_stream_id
psmBuffer.WriteUint16(0) // elementary_stream_info_length
}
if pesVideo != nil {
psmBuffer.WriteByte(pesVideo.StreamType) // stream_type
psmBuffer.WriteByte(pesVideo.StreamID) // elementary_stream_id
psmBuffer.WriteUint16(0) // elementary_stream_info_length
}
// Verify PSM header
if len(psmBuffer) != PSMHeaderSize+int(elementary_stream_map_length) {
t.Errorf("Expected PSM size %d, got %d", PSMHeaderSize+int(elementary_stream_map_length), len(psmBuffer))
}
// Check for PSM start code
if !bytes.Contains(psmBuffer, []byte{0x00, 0x00, 0x01, 0xBC}) {
t.Error("PSM header does not contain PSM start code")
}
t.Logf("PSM Header: %x", psmBuffer)
t.Logf("PSM Header size: %d bytes", len(psmBuffer))
// Test ReadPayload function directly
t.Run("ReadPayload", func(t *testing.T) {
// Create test payload data
testPayload := []byte{0x01, 0x02, 0x03, 0x04, 0x05}
// Create a packet with length prefix
packetData := make([]byte, 0, 2+len(testPayload))
packetData = append(packetData, byte(len(testPayload)>>8), byte(len(testPayload)))
packetData = append(packetData, testPayload...)
reader := util.NewBufReader(bytes.NewReader(packetData))
demuxer := &MpegPsDemuxer{}
// Test ReadPayload function
payload, err := demuxer.ReadPayload(reader)
if err != nil {
t.Fatalf("ReadPayload failed: %v", err)
}
if payload.Size != len(testPayload) {
t.Errorf("Expected payload size %d, got %d", len(testPayload), payload.Size)
}
if !bytes.Equal(payload.ToBytes(), testPayload) {
t.Errorf("Expected payload %x, got %x", testPayload, payload.ToBytes())
}
t.Logf("ReadPayload test passed: %x", payload.ToBytes())
})
// Test basic demuxing with PS header only
t.Run("PSHeader", func(t *testing.T) {
// Create a simple test that just verifies the PS header structure
// without trying to demux it (which expects more data)
if len(outputBuffer) < 4 {
t.Errorf("PS header too short: %d bytes", len(outputBuffer))
}
// Check that it starts with the correct start code
if !bytes.HasPrefix(outputBuffer, []byte{0x00, 0x00, 0x01, 0xBA}) {
t.Errorf("PS header does not start with correct start code: %x", outputBuffer[:4])
}
t.Logf("PS header structure test passed")
})
t.Logf("Basic mux/demux test completed successfully")
})
// Test basic PES packet generation without PlayBlock
t.Run("PESGeneration", func(t *testing.T) {
// Create a test that simulates PES packet generation
// without requiring a full subscriber setup
// Create test payload
testPayload := make([]byte, 5000)
for i := range testPayload {
testPayload[i] = byte(i % 256)
}
// Create PES frame
pesFrame := &MpegpsPESFrame{
StreamType: 0x1B, // H.264
}
pesFrame.Pts = 90000
pesFrame.Dts = 90000
// Create allocator for testing
allocator := util.NewScalableMemoryAllocator(1024*1024)
packet := util.NewRecyclableMemory(allocator)
// Write PES packet
err := pesFrame.WritePESPacket(util.NewMemory(testPayload), &packet)
if err != nil {
t.Fatalf("WritePESPacket failed: %v", err)
}
// Verify packet was written
packetData := packet.ToBytes()
if len(packetData) == 0 {
t.Fatal("No data was written to packet")
}
t.Logf("PES packet generated: %d bytes", len(packetData))
t.Logf("Packet data (first 64 bytes): %x", packetData[:min(64, len(packetData))])
// Verify PS header is present
if !bytes.Contains(packetData, []byte{0x00, 0x00, 0x01, 0xBA}) {
t.Error("PES packet does not contain PS start code")
}
// Test reading back the packet
reader := util.NewBufReader(bytes.NewReader(packetData))
// Skip PS header
code, err := reader.ReadBE32(4)
if err != nil {
t.Fatalf("Failed to read start code: %v", err)
}
if code != StartCodePS {
t.Errorf("Expected PS start code %x, got %x", StartCodePS, code)
}
// Skip PS header
if err = reader.Skip(9); err != nil {
t.Fatalf("Failed to skip PS header: %v", err)
}
psl, err := reader.ReadByte()
if err != nil {
t.Fatalf("Failed to read stuffing length: %v", err)
}
psl &= 0x07
if err = reader.Skip(int(psl)); err != nil {
t.Fatalf("Failed to skip stuffing bytes: %v", err)
}
// Read PES packets directly by parsing the PES structure
totalPayloadSize := 0
packetCount := 0
for reader.Buffered() > 0 {
// Read PES packet start code (0x00000100 + stream_id)
pesStartCode, err := reader.ReadBE32(4)
if err != nil {
if err == io.EOF {
break
}
t.Fatalf("Failed to read PES start code: %v", err)
}
// Check if it's a PES packet (starts with 0x000001)
if pesStartCode&0xFFFFFF00 != 0x00000100 {
t.Errorf("Invalid PES start code: %x", pesStartCode)
break
}
// // streamID := byte(pesStartCode & 0xFF)
t.Logf("PES packet %d: stream_id=0x%02x", packetCount+1, pesStartCode&0xFF)
// Read PES packet length
pesLength, err := reader.ReadBE(2)
if err != nil {
t.Fatalf("Failed to read PES length: %v", err)
}
// Read PES header
// Skip the first byte (flags)
_, err = reader.ReadByte()
if err != nil {
t.Fatalf("Failed to read PES flags1: %v", err)
}
// Skip the second byte (flags)
_, err = reader.ReadByte()
if err != nil {
t.Fatalf("Failed to read PES flags2: %v", err)
}
// Read header data length
headerDataLength, err := reader.ReadByte()
if err != nil {
t.Fatalf("Failed to read PES header data length: %v", err)
}
// Skip header data
if err = reader.Skip(int(headerDataLength)); err != nil {
t.Fatalf("Failed to skip PES header data: %v", err)
}
// Calculate payload size
payloadSize := pesLength - 3 - int(headerDataLength) // 3 = flags1 + flags2 + headerDataLength
if payloadSize > 0 {
// Read payload data
payload, err := reader.ReadBytes(payloadSize)
if err != nil {
t.Fatalf("Failed to read PES payload: %v", err)
}
totalPayloadSize += payload.Size
t.Logf("PES packet %d: %d bytes payload", packetCount+1, payload.Size)
}
packetCount++
}
// Verify total payload size matches
if totalPayloadSize != len(testPayload) {
t.Errorf("Expected total payload size %d, got %d", len(testPayload), totalPayloadSize)
}
t.Logf("PES generation test completed successfully: %d packets, total %d bytes", packetCount, totalPayloadSize)
})
}
func TestPESPacketWriteRead(t *testing.T) {
// Test PES packet writing and reading functionality
t.Run("PESWriteRead", func(t *testing.T) {
// Create test payload data
testPayload := make([]byte, 1000)
for i := range testPayload {
testPayload[i] = byte(i % 256)
}
// Create PES frame
pesFrame := &MpegpsPESFrame{
StreamType: 0x1B, // H.264
}
pesFrame.Pts = 90000 // 1 second in 90kHz clock
pesFrame.Dts = 90000
// Create allocator for testing
allocator := util.NewScalableMemoryAllocator(1024)
packet := util.NewRecyclableMemory(allocator)
// Write PES packet
err := pesFrame.WritePESPacket(util.NewMemory(testPayload), &packet)
if err != nil {
t.Fatalf("WritePESPacket failed: %v", err)
}
// Verify that packet was written
packetData := packet.ToBytes()
if len(packetData) == 0 {
t.Fatal("No data was written to packet")
}
t.Logf("PES packet written: %d bytes", len(packetData))
t.Logf("Packet data (first 64 bytes): %x", packetData[:min(64, len(packetData))])
// Verify PS header is present
if !bytes.Contains(packetData, []byte{0x00, 0x00, 0x01, 0xBA}) {
t.Error("PES packet does not contain PS start code")
}
// Now test reading the PES packet back
reader := util.NewBufReader(bytes.NewReader(packetData))
// Read and process the PS header
code, err := reader.ReadBE32(4)
if err != nil {
t.Fatalf("Failed to read start code: %v", err)
}
if code != StartCodePS {
t.Errorf("Expected PS start code %x, got %x", StartCodePS, code)
}
// Skip PS header (9 bytes + stuffing length)
if err = reader.Skip(9); err != nil {
t.Fatalf("Failed to skip PS header: %v", err)
}
psl, err := reader.ReadByte()
if err != nil {
t.Fatalf("Failed to read stuffing length: %v", err)
}
psl &= 0x07
if err = reader.Skip(int(psl)); err != nil {
t.Fatalf("Failed to skip stuffing bytes: %v", err)
}
// Read PES packet directly by parsing the PES structure
totalPayloadSize := 0
packetCount := 0
for reader.Buffered() > 0 {
// Read PES packet start code (0x00000100 + stream_id)
pesStartCode, err := reader.ReadBE32(4)
if err != nil {
if err == io.EOF {
break
}
t.Fatalf("Failed to read PES start code: %v", err)
}
// Check if it's a PES packet (starts with 0x000001)
if pesStartCode&0xFFFFFF00 != 0x00000100 {
t.Errorf("Invalid PES start code: %x", pesStartCode)
break
}
// // streamID := byte(pesStartCode & 0xFF)
t.Logf("PES packet %d: stream_id=0x%02x", packetCount+1, pesStartCode&0xFF)
// Read PES packet length
pesLength, err := reader.ReadBE(2)
if err != nil {
t.Fatalf("Failed to read PES length: %v", err)
}
// Read PES header
// Skip the first byte (flags)
_, err = reader.ReadByte()
if err != nil {
t.Fatalf("Failed to read PES flags1: %v", err)
}
// Skip the second byte (flags)
_, err = reader.ReadByte()
if err != nil {
t.Fatalf("Failed to read PES flags2: %v", err)
}
// Read header data length
headerDataLength, err := reader.ReadByte()
if err != nil {
t.Fatalf("Failed to read PES header data length: %v", err)
}
// Skip header data
if err = reader.Skip(int(headerDataLength)); err != nil {
t.Fatalf("Failed to skip PES header data: %v", err)
}
// Calculate payload size
payloadSize := pesLength - 3 - int(headerDataLength) // 3 = flags1 + flags2 + headerDataLength
if payloadSize > 0 {
// Read payload data
payload, err := reader.ReadBytes(payloadSize)
if err != nil {
t.Fatalf("Failed to read PES payload: %v", err)
}
totalPayloadSize += payload.Size
t.Logf("PES packet %d: %d bytes payload", packetCount+1, payload.Size)
}
packetCount++
}
t.Logf("PES payload read: %d bytes", totalPayloadSize)
// Verify payload size
if totalPayloadSize != len(testPayload) {
t.Errorf("Expected payload size %d, got %d", len(testPayload), totalPayloadSize)
}
// Note: We can't easily verify the content because the payload is fragmented across multiple PES packets
// But we can verify the total size is correct
t.Logf("PES packet write-read test completed successfully")
})
}
func TestLargePESPacket(t *testing.T) {
// Test large PES packet handling (payload > 65535 bytes)
t.Run("LargePESPacket", func(t *testing.T) {
// Create large test payload (exceeds 65535 bytes)
largePayload := make([]byte, 70000) // 70KB payload
for i := range largePayload {
largePayload[i] = byte(i % 256)
}
// Create PES frame
pesFrame := &MpegpsPESFrame{
StreamType: 0x1B, // H.264
}
pesFrame.Pts = 180000 // 2 seconds in 90kHz clock
pesFrame.Dts = 180000
// Create allocator for testing
allocator := util.NewScalableMemoryAllocator(1024*1024) // 1MB allocator
packet := util.NewRecyclableMemory(allocator)
// Write large PES packet
t.Logf("Writing large PES packet with %d bytes payload", len(largePayload))
err := pesFrame.WritePESPacket(util.NewMemory(largePayload), &packet)
if err != nil {
t.Fatalf("WritePESPacket failed for large payload: %v", err)
}
// Verify that packet was written
packetData := packet.ToBytes()
if len(packetData) == 0 {
t.Fatal("No data was written to packet")
}
t.Logf("Large PES packet written: %d bytes", len(packetData))
// Verify PS header is present
if !bytes.Contains(packetData, []byte{0x00, 0x00, 0x01, 0xBA}) {
t.Error("Large PES packet does not contain PS start code")
}
// Count number of PES packets (should be multiple due to size limitation)
pesCount := 0
reader := util.NewBufReader(bytes.NewReader(packetData))
// Skip PS header
code, err := reader.ReadBE32(4)
if err != nil {
t.Fatalf("Failed to read start code: %v", err)
}
if code != StartCodePS {
t.Errorf("Expected PS start code %x, got %x", StartCodePS, code)
}
// Skip PS header
if err = reader.Skip(9); err != nil {
t.Fatalf("Failed to skip PS header: %v", err)
}
psl, err := reader.ReadByte()
if err != nil {
t.Fatalf("Failed to read stuffing length: %v", err)
}
psl &= 0x07
if err = reader.Skip(int(psl)); err != nil {
t.Fatalf("Failed to skip stuffing bytes: %v", err)
}
// Read and count PES packets
totalPayloadSize := 0
for reader.Buffered() > 0 {
// Read PES packet start code (0x00000100 + stream_id)
pesStartCode, err := reader.ReadBE32(4)
if err != nil {
if err == io.EOF {
break
}
t.Fatalf("Failed to read PES start code: %v", err)
}
// Check if it's a PES packet (starts with 0x000001)
if pesStartCode&0xFFFFFF00 != 0x00000100 {
t.Errorf("Invalid PES start code: %x", pesStartCode)
break
}
// streamID := byte(pesStartCode & 0xFF)
// Read PES packet length
pesLength, err := reader.ReadBE(2)
if err != nil {
t.Fatalf("Failed to read PES length: %v", err)
}
// Read PES header
// Skip the first byte (flags)
_, err = reader.ReadByte()
if err != nil {
t.Fatalf("Failed to read PES flags1: %v", err)
}
// Skip the second byte (flags)
_, err = reader.ReadByte()
if err != nil {
t.Fatalf("Failed to read PES flags2: %v", err)
}
// Read header data length
headerDataLength, err := reader.ReadByte()
if err != nil {
t.Fatalf("Failed to read PES header data length: %v", err)
}
// Skip header data
if err = reader.Skip(int(headerDataLength)); err != nil {
t.Fatalf("Failed to skip PES header data: %v", err)
}
// Calculate payload size
payloadSize := pesLength - 3 - int(headerDataLength) // 3 = flags1 + flags2 + headerDataLength
if payloadSize > 0 {
// Read payload data
payload, err := reader.ReadBytes(payloadSize)
if err != nil {
t.Fatalf("Failed to read PES payload: %v", err)
}
totalPayloadSize += payload.Size
t.Logf("PES packet %d: %d bytes payload", pesCount+1, payload.Size)
}
pesCount++
}
// Verify that we got multiple PES packets
if pesCount < 2 {
t.Errorf("Expected multiple PES packets for large payload, got %d", pesCount)
}
// Verify total payload size
if totalPayloadSize != len(largePayload) {
t.Errorf("Expected total payload size %d, got %d", len(largePayload), totalPayloadSize)
}
// Verify individual PES packet sizes don't exceed maximum
maxPacketSize := MaxPESPayloadSize + PESHeaderMinSize
if pesCount == 1 && len(packetData) > maxPacketSize {
t.Errorf("Single PES packet exceeds maximum size: %d > %d", len(packetData), maxPacketSize)
}
t.Logf("Large PES packet test completed successfully: %d packets, total %d bytes", pesCount, totalPayloadSize)
})
}
func TestPESPacketBoundaryConditions(t *testing.T) {
// Test PES packet boundary conditions
t.Run("BoundaryConditions", func(t *testing.T) {
testCases := []struct {
name string
payloadSize int
}{
{"EmptyPayload", 0},
{"SmallPayload", 1},
{"ExactBoundary", MaxPESPayloadSize},
{"JustOverBoundary", MaxPESPayloadSize + 1},
{"MultipleBoundary", MaxPESPayloadSize * 2 + 100},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
// Create test payload
testPayload := make([]byte, tc.payloadSize)
for i := range testPayload {
testPayload[i] = byte(i % 256)
}
// Create PES frame
pesFrame := &MpegpsPESFrame{
StreamType: 0x1B, // H.264
}
pesFrame.Pts = uint64(tc.payloadSize) * 90 // Use payload size as PTS
pesFrame.Dts = uint64(tc.payloadSize) * 90
// Create allocator for testing
allocator := util.NewScalableMemoryAllocator(1024*1024)
packet := util.NewRecyclableMemory(allocator)
// Write PES packet
err := pesFrame.WritePESPacket(util.NewMemory(testPayload), &packet)
if err != nil {
t.Fatalf("WritePESPacket failed: %v", err)
}
// Verify that packet was written
packetData := packet.ToBytes()
if len(packetData) == 0 && tc.payloadSize > 0 {
t.Fatal("No data was written to packet for non-empty payload")
}
t.Logf("%s: %d bytes payload -> %d bytes packet", tc.name, tc.payloadSize, len(packetData))
// For non-empty payloads, verify we can read them back
if tc.payloadSize > 0 {
reader := util.NewBufReader(bytes.NewReader(packetData))
// Skip PS header
code, err := reader.ReadBE32(4)
if err != nil {
t.Fatalf("Failed to read start code: %v", err)
}
if code != StartCodePS {
t.Errorf("Expected PS start code %x, got %x", StartCodePS, code)
}
// Skip PS header
if err = reader.Skip(9); err != nil {
t.Fatalf("Failed to skip PS header: %v", err)
}
psl, err := reader.ReadByte()
if err != nil {
t.Fatalf("Failed to read stuffing length: %v", err)
}
psl &= 0x07
if err = reader.Skip(int(psl)); err != nil {
t.Fatalf("Failed to skip stuffing bytes: %v", err)
}
// Read PES packets
totalPayloadSize := 0
packetCount := 0
for reader.Buffered() > 0 {
// Read PES packet start code (0x00000100 + stream_id)
pesStartCode, err := reader.ReadBE32(4)
if err != nil {
if err == io.EOF {
break
}
t.Fatalf("Failed to read PES start code: %v", err)
}
// Check if it's a PES packet (starts with 0x000001)
if pesStartCode&0xFFFFFF00 != 0x00000100 {
t.Errorf("Invalid PES start code: %x", pesStartCode)
break
}
// // streamID := byte(pesStartCode & 0xFF)
// Read PES packet length
pesLength, err := reader.ReadBE(2)
if err != nil {
t.Fatalf("Failed to read PES length: %v", err)
}
// Read PES header
// Skip the first byte (flags)
_, err = reader.ReadByte()
if err != nil {
t.Fatalf("Failed to read PES flags1: %v", err)
}
// Skip the second byte (flags)
_, err = reader.ReadByte()
if err != nil {
t.Fatalf("Failed to read PES flags2: %v", err)
}
// Read header data length
headerDataLength, err := reader.ReadByte()
if err != nil {
t.Fatalf("Failed to read PES header data length: %v", err)
}
// Skip header data
if err = reader.Skip(int(headerDataLength)); err != nil {
t.Fatalf("Failed to skip PES header data: %v", err)
}
// Calculate payload size
payloadSize := pesLength - 3 - int(headerDataLength) // 3 = flags1 + flags2 + headerDataLength
if payloadSize > 0 {
// Read payload data
payload, err := reader.ReadBytes(payloadSize)
if err != nil {
t.Fatalf("Failed to read PES payload: %v", err)
}
totalPayloadSize += payload.Size
}
packetCount++
}
// Verify total payload size matches
if totalPayloadSize != tc.payloadSize {
t.Errorf("Expected total payload size %d, got %d", tc.payloadSize, totalPayloadSize)
}
t.Logf("%s: Successfully read back %d PES packets", tc.name, packetCount)
}
})
}
})
}

35
pkg/format/ps/pes.go Normal file
View File

@@ -0,0 +1,35 @@
package mpegps
import (
mpegts "m7s.live/v5/pkg/format/ts"
"m7s.live/v5/pkg/util"
)
type MpegpsPESFrame struct {
StreamType byte // Stream type (e.g., video, audio)
mpegts.MpegPESHeader
}
func (frame *MpegpsPESFrame) WritePESPacket(payload util.Memory, allocator *util.RecyclableMemory) (err error) {
frame.DataAlignmentIndicator = 1
pesReader := payload.NewReader()
var outputMemory util.Buffer = allocator.NextN(PSPackHeaderSize)
outputMemory.Reset()
MuxPSHeader(&outputMemory)
for pesReader.Length > 0 {
currentPESPayload := min(pesReader.Length, MaxPESPayloadSize)
var pesHeadItem util.Buffer
pesHeadItem, err = frame.WritePESHeader(currentPESPayload)
if err != nil {
return
}
copy(allocator.NextN(pesHeadItem.Len()), pesHeadItem)
// 申请输出缓冲
outputMemory = allocator.NextN(currentPESPayload)
pesReader.Read(outputMemory)
frame.DataAlignmentIndicator = 0
}
return nil
}

131
pkg/format/raw.go Normal file
View File

@@ -0,0 +1,131 @@
package format
import (
"bytes"
"fmt"
"github.com/deepch/vdk/codec/h264parser"
"github.com/deepch/vdk/codec/h265parser"
"m7s.live/v5/pkg"
"m7s.live/v5/pkg/codec"
"m7s.live/v5/pkg/util"
)
var _ pkg.IAVFrame = (*RawAudio)(nil)
type RawAudio struct {
pkg.Sample
}
func (r *RawAudio) GetSize() int {
return r.Raw.(*util.Memory).Size
}
func (r *RawAudio) Demux() error {
r.Raw = &r.Memory
return nil
}
func (r *RawAudio) Mux(from *pkg.Sample) (err error) {
r.InitRecycleIndexes(0)
r.Memory = *from.Raw.(*util.Memory)
r.ICodecCtx = from.GetBase()
return
}
func (r *RawAudio) String() string {
return fmt.Sprintf("RawAudio{FourCC: %s, Timestamp: %s, Size: %d}", r.FourCC(), r.Timestamp, r.Size)
}
var _ pkg.IAVFrame = (*H26xFrame)(nil)
type H26xFrame struct {
pkg.Sample
}
func (h *H26xFrame) CheckCodecChange() (err error) {
if h.ICodecCtx == nil {
return pkg.ErrUnsupportCodec
}
var hasVideoFrame bool
switch ctx := h.GetBase().(type) {
case *codec.H264Ctx:
var sps, pps []byte
for nalu := range h.Raw.(*pkg.Nalus).RangePoint {
switch codec.ParseH264NALUType(nalu.Buffers[0][0]) {
case codec.NALU_SPS:
sps = nalu.ToBytes()
case codec.NALU_PPS:
pps = nalu.ToBytes()
case codec.NALU_IDR_Picture:
h.IDR = true
case codec.NALU_Non_IDR_Picture:
hasVideoFrame = true
}
}
if sps != nil && pps != nil {
var codecData h264parser.CodecData
codecData, err = h264parser.NewCodecDataFromSPSAndPPS(sps, pps)
if err != nil {
return
}
if !bytes.Equal(codecData.Record, ctx.Record) {
h.ICodecCtx = &codec.H264Ctx{
CodecData: codecData,
}
}
}
case *codec.H265Ctx:
var vps, sps, pps []byte
for nalu := range h.Raw.(*pkg.Nalus).RangePoint {
switch codec.ParseH265NALUType(nalu.Buffers[0][0]) {
case h265parser.NAL_UNIT_VPS:
vps = nalu.ToBytes()
case h265parser.NAL_UNIT_SPS:
sps = nalu.ToBytes()
case h265parser.NAL_UNIT_PPS:
pps = nalu.ToBytes()
case h265parser.NAL_UNIT_CODED_SLICE_BLA_W_LP,
h265parser.NAL_UNIT_CODED_SLICE_BLA_W_RADL,
h265parser.NAL_UNIT_CODED_SLICE_BLA_N_LP,
h265parser.NAL_UNIT_CODED_SLICE_IDR_W_RADL,
h265parser.NAL_UNIT_CODED_SLICE_IDR_N_LP,
h265parser.NAL_UNIT_CODED_SLICE_CRA:
h.IDR = true
case 1, 2, 3, 4, 5, 6, 7, 8, 9:
hasVideoFrame = true
}
}
if vps != nil && sps != nil && pps != nil {
var codecData h265parser.CodecData
codecData, err = h265parser.NewCodecDataFromVPSAndSPSAndPPS(vps, sps, pps)
if err != nil {
return
}
if !bytes.Equal(codecData.Record, ctx.Record) {
h.ICodecCtx = &codec.H265Ctx{
CodecData: codecData,
}
}
}
}
// Return ErrSkip if no video frames are present (only metadata NALUs)
if !hasVideoFrame && !h.IDR {
return pkg.ErrSkip
}
return
}
func (r *H26xFrame) GetSize() (ret int) {
switch raw := r.Raw.(type) {
case *pkg.Nalus:
for nalu := range raw.RangePoint {
ret += nalu.Size
}
}
return
}
func (h *H26xFrame) String() string {
return fmt.Sprintf("H26xFrame{FourCC: %s, Timestamp: %s, CTS: %s}", h.FourCC, h.Timestamp, h.CTS)
}

View File

@@ -4,7 +4,11 @@ import (
"bytes"
"errors"
"io"
"io/ioutil"
"time"
"m7s.live/v5"
"m7s.live/v5/pkg/codec"
"m7s.live/v5/pkg/format"
"m7s.live/v5/pkg/util"
//"sync"
)
@@ -101,22 +105,16 @@ const (
//
type MpegTsStream struct {
PAT MpegTsPAT // PAT表信息
PMT MpegTsPMT // PMT表信息
PESBuffer map[uint16]*MpegTsPESPacket
PESChan chan *MpegTsPESPacket
PAT MpegTsPAT // PAT表信息
PMT MpegTsPMT // PMT表信息
Publisher *m7s.Publisher
Allocator *util.ScalableMemoryAllocator
writer m7s.PublishWriter[*format.Mpeg2Audio, *VideoFrame]
audioPID, videoPID, pmtPID uint16
tsPacket [TS_PACKET_SIZE]byte
}
// ios13818-1-CN.pdf 33/165
//
// TS
//
// Packet == Header + Payload == 188 bytes
type MpegTsPacket struct {
Header MpegTsHeader
Payload []byte
}
// 前面32bit的数据即TS分组首部,它指出了这个分组的属性
type MpegTsHeader struct {
@@ -185,25 +183,6 @@ type MpegTsDescriptor struct {
Data []byte
}
func ReadTsPacket(r io.Reader) (packet MpegTsPacket, err error) {
lr := &io.LimitedReader{R: r, N: TS_PACKET_SIZE}
// header
packet.Header, err = ReadTsHeader(lr)
if err != nil {
return
}
// payload
packet.Payload = make([]byte, lr.N)
_, err = lr.Read(packet.Payload)
if err != nil {
return
}
return
}
func ReadTsHeader(r io.Reader) (header MpegTsHeader, err error) {
var h uint32
@@ -365,7 +344,7 @@ func ReadTsHeader(r io.Reader) (header MpegTsHeader, err error) {
// Discard 是一个 io.Writer,对它进行的任何 Write 调用都将无条件成功
// 但是ioutil.Discard不记录copy得到的数值
// 用于发送需要读取但不想存储的数据,目的是耗尽读取端的数据
if _, err = io.CopyN(ioutil.Discard, lr, int64(lr.N)); err != nil {
if _, err = io.CopyN(io.Discard, lr, int64(lr.N)); err != nil {
return
}
}
@@ -440,138 +419,96 @@ func WriteTsHeader(w io.Writer, header MpegTsHeader) (written int, err error) {
return
}
//
//func (s *MpegTsStream) TestWrite(fileName string) error {
//
// if fileName != "" {
// file, err := os.Create(fileName)
// if err != nil {
// panic(err)
// }
// defer file.Close()
//
// patTsHeader := []byte{0x47, 0x40, 0x00, 0x10}
//
// if err := WritePATPacket(file, patTsHeader, *s.pat); err != nil {
// panic(err)
// }
//
// // TODO:这里的pid应该是由PAT给的
// pmtTsHeader := []byte{0x47, 0x41, 0x00, 0x10}
//
// if err := WritePMTPacket(file, pmtTsHeader, *s.pmt); err != nil {
// panic(err)
// }
// }
//
// var videoFrame int
// var audioFrame int
// for {
// tsPesPkt, ok := <-s.TsPesPktChan
// if !ok {
// fmt.Println("frame index, video , audio :", videoFrame, audioFrame)
// break
// }
//
// if tsPesPkt.PesPkt.Header.StreamID == STREAM_ID_AUDIO {
// audioFrame++
// }
//
// if tsPesPkt.PesPkt.Header.StreamID == STREAM_ID_VIDEO {
// println(tsPesPkt.PesPkt.Header.Pts)
// videoFrame++
// }
//
// fmt.Sprintf("%s", tsPesPkt)
//
// // if err := WritePESPacket(file, tsPesPkt.TsPkt.Header, tsPesPkt.PesPkt); err != nil {
// // return err
// // }
//
// }
//
// return nil
//}
func (s *MpegTsStream) ReadPAT(packet *MpegTsPacket, pr io.Reader) (err error) {
// 首先找到PID==0x00的TS包(PAT)
if PID_PAT == packet.Header.Pid {
if len(packet.Payload) == 188 {
pr = &util.Crc32Reader{R: pr, Crc32: 0xffffffff}
}
// Header + PSI + Paylod
s.PAT, err = ReadPAT(pr)
}
return
}
func (s *MpegTsStream) ReadPMT(packet *MpegTsPacket, pr io.Reader) (err error) {
// 在读取PAT中已经将所有频道节目信息(PMT_PID)保存了起来
// 接着读取所有TS包里面的PID,找出PID==PMT_PID的TS包,就是PMT表
for _, v := range s.PAT.Program {
if v.ProgramMapPID == packet.Header.Pid {
if len(packet.Payload) == 188 {
pr = &util.Crc32Reader{R: pr, Crc32: 0xffffffff}
}
// Header + PSI + Paylod
s.PMT, err = ReadPMT(pr)
}
}
return
}
func (s *MpegTsStream) Feed(ts io.Reader) (err error) {
writer := &s.writer
var reader bytes.Reader
var lr io.LimitedReader
lr.R = &reader
var tsHeader MpegTsHeader
tsData := make([]byte, TS_PACKET_SIZE)
for {
_, err = io.ReadFull(ts, tsData)
var pesHeader MpegPESHeader
for !s.Publisher.IsStopped() {
_, err = io.ReadFull(ts, s.tsPacket[:])
if err == io.EOF {
// 文件结尾 把最后面的数据发出去
for _, pesPkt := range s.PESBuffer {
if pesPkt != nil {
s.PESChan <- pesPkt
}
}
return nil
} else if err != nil {
return
}
reader.Reset(tsData)
reader.Reset(s.tsPacket[:])
lr.N = TS_PACKET_SIZE
if tsHeader, err = ReadTsHeader(&lr); err != nil {
return
}
if tsHeader.Pid == PID_PAT {
switch tsHeader.Pid {
case PID_PAT:
if s.PAT, err = ReadPAT(&lr); err != nil {
return
}
s.pmtPID = s.PAT.Program[0].ProgramMapPID
continue
}
if len(s.PMT.Stream) == 0 {
for _, v := range s.PAT.Program {
if v.ProgramMapPID == tsHeader.Pid {
if s.PMT, err = ReadPMT(&lr); err != nil {
return
}
for _, v := range s.PMT.Stream {
s.PESBuffer[v.ElementaryPID] = nil
}
}
case s.pmtPID:
if len(s.PMT.Stream) != 0 {
continue
}
} else if pesPkt, ok := s.PESBuffer[tsHeader.Pid]; ok {
if tsHeader.PayloadUnitStartIndicator == 1 {
if pesPkt != nil {
s.PESChan <- pesPkt
}
pesPkt = &MpegTsPESPacket{}
s.PESBuffer[tsHeader.Pid] = pesPkt
if pesPkt.Header, err = ReadPESHeader(&lr); err != nil {
return
if s.PMT, err = ReadPMT(&lr); err != nil {
return
}
for _, pmt := range s.PMT.Stream {
switch pmt.StreamType {
case STREAM_TYPE_H265:
s.videoPID = pmt.ElementaryPID
writer.PublishVideoWriter = m7s.NewPublishVideoWriter[*VideoFrame](s.Publisher, s.Allocator)
writer.VideoFrame.ICodecCtx = &codec.H265Ctx{}
case STREAM_TYPE_H264:
s.videoPID = pmt.ElementaryPID
writer.PublishVideoWriter = m7s.NewPublishVideoWriter[*VideoFrame](s.Publisher, s.Allocator)
writer.VideoFrame.ICodecCtx = &codec.H264Ctx{}
case STREAM_TYPE_AAC:
s.audioPID = pmt.ElementaryPID
writer.PublishAudioWriter = m7s.NewPublishAudioWriter[*format.Mpeg2Audio](s.Publisher, s.Allocator)
writer.AudioFrame.ICodecCtx = &codec.AACCtx{}
case STREAM_TYPE_G711A:
s.audioPID = pmt.ElementaryPID
writer.PublishAudioWriter = m7s.NewPublishAudioWriter[*format.Mpeg2Audio](s.Publisher, s.Allocator)
writer.AudioFrame.ICodecCtx = codec.NewPCMACtx()
case STREAM_TYPE_G711U:
s.audioPID = pmt.ElementaryPID
writer.PublishAudioWriter = m7s.NewPublishAudioWriter[*format.Mpeg2Audio](s.Publisher, s.Allocator)
writer.AudioFrame.ICodecCtx = codec.NewPCMUCtx()
}
}
io.Copy(&pesPkt.Payload, &lr)
case s.audioPID:
if tsHeader.PayloadUnitStartIndicator == 1 {
if pesHeader, err = ReadPESHeader0(&lr); err != nil {
return
}
if !s.Publisher.PubAudio {
continue
}
if writer.AudioFrame.Size > 0 {
if err = writer.NextAudio(); err != nil {
continue
}
}
writer.AudioFrame.SetDTS(time.Duration(pesHeader.Pts))
}
lr.Read(writer.AudioFrame.NextN(int(lr.N)))
case s.videoPID:
if tsHeader.PayloadUnitStartIndicator == 1 {
if pesHeader, err = ReadPESHeader0(&lr); err != nil {
return
}
if !s.Publisher.PubVideo {
continue
}
if writer.VideoFrame.Size > 0 {
if err = writer.NextVideo(); err != nil {
continue
}
}
writer.VideoFrame.SetDTS(time.Duration(pesHeader.Dts))
writer.VideoFrame.SetPTS(time.Duration(pesHeader.Pts))
}
lr.Read(writer.VideoFrame.NextN(int(lr.N)))
}
}
return
}

View File

@@ -2,39 +2,19 @@ package mpegts
import (
"errors"
"fmt"
"io"
"m7s.live/v5/pkg/util"
"net"
)
// ios13818-1-CN.pdf 45/166
//
// PES
//
// 每个传输流和节目流在逻辑上都是由 PES 包构造的
type MpegTsPesStream struct {
TsPkt MpegTsPacket
PesPkt MpegTsPESPacket
}
// PES--Packetized Elementary Streams (分组的ES),ES形成的分组称为PES分组,是用来传递ES的一种数据结构
// 1110 xxxx 为视频流(0xE0)
// 110x xxxx 为音频流(0xC0)
type MpegTsPESPacket struct {
Header MpegTsPESHeader
Payload util.Buffer //从TS包中读取的数据
Buffers net.Buffers //用于写TS包
}
type MpegTsPESHeader struct {
PacketStartCodePrefix uint32 // 24 bits 同跟随它的 stream_id 一起组成标识包起始端的包起始码.packet_start_code_prefix 为比特串"0000 0000 0000 0000 0000 0001"(0x000001)
StreamID byte // 8 bits stream_id 指示基本流的类型和编号,如 stream_id 表 2-22 所定义的.传输流中,stream_id 可以设置为准确描述基本流类型的任何有效值,如表 2-22 所规定的.传输流中,基本流类型在 2.4.4 中所指示的节目特定信息中指定
PesPacketLength uint16 // 16 bits 指示 PES 包中跟随该字段最后字节的字节数.0->指示 PES 包长度既未指示也未限定并且仅在这样的 PES 包中才被允许,该 PES 包的有效载荷由来自传输流包中所包含的视频基本流的字节组成
type MpegPESHeader struct {
header [32]byte
StreamID byte // 8 bits stream_id 指示基本流的类型和编号,如 stream_id 表 2-22 所定义的.传输流中,stream_id 可以设置为准确描述基本流类型的任何有效值,如表 2-22 所规定的.传输流中,基本流类型在 2.4.4 中所指示的节目特定信息中指定
PesPacketLength uint16 // 16 bits 指示 PES 包中跟随该字段最后字节的字节数.0->指示 PES 包长度既未指示也未限定并且仅在这样的 PES 包中才被允许,该 PES 包的有效载荷由来自传输流包中所包含的视频基本流的字节组成
MpegTsOptionalPESHeader
PayloadLength uint64 // 这个不是标准文档里面的字段,是自己添加的,方便计算
}
// 可选的PES Header = MpegTsOptionalPESHeader + stuffing bytes(0xFF) m * 8
@@ -99,23 +79,35 @@ type MpegTsOptionalPESHeader struct {
// pts_dts_Flags == "11" -> PTS + DTS
type MpegtsPESFrame struct {
Pid uint16
IsKeyFrame bool
ContinuityCounter byte
ProgramClockReferenceBase uint64
Pid uint16
IsKeyFrame bool
ContinuityCounter byte
MpegPESHeader
}
func ReadPESHeader(r io.Reader) (header MpegTsPESHeader, err error) {
var flags uint8
var length uint
func CreatePESWriters() (pesAudio, pesVideo MpegtsPESFrame) {
pesAudio, pesVideo = MpegtsPESFrame{
Pid: PID_AUDIO,
}, MpegtsPESFrame{
Pid: PID_VIDEO,
}
pesAudio.DataAlignmentIndicator = 1
pesVideo.DataAlignmentIndicator = 1
pesAudio.StreamID = STREAM_ID_AUDIO
pesVideo.StreamID = STREAM_ID_VIDEO
return
}
func ReadPESHeader0(r *io.LimitedReader) (header MpegPESHeader, err error) {
var length uint
var packetStartCodePrefix uint32
// packetStartCodePrefix(24) (0x000001)
header.PacketStartCodePrefix, err = util.ReadByteToUint24(r, true)
packetStartCodePrefix, err = util.ReadByteToUint24(r, true)
if err != nil {
return
}
if header.PacketStartCodePrefix != 0x0000001 {
if packetStartCodePrefix != 0x0000001 {
err = errors.New("read PacketStartCodePrefix is not 0x0000001")
return
}
@@ -141,18 +133,27 @@ func ReadPESHeader(r io.Reader) (header MpegTsPESHeader, err error) {
if length == 0 {
length = 1 << 31
}
var header1 MpegPESHeader
header1, err = ReadPESHeader(r)
if err == nil {
if header.PesPacketLength == 0 {
header1.PesPacketLength = uint16(r.N)
}
header1.StreamID = header.StreamID
return header1, nil
}
return
}
// lrPacket 和 lrHeader 位置指针是在同一位置的
lrPacket := &io.LimitedReader{R: r, N: int64(length)}
lrHeader := lrPacket
func ReadPESHeader(lrPacket *io.LimitedReader) (header MpegPESHeader, err error) {
var flags uint8
// constTen(2)
// pes_ScramblingControl(2)
// pes_Priority(1)
// dataAlignmentIndicator(1)
// copyright(1)
// originalOrCopy(1)
flags, err = util.ReadByteToUint8(lrHeader)
flags, err = util.ReadByteToUint8(lrPacket)
if err != nil {
return
}
@@ -171,7 +172,7 @@ func ReadPESHeader(r io.Reader) (header MpegTsPESHeader, err error) {
// additionalCopyInfoFlag(1)
// pes_CRCFlag(1)
// pes_ExtensionFlag(1)
flags, err = util.ReadByteToUint8(lrHeader)
flags, err = util.ReadByteToUint8(lrPacket)
if err != nil {
return
}
@@ -185,14 +186,14 @@ func ReadPESHeader(r io.Reader) (header MpegTsPESHeader, err error) {
header.PesExtensionFlag = flags & 0x01
// pes_HeaderDataLength(8)
header.PesHeaderDataLength, err = util.ReadByteToUint8(lrHeader)
header.PesHeaderDataLength, err = util.ReadByteToUint8(lrPacket)
if err != nil {
return
}
length = uint(header.PesHeaderDataLength)
length := uint(header.PesHeaderDataLength)
lrHeader = &io.LimitedReader{R: lrHeader, N: int64(length)}
lrHeader := &io.LimitedReader{R: lrPacket, N: int64(length)}
// 00 -> PES 包头中既无任何PTS 字段也无任何DTS 字段存在
// 10 -> PES 包头中PTS 字段存在
@@ -219,6 +220,8 @@ func ReadPESHeader(r io.Reader) (header MpegTsPESHeader, err error) {
}
header.Dts = util.GetPtsDts(dts)
} else {
header.Dts = header.Pts
}
// reserved(2) + escr_Base1(3) + marker_bit(1) +
@@ -336,48 +339,31 @@ func ReadPESHeader(r io.Reader) (header MpegTsPESHeader, err error) {
}
}
// 2的16次方,16个字节
if lrPacket.N < 65536 {
// 这里得到的其实是负载长度,因为已经偏移过了Header部分.
//header.pes_PacketLength = uint16(lrPacket.N)
header.PayloadLength = uint64(lrPacket.N)
}
return
}
func WritePESHeader(w io.Writer, header MpegTsPESHeader) (written int, err error) {
if header.PacketStartCodePrefix != 0x0000001 {
err = errors.New("write PacketStartCodePrefix is not 0x0000001")
return
func (header *MpegPESHeader) WritePESHeader(esSize int) (w util.Buffer, err error) {
if header.DataAlignmentIndicator == 1 {
if header.Pts == header.Dts {
header.PtsDtsFlags = 0x80
header.PesHeaderDataLength = 5
} else {
header.PtsDtsFlags = 0xC0
header.PesHeaderDataLength = 10
}
} else {
header.PtsDtsFlags = 0
header.PesHeaderDataLength = 0
}
// packetStartCodePrefix(24) (0x000001)
if err = util.WriteUint24ToByte(w, header.PacketStartCodePrefix, true); err != nil {
return
pktLength := esSize + int(header.PesHeaderDataLength) + 3
if pktLength > 0xffff {
pktLength = 0
}
header.PesPacketLength = uint16(pktLength)
written += 3
// streamID(8)
if err = util.WriteUint8ToByte(w, header.StreamID); err != nil {
return
}
written += 1
// pes_PacketLength(16)
// PES包长度可能为0,这个时候,需要自己去算
// 0 <= len <= 65535
if err = util.WriteUint16ToByte(w, header.PesPacketLength, true); err != nil {
return
}
//fmt.Println("Length :", payloadLength)
//fmt.Println("PES Packet Length :", header.pes_PacketLength)
written += 2
w = header.header[:0]
w.WriteUint32(0x00000100 | uint32(header.StreamID))
w.WriteUint16(header.PesPacketLength)
// constTen(2)
// pes_ScramblingControl(2)
// pes_Priority(1)
@@ -385,18 +371,9 @@ func WritePESHeader(w io.Writer, header MpegTsPESHeader) (written int, err error
// copyright(1)
// originalOrCopy(1)
// 1000 0001
if header.ConstTen != 0x80 {
err = errors.New("pes header ConstTen != 0x80")
return
}
flags := header.ConstTen | header.PesScramblingControl | header.PesPriority | header.DataAlignmentIndicator | header.Copyright | header.OriginalOrCopy
if err = util.WriteUint8ToByte(w, flags); err != nil {
return
}
written += 1
flags := 0x80 | header.PesScramblingControl | header.PesPriority | header.DataAlignmentIndicator | header.Copyright | header.OriginalOrCopy
w.WriteByte(flags)
// pts_dts_Flags(2)
// escr_Flag(1)
// es_RateFlag(1)
@@ -405,19 +382,8 @@ func WritePESHeader(w io.Writer, header MpegTsPESHeader) (written int, err error
// pes_CRCFlag(1)
// pes_ExtensionFlag(1)
sevenFlags := header.PtsDtsFlags | header.EscrFlag | header.EsRateFlag | header.DsmTrickModeFlag | header.AdditionalCopyInfoFlag | header.PesCRCFlag | header.PesExtensionFlag
if err = util.WriteUint8ToByte(w, sevenFlags); err != nil {
return
}
written += 1
// pes_HeaderDataLength(8)
if err = util.WriteUint8ToByte(w, header.PesHeaderDataLength); err != nil {
return
}
written += 1
w.WriteByte(sevenFlags)
w.WriteByte(header.PesHeaderDataLength)
// PtsDtsFlags == 192(11), 128(10), 64(01)禁用, 0(00)
if header.PtsDtsFlags&0x80 != 0 {
// PTS和DTS都存在(11),否则只有PTS(10)
@@ -425,30 +391,121 @@ func WritePESHeader(w io.Writer, header MpegTsPESHeader) (written int, err error
// 11:PTS和DTS
// PTS(33) + 4 + 3
pts := util.PutPtsDts(header.Pts) | 3<<36
if err = util.WriteUint40ToByte(w, pts, true); err != nil {
if err = util.WriteUint40ToByte(&w, pts, true); err != nil {
return
}
written += 5
// DTS(33) + 4 + 3
dts := util.PutPtsDts(header.Dts) | 1<<36
if err = util.WriteUint40ToByte(w, dts, true); err != nil {
if err = util.WriteUint40ToByte(&w, dts, true); err != nil {
return
}
written += 5
} else {
// 10:只有PTS
// PTS(33) + 4 + 3
pts := util.PutPtsDts(header.Pts) | 2<<36
if err = util.WriteUint40ToByte(w, pts, true); err != nil {
if err = util.WriteUint40ToByte(&w, pts, true); err != nil {
return
}
}
}
return
}
written += 5
func (frame *MpegtsPESFrame) WritePESPacket(payload util.Memory, allocator *util.RecyclableMemory) (err error) {
var pesHeadItem util.Buffer
pesHeadItem, err = frame.WritePESHeader(payload.Size)
if err != nil {
return
}
pesBuffers := util.NewMemory(pesHeadItem)
payload.Range(pesBuffers.PushOne)
pesPktLength := int64(pesBuffers.Size)
pesReader := pesBuffers.NewReader()
var tsHeaderLength int
for i := 0; pesPktLength > 0; i++ {
var buffer util.Buffer = allocator.NextN(TS_PACKET_SIZE)
bwTsHeader := &buffer
bwTsHeader.Reset()
tsHeader := MpegTsHeader{
SyncByte: 0x47,
TransportErrorIndicator: 0,
PayloadUnitStartIndicator: 0,
TransportPriority: 0,
Pid: frame.Pid,
TransportScramblingControl: 0,
AdaptionFieldControl: 1,
ContinuityCounter: frame.ContinuityCounter,
}
frame.ContinuityCounter++
frame.ContinuityCounter = frame.ContinuityCounter % 16
// 每一帧的开头,当含有pcr的时候,包含调整字段
if i == 0 {
tsHeader.PayloadUnitStartIndicator = 1
// 当PCRFlag为1的时候,包含调整字段
if frame.IsKeyFrame {
tsHeader.AdaptionFieldControl = 0x03
tsHeader.AdaptationFieldLength = 7
tsHeader.PCRFlag = 1
tsHeader.RandomAccessIndicator = 1
tsHeader.ProgramClockReferenceBase = frame.Pts
}
}
// 每一帧的结尾,当不满足188个字节的时候,包含调整字段
if pesPktLength < TS_PACKET_SIZE-4 {
var tsStuffingLength uint8
tsHeader.AdaptionFieldControl = 0x03
tsHeader.AdaptationFieldLength = uint8(TS_PACKET_SIZE - 4 - 1 - pesPktLength)
// TODO:如果第一个TS包也是最后一个TS包,是不是需要考虑这个情况?
// MpegTsHeader最少占6个字节.(前4个走字节 + AdaptationFieldLength(1 byte) + 3个指示符5个标志位(1 byte))
if tsHeader.AdaptationFieldLength >= 1 {
tsStuffingLength = tsHeader.AdaptationFieldLength - 1
} else {
tsStuffingLength = 0
}
// error
tsHeaderLength, err = WriteTsHeader(bwTsHeader, tsHeader)
if err != nil {
return
}
if tsStuffingLength > 0 {
if _, err = bwTsHeader.Write(Stuffing[:tsStuffingLength]); err != nil {
return
}
}
tsHeaderLength += int(tsStuffingLength)
} else {
tsHeaderLength, err = WriteTsHeader(bwTsHeader, tsHeader)
if err != nil {
return
}
}
tsPayloadLength := TS_PACKET_SIZE - tsHeaderLength
//fmt.Println("tsPayloadLength :", tsPayloadLength)
// 这里不断的减少PES包
written, _ := io.CopyN(bwTsHeader, &pesReader, int64(tsPayloadLength))
// tmp := tsHeaderByte[3] << 2
// tmp = tmp >> 6
// if tmp == 2 {
// fmt.Println("fuck you mother.")
// }
pesPktLength -= written
tsPktByteLen := bwTsHeader.Len()
if tsPktByteLen != TS_PACKET_SIZE {
err = errors.New(fmt.Sprintf("%s, packet size=%d", "TS_PACKET_SIZE != 188,", tsPktByteLen))
return
}
}
return
return nil
}

View File

@@ -1,9 +1,11 @@
package mpegts
import (
"bytes"
"errors"
"fmt"
"io"
"m7s.live/v5/pkg/util"
)
@@ -179,50 +181,56 @@ func WritePSI(w io.Writer, pt uint32, psi MpegTsPSI, data []byte) (err error) {
return
}
cw := &util.Crc32Writer{W: w, Crc32: 0xffffffff}
// 使用buffer收集所有需要计算CRC32的数据
bw := &bytes.Buffer{}
// table id(8)
if err = util.WriteUint8ToByte(cw, tableId); err != nil {
if err = util.WriteUint8ToByte(bw, tableId); err != nil {
return
}
// sectionSyntaxIndicator(1) + zero(1) + reserved1(2) + sectionLength(12)
// sectionLength 前两个字节固定为00
// 1 0 11 sectionLength
if err = util.WriteUint16ToByte(cw, sectionSyntaxIndicatorAndSectionLength, true); err != nil {
if err = util.WriteUint16ToByte(bw, sectionSyntaxIndicatorAndSectionLength, true); err != nil {
return
}
// PAT TransportStreamID(16) or PMT ProgramNumber(16)
if err = util.WriteUint16ToByte(cw, transportStreamIdOrProgramNumber, true); err != nil {
if err = util.WriteUint16ToByte(bw, transportStreamIdOrProgramNumber, true); err != nil {
return
}
// reserved2(2) + versionNumber(5) + currentNextIndicator(1)
// 0x3 << 6 -> 1100 0000
// 0x3 << 6 | 1 -> 1100 0001
if err = util.WriteUint8ToByte(cw, versionNumberAndCurrentNextIndicator); err != nil {
if err = util.WriteUint8ToByte(bw, versionNumberAndCurrentNextIndicator); err != nil {
return
}
// sectionNumber(8)
if err = util.WriteUint8ToByte(cw, sectionNumber); err != nil {
if err = util.WriteUint8ToByte(bw, sectionNumber); err != nil {
return
}
// lastSectionNumber(8)
if err = util.WriteUint8ToByte(cw, lastSectionNumber); err != nil {
if err = util.WriteUint8ToByte(bw, lastSectionNumber); err != nil {
return
}
// data
if _, err = cw.Write(data); err != nil {
if _, err = bw.Write(data); err != nil {
return
}
// crc32
crc32 := util.BigLittleSwap(uint(cw.Crc32))
if err = util.WriteUint32ToByte(cw, uint32(crc32), true); err != nil {
// 写入PSI数据
if _, err = w.Write(bw.Bytes()); err != nil {
return
}
// 使用MPEG-TS CRC32算法计算CRC32
crc32 := GetCRC32(bw.Bytes())
if err = util.WriteUint32ToByte(w, crc32, true); err != nil {
return
}

20
pkg/format/ts/video.go Normal file
View File

@@ -0,0 +1,20 @@
package mpegts
import (
"m7s.live/v5/pkg"
"m7s.live/v5/pkg/codec"
"m7s.live/v5/pkg/format"
)
type VideoFrame struct {
format.AnnexB
}
func (a *VideoFrame) Mux(fromBase *pkg.Sample) (err error) {
if fromBase.GetBase().FourCC().Is(codec.FourCC_H265) {
a.PushOne(codec.AudNalu)
} else {
a.PushOne(codec.NALU_AUD_BYTE)
}
return a.AnnexB.Mux(fromBase)
}

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

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