]> 127.0.0.1 Git - bili_danmu/.git/commitdiff
局域网转直播服务
authorqydysky <qydysky@foxmail.com>
Tue, 13 Apr 2021 12:22:42 +0000 (20:22 +0800)
committerqydysky <qydysky@foxmail.com>
Tue, 13 Apr 2021 12:22:42 +0000 (20:22 +0800)
16 files changed:
CV/Var.go
F/api.go
F/cmd.go
F/xinxin.go
README.md
Reply/F.go
Reply/README.md
Reply/Reply.go
Reply/flvDecode.go [new file with mode: 0644]
Reply/tts.go
Send/Send.go
Send/Send_gift.go
Send/Send_pm.go
demo/config/config_K_v.json
demo/go.mod
demo/go.sum

index de5384fc055bda8adcd0669817652e2daf964e89..42f409a85ccc27f56335560b29cf18ff7bd111fc 100644 (file)
--- a/CV/Var.go
+++ b/CV/Var.go
@@ -35,6 +35,10 @@ var (
        LIVE_BUVID bool//cookies含LIVE_BUVID
 )
 
+var (
+       Stream_url string//直播Web服务
+)
+
 //消息队列
 type Danmu_Main_mq_item struct {
        Class string
index 7be40cbf430e1b7121d74500fafceeef1589bc35..5db478a5540370cd01018273ccc12bf58f57a8e0 100644 (file)
--- a/F/api.go
+++ b/F/api.go
@@ -17,6 +17,7 @@ import (
        g "github.com/qydysky/part/get"
        web "github.com/qydysky/part/web"
        reqf "github.com/qydysky/part/reqf"
+       limit "github.com/qydysky/part/limit"
        funcCtrl "github.com/qydysky/part/funcCtrl"
 
        uuid "github.com/gofrs/uuid"
@@ -26,7 +27,7 @@ import (
 )
 
 var apilog = c.Log.Base(`api`)
-var api_limit = p.Limit(1,2000,30000)//频率限制1次/2s,最大等待时间30s
+var api_limit = limit.New(1,2000,30000)//频率限制1次/2s,最大等待时间30s
 
 func Get(key string) {
        apilog := apilog.Base_add(`Get`).L(`T: `,key)
index b536ffc1bc8d26e0ab12fb273d8d8edc6293debd..2dcd6a74ab61c4d48c72070c2a668cecba42f4e3 100644 (file)
--- a/F/cmd.go
+++ b/F/cmd.go
@@ -82,6 +82,8 @@ func Cmd() {
                                }
                                cmdlog.L(`I: `, `舰长数:`, c.GuardNum)
                                cmdlog.L(`I: `, `分区排行:`, c.Note, `人气:`, c.Renqi)
+                               if c.Stream_url != ""{cmdlog.L(`I: `, `直播Web服务:`, c.Stream_url)}
+
                                continue
                        }
                        {//弹幕发送
index e83456ae782e1f621ea6ccea1001f68d8a8bc271..53050e579981e07bb6f62bb535b7aa49196ace8c 100644 (file)
@@ -114,7 +114,9 @@ func server() {
                })
        }
 
-       w := web.New(&http.Server{})//新建web实例
+       w := web.New(&http.Server{
+               Addr: "127.0.0.1:"+strconv.Itoa(p.Sys().GetFreePort()),
+       })//新建web实例
        w.Handle(map[string]func(http.ResponseWriter,*http.Request){//路径处理函数
                `/`:func(w http.ResponseWriter,r *http.Request){
                        var path string = r.URL.Path[1:]
index dbe7a36a2e11b63d1a210cd12b0f8dbde15923fb..3f4baed3d3ebc6d2b346cbef865c3874ea12b179 100644 (file)
--- a/README.md
+++ b/README.md
@@ -99,6 +99,22 @@ golang go version go1.15 linux/amd64
 ### 说明
 本项目使用github action自动构建,构建过程详见[yml](https://github.com/qydysky/bili_danmu/blob/master/.github/workflows/go.yml)
 
+#### 局域网转直播流服务
+启动Web流服务,为下载的直播流提供局域网内的流服务。
+
+在`demo/config/config_K_v.json`中可找到配置项,默认开启。
+
+```
+    "直播保存位置Web服务":true,
+```
+
+开启之后,启动会显示服务地址,在局域网内打开网址可以取得当前直播流(`dtmp`结尾)的flv流地址。可以在其他设备进行网络串流观看(如安卓mx player)。通过对获取flv直播流进行修改,使得无论何时打开局域网串流,播放的进度与当前直播时刻相同。服务地址也可通过命令行` room`查看。
+
+```
+I: 2021/04/13 20:07:45 命令行操作 [直播Web服务: http://192.168.31.245:38259]
+```
+
+
 #### 命令行操作
 在准备动作完成(`T: 2021/03/06 16:22:39 命令行操作 [回车查看帮助]`)后,输入回车将显示帮助
 ```
@@ -136,12 +152,13 @@ I: 2021/03/06 16:21:17 弹幕发送 [发送 1 至 7734200]
 - 查看房间信息
 ```
  room
-I: 2021/03/08 01:06:53 命令行操作 [当前直播间信息]
-I: 2021/03/08 01:06:53 命令行操作 [C酱です 213电竞俱乐部开业了! 直播中]
-I: 2021/03/08 01:06:53 命令行操作 [已直播时长: 07:56:53]
-I: 2021/03/08 01:06:53 命令行操作 [营收: ¥0.00]
-I: 2021/03/08 01:06:53 命令行操作 [舰长数: 1427]
-I: 2021/03/08 01:06:53 命令行操作 [分区排行: 单机游戏 19 人气: 1321956]
+I: 2021/04/13 20:04:56 命令行操作 [当前直播间信息]
+I: 2021/04/13 20:04:56 命令行操作 [哔哩哔哩英雄联盟赛事 【直播】EDG vs RNG 直播中]
+I: 2021/04/13 20:04:56 命令行操作 [已直播时长: 244:02:58]
+I: 2021/04/13 20:04:56 命令行操作 [营收: ¥3193.10]
+I: 2021/04/13 20:04:56 命令行操作 [舰长数: 16]
+I: 2021/04/13 20:04:56 命令行操作 [分区排行: 50+ 人气: 41802746]
+I: 2021/04/13 20:04:56 命令行操作 [直播Web服务: http://192.168.31.245:38259]
 ```
 #### cookie加密
 保护cookie.txt
index 259930bbe2618a5ae4d5040ae5eb1ce1c8caa3d0..5e5e50a2c3c2d4e9735b26d480373c9e224b710e 100644 (file)
@@ -9,6 +9,8 @@ import (
        "time"
        "os/exec"
     "path/filepath"
+    "net/http"
+       "context"
 
        c "github.com/qydysky/bili_danmu/CV"
        F "github.com/qydysky/bili_danmu/F"
@@ -18,8 +20,10 @@ import (
        funcCtrl "github.com/qydysky/part/funcCtrl"
        msgq "github.com/qydysky/part/msgq"
        reqf "github.com/qydysky/part/reqf"
+       web "github.com/qydysky/part/web"
        b "github.com/qydysky/part/buf"
        s "github.com/qydysky/part/signal"
+       limit "github.com/qydysky/part/limit"
 
        "github.com/christopher-dG/go-obs-websocket"
 )
@@ -618,24 +622,68 @@ func Autoskipf(s string) uint {
 
 type Lessdanmu struct {
        buf []string
+       limit *limit.Limit
+       max_num int
+       threshold float32
+
+       sync.RWMutex
 }
 
 var lessdanmu = Lessdanmu{
+       threshold:0.7,
 }
 
-func Lessdanmuf(s string, bufsize int) float32 {
-       if !IsOn("相似弹幕忽略") {return 0}
-       if len(lessdanmu.buf) < bufsize {
+func init() {
+       if max_num,ok := c.K_v.LoadV(`每10秒显示弹幕数`).(float64);ok && int(max_num) >= 1 {
+               flog.Base_add(`更少弹幕`).L(`T: `,`每10秒弹幕数:`,int(max_num))
+               lessdanmu.max_num = int(max_num)
+               lessdanmu.limit = limit.New(int(max_num),10000,0)
+               go func(){
+                       //等待启动
+                       for lessdanmu.limit.PTK() == lessdanmu.max_num {
+                               time.Sleep(time.Second*3)
+                       }
+       
+                       for {
+                               time.Sleep(time.Second*10)
+       
+                               lessdanmu.Lock()
+                               if ptk := lessdanmu.limit.PTK();ptk == lessdanmu.max_num {
+                                       if lessdanmu.threshold > 0.71 {
+                                               lessdanmu.threshold -= 0.01
+                                       }
+                               } else if ptk == 0 {
+                                       if lessdanmu.threshold < 0.99 {
+                                               lessdanmu.threshold += 0.01
+                                       }
+                               }
+                               lessdanmu.Unlock()
+                       }
+               }()
+       }
+}
+
+func Lessdanmuf(s string) (show bool) {
+       if !IsOn("相似弹幕忽略") {return true}
+       if len(lessdanmu.buf) < 20 {
                lessdanmu.buf = append(lessdanmu.buf, s)
-               return 0
+               return true
        }
 
        o := cross(s, lessdanmu.buf)
-       if o == 1 {return 1}//完全无用
+       if o == 1 {return false}//完全无用
+       
        Jiezouf(lessdanmu.buf)
        lessdanmu.buf = append(lessdanmu.buf[1:], s)
 
-       return o
+       lessdanmu.RLock()
+       show = o > lessdanmu.threshold
+       lessdanmu.RUnlock()
+
+       if show && lessdanmu.max_num > 0 {
+               lessdanmu.limit.TO()
+       }
+       return 
 }
 
 /*
@@ -893,4 +941,65 @@ func AutoSend_silver_gift() {
        } else {
                flog.L(`T: `,`完成`)
        }
+}
+
+//直播保存位置Web服务
+func init() {
+       if v,ok := c.K_v.LoadV(`直播保存位置Web服务`).(bool);ok && v {
+               base_dir := ""
+               if path,ok := c.K_v.LoadV(`直播流保存位置`).(string);ok && path !="" {
+                       if path,err := filepath.Abs(path);err == nil{
+                               base_dir = path+"/"
+                       }
+               }
+               s := web.New(&http.Server{
+                       Addr: "0.0.0.0:"+strconv.Itoa(p.Sys().GetFreePort()),
+               })
+               s.Handle(map[string]func(http.ResponseWriter,*http.Request){
+                       `/`:func(w http.ResponseWriter,r *http.Request){
+                               var path string = r.URL.Path[1:]
+                               if path == `` {
+                                       http.FileServer(http.Dir(base_dir)).ServeHTTP(w,r)
+                               } else {
+                                       path = base_dir+path
+
+                                       if !p.Checkfile().IsExist(path) {
+                                               w.WriteHeader(404)
+                                               return
+                                       }
+
+                                       w.WriteHeader(200)
+                                       if f, ok := w.(http.Flusher); ok { 
+                                               f.Flush() 
+                                       }
+
+                                       byteC := make(chan []byte,1024*1024)
+                                       cancel := make(chan struct{})
+                                       defer close(cancel)
+                                       go func() {
+                                               if e := Stream(path,byteC,cancel);e != nil {
+                                                       flog.Base_add(`直播Web服务`).L(`T: `,`E: `,e);
+                                                       return
+                                               }
+                                       }()
+
+                                       for {
+                                               buf := <- byteC
+                                               if len(buf) == 0 {break}
+
+                                               if _,err := w.Write(buf);err != nil {
+                                                       flog.Base_add(`直播Web服务`).L(`T: `,`E: `,err);
+                                                       break
+                                               }
+                                       }
+                               }
+                       },
+                       `/exit`:func(w http.ResponseWriter,r *http.Request){
+                               s.Server.Shutdown(context.Background())
+                       },
+               })
+               host := p.Sys().GetIntranetIp()
+               c.Stream_url = strings.Replace(`http://`+s.Server.Addr,`0.0.0.0`,host,-1)
+               flog.Base_add(`直播Web服务`).L(`I: `,`启动于`,c.Stream_url)
+       }
 }
\ No newline at end of file
index 7e40a9d4a14982c77436b5a29b37cf96e560e3bd..7ab39e1c78133ceb1ac9ae239019de6c07580d9a 100644 (file)
@@ -8,6 +8,7 @@
 |ws_msg/|弹幕服务器传回的Msg类型json的golang struct表述|
 |0Init.go|最初始载入配置|
 |F.go|附加功能|
+|flvDecode.go|直播流转局域网直播流|
 |Heartbeat.go|人气数据分派|
 |Msg.go|消息数据分派|
 |Reply.go|分派数据处理|
index f140007210f1490ad29ddb173909af28c93fd3d0..c4efe132aa51cc10af8b32af5b79da8731ff07a1 100644 (file)
@@ -25,6 +25,8 @@ var reply_log = c.Log.Base(`Reply`)
 func Reply(b []byte) {
        reply_log := reply_log.Base_add(`返回分派`)
 
+       if len(b) <= c.WS_PACKAGE_HEADER_TOTAL_LENGTH {reply_log.L(`W: `,"包缺损");return}
+
        head := F.HeadChe(b[:c.WS_PACKAGE_HEADER_TOTAL_LENGTH])
        if int(head.PackL) > len(b) {reply_log.L(`E: `,"包缺损");return}
 
@@ -744,7 +746,7 @@ func Msg_showdanmu(auth interface{}, m ...string) {
        msg := m[0]
        msglog := msglog.Log_show_control(false)
        {//附加功能 更少弹幕
-               if Lessdanmuf(msg, 20) > 0.7 {//与前20条弹幕重复的字数占比度>0.7的屏蔽
+               if !Lessdanmuf(msg) {
                        if auth != nil {msglog.L(`I: `, auth, ":", msg)}
                        return
                }
diff --git a/Reply/flvDecode.go b/Reply/flvDecode.go
new file mode 100644 (file)
index 0000000..4a2ed33
--- /dev/null
@@ -0,0 +1,130 @@
+package reply
+
+import (
+       "os"
+       "bytes"
+       "time"
+       "errors"
+
+       c "github.com/qydysky/bili_danmu/CV"
+       F "github.com/qydysky/bili_danmu/F"
+)
+
+const (
+       flv_header_size = 9
+       tag_header_size = 11
+       previou_tag_size = 4
+
+       video_tag = byte(0x09)
+       audio_tag = byte(0x08)
+       script_tag = byte(0x12)
+
+       //custom define
+       eof_tag = byte(0x00)
+       copy_buf_size = 1024*1024
+)
+
+var (
+       flv_header_sign = []byte{0x46,0x4c,0x56}
+       flvlog = c.Log.Base(`flv解码`)
+)
+
+func Stream(path string,streamChan chan []byte,cancel chan struct{}) (error) {
+       //
+       defer flvlog.L(`T: `,`退出`)
+       //file
+       f,err := os.OpenFile(path,os.O_RDONLY,0644)
+       if err != nil {
+               return err
+       }
+       defer f.Close()
+       defer close(streamChan)
+
+       //get flv header(9byte) + FirstTagSize(4byte)
+       {
+               f.Seek(0,0)
+               buf := make([]byte, flv_header_size+previou_tag_size)
+               if _,err := f.Read(buf);err != nil {return err}
+               if !bytes.Contains(buf,flv_header_sign) {return errors.New(`no flv`)}
+               streamChan <- buf
+       }
+
+       //get tag func
+       var getTag = func(f *os.File)(tag byte,offset int64,buf_p *[]byte,data_p *[]byte){
+               buf := make([]byte, tag_header_size)
+               if _,err := f.Read(buf);err != nil {tag = eof_tag;return}
+               tag = buf[0]
+               size := F.Btoi32(append([]byte{0x00},buf[1:4]...),0)
+
+               data := make([]byte, size+previou_tag_size)
+               if _,err := f.Read(data);err != nil {tag = eof_tag;return}
+               
+               offset,_ = f.Seek(0,1)
+               offset -= tag_header_size+int64(size)+previou_tag_size
+               return tag,offset,&buf,&data
+       }
+
+       //get first video and audio tag
+       //find last_keyframe_video_offset
+       var last_keyframe_video_offset int64
+       first_video_tag,first_audio_tag := false,false
+       for {
+               tag,offset,buf_p,data_p := getTag(f)
+               if tag == script_tag {
+                       streamChan <- *buf_p
+                       streamChan <- *data_p
+               } else if tag == video_tag {
+                       if !first_video_tag {
+                               first_video_tag = true
+                               streamChan <- *buf_p
+                               streamChan <- *data_p
+                       }
+
+                       if (*data_p)[0] & 0xf0 == 0x10 {
+                               last_keyframe_video_offset = offset
+                       }
+               } else if tag == audio_tag {
+                       if !first_audio_tag {
+                               first_audio_tag = true
+                               streamChan <- *buf_p
+                               streamChan <- *data_p
+                       }
+               } else {//eof_tag
+                       break;
+               }
+       }
+
+       //seed to last tag
+       f.Seek(last_keyframe_video_offset,0)
+
+       //copy
+       {
+               buf := make([]byte, copy_buf_size)
+               eof_wait_turn := 3
+               for {
+
+                       //退出
+                       select {
+                       case <-cancel:return nil;
+                       default:;
+                       }
+
+                       size,err := f.Read(buf)
+                       if err != nil {
+                               if err.Error() != `EOF` {
+                                       return err
+                               }
+                               if eof_wait_turn < 0 {break}
+                               eof_wait_turn -= 1
+                       }
+
+                       if size > 0 {
+                               streamChan <- buf[:size]
+                       }
+
+                       if eof_wait_turn > 0 {time.Sleep(time.Second*3)}
+               }
+       }
+
+       return nil
+}
index 3cd253d03ce5fddc3ddcf6e261c21f42d4e573b0..edc6b0eebccce5cb81ba5d8559f00034a80bfc7b 100644 (file)
@@ -10,6 +10,7 @@ import (
        msgq "github.com/qydysky/part/msgq"
        s "github.com/qydysky/part/buf"
        reqf "github.com/qydysky/part/reqf"
+       limit "github.com/qydysky/part/limit"
 )
 
 var tts_setting = map[string]string{
@@ -19,7 +20,7 @@ var tts_setting = map[string]string{
 }
 var tts_List = make(chan interface{},20)
 
-var tts_limit = p.Limit(1,5000,15000)//频率限制1次/5s,最大等待时间15s
+var tts_limit = limit.New(1,5000,15000)//频率限制1次/5s,最大等待时间15s
 
 var tts_log = c.Log.Base_add(`TTS`)
 
index dd9d760bdddbbe5f79d4e7ef7e84fdcc61c245f9..6fd863c5d5446312508db145b85c32fc33293da3 100644 (file)
@@ -9,10 +9,11 @@ import (
 
        p "github.com/qydysky/part"
        reqf "github.com/qydysky/part/reqf"
+       limit "github.com/qydysky/part/limit"
 )
 
 //每5s一个令牌,最多等20秒
-var danmu_s_limit = p.Limit(1, 5000, 20000)
+var danmu_s_limit = limit.New(1, 5000, 20000)
 
 //弹幕发送
 func Danmu_s(msg string, roomid int) {
index 338f0e83baf6578622da660fa8658c6c791a70d3..08570930aaee61db3a15f284102e541648188859 100644 (file)
@@ -8,12 +8,12 @@ import (
 
        c "github.com/qydysky/bili_danmu/CV"
 
-       p "github.com/qydysky/part"
        reqf "github.com/qydysky/part/reqf"
+       limit "github.com/qydysky/part/limit"
 )
 
 //每2s一个令牌,最多等10秒
-var gift_limit = p.Limit(1, 2000, 10000)
+var gift_limit = limit.New(1, 2000, 10000)
 
 func Send_gift(gift_id,bag_id,gift_num int) {
        log := c.Log.Base_add(`发送礼物`)
index eca5e8150aeb614d964581f6d0132b79e5ff11e2..338ac0fe38c2128fbb2b19a98d052baa248c67a9 100644 (file)
@@ -9,6 +9,7 @@ import (
 
        p "github.com/qydysky/part"
        reqf "github.com/qydysky/part/reqf"
+       limit "github.com/qydysky/part/limit"
 
        uuid "github.com/gofrs/uuid"
 )
@@ -19,7 +20,7 @@ type Pm_item struct {
 }
 
 //每5s一个令牌,最多等10秒
-var pm_limit = p.Limit(1, 5000, 10000)
+var pm_limit = limit.New(1, 5000, 10000)
 
 func Send_pm(uid int, msg string) error {
        if msg == `` || uid == 0 {
index fb03596ead55b70a4c3b9027acdf55e4fd39963f..2b0c7f0224a5fada112c8d2e01f6122c3ae12fd5 100644 (file)
     "直播流清晰度-help": "清晰度可选-1:不保存 0:默认 10000:原画 800:4K 401:蓝光(杜比) 400:蓝光 250:超清 150:高清 80:流畅,无提供所选清晰度时,使用低一档清晰度",
     "flv直播流清晰度": 150,
     "直播流保存位置": "./live",
+    "直播保存位置Web服务":true,
     "ass-help": "只有保存直播流时才考虑生成ass",
     "生成Ass弹幕": true,
     "弹幕处理": "",
     "弹幕合并": true,
     "相似弹幕忽略": true,
+    "每10秒显示弹幕数-help": "为0时禁用,相似弹幕忽略需设为true",
+    "每10秒显示弹幕数": 14,
     "精简弹幕": true,
     "弹幕机": "",
     "反射弹幕机": true,
index 73f53b373ac7d297aa66f50f97ae212aab656a3b..9dbd872ef6b11c1c5c5eb14ce4f5703311b964be 100644 (file)
@@ -13,7 +13,7 @@ require (
        github.com/miekg/dns v1.1.41 // indirect
        github.com/mitchellh/mapstructure v1.4.1 // indirect
        github.com/qydysky/bili_danmu v0.5.7
-       github.com/qydysky/part v0.5.4 // indirect
+       github.com/qydysky/part v0.5.9 // indirect
        github.com/shirou/gopsutil v3.21.3+incompatible // indirect
        github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e // indirect
        github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966 // indirect
index 75ecaea1bd1883ecca0f216347d6091d72d284ff..8b82e9d1695b49c8d8749179a7ff8e52acbd6180 100644 (file)
@@ -257,6 +257,16 @@ github.com/qydysky/part v0.5.3 h1:bvNvXOdzZr0nd3p+VQDgvGzugwuvk+yNtO6oTD32trw=
 github.com/qydysky/part v0.5.3/go.mod h1:43opuciW71sZvOR67kye50jgMDSDrn/t6+LefNdlXPg=
 github.com/qydysky/part v0.5.4 h1:o7nslWdOM94wGEGGPWK0uscx8NR3UlJpnKTQCQxBPfQ=
 github.com/qydysky/part v0.5.4/go.mod h1:43opuciW71sZvOR67kye50jgMDSDrn/t6+LefNdlXPg=
+github.com/qydysky/part v0.5.5 h1:FDhG5s3tmVBm3tzN0p7cwFPkAOH6Z9zl91zznqNS/Yk=
+github.com/qydysky/part v0.5.5/go.mod h1:43opuciW71sZvOR67kye50jgMDSDrn/t6+LefNdlXPg=
+github.com/qydysky/part v0.5.6 h1:EDzYYb9H9rHB0uwnOeDrhKU/7VFjxk1vS6Dh+x4ZgcM=
+github.com/qydysky/part v0.5.6/go.mod h1:43opuciW71sZvOR67kye50jgMDSDrn/t6+LefNdlXPg=
+github.com/qydysky/part v0.5.7 h1:xHwEZIV95I9N6rJqvfDGbn6ivJec6mk7RpMYnLRxlV0=
+github.com/qydysky/part v0.5.7/go.mod h1:43opuciW71sZvOR67kye50jgMDSDrn/t6+LefNdlXPg=
+github.com/qydysky/part v0.5.8 h1:sEGsa9ftwHSCu1yc9AjG2PCVE/kLHLkUpYTVYWJvV6Y=
+github.com/qydysky/part v0.5.8/go.mod h1:43opuciW71sZvOR67kye50jgMDSDrn/t6+LefNdlXPg=
+github.com/qydysky/part v0.5.9 h1:XZxBs6bBVQCKH48791X8ebrReY50aGw9IFCdaM1QhGA=
+github.com/qydysky/part v0.5.9/go.mod h1:43opuciW71sZvOR67kye50jgMDSDrn/t6+LefNdlXPg=
 github.com/qydysky/part/msgq v0.0.0-20201213031129-ca3253dc72ad h1:Jtzf509lQrkUMGTV0Sc6IDCAiR1VrBcHrIban7hpye4=
 github.com/qydysky/part/msgq v0.0.0-20201213031129-ca3253dc72ad/go.mod h1:w32TkJNVtTJd4LOS09cq+4uYG6itcN2vsqw+slp44Rg=
 github.com/qydysky/part/msgq v0.0.0-20201213120821-f36e49c32bba h1:1ew9dRpc0Rux0WkWeT/4AE15ynYWmL2D7onJEJIFOB8=