--- /dev/null
+.directory
--- /dev/null
+package bili_danmu
+
+import (
+ "bytes"
+ "encoding/binary"
+
+ p "github.com/qydysky/part"
+)
+/*
+ 整数 字节转换区
+ 32 4字节
+ 16 2字节
+*/
+func Itob32(num int32) []byte {
+ var buffer bytes.Buffer
+ err := binary.Write(&buffer, binary.BigEndian, num)
+ if err != nil {p.Logf().E(err)}
+ return buffer.Bytes()
+}
+
+func Itob16(num int16) []byte {
+ var buffer bytes.Buffer
+ err := binary.Write(&buffer, binary.BigEndian, num)
+ if err != nil {p.Logf().E(err)}
+ return buffer.Bytes()
+}
+
+func Btoi32(b []byte) int32 {
+ var buffer int32
+ err := binary.Read(bytes.NewReader(b), binary.BigEndian, &buffer)
+ if err != nil {p.Logf().E(err)}
+ return buffer
+}
+
+func Btoi16(b []byte) int16 {
+ var buffer int16
+ err := binary.Read(bytes.NewReader(b), binary.BigEndian, &buffer)
+ if err != nil {p.Logf().E(err)}
+ return buffer
+}
\ No newline at end of file
--- /dev/null
+package bili_danmu
+
+import (
+ "bytes"
+ "compress/zlib"
+
+ p "github.com/qydysky/part"
+)
+/*
+ 数据为WS_OP_MESSAGE类型的
+*/
+
+var msglog = p.Logf().New().Level(0)
+
+func Msg(b []byte, compress bool) {
+ if compress {
+ readc, err := zlib.NewReader(bytes.NewReader(b[16:]))
+ if err != nil {msglog.E("解压错误");return}
+ defer readc.Close()
+
+ buf := bytes.NewBuffer(nil)
+ if _, err := buf.ReadFrom(readc);err != nil {msglog.E("解压错误");return}
+ b = buf.Bytes()
+ }
+
+ for len(b) != 0 {
+
+ var packL int32
+ if ist, packl := headChe(b[:16], len(b), WS_BODY_PROTOCOL_VERSION_NORMAL, WS_OP_MESSAGE, 0, 0); !ist {
+ msglog.E("头错误");return
+ } else {
+ packL = packl
+ }
+
+ s := string(b[16:packL])
+ b = b[packL:]
+ if cmd := p.Json().GetValFromS(s, "cmd");cmd == nil {
+ msglog.E("->", "cmd", s)
+ return
+ } else {
+ switch cmd.(string) {
+ case "INTERACT_WORD":;
+ case "ACTIVITY_BANNER_UPDATE_V2":;
+ case "SEND_GIFT":;//礼物
+ case "NOTICE_MSG":;//礼物公告
+ case "ROOM_BANNER":;//未知
+ case "ONLINERANK":;//未知
+ case "WELCOME":;//进入提示
+ case "ROOM_SILENT_OFF", "ROOM_SILENT_ON":;
+ case "HOUR_RANK_AWARDS":;
+ case "ROOM_RANK":;
+ case "WELCOME_GUARD":;
+ case "SUPER_CHAT_MESSAGE", "SUPER_CHAT_MESSAGE_JPN":super_chat_message(s);
+ case "PANEL":panel(s);
+ case "ENTRY_EFFECT":entry_effect(s)
+ case "ROOM_REAL_TIME_MESSAGE_UPDATE":roominfo(s)
+ case "DANMU_MSG":danmu(s)
+ default:msglog.I("Unknow cmd", s)
+ }
+ }
+ }
+
+ return
+}
+
+func super_chat_message(s string){
+ uname := p.Json().GetValFromS(s, "data.user_info.uname");
+ price := p.Json().GetValFromS(s, "data.price");
+ message := p.Json().GetValFromS(s, "data.message");
+ message_jpn := p.Json().GetValFromS(s, "data.message_jpn");
+
+ var sh []interface{}
+
+ if uname != nil {
+ sh = append(sh, []interface{}{uname.(string)})
+ }
+ if price != nil {
+ sh = append(sh, []interface{}{"¥", int64(price.(float64))})
+ }
+ if message != nil {
+ sh = append(sh, []interface{}{message.(string)})
+ }
+ if message_jpn != nil {
+ sh = append(sh, []interface{}{message_jpn.(string)})
+ }
+
+ if len(sh) != 0 {msglog.I("打赏: ", sh)}
+}
+
+func panel(s string){
+ if note := p.Json().GetValFromS(s, "data.note");note == nil {
+ msglog.E("->", "note", note)
+ return
+ } else {
+ msglog.I(note.(string))
+ }
+
+}
+
+func entry_effect(s string){
+ if copy_writing := p.Json().GetValFromS(s, "data.copy_writing");copy_writing == nil {
+ msglog.E("->", "copy_writing", copy_writing)
+ return
+ } else {
+ msglog.I(copy_writing.(string))
+ }
+
+}
+
+func roomsilent(s string){
+ if level := p.Json().GetValFromS(s, "data.level");level == nil {
+ msglog.E("->", "level", level)
+ return
+ } else {
+ if level.(float64) == 0 {msglog.I("主播关闭了禁言")}
+ msglog.I("主播开启了等级禁言:", int64(level.(float64)))
+ }
+
+}
+
+func roominfo(s string){
+ fans := p.Json().GetValFromS(s, "data.fans");
+ fans_club := p.Json().GetValFromS(s, "data.fans_club");
+
+ var sh []interface{}
+
+ if fans != nil {
+ sh = append(sh, []interface{}{"粉丝总人数:", int64(fans.(float64))})
+ }
+ if fans_club != nil {
+ sh = append(sh, []interface{}{"粉丝团人数:", int64(fans_club.(float64))})
+ }
+
+ if len(sh) != 0 {msglog.I(sh)}
+}
+
+func danmu(s string) {
+ if info := p.Json().GetValFromS(s, "info");info == nil {
+ msglog.E("->", "info", info)
+ return
+ } else {
+ infob := info.([]interface{})
+ msg := infob[1].(string)
+ auth := infob[2].([]interface{})[1].(string)
+ msglog.I(auth, ":", msg)
+ }
+}
--- /dev/null
+package bili_danmu
+
+import (
+ "strconv"
+
+ p "github.com/qydysky/part"
+)
+
+type api struct {
+ Roomid int
+ Uid int
+ Url []string
+ Token string
+}
+
+func New_api(Roomid int) (o *api) {
+ l := p.Logf().New().Level(LogLevel).I("New_api")
+ defer l.Block()
+
+ l.I("->", "ok")
+ o = new(api)
+ o.Roomid = Roomid
+ o.Get_info()
+
+ return
+}
+
+func (i *api) Get_info() (o *api) {
+ o = i
+ l := p.Logf().New().Level(LogLevel).I("*api.Get_info")
+ defer l.Block()
+
+ if o.Roomid == 0 {
+ l.E("->", "还未New_api")
+ return
+ }
+ Roomid := strconv.Itoa(o.Roomid)
+
+ req := p.Req()
+ if err := req.Reqf(p.Rval{
+ Url:"https://api.live.bilibili.com/room/v1/Room/room_init?id=" + Roomid,
+ Referer:"https://live.bilibili.com/" + Roomid,
+ Timeout:10,
+ Retry:2,
+ });err != nil {
+ l.E("->", err)
+ return
+ }
+ res := string(req.Respon)
+ if msg := p.Json().GetValFrom(res, "msg");msg == nil || msg != "ok" {
+ l.E("->", "msg", msg)
+ return
+ }
+ if Uid := p.Json().GetValFrom(res, "data.uid");Uid == nil {
+ l.E("->", "data.uid", Uid)
+ return
+ } else {
+ o.Uid = int(Uid.(float64))
+ }
+
+ if room_id := p.Json().GetValFrom(res, "data.room_id");room_id == nil {
+ l.E("->", "data.room_id", room_id)
+ return
+ } else {
+ l.I("->", "ok")
+ o.Roomid = int(room_id.(float64))
+ }
+ return
+}
+
+func (i *api) Get_host_Token() (o *api) {
+ o = i
+ l := p.Logf().New().Level(LogLevel).I("*api.Get_host_Token")
+ defer l.Block()
+
+ if o.Roomid == 0 {
+ l.E("->", "还未New_api")
+ return
+ }
+ Roomid := strconv.Itoa(o.Roomid)
+
+
+ req := p.Req()
+ if err := req.Reqf(p.Rval{
+ Url:"https://api.live.bilibili.com/xlive/web-room/v1/index/getDanmuInfo?type=0&id=" + Roomid,
+ Referer:"https://live.bilibili.com/" + Roomid,
+ Timeout:10,
+ Retry:2,
+ });err != nil {
+ l.E("->", err)
+ return
+ }
+ res := string(req.Respon)
+ if msg := p.Json().GetValFrom(res, "message");msg == nil || msg != "0" {
+ l.E("->", "message", msg)
+ return
+ }
+
+ _Token := p.Json().GetValFrom(res, "data.token")
+ if _Token == nil {
+ l.E("->", "data.token", _Token, res)
+ return
+ }
+ o.Token = _Token.(string)
+
+ if host_list := p.Json().GetValFrom(res, "data.host_list");host_list == nil {
+ l.E("->", "data.host_list", host_list)
+ return
+ } else {
+ for k, v := range host_list.([]interface{}) {
+ if _host := p.Json().GetValFrom(v, "host");_host == nil {
+ l.E("->", "data.host_list[", k, "].host", _host)
+ continue
+ } else {
+ o.Url = append(o.Url, "wss://" + _host.(string) + "/sub")
+ }
+ }
+ l.I("->", "ok")
+ }
+
+ return
+}
\ No newline at end of file
--- /dev/null
+package bili_danmu
+
+import (
+ "fmt"
+ "bytes"
+ "strconv"
+ "os"
+ "os/signal"
+
+ p "github.com/qydysky/part"
+)
+
+const LogLevel = 3
+
+func Demo() {
+ l:=p.Logf().New().Level(LogLevel)
+ defer l.Block()
+
+ //ctrl+c退出
+ interrupt := make(chan os.Signal, 1)
+ signal.Notify(interrupt, os.Interrupt)
+
+ {
+ var room int
+ fmt.Printf("输入房间号: ")
+ _, err := fmt.Scanln(&room)
+ if err != nil {
+ l.E("输入错误", err)
+ return
+ }
+
+ var break_sign bool
+ for !break_sign {
+ //获取房间相关信息
+ api := New_api(room).Get_host_Token()
+ if len(api.Url) == 0 || api.Roomid == 0 || api.Token == "" {
+ l.E("some err")
+ return
+ }
+
+ //对每个弹幕服务器尝试
+ for _, v := range api.Url {
+ //ws启动
+ ws := New_ws(v).Handle()
+ go func(){
+ <- interrupt
+ ws.Close()
+ break_sign = true
+ }()
+
+ //SendChan 传入发送[]byte
+ //RecvChan 接收[]byte
+ l.I("send hello to", v)
+ ws.SendChan <- hello_send(api.Roomid, api.Token)
+ if hello_ok(<- ws.RecvChan) {
+ l.I(v, "hello!")
+
+ //开始心跳
+ go func(){
+ p.Sys().MTimeoutf(500)//500ms
+ heartbeatmsg, heartinterval := heartbeat()
+ ws.Heartbeat(1000 * heartinterval, heartbeatmsg)
+ }()
+ }
+
+ for {
+ i := <- ws.RecvChan
+ if len(i) == 0 && ws.Isclose() {
+ break
+ } else {
+ go Reply(i)
+ }
+ }
+
+ if break_sign {break}
+
+ p.Sys().Timeoutf(1)
+ }
+ }
+ }
+}
+
+//from player-loader-2.0.4.min.js
+const (
+ WS_PACKAGE_HEADER_TOTAL_LENGTH = 16
+ WS_HEADER_DEFAULT_VERSION = 1
+ WS_BODY_PROTOCOL_VERSION_NORMAL = 0
+ WS_BODY_PROTOCOL_VERSION_DEFLATE = 2
+ WS_OP_USER_AUTHENTICATION = 7
+ WS_OP_HEARTBEAT = 2
+ WS_HEADER_DEFAULT_SEQUENCE = 1
+ WS_OP_CONNECT_SUCCESS = 8
+ WS_OP_MESSAGE = 5
+ WS_OP_HEARTBEAT_REPLY = 3
+)
+
+//返回数据分派
+func Reply(b []byte) {
+ l := p.Logf().New().Level(LogLevel)
+ defer l.Block()
+
+ if ist, _ := headChe(b[:16], len(b), WS_BODY_PROTOCOL_VERSION_DEFLATE, WS_OP_MESSAGE, 0, 4); ist {
+ Msg(b, true);return
+ }
+ if ist, _ := headChe(b[:16], len(b), WS_BODY_PROTOCOL_VERSION_NORMAL, WS_OP_MESSAGE, 0, 4); ist {
+ Msg(b, false);return
+ }
+
+ if ist, _ := headChe(b[:16], len(b), WS_HEADER_DEFAULT_VERSION, WS_OP_HEARTBEAT_REPLY, WS_HEADER_DEFAULT_SEQUENCE, 4); ist {
+ l.I("heartbeat replay!");
+ return
+ }
+
+ l.I("unknow reply", b)
+}
+
+//头部生成与检查
+func headGen(datalenght,Opeation,Sequence int) []byte {
+ var buffer bytes.Buffer //Buffer是一个实现了读写方法的可变大小的字节缓冲
+
+ buffer.Write(Itob32(int32(datalenght + WS_PACKAGE_HEADER_TOTAL_LENGTH)))
+ buffer.Write(Itob16(WS_PACKAGE_HEADER_TOTAL_LENGTH))
+ buffer.Write(Itob16(WS_HEADER_DEFAULT_VERSION))
+ buffer.Write(Itob32(int32(Opeation)))
+ buffer.Write(Itob32(int32(Sequence)))
+
+ return buffer.Bytes()
+}
+
+func headChe(head []byte, datalenght,Bodyv,Opeation,Sequence,show int) (bool,int32) {
+ if len(head) != WS_PACKAGE_HEADER_TOTAL_LENGTH {return false, 0}
+
+ l := p.Logf().New().Level(show)
+ defer l.Block()
+
+ packL := Btoi32(head[:4])
+ headL := Btoi16(head[4:6])
+ BodyV := Btoi16(head[6:8])
+ OpeaT := Btoi32(head[8:12])
+ Seque := Btoi32(head[12:16])
+
+ if packL > int32(datalenght) {l.E("包缺损", packL, datalenght);return false, packL}
+ if headL != WS_PACKAGE_HEADER_TOTAL_LENGTH {l.E("头错误", headL);return false, packL}
+ if OpeaT != int32(Opeation) {l.E("类型错误");return false, packL}
+ if Seque != int32(Sequence) {l.E("Seq错误");return false, packL}
+ if BodyV != int16(Bodyv) {l.E("压缩算法错误");return false, packL}
+ return true, packL
+}
+
+//认证生成与检查
+func hello_send(roomid int, key string) []byte {
+ l := p.Logf().New().Level(LogLevel).I("hello_ws")
+ defer l.Block()
+
+ if roomid == 0 || key == "" {
+ l.E("->", "roomid == 0 || key == \"\"")
+ return []byte("")
+ }
+
+ //from player-loader-2.0.4.min.js
+ /*
+ customAuthParam
+ */
+ const (
+ _uid = 0
+ _protover = 2
+ _platform = "web"
+ VERSION = "2.0.4"
+ _type = 2
+ )
+
+ var obj = `{"uid":` + strconv.Itoa(_uid) +
+ `,"roomid":` + strconv.Itoa(roomid) +
+ `,"protover":` + strconv.Itoa(_protover) +
+ `,"platform":"`+ _platform +
+ `","clientver":"` + VERSION +
+ `","type":` + strconv.Itoa(_type) +
+ `,"key":"` + key + `"}`
+
+ var buffer bytes.Buffer //Buffer是一个实现了读写方法的可变大小的字节缓冲
+
+ buffer.Write(headGen(len(obj), WS_OP_USER_AUTHENTICATION, WS_HEADER_DEFAULT_SEQUENCE))
+
+ buffer.Write([]byte(obj))
+
+ return buffer.Bytes()
+}
+
+func hello_ok(r []byte) bool {
+ if len(r) == 0 {return false}
+
+ var obj = `{"code":0}`
+
+ var buffer bytes.Buffer //Buffer是一个实现了读写方法的可变大小的字节缓冲
+
+ buffer.Write(headGen(len(obj), WS_OP_CONNECT_SUCCESS, WS_HEADER_DEFAULT_SEQUENCE))
+
+ buffer.Write([]byte(obj))
+
+ h := buffer.Bytes()
+
+ if len(h) != len(r) {return false}
+
+ for k, v := range r {
+ if v != h[k] {return false}
+ }
+ return true
+}
+
+//心跳生成
+func heartbeat() ([]byte, int) {
+ //from player-loader-2.0.4.min.js
+ const heartBeatInterval = 30
+
+ var obj = `[object Object]`
+
+ var buffer bytes.Buffer //Buffer是一个实现了读写方法的可变大小的字节缓冲
+
+ buffer.Write(headGen(len(obj), WS_OP_HEARTBEAT, WS_HEADER_DEFAULT_SEQUENCE))
+
+ buffer.Write([]byte(obj))
+
+ return buffer.Bytes(), heartBeatInterval
+
+}
\ No newline at end of file
--- /dev/null
+package bili_danmu
+
+import (
+ "testing"
+)
+
+func Test_bili_danmu(t *testing.T) {
+
+}
+
--- /dev/null
+module github.com/qydysky/bili_danmu/demo
+
+go 1.14
--- /dev/null
+package main
+
+import (
+ q "github.com/qydysky/bili_danmu"
+)
+
+func main() {
+ q.Demo()
+}
\ No newline at end of file
--- /dev/null
+module github.com/qydysky/bili_danmu
+
+go 1.14
+
+require (
+ github.com/gorilla/websocket v1.4.2
+ github.com/klauspost/compress v1.11.0 // indirect
+ github.com/qydysky/part v0.0.0-20200914123330-afade058e33d
+ github.com/shirou/gopsutil v2.20.8+incompatible // indirect
+ golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a // indirect
+ golang.org/x/net v0.0.0-20200904194848-62affa334b73 // indirect
+ golang.org/x/sys v0.0.0-20200909081042-eff7692f9009 // indirect
+)
+
+//replace github.com/qydysky/part => ../part
--- /dev/null
+github.com/StackExchange/wmi v0.0.0-20190523213315-cbe66965904d h1:G0m3OIz70MZUWq3EgK3CesDbo8upS2Vm9/P3FtgI+Jk=
+github.com/StackExchange/wmi v0.0.0-20190523213315-cbe66965904d/go.mod h1:3eOhrUMpNV+6aFIbp5/iudMxNCF27Vw2OZgy4xEx0Fg=
+github.com/go-ole/go-ole v1.2.4 h1:nNBDSCOigTSiarFpYE9J/KtEA1IOW4CNeqT9TQDqCxI=
+github.com/go-ole/go-ole v1.2.4/go.mod h1:XCwSNxSkXRo4vlyPy93sltvi/qJq0jqQhjqQNIwKuxM=
+github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc=
+github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
+github.com/klauspost/compress v1.10.10 h1:a/y8CglcM7gLGYmlbP/stPE5sR3hbhFRUjCBfd/0B3I=
+github.com/klauspost/compress v1.10.10/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs=
+github.com/klauspost/compress v1.11.0 h1:wJbzvpYMVGG9iTI9VxpnNZfd4DzMPoCWze3GgSqz8yg=
+github.com/klauspost/compress v1.11.0/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs=
+github.com/miekg/dns v1.1.31 h1:sJFOl9BgwbYAWOGEwr61FU28pqsBNdpRBnhGXtO06Oo=
+github.com/miekg/dns v1.1.31/go.mod h1:KNUDUusw/aVsxyTYZM1oqvCicbwhgbNgztCETuNZ7xM=
+github.com/qydysky/part v0.0.0-20200912061031-0f3a305b4afb h1:p2R5IUkza8POjdulQWSJrFSxhpjIzSWLX4KMt/YyS9E=
+github.com/qydysky/part v0.0.0-20200912061031-0f3a305b4afb/go.mod h1:+8N3UgJBVyJj8ar31eZtucwrKpLpay854Y5qq0xk3x0=
+github.com/qydysky/part v0.0.0-20200914123330-afade058e33d h1:um1WzsGzwD6h+1W3jqGcWJPVr4kT5gmRRaXXVwO7nb4=
+github.com/qydysky/part v0.0.0-20200914123330-afade058e33d/go.mod h1:+8N3UgJBVyJj8ar31eZtucwrKpLpay854Y5qq0xk3x0=
+github.com/shirou/gopsutil v2.20.7+incompatible h1:Ymv4OD12d6zm+2yONe39VSmp2XooJe8za7ngOLW/o/w=
+github.com/shirou/gopsutil v2.20.7+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA=
+github.com/shirou/gopsutil v2.20.8+incompatible h1:8c7Atn0FAUZJo+f4wYbN0iVpdWniCQk7IYwGtgdh1mY=
+github.com/shirou/gopsutil v2.20.8+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA=
+github.com/thedevsaddam/gojsonq v2.3.0+incompatible h1:i2lFTvGY4LvoZ2VUzedsFlRiyaWcJm3Uh6cQ9+HyQA8=
+github.com/thedevsaddam/gojsonq v2.3.0+incompatible/go.mod h1:RBcQaITThgJAAYKH7FNp2onYodRz8URfsuEGpAch0NA=
+github.com/thedevsaddam/gojsonq/v2 v2.5.2 h1:CoMVaYyKFsVj6TjU6APqAhAvC07hTI6IQen8PHzHYY0=
+github.com/thedevsaddam/gojsonq/v2 v2.5.2/go.mod h1:bv6Xa7kWy82uT0LnXPE2SzGqTj33TAEeR560MdJkiXs=
+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-20200728195943-123391ffb6de h1:ikNHVSjEfnvz6sxdSPCaPt572qowuyMDMJLLm3Db3ig=
+golang.org/x/crypto v0.0.0-20200728195943-123391ffb6de/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
+golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a h1:vclmkQCjlDX5OydZ9wv8rBCcS0QyQY66Mpf/7BZbInM=
+golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
+golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
+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-20200707034311-ab3426394381 h1:VXak5I6aEWmAXeQjA+QSZzlgNrpq9mjcfDemuexIKsU=
+golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
+golang.org/x/net v0.0.0-20200904194848-62affa334b73 h1:MXfv8rhZWmFeqX3GNZRsd6vOLoaCHjYEX3qkRo3YBUA=
+golang.org/x/net v0.0.0-20200904194848-62affa334b73/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
+golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+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-20190924154521-2837fb4f24fe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200810151505-1b9f1253b3ed h1:WBkVNH1zd9jg/dK4HCM4lNANnmd12EHC9z+LmcCG4ns=
+golang.org/x/sys v0.0.0-20200810151505-1b9f1253b3ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200909081042-eff7692f9009 h1:W0lCpv29Hv0UaM1LXb9QlBHLNP8UFfcKjblhVCWftOM=
+golang.org/x/sys v0.0.0-20200909081042-eff7692f9009/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
+golang.org/x/tools v0.0.0-20191216052735-49a3e744a425/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
+golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
--- /dev/null
+package bili_danmu
+
+import (
+ "time"
+
+ "github.com/gorilla/websocket"
+
+ p "github.com/qydysky/part"
+)
+
+type ws struct {
+ used bool
+
+ SendChan chan []byte
+ RecvChan chan []byte
+ interrupt chan struct{}
+ url string
+}
+
+func New_ws(url string) (o *ws) {
+ l := p.Logf().New().Level(LogLevel).I("New_ws")
+ defer l.Block()
+
+ l.I("->", "ok")
+ o = new(ws)
+ o.url = url
+ o.SendChan = make(chan []byte, 1e4)
+ o.RecvChan = make(chan []byte, 1e4)
+ return
+}
+
+func (i *ws) Handle() (o *ws) {
+ o = i
+ l := p.Logf().New().Level(LogLevel).I("*ws.handle")
+ defer l.Block()
+
+ if o.used {
+ l.E("->", "o.used")
+ return
+ }
+
+ if o.url == "" {
+ l.E("->", "o.url == \"\"")
+ return
+ }
+
+ started := make(chan struct{})
+
+ go func() {
+ defer close(o.RecvChan)
+
+ c, _, err := websocket.DefaultDialer.Dial(o.url, nil)
+ if err != nil {
+ l.E("->", err)
+ return
+ }
+ defer c.Close()
+
+ l.I("->", "ok")
+ o.interrupt = make(chan struct{})
+ done := make(chan struct{})
+
+ go func() {
+ defer close(done)
+
+ for {
+ _, message, err := c.ReadMessage()
+ if err != nil {
+ if !websocket.IsCloseError(err, websocket.CloseNormalClosure) {
+ l.E("->", err)
+ }
+ o.used = false
+ return
+ }
+ o.RecvChan <- message
+ }
+ }()
+
+ close(started)
+
+ for {
+ select {
+ case <- done:
+ o.used = false
+ return
+ case t := <- o.SendChan:
+ err := c.WriteMessage(websocket.TextMessage, t)
+ if err != nil {
+ l.I("->", "write:", err)
+ o.used = false
+ return
+ }
+ case <- o.interrupt:
+ l.I("->", "interrupt")
+ // Cleanly close the connection by sending a close message and then
+ // waiting (with timeout) for the server to close the connection.
+ err := c.WriteMessage(websocket.CloseMessage, websocket.FormatCloseMessage(websocket.CloseNormalClosure, ""))
+ if err != nil {
+ l.E("->", err)
+ }
+ select {
+ case <- done:
+ case <- time.After(time.Second):
+ }
+ o.used = false
+ return
+ }
+ }
+ }()
+
+ <- started
+ o.used = true
+ return
+}
+
+func (i *ws) Heartbeat(Millisecond int, msg []byte) (o *ws) {
+ o = i
+ l := p.Logf().New().Level(LogLevel).I("*ws.heartbeat")
+ defer l.Block()
+
+ if !o.used {
+ l.E("->", "!o.used")
+ return
+ }
+ o.SendChan <- msg
+ l.I("->", "ok")
+
+ go func(){
+ ticker := time.NewTicker(time.Duration(Millisecond)*time.Millisecond)
+ defer ticker.Stop()
+
+ for {
+ select {
+ case <-ticker.C:
+ o.SendChan <- msg
+ case <- o.interrupt:
+ l.I("->", "fin")
+ return
+ }
+ }
+ }()
+
+ return
+}
+
+func (o *ws) Close() {
+ l := p.Logf().New().Level(LogLevel).I("*ws.Close")
+ defer l.Block()
+
+ if !o.used {
+ l.I("->", "!o.used")
+ return
+ }
+ o.used = false
+
+ close(o.interrupt)
+ l.I("->", "ok")
+}
+
+func (o *ws) Isclose() bool {
+ return !o.used
+}
\ No newline at end of file