You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

338 lines
7.7 KiB
Go

3 years ago
package bilibili
import (
"encoding/json"
"fmt"
"git.noahlan.cn/northlan/ntools-go/logger"
"github.com/go-ping/ping"
"github.com/pkg/errors"
3 years ago
"io/ioutil"
"live-gateway/bilibili/msg_handler"
"live-gateway/config"
3 years ago
"live-gateway/live"
"live-gateway/ws"
"net/http"
"strconv"
"strings"
3 years ago
"time"
)
type MsgHandler interface {
HandlerMessage(data []byte)
CMD() string
}
// 实现 live.Handler 接口
var _ live.Handler = (*LiveBilibili)(nil)
type (
LiveBilibili struct {
*live.Live
sequenceId uint32
3 years ago
roomInfo *RoomInfo // 房间信息
webSocketInfo *WebsocketInfoResp // ws信息
3 years ago
msgHandlerMapper map[string]MsgHandler
entered chan struct{} // 进入房间
}
)
3 years ago
func NewLiveBilibili() *LiveBilibili {
3 years ago
bl := &LiveBilibili{
msgHandlerMapper: make(map[string]MsgHandler, 6),
entered: make(chan struct{}),
3 years ago
}
l := live.NewLive(
live.WithWsOptions(
ws.WithPacker(NewPackBilibili()),
),
)
l.PreConnect(bl.preConnect)
3 years ago
l.Init(bl.Init)
l.Handler(bl)
bl.Live = l
return bl
3 years ago
}
func (l *LiveBilibili) registerMessageHandler(h ...MsgHandler) {
3 years ago
for _, handler := range h {
l.msgHandlerMapper[handler.CMD()] = handler
}
}
func (l *LiveBilibili) preConnect() (url string, err error) {
cfg := config.Config.Bilibili
if err = l.initRoomInfo(); err != nil {
return "", err
}
switch cfg.Type {
case config.BilibiliTypeOfficial:
err = l.initWebsocketInfo()
if err != nil {
logger.SLog.Errorf("获取wss信息失败, err:%+v", err)
return "", err
}
if len(l.webSocketInfo.Ip) == 0 || len(l.webSocketInfo.WssPort) == 0 {
return "", errors.New("获取到的wss信息中地址或端口为空")
}
logger.SLog.Info("正在寻找连接最快的API地址...")
// 获取最快的地址
var minDuration = 1 * time.Hour
var minIdx = 0
for i, ip := range l.webSocketInfo.Host {
pinger, err := ping.NewPinger(ip)
if err != nil {
continue
}
pinger.SetPrivileged(true)
pinger.Count = 3
pinger.Interval = 100 * time.Nanosecond // 100ns /time
pinger.Timeout = 200 * time.Millisecond // 200ms
err = pinger.Run()
if err != nil {
continue
}
stat := pinger.Statistics()
if stat.AvgRtt < minDuration {
minDuration = stat.AvgRtt
minIdx = i
}
}
url = fmt.Sprintf("wss://%s:%d/sub", l.webSocketInfo.Host[minIdx], l.webSocketInfo.WssPort[0])
logger.SLog.Info("找到当前最快的弹幕服务器地址 ", url, " 延迟:", minDuration)
l.registerMessageHandler(
msg_handler.NewDanmakuOfficialHandler(l.roomInfo.RoomId),
msg_handler.NewGuardBuyOfficialHandler(l.roomInfo.RoomId),
msg_handler.NewGiftOfficialHandler(l.roomInfo.RoomId),
)
case config.BilibiliTypeCustom:
url = cfg.Custom.Url
l.registerMessageHandler(
msg_handler.NewDanmakuHandler(l.roomInfo.RoomId),
msg_handler.NewSendGiftHandler(l.roomInfo.RoomId),
msg_handler.NewGuardBuyHandler(l.roomInfo.RoomId),
msg_handler.NewRedPocketHandler(l.roomInfo.RoomId),
)
}
return
}
func (l *LiveBilibili) initRoomInfo() error {
3 years ago
// 避免重复请求接口
if l.roomInfo != nil {
return nil
3 years ago
}
cfg := config.Config.Bilibili
3 years ago
response, err := http.Get(fmt.Sprintf(`%s%d`, cfg.GetRoomUrl, cfg.RoomId))
3 years ago
if err != nil {
return err
3 years ago
}
res, err := ioutil.ReadAll(response.Body)
if err != nil {
return err
3 years ago
}
var getRoomInfoResp struct {
Code int `json:"code"`
Msg string `json:"msg"`
Message string `json:"message"`
Data *RoomInfo `json:"data"`
}
err = json.Unmarshal(res, &getRoomInfoResp)
if err != nil {
return err
3 years ago
}
l.roomInfo = getRoomInfoResp.Data
logger.SLog.Infof("获取房间数据: %+v\n", l.roomInfo)
return nil
}
func (l *LiveBilibili) initWebsocketInfo() error {
cfg := config.Config.Bilibili
req := &WebsocketInfoReq{
RoomId: l.roomInfo.RoomId,
}
content, _ := json.Marshal(req)
header := &CommonHeader{
ContentType: JsonType,
ContentAcceptType: JsonType,
Timestamp: strconv.FormatInt(time.Now().Unix(), 10),
SignatureMethod: HmacSha256,
SignatureVersion: BiliVersion,
Authorization: "",
Nonce: strconv.FormatInt(time.Now().UnixNano(), 10),
AccessKeyId: cfg.Official.AkId,
ContentMD5: Md5(string(content)),
}
header.Authorization = CreateSignature(header, cfg.Official.AkSecret)
request, err := http.NewRequest("POST", fmt.Sprintf("%s/v1/common/websocketInfo", cfg.Official.Api), strings.NewReader(string(content)))
if err != nil {
return err
}
for k, v := range header.ToMap() {
request.Header.Add(k, v)
}
response, err := http.DefaultClient.Do(request)
if err != nil {
return err
}
res, err := ioutil.ReadAll(response.Body)
if err != nil {
return err
}
var baseResp BaseResp
err = json.Unmarshal(res, &baseResp)
if err != nil {
return err
}
if baseResp.Code != 0 {
return errors.New(fmt.Sprintf("调用API失败, result: %+v", baseResp))
}
var infoData WebsocketInfoResp
err = json.Unmarshal(baseResp.Data, &infoData)
if err != nil {
return err
}
l.webSocketInfo = &infoData
logger.SLog.Debugf("获取到wss信息为: %+v", infoData)
return nil
3 years ago
}
func (l *LiveBilibili) Init(conn *ws.NWebsocket) (err error) {
cfg := config.Config.Bilibili
switch cfg.Type {
case config.BilibiliTypeOfficial:
if err = l.auth(conn); err != nil {
logger.SLog.Error(err)
return
}
case config.BilibiliTypeCustom:
if err = l.joinRoom(conn); err != nil {
logger.SLog.Error(err)
return
}
3 years ago
}
// 等待进入房间或登录成功
3 years ago
<-l.entered
go l.heartbeat(conn, cfg.HeartbeatInterval*time.Second)
3 years ago
return
}
func (l *LiveBilibili) HandlerMessage(v interface{}) {
entry, ok := v.(*WsEntry)
if !ok {
logger.SLog.Warn("读取消息错误, 数据类型不匹配 WsEntry")
3 years ago
return
}
// 处理消息
switch entry.operation {
case WsOpEnterRoomOrAuthReply:
3 years ago
go func() {
select {
case l.entered <- struct{}{}:
}
}()
case WsOpHeartbeatReply:
// TODO 心跳回复(房间人数)
case WsOpMessage:
// message with operation
if entry.protoVer == WsVerPlain {
var cmd CMD
err := json.Unmarshal(entry.data, &cmd)
if err != nil {
logger.SLog.Error("读取消息CMD错误")
3 years ago
return
}
// 暂时忽略CMD错误
if strings.Contains(cmd.CMD, "DANMU_MSG") {
cmd.CMD = "DANMU_MSG"
}
3 years ago
handler, ok := l.msgHandlerMapper[cmd.CMD]
if !ok {
logger.SLog.Debugf("未发现 %s 处理器", cmd.CMD)
3 years ago
return
}
handler.HandlerMessage(entry.data)
}
}
}
func (l *LiveBilibili) auth(conn *ws.NWebsocket) error {
if l.webSocketInfo == nil {
return errors.New("websocket信息未获取成功不能登录")
}
data := &WsEntry{
operation: WsOpEnterRoomOrAuth,
data: []byte(l.webSocketInfo.AuthBody),
}
l.sequenceId++
err := conn.SendBinaryMessage(data)
if err != nil {
return errors.Wrap(err, "发送AUTH消息到wss失败")
}
return nil
}
3 years ago
func (l *LiveBilibili) joinRoom(conn *ws.NWebsocket) error {
cfg := config.Config.Bilibili
3 years ago
msg := JoinRoomReq{
Platform: "web",
ProtoVer: 1,
RoomId: l.roomInfo.RoomId,
Uid: cfg.UserId,
3 years ago
Type: 2,
}
body, err := json.Marshal(msg)
if err != nil {
return err
}
data := &WsEntry{
protoVer: 0,
operation: WsOpEnterRoomOrAuth,
3 years ago
sequenceId: WsHeaderDefaultSequence,
data: body,
}
if err = conn.SendBinaryMessage(data); err != nil {
return err
}
return nil
}
func (l *LiveBilibili) heartbeat(conn *ws.NWebsocket, t time.Duration) {
hb := func(conn *ws.NWebsocket) {
//logger.SLog.Info("heartbeat !!!")
3 years ago
data := &WsEntry{
protoVer: 0,
operation: WsOpHeartbeat,
sequenceId: l.sequenceId,
3 years ago
data: nil,
}
l.sequenceId++
3 years ago
err := conn.SendBinaryMessage(data)
if err != nil {
return
}
}
hb(conn)
ticker := time.NewTicker(t)
defer ticker.Stop()
for {
select {
case <-ticker.C:
hb(conn)
}
}
}