2019-02-19 23:15:00 +08:00
|
|
|
|
# -*- coding: utf-8 -*-
|
2019-02-20 00:25:14 +08:00
|
|
|
|
import asyncio
|
2021-12-12 21:54:07 +08:00
|
|
|
|
import enum
|
2019-02-14 18:36:37 +08:00
|
|
|
|
import json
|
2019-02-20 14:53:50 +08:00
|
|
|
|
import logging
|
2019-06-12 22:32:34 +08:00
|
|
|
|
import ssl as ssl_
|
2019-02-19 23:15:00 +08:00
|
|
|
|
import struct
|
2019-06-06 21:50:51 +08:00
|
|
|
|
from typing import *
|
2019-02-19 23:15:00 +08:00
|
|
|
|
|
2018-06-03 14:06:00 +08:00
|
|
|
|
import aiohttp
|
2021-12-18 20:26:51 +08:00
|
|
|
|
import brotli
|
2018-05-13 21:57:36 +08:00
|
|
|
|
|
2023-09-02 11:52:04 +08:00
|
|
|
|
from . import open_live_client
|
2021-12-13 00:07:00 +08:00
|
|
|
|
from . import handlers
|
2021-12-12 21:54:07 +08:00
|
|
|
|
|
2023-09-02 11:52:04 +08:00
|
|
|
|
OpenLiveClient = open_live_client.OpenLiveClient
|
|
|
|
|
|
2021-12-18 14:42:11 +08:00
|
|
|
|
__all__ = (
|
|
|
|
|
'BLiveClient',
|
|
|
|
|
)
|
|
|
|
|
|
2021-12-12 19:28:12 +08:00
|
|
|
|
logger = logging.getLogger('blivedm')
|
2019-02-20 14:53:50 +08:00
|
|
|
|
|
2020-07-19 22:17:32 +08:00
|
|
|
|
ROOM_INIT_URL = 'https://api.live.bilibili.com/xlive/web-room/v1/index/getInfoByRoom'
|
|
|
|
|
DANMAKU_SERVER_CONF_URL = 'https://api.live.bilibili.com/xlive/web-room/v1/index/getDanmuInfo'
|
2020-10-11 12:43:11 +08:00
|
|
|
|
DEFAULT_DANMAKU_SERVER_LIST = [
|
|
|
|
|
{'host': 'broadcastlv.chat.bilibili.com', 'port': 2243, 'wss_port': 443, 'ws_port': 2244}
|
|
|
|
|
]
|
2018-05-13 21:57:36 +08:00
|
|
|
|
|
2019-04-22 19:47:05 +08:00
|
|
|
|
HEADER_STRUCT = struct.Struct('>I2H2I')
|
2023-03-26 00:51:50 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class HeaderTuple(NamedTuple):
|
|
|
|
|
pack_len: int
|
|
|
|
|
raw_header_size: int
|
|
|
|
|
ver: int
|
|
|
|
|
operation: int
|
|
|
|
|
seq_id: int
|
2021-12-18 14:42:11 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# WS_BODY_PROTOCOL_VERSION
|
|
|
|
|
class ProtoVer(enum.IntEnum):
|
|
|
|
|
NORMAL = 0
|
|
|
|
|
HEARTBEAT = 1
|
|
|
|
|
DEFLATE = 2
|
|
|
|
|
BROTLI = 3
|
2019-04-22 19:47:05 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# go-common\app\service\main\broadcast\model\operation.go
|
2021-12-12 21:54:07 +08:00
|
|
|
|
class Operation(enum.IntEnum):
|
2019-04-22 19:47:05 +08:00
|
|
|
|
HANDSHAKE = 0
|
|
|
|
|
HANDSHAKE_REPLY = 1
|
|
|
|
|
HEARTBEAT = 2
|
|
|
|
|
HEARTBEAT_REPLY = 3
|
|
|
|
|
SEND_MSG = 4
|
|
|
|
|
SEND_MSG_REPLY = 5
|
|
|
|
|
DISCONNECT_REPLY = 6
|
2019-02-19 23:15:00 +08:00
|
|
|
|
AUTH = 7
|
2019-04-22 19:47:05 +08:00
|
|
|
|
AUTH_REPLY = 8
|
|
|
|
|
RAW = 9
|
|
|
|
|
PROTO_READY = 10
|
|
|
|
|
PROTO_FINISH = 11
|
|
|
|
|
CHANGE_ROOM = 12
|
|
|
|
|
CHANGE_ROOM_REPLY = 13
|
|
|
|
|
REGISTER = 14
|
|
|
|
|
REGISTER_REPLY = 15
|
|
|
|
|
UNREGISTER = 16
|
|
|
|
|
UNREGISTER_REPLY = 17
|
|
|
|
|
# B站业务自定义OP
|
|
|
|
|
# MinBusinessOp = 1000
|
|
|
|
|
# MaxBusinessOp = 10000
|
2018-05-13 21:57:36 +08:00
|
|
|
|
|
2019-02-19 23:15:00 +08:00
|
|
|
|
|
2021-12-18 15:46:30 +08:00
|
|
|
|
# WS_AUTH
|
|
|
|
|
class AuthReplyCode(enum.IntEnum):
|
|
|
|
|
OK = 0
|
|
|
|
|
TOKEN_ERROR = -101
|
|
|
|
|
|
|
|
|
|
|
2019-09-15 18:46:45 +08:00
|
|
|
|
class InitError(Exception):
|
|
|
|
|
"""初始化失败"""
|
|
|
|
|
|
|
|
|
|
|
2021-12-18 15:46:30 +08:00
|
|
|
|
class AuthError(Exception):
|
|
|
|
|
"""认证失败"""
|
|
|
|
|
|
|
|
|
|
|
2019-02-19 23:15:00 +08:00
|
|
|
|
class BLiveClient:
|
2021-12-13 00:07:00 +08:00
|
|
|
|
"""
|
|
|
|
|
B站直播弹幕客户端,负责连接房间
|
|
|
|
|
|
|
|
|
|
:param room_id: URL中的房间ID,可以用短ID
|
|
|
|
|
:param uid: B站用户ID,0表示未登录
|
|
|
|
|
:param session: cookie、连接池
|
|
|
|
|
:param heartbeat_interval: 发送心跳包的间隔时间(秒)
|
|
|
|
|
:param ssl: True表示用默认的SSLContext验证,False表示不验证,也可以传入SSLContext
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
def __init__(
|
|
|
|
|
self,
|
2023-09-02 11:52:04 +08:00
|
|
|
|
room_id=0,
|
2021-12-13 00:07:00 +08:00
|
|
|
|
uid=0,
|
2021-12-15 00:09:07 +08:00
|
|
|
|
session: Optional[aiohttp.ClientSession] = None,
|
2021-12-13 00:07:00 +08:00
|
|
|
|
heartbeat_interval=30,
|
|
|
|
|
ssl: Union[bool, ssl_.SSLContext] = True,
|
2023-09-02 11:52:04 +08:00
|
|
|
|
|
2023-09-02 12:42:40 +08:00
|
|
|
|
use_open_live: bool = False,
|
2023-09-02 11:52:04 +08:00
|
|
|
|
open_live_app_id: Optional[int] = None,
|
|
|
|
|
open_live_access_key: Optional[str] = None,
|
|
|
|
|
open_live_access_secret: Optional[str] = None,
|
|
|
|
|
open_live_code: Optional[str] = None,
|
2019-03-23 23:58:02 +08:00
|
|
|
|
):
|
2019-06-06 21:50:51 +08:00
|
|
|
|
self._tmp_room_id = room_id
|
2023-03-26 00:51:50 +08:00
|
|
|
|
"""用来init_room的临时房间ID,可以用短ID"""
|
2019-02-20 00:25:14 +08:00
|
|
|
|
self._uid = uid
|
2019-02-19 23:15:00 +08:00
|
|
|
|
|
2019-02-20 00:25:14 +08:00
|
|
|
|
if session is None:
|
2023-03-25 18:28:32 +08:00
|
|
|
|
self._session = aiohttp.ClientSession(timeout=aiohttp.ClientTimeout(total=10))
|
2019-02-20 00:25:14 +08:00
|
|
|
|
self._own_session = True
|
|
|
|
|
else:
|
|
|
|
|
self._session = session
|
|
|
|
|
self._own_session = False
|
2023-03-25 18:28:32 +08:00
|
|
|
|
assert self._session.loop is asyncio.get_event_loop() # noqa
|
2019-09-15 18:46:45 +08:00
|
|
|
|
|
|
|
|
|
self._heartbeat_interval = heartbeat_interval
|
2021-12-12 21:54:07 +08:00
|
|
|
|
self._ssl = ssl if ssl else ssl_._create_unverified_context() # noqa
|
2019-02-19 23:15:00 +08:00
|
|
|
|
|
2021-12-13 00:07:00 +08:00
|
|
|
|
self._handlers: List[handlers.HandlerInterface] = []
|
2023-03-26 00:51:50 +08:00
|
|
|
|
"""消息处理器,可动态增删"""
|
2021-12-13 00:07:00 +08:00
|
|
|
|
|
2021-12-15 00:09:07 +08:00
|
|
|
|
# 在调用init_room后初始化的字段
|
|
|
|
|
self._room_id = None
|
2023-03-26 00:51:50 +08:00
|
|
|
|
"""真实房间ID"""
|
2021-12-15 00:09:07 +08:00
|
|
|
|
self._room_short_id = None
|
2023-03-26 00:51:50 +08:00
|
|
|
|
"""房间短ID,没有则为0"""
|
2021-12-15 00:09:07 +08:00
|
|
|
|
self._room_owner_uid = None
|
2023-03-26 00:51:50 +08:00
|
|
|
|
"""主播用户ID"""
|
2021-12-15 00:09:07 +08:00
|
|
|
|
self._host_server_list: Optional[List[dict]] = None
|
2023-03-26 00:51:50 +08:00
|
|
|
|
"""
|
|
|
|
|
弹幕服务器列表
|
|
|
|
|
[{host: "tx-bj4-live-comet-04.chat.bilibili.com", port: 2243, wss_port: 443, ws_port: 2244}, ...]
|
|
|
|
|
"""
|
2021-12-15 00:09:07 +08:00
|
|
|
|
self._host_server_token = None
|
2023-03-26 00:51:50 +08:00
|
|
|
|
"""连接弹幕服务器用的token"""
|
2021-12-15 00:09:07 +08:00
|
|
|
|
|
|
|
|
|
# 在运行时初始化的字段
|
|
|
|
|
self._websocket: Optional[aiohttp.ClientWebSocketResponse] = None
|
2023-03-26 00:51:50 +08:00
|
|
|
|
"""WebSocket连接"""
|
2021-12-15 00:09:07 +08:00
|
|
|
|
self._network_future: Optional[asyncio.Future] = None
|
2023-03-26 00:51:50 +08:00
|
|
|
|
"""网络协程的future"""
|
2021-12-15 00:09:07 +08:00
|
|
|
|
self._heartbeat_timer_handle: Optional[asyncio.TimerHandle] = None
|
2023-03-26 00:51:50 +08:00
|
|
|
|
"""发心跳包定时器的handle"""
|
2021-12-15 00:09:07 +08:00
|
|
|
|
|
2023-09-02 12:42:40 +08:00
|
|
|
|
self._open_live_client = None
|
2023-09-02 11:52:04 +08:00
|
|
|
|
self._host_server_auth_body: Dict = None
|
|
|
|
|
"""开放平台的完整鉴权body"""
|
|
|
|
|
|
2023-09-02 12:42:40 +08:00
|
|
|
|
if use_open_live:
|
2023-09-02 11:52:04 +08:00
|
|
|
|
self._open_live_client = OpenLiveClient(open_live_app_id, open_live_access_key, open_live_access_secret, self._session, self._ssl)
|
|
|
|
|
self._open_live_auth_code = open_live_code
|
|
|
|
|
|
2019-02-20 14:12:11 +08:00
|
|
|
|
@property
|
2021-12-15 00:09:07 +08:00
|
|
|
|
def is_running(self) -> bool:
|
|
|
|
|
"""
|
|
|
|
|
本客户端正在运行,注意调用stop后还没完全停止也算正在运行
|
|
|
|
|
"""
|
|
|
|
|
return self._network_future is not None
|
2019-06-06 21:50:51 +08:00
|
|
|
|
|
|
|
|
|
@property
|
2021-12-15 00:09:07 +08:00
|
|
|
|
def room_id(self) -> Optional[int]:
|
2019-06-06 21:50:51 +08:00
|
|
|
|
"""
|
|
|
|
|
房间ID,调用init_room后初始化
|
|
|
|
|
"""
|
|
|
|
|
return self._room_id
|
|
|
|
|
|
|
|
|
|
@property
|
2021-12-15 00:09:07 +08:00
|
|
|
|
def room_short_id(self) -> Optional[int]:
|
2019-06-06 21:50:51 +08:00
|
|
|
|
"""
|
|
|
|
|
房间短ID,没有则为0,调用init_room后初始化
|
|
|
|
|
"""
|
|
|
|
|
return self._room_short_id
|
|
|
|
|
|
|
|
|
|
@property
|
2021-12-15 00:09:07 +08:00
|
|
|
|
def room_owner_uid(self) -> Optional[int]:
|
2019-06-06 21:50:51 +08:00
|
|
|
|
"""
|
2021-12-15 00:09:07 +08:00
|
|
|
|
主播用户ID,调用init_room后初始化
|
2019-06-06 21:50:51 +08:00
|
|
|
|
"""
|
|
|
|
|
return self._room_owner_uid
|
2019-02-20 14:12:11 +08:00
|
|
|
|
|
2021-12-13 00:07:00 +08:00
|
|
|
|
def add_handler(self, handler: 'handlers.HandlerInterface'):
|
2019-02-20 00:25:14 +08:00
|
|
|
|
"""
|
2021-12-13 00:07:00 +08:00
|
|
|
|
添加消息处理器
|
2021-12-15 23:44:44 +08:00
|
|
|
|
注意多个处理器是并发处理的,不要依赖处理的顺序
|
|
|
|
|
消息处理器和接收消息运行在同一协程,如果处理消息耗时太长会阻塞接收消息,这种情况建议将消息推到队列,让另一个协程处理
|
2021-12-15 00:09:07 +08:00
|
|
|
|
|
2021-12-13 00:07:00 +08:00
|
|
|
|
:param handler: 消息处理器
|
2019-02-20 00:25:14 +08:00
|
|
|
|
"""
|
2021-12-13 00:07:00 +08:00
|
|
|
|
if handler not in self._handlers:
|
|
|
|
|
self._handlers.append(handler)
|
|
|
|
|
|
|
|
|
|
def remove_handler(self, handler: 'handlers.HandlerInterface'):
|
|
|
|
|
"""
|
|
|
|
|
移除消息处理器
|
2021-12-15 00:09:07 +08:00
|
|
|
|
|
2021-12-13 00:07:00 +08:00
|
|
|
|
:param handler: 消息处理器
|
|
|
|
|
"""
|
|
|
|
|
try:
|
|
|
|
|
self._handlers.remove(handler)
|
|
|
|
|
except ValueError:
|
|
|
|
|
pass
|
2019-02-19 23:15:00 +08:00
|
|
|
|
|
2019-06-06 21:50:51 +08:00
|
|
|
|
def start(self):
|
2019-02-19 23:15:00 +08:00
|
|
|
|
"""
|
2021-12-15 00:09:07 +08:00
|
|
|
|
启动本客户端
|
2019-02-19 23:15:00 +08:00
|
|
|
|
"""
|
2021-12-15 23:44:44 +08:00
|
|
|
|
if self.is_running:
|
2021-12-18 00:50:45 +08:00
|
|
|
|
logger.warning('room=%s client is running, cannot start() again', self.room_id)
|
2021-12-15 00:09:07 +08:00
|
|
|
|
return
|
|
|
|
|
|
2023-03-26 00:51:50 +08:00
|
|
|
|
self._network_future = asyncio.create_task(self._network_coroutine_wrapper())
|
2019-06-12 22:32:34 +08:00
|
|
|
|
|
2019-06-06 21:50:51 +08:00
|
|
|
|
def stop(self):
|
|
|
|
|
"""
|
2021-12-15 00:09:07 +08:00
|
|
|
|
停止本客户端
|
|
|
|
|
"""
|
2021-12-15 23:44:44 +08:00
|
|
|
|
if not self.is_running:
|
2021-12-18 00:50:45 +08:00
|
|
|
|
logger.warning('room=%s client is stopped, cannot stop() again', self.room_id)
|
2021-12-15 00:09:07 +08:00
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
self._network_future.cancel()
|
|
|
|
|
|
|
|
|
|
async def stop_and_close(self):
|
|
|
|
|
"""
|
2021-12-18 18:05:07 +08:00
|
|
|
|
便利函数,停止本客户端并释放本客户端的资源,调用后本客户端将不可用
|
2019-06-06 21:50:51 +08:00
|
|
|
|
"""
|
2021-12-18 18:05:07 +08:00
|
|
|
|
if self.is_running:
|
|
|
|
|
self.stop()
|
|
|
|
|
await self.join()
|
2021-12-15 00:09:07 +08:00
|
|
|
|
await self.close()
|
|
|
|
|
|
|
|
|
|
async def join(self):
|
|
|
|
|
"""
|
|
|
|
|
等待本客户端停止
|
|
|
|
|
"""
|
2021-12-15 23:44:44 +08:00
|
|
|
|
if not self.is_running:
|
2021-12-18 00:50:45 +08:00
|
|
|
|
logger.warning('room=%s client is stopped, cannot join()', self.room_id)
|
2021-12-15 00:09:07 +08:00
|
|
|
|
return
|
|
|
|
|
|
2021-12-18 18:05:07 +08:00
|
|
|
|
await asyncio.shield(self._network_future)
|
2019-06-06 21:50:51 +08:00
|
|
|
|
|
2021-12-13 00:07:00 +08:00
|
|
|
|
async def close(self):
|
|
|
|
|
"""
|
2021-12-15 00:09:07 +08:00
|
|
|
|
释放本客户端的资源,调用后本客户端将不可用
|
2021-12-13 00:07:00 +08:00
|
|
|
|
"""
|
2021-12-15 23:44:44 +08:00
|
|
|
|
if self.is_running:
|
2021-12-18 00:50:45 +08:00
|
|
|
|
logger.warning('room=%s is calling close(), but client is running', self.room_id)
|
2021-12-15 00:09:07 +08:00
|
|
|
|
|
|
|
|
|
# 如果session是自己创建的则关闭session
|
2021-12-13 00:07:00 +08:00
|
|
|
|
if self._own_session:
|
|
|
|
|
await self._session.close()
|
|
|
|
|
|
2019-06-06 21:50:51 +08:00
|
|
|
|
async def init_room(self):
|
2020-10-11 12:43:11 +08:00
|
|
|
|
"""
|
2021-12-15 21:12:09 +08:00
|
|
|
|
初始化连接房间需要的字段
|
|
|
|
|
|
2020-10-11 12:43:11 +08:00
|
|
|
|
:return: True代表没有降级,如果需要降级后还可用,重载这个函数返回True
|
|
|
|
|
"""
|
|
|
|
|
res = True
|
2023-09-02 11:52:04 +08:00
|
|
|
|
if self._open_live_client and await self._init_room_by_open_live():
|
|
|
|
|
return res
|
|
|
|
|
|
2020-10-11 12:43:11 +08:00
|
|
|
|
if not await self._init_room_id_and_owner():
|
|
|
|
|
res = False
|
|
|
|
|
# 失败了则降级
|
|
|
|
|
self._room_id = self._room_short_id = self._tmp_room_id
|
|
|
|
|
self._room_owner_uid = 0
|
|
|
|
|
|
|
|
|
|
if not await self._init_host_server():
|
|
|
|
|
res = False
|
|
|
|
|
# 失败了则降级
|
|
|
|
|
self._host_server_list = DEFAULT_DANMAKU_SERVER_LIST
|
|
|
|
|
self._host_server_token = None
|
|
|
|
|
return res
|
2023-09-02 11:52:04 +08:00
|
|
|
|
|
|
|
|
|
async def _init_room_by_open_live(self):
|
|
|
|
|
"""
|
|
|
|
|
通过开放平台初始化房间
|
|
|
|
|
"""
|
|
|
|
|
if not self._open_live_client:
|
|
|
|
|
logger.warning('_init_room_by_open_live() failed, open_live_client is None')
|
|
|
|
|
return False
|
|
|
|
|
if not await self._open_live_client.start(self._open_live_auth_code):
|
|
|
|
|
logger.warning('app=%d _init_room_by_open_live() failed, open_live_client.start() failed', self._open_live_client.app_id)
|
|
|
|
|
return False
|
|
|
|
|
self._room_id = self._open_live_client.anchor_room_id
|
|
|
|
|
self._room_owner_uid = self._open_live_client.anchor_uid
|
|
|
|
|
self._host_server_auth_body = self._open_live_client.ws_auth_body
|
|
|
|
|
self._host_server_list = self._open_live_client.wss_link
|
|
|
|
|
return True
|
2020-10-11 12:43:11 +08:00
|
|
|
|
|
|
|
|
|
async def _init_room_id_and_owner(self):
|
2019-09-15 18:46:45 +08:00
|
|
|
|
try:
|
2022-06-18 21:27:23 +08:00
|
|
|
|
async with self._session.get(
|
|
|
|
|
ROOM_INIT_URL,
|
|
|
|
|
headers={
|
|
|
|
|
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko)'
|
|
|
|
|
' Chrome/102.0.0.0 Safari/537.36'
|
|
|
|
|
},
|
|
|
|
|
params={
|
|
|
|
|
'room_id': self._tmp_room_id
|
|
|
|
|
},
|
|
|
|
|
ssl=self._ssl
|
|
|
|
|
) as res:
|
2019-09-15 18:46:45 +08:00
|
|
|
|
if res.status != 200:
|
2021-12-18 00:50:45 +08:00
|
|
|
|
logger.warning('room=%d _init_room_id_and_owner() failed, status=%d, reason=%s', self._tmp_room_id,
|
2019-09-15 18:46:45 +08:00
|
|
|
|
res.status, res.reason)
|
|
|
|
|
return False
|
|
|
|
|
data = await res.json()
|
|
|
|
|
if data['code'] != 0:
|
2021-12-18 00:50:45 +08:00
|
|
|
|
logger.warning('room=%d _init_room_id_and_owner() failed, message=%s', self._tmp_room_id,
|
|
|
|
|
data['message'])
|
2019-09-15 18:46:45 +08:00
|
|
|
|
return False
|
|
|
|
|
if not self._parse_room_init(data['data']):
|
|
|
|
|
return False
|
2021-03-28 17:47:23 +08:00
|
|
|
|
except (aiohttp.ClientConnectionError, asyncio.TimeoutError):
|
2021-12-18 00:50:45 +08:00
|
|
|
|
logger.exception('room=%d _init_room_id_and_owner() failed:', self._tmp_room_id)
|
2019-09-15 18:46:45 +08:00
|
|
|
|
return False
|
2020-10-11 12:43:11 +08:00
|
|
|
|
return True
|
|
|
|
|
|
|
|
|
|
def _parse_room_init(self, data):
|
|
|
|
|
room_info = data['room_info']
|
|
|
|
|
self._room_id = room_info['room_id']
|
|
|
|
|
self._room_short_id = room_info['short_id']
|
|
|
|
|
self._room_owner_uid = room_info['uid']
|
|
|
|
|
return True
|
2019-09-15 18:46:45 +08:00
|
|
|
|
|
2020-10-11 12:43:11 +08:00
|
|
|
|
async def _init_host_server(self):
|
2019-09-15 18:46:45 +08:00
|
|
|
|
try:
|
2022-06-18 21:27:23 +08:00
|
|
|
|
async with self._session.get(
|
|
|
|
|
DANMAKU_SERVER_CONF_URL,
|
|
|
|
|
headers={
|
|
|
|
|
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko)'
|
|
|
|
|
' Chrome/102.0.0.0 Safari/537.36'
|
|
|
|
|
},
|
|
|
|
|
params={
|
|
|
|
|
'id': self._room_id,
|
|
|
|
|
'type': 0
|
|
|
|
|
},
|
|
|
|
|
ssl=self._ssl
|
|
|
|
|
) as res:
|
2019-09-15 18:46:45 +08:00
|
|
|
|
if res.status != 200:
|
2021-12-18 00:50:45 +08:00
|
|
|
|
logger.warning('room=%d _init_host_server() failed, status=%d, reason=%s', self._room_id,
|
2019-09-15 18:46:45 +08:00
|
|
|
|
res.status, res.reason)
|
|
|
|
|
return False
|
|
|
|
|
data = await res.json()
|
|
|
|
|
if data['code'] != 0:
|
2021-12-18 00:50:45 +08:00
|
|
|
|
logger.warning('room=%d _init_host_server() failed, message=%s', self._room_id, data['message'])
|
2019-09-15 18:46:45 +08:00
|
|
|
|
return False
|
2020-10-11 12:43:11 +08:00
|
|
|
|
if not self._parse_danmaku_server_conf(data['data']):
|
2019-09-15 18:46:45 +08:00
|
|
|
|
return False
|
2021-03-28 17:47:23 +08:00
|
|
|
|
except (aiohttp.ClientConnectionError, asyncio.TimeoutError):
|
2021-12-18 00:50:45 +08:00
|
|
|
|
logger.exception('room=%d _init_host_server() failed:', self._room_id)
|
2019-09-15 18:46:45 +08:00
|
|
|
|
return False
|
|
|
|
|
return True
|
2019-06-06 21:50:51 +08:00
|
|
|
|
|
2020-10-11 12:43:11 +08:00
|
|
|
|
def _parse_danmaku_server_conf(self, data):
|
|
|
|
|
self._host_server_list = data['host_list']
|
|
|
|
|
self._host_server_token = data['token']
|
|
|
|
|
if not self._host_server_list:
|
2021-12-18 00:50:45 +08:00
|
|
|
|
logger.warning('room=%d _parse_danmaku_server_conf() failed: host_server_list is empty', self._room_id)
|
2020-10-11 12:43:11 +08:00
|
|
|
|
return False
|
2019-09-15 18:46:45 +08:00
|
|
|
|
return True
|
2019-02-19 23:15:00 +08:00
|
|
|
|
|
2021-01-31 13:11:20 +08:00
|
|
|
|
@staticmethod
|
2021-12-15 21:12:09 +08:00
|
|
|
|
def _make_packet(data: dict, operation: int) -> bytes:
|
|
|
|
|
"""
|
|
|
|
|
创建一个要发送给服务器的包
|
|
|
|
|
|
|
|
|
|
:param data: 包体JSON数据
|
|
|
|
|
:param operation: 操作码,见Operation
|
|
|
|
|
:return: 整个包的数据
|
|
|
|
|
"""
|
2019-02-19 23:15:00 +08:00
|
|
|
|
body = json.dumps(data).encode('utf-8')
|
2021-12-18 14:42:11 +08:00
|
|
|
|
header = HEADER_STRUCT.pack(*HeaderTuple(
|
|
|
|
|
pack_len=HEADER_STRUCT.size + len(body),
|
|
|
|
|
raw_header_size=HEADER_STRUCT.size,
|
|
|
|
|
ver=1,
|
|
|
|
|
operation=operation,
|
|
|
|
|
seq_id=1
|
|
|
|
|
))
|
2019-02-19 23:15:00 +08:00
|
|
|
|
return header + body
|
|
|
|
|
|
2021-12-15 23:44:44 +08:00
|
|
|
|
async def _network_coroutine_wrapper(self):
|
2021-12-15 21:12:09 +08:00
|
|
|
|
"""
|
2021-12-15 23:44:44 +08:00
|
|
|
|
负责处理网络协程的异常,网络协程具体逻辑在_network_coroutine里
|
2021-12-15 21:12:09 +08:00
|
|
|
|
"""
|
2021-12-15 23:44:44 +08:00
|
|
|
|
try:
|
|
|
|
|
await self._network_coroutine()
|
|
|
|
|
except asyncio.CancelledError:
|
|
|
|
|
# 正常停止
|
|
|
|
|
pass
|
2023-03-26 00:51:50 +08:00
|
|
|
|
except Exception: # noqa
|
2021-12-18 00:50:45 +08:00
|
|
|
|
logger.exception('room=%s _network_coroutine() finished with exception:', self.room_id)
|
2021-12-15 23:44:44 +08:00
|
|
|
|
finally:
|
2021-12-18 00:50:45 +08:00
|
|
|
|
logger.debug('room=%s _network_coroutine() finished', self.room_id)
|
2021-12-15 23:44:44 +08:00
|
|
|
|
self._network_future = None
|
2019-02-19 23:15:00 +08:00
|
|
|
|
|
2021-12-15 00:09:07 +08:00
|
|
|
|
async def _network_coroutine(self):
|
2021-12-15 21:12:09 +08:00
|
|
|
|
"""
|
|
|
|
|
网络协程,负责连接服务器、接收消息、解包
|
|
|
|
|
"""
|
2019-09-15 18:46:45 +08:00
|
|
|
|
# 如果之前未初始化则初始化
|
2023-09-02 11:52:04 +08:00
|
|
|
|
if self._host_server_auth_body is None and self._host_server_token is None:
|
2021-12-15 23:44:44 +08:00
|
|
|
|
if not await self.init_room():
|
2021-12-18 15:46:30 +08:00
|
|
|
|
raise InitError('init_room() failed')
|
2019-02-19 23:15:00 +08:00
|
|
|
|
|
2019-06-07 20:02:37 +08:00
|
|
|
|
retry_count = 0
|
2018-05-14 01:02:53 +08:00
|
|
|
|
while True:
|
2019-02-19 23:15:00 +08:00
|
|
|
|
try:
|
|
|
|
|
# 连接
|
2019-09-15 18:46:45 +08:00
|
|
|
|
host_server = self._host_server_list[retry_count % len(self._host_server_list)]
|
|
|
|
|
async with self._session.ws_connect(
|
2023-09-02 11:52:04 +08:00
|
|
|
|
host_server if isinstance(host_server, str) else
|
2021-12-15 21:12:09 +08:00
|
|
|
|
f"wss://{host_server['host']}:{host_server['wss_port']}/sub",
|
2022-06-18 21:27:23 +08:00
|
|
|
|
headers={
|
|
|
|
|
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko)'
|
|
|
|
|
' Chrome/102.0.0.0 Safari/537.36'
|
|
|
|
|
},
|
2021-01-31 13:47:32 +08:00
|
|
|
|
receive_timeout=self._heartbeat_interval + 5,
|
2019-09-15 18:46:45 +08:00
|
|
|
|
ssl=self._ssl
|
|
|
|
|
) as websocket:
|
2019-02-19 23:15:00 +08:00
|
|
|
|
self._websocket = websocket
|
2021-12-15 21:12:09 +08:00
|
|
|
|
await self._on_ws_connect()
|
2018-05-14 01:02:53 +08:00
|
|
|
|
|
2019-02-19 23:15:00 +08:00
|
|
|
|
# 处理消息
|
2021-12-12 21:54:07 +08:00
|
|
|
|
message: aiohttp.WSMessage
|
|
|
|
|
async for message in websocket:
|
2021-12-15 21:12:09 +08:00
|
|
|
|
await self._on_ws_message(message)
|
2021-12-18 15:46:30 +08:00
|
|
|
|
# 至少成功处理1条消息
|
|
|
|
|
retry_count = 0
|
2018-05-13 23:49:32 +08:00
|
|
|
|
|
2019-06-21 17:45:21 +08:00
|
|
|
|
except (aiohttp.ClientConnectionError, asyncio.TimeoutError):
|
2021-12-15 23:44:44 +08:00
|
|
|
|
# 掉线重连
|
2019-06-07 20:02:37 +08:00
|
|
|
|
pass
|
2021-12-18 15:46:30 +08:00
|
|
|
|
except AuthError:
|
|
|
|
|
# 认证失败了,应该重新获取token再重连
|
|
|
|
|
logger.exception('room=%d auth failed, trying init_room() again', self.room_id)
|
|
|
|
|
if not await self.init_room():
|
|
|
|
|
raise InitError('init_room() failed')
|
2019-06-12 22:32:34 +08:00
|
|
|
|
except ssl_.SSLError:
|
2021-12-18 00:50:45 +08:00
|
|
|
|
logger.error('room=%d a SSLError happened, cannot reconnect', self.room_id)
|
2021-12-15 23:44:44 +08:00
|
|
|
|
raise
|
2019-02-19 23:15:00 +08:00
|
|
|
|
finally:
|
|
|
|
|
self._websocket = None
|
2021-12-15 21:12:09 +08:00
|
|
|
|
await self._on_ws_close()
|
2019-02-19 23:15:00 +08:00
|
|
|
|
|
2021-12-15 21:12:09 +08:00
|
|
|
|
# 准备重连
|
2019-06-07 20:02:37 +08:00
|
|
|
|
retry_count += 1
|
2021-12-18 00:50:45 +08:00
|
|
|
|
logger.warning('room=%d is reconnecting, retry_count=%d', self.room_id, retry_count)
|
2023-03-25 18:28:32 +08:00
|
|
|
|
await asyncio.sleep(1)
|
2019-06-07 20:02:37 +08:00
|
|
|
|
|
2021-12-15 21:12:09 +08:00
|
|
|
|
async def _on_ws_connect(self):
|
|
|
|
|
"""
|
2023-03-26 00:51:50 +08:00
|
|
|
|
WebSocket连接成功
|
2021-12-15 21:12:09 +08:00
|
|
|
|
"""
|
|
|
|
|
await self._send_auth()
|
2023-03-25 18:28:32 +08:00
|
|
|
|
self._heartbeat_timer_handle = asyncio.get_running_loop().call_later(
|
|
|
|
|
self._heartbeat_interval, self._on_send_heartbeat
|
|
|
|
|
)
|
2021-12-15 21:12:09 +08:00
|
|
|
|
|
2021-12-15 23:44:44 +08:00
|
|
|
|
async def _on_ws_close(self):
|
|
|
|
|
"""
|
2023-03-26 00:51:50 +08:00
|
|
|
|
WebSocket连接断开
|
2021-12-15 23:44:44 +08:00
|
|
|
|
"""
|
|
|
|
|
if self._heartbeat_timer_handle is not None:
|
|
|
|
|
self._heartbeat_timer_handle.cancel()
|
|
|
|
|
self._heartbeat_timer_handle = None
|
|
|
|
|
|
|
|
|
|
async def _send_auth(self):
|
|
|
|
|
"""
|
|
|
|
|
发送认证包
|
|
|
|
|
"""
|
|
|
|
|
auth_params = {
|
2023-07-03 16:02:11 +08:00
|
|
|
|
'uid': self._uid or self.room_owner_uid or 0,
|
2021-12-15 23:44:44 +08:00
|
|
|
|
'roomid': self._room_id,
|
2021-12-18 20:26:51 +08:00
|
|
|
|
'protover': 3,
|
2021-12-15 23:44:44 +08:00
|
|
|
|
'platform': 'web',
|
|
|
|
|
'type': 2
|
|
|
|
|
}
|
|
|
|
|
if self._host_server_token is not None:
|
|
|
|
|
auth_params['key'] = self._host_server_token
|
2023-09-02 11:52:04 +08:00
|
|
|
|
|
|
|
|
|
# 开放平台连接则直接替换认证包
|
|
|
|
|
if self._host_server_auth_body is not None:
|
|
|
|
|
auth_params = self._host_server_auth_body
|
2021-12-15 23:44:44 +08:00
|
|
|
|
await self._websocket.send_bytes(self._make_packet(auth_params, Operation.AUTH))
|
|
|
|
|
|
2021-01-31 13:11:20 +08:00
|
|
|
|
def _on_send_heartbeat(self):
|
2021-12-15 21:12:09 +08:00
|
|
|
|
"""
|
|
|
|
|
定时发送心跳包的回调
|
|
|
|
|
"""
|
2021-12-18 01:18:05 +08:00
|
|
|
|
if self._websocket is None or self._websocket.closed:
|
|
|
|
|
self._heartbeat_timer_handle = None
|
|
|
|
|
return
|
|
|
|
|
|
2023-03-25 18:28:32 +08:00
|
|
|
|
self._heartbeat_timer_handle = asyncio.get_running_loop().call_later(
|
|
|
|
|
self._heartbeat_interval, self._on_send_heartbeat
|
|
|
|
|
)
|
2023-03-26 00:51:50 +08:00
|
|
|
|
asyncio.create_task(self._send_heartbeat())
|
2021-12-18 01:18:05 +08:00
|
|
|
|
|
|
|
|
|
async def _send_heartbeat(self):
|
|
|
|
|
"""
|
|
|
|
|
发送心跳包
|
|
|
|
|
"""
|
|
|
|
|
if self._websocket is None or self._websocket.closed:
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
await self._websocket.send_bytes(self._make_packet({}, Operation.HEARTBEAT))
|
2021-12-18 15:46:30 +08:00
|
|
|
|
except (ConnectionResetError, aiohttp.ClientConnectionError) as e:
|
2021-12-18 01:18:05 +08:00
|
|
|
|
logger.warning('room=%d _send_heartbeat() failed: %r', self.room_id, e)
|
|
|
|
|
except Exception: # noqa
|
|
|
|
|
logger.exception('room=%d _send_heartbeat() failed:', self.room_id)
|
2019-02-19 23:15:00 +08:00
|
|
|
|
|
2021-12-15 21:12:09 +08:00
|
|
|
|
async def _on_ws_message(self, message: aiohttp.WSMessage):
|
|
|
|
|
"""
|
2023-03-26 00:51:50 +08:00
|
|
|
|
收到WebSocket消息
|
2021-12-15 21:12:09 +08:00
|
|
|
|
|
2023-03-26 00:51:50 +08:00
|
|
|
|
:param message: WebSocket消息
|
2021-12-15 21:12:09 +08:00
|
|
|
|
"""
|
|
|
|
|
if message.type != aiohttp.WSMsgType.BINARY:
|
2021-12-18 00:50:45 +08:00
|
|
|
|
logger.warning('room=%d unknown websocket message type=%s, data=%s', self.room_id,
|
2021-12-15 21:12:09 +08:00
|
|
|
|
message.type, message.data)
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
await self._parse_ws_message(message.data)
|
2021-12-18 15:46:30 +08:00
|
|
|
|
except (asyncio.CancelledError, AuthError):
|
|
|
|
|
# 正常停止、认证失败,让外层处理
|
2021-12-15 21:12:09 +08:00
|
|
|
|
raise
|
|
|
|
|
except Exception: # noqa
|
2021-12-18 00:50:45 +08:00
|
|
|
|
logger.exception('room=%d _parse_ws_message() error:', self.room_id)
|
2021-12-15 21:12:09 +08:00
|
|
|
|
|
|
|
|
|
async def _parse_ws_message(self, data: bytes):
|
|
|
|
|
"""
|
2023-03-26 00:51:50 +08:00
|
|
|
|
解析WebSocket消息
|
2021-12-15 21:12:09 +08:00
|
|
|
|
|
2023-03-26 00:51:50 +08:00
|
|
|
|
:param data: WebSocket消息数据
|
2021-12-15 21:12:09 +08:00
|
|
|
|
"""
|
2019-02-19 23:15:00 +08:00
|
|
|
|
offset = 0
|
2021-12-18 00:50:45 +08:00
|
|
|
|
try:
|
|
|
|
|
header = HeaderTuple(*HEADER_STRUCT.unpack_from(data, offset))
|
|
|
|
|
except struct.error:
|
|
|
|
|
logger.exception('room=%d parsing header failed, offset=%d, data=%s', self.room_id, offset, data)
|
|
|
|
|
return
|
2019-02-19 23:15:00 +08:00
|
|
|
|
|
2021-12-18 00:50:45 +08:00
|
|
|
|
if header.operation in (Operation.SEND_MSG_REPLY, Operation.AUTH_REPLY):
|
|
|
|
|
# 业务消息,可能有多个包一起发,需要分包
|
|
|
|
|
while True:
|
2021-12-18 15:46:30 +08:00
|
|
|
|
body = data[offset + header.raw_header_size: offset + header.pack_len]
|
2021-12-18 18:05:07 +08:00
|
|
|
|
await self._parse_business_message(header, body)
|
2021-12-18 00:50:45 +08:00
|
|
|
|
|
|
|
|
|
offset += header.pack_len
|
|
|
|
|
if offset >= len(data):
|
|
|
|
|
break
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
header = HeaderTuple(*HEADER_STRUCT.unpack_from(data, offset))
|
|
|
|
|
except struct.error:
|
|
|
|
|
logger.exception('room=%d parsing header failed, offset=%d, data=%s', self.room_id, offset, data)
|
|
|
|
|
break
|
|
|
|
|
|
|
|
|
|
elif header.operation == Operation.HEARTBEAT_REPLY:
|
|
|
|
|
# 服务器心跳包,前4字节是人气值,后面是客户端发的心跳包内容
|
|
|
|
|
# pack_len不包括客户端发的心跳包内容,不知道是不是服务器BUG
|
2021-12-18 15:46:30 +08:00
|
|
|
|
body = data[offset + header.raw_header_size: offset + header.raw_header_size + 4]
|
|
|
|
|
popularity = int.from_bytes(body, 'big')
|
2021-12-18 00:50:45 +08:00
|
|
|
|
# 自己造个消息当成业务消息处理
|
|
|
|
|
body = {
|
|
|
|
|
'cmd': '_HEARTBEAT',
|
|
|
|
|
'data': {
|
|
|
|
|
'popularity': popularity
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
await self._handle_command(body)
|
2019-02-19 23:15:00 +08:00
|
|
|
|
|
2021-12-18 00:50:45 +08:00
|
|
|
|
else:
|
|
|
|
|
# 未知消息
|
2021-12-18 14:42:11 +08:00
|
|
|
|
body = data[offset + header.raw_header_size: offset + header.pack_len]
|
2021-12-18 00:50:45 +08:00
|
|
|
|
logger.warning('room=%d unknown message operation=%d, header=%s, body=%s', self.room_id,
|
|
|
|
|
header.operation, header, body)
|
2019-02-19 23:15:00 +08:00
|
|
|
|
|
2021-12-18 18:05:07 +08:00
|
|
|
|
async def _parse_business_message(self, header: HeaderTuple, body: bytes):
|
2021-12-18 15:46:30 +08:00
|
|
|
|
"""
|
|
|
|
|
解析业务消息
|
|
|
|
|
"""
|
|
|
|
|
if header.operation == Operation.SEND_MSG_REPLY:
|
|
|
|
|
# 业务消息
|
2021-12-18 20:26:51 +08:00
|
|
|
|
if header.ver == ProtoVer.BROTLI:
|
2021-12-18 15:46:30 +08:00
|
|
|
|
# 压缩过的先解压,为了避免阻塞网络线程,放在其他线程执行
|
2023-03-25 18:28:32 +08:00
|
|
|
|
body = await asyncio.get_running_loop().run_in_executor(None, brotli.decompress, body)
|
2021-12-18 15:46:30 +08:00
|
|
|
|
await self._parse_ws_message(body)
|
|
|
|
|
elif header.ver == ProtoVer.NORMAL:
|
|
|
|
|
# 没压缩过的直接反序列化,因为有万恶的GIL,这里不能并行避免阻塞
|
|
|
|
|
if len(body) != 0:
|
|
|
|
|
try:
|
|
|
|
|
body = json.loads(body.decode('utf-8'))
|
|
|
|
|
await self._handle_command(body)
|
2021-12-19 18:10:54 +08:00
|
|
|
|
except asyncio.CancelledError:
|
|
|
|
|
raise
|
2021-12-18 15:46:30 +08:00
|
|
|
|
except Exception:
|
|
|
|
|
logger.error('room=%d, body=%s', self.room_id, body)
|
|
|
|
|
raise
|
|
|
|
|
else:
|
|
|
|
|
# 未知格式
|
|
|
|
|
logger.warning('room=%d unknown protocol version=%d, header=%s, body=%s', self.room_id,
|
|
|
|
|
header.ver, header, body)
|
|
|
|
|
|
|
|
|
|
elif header.operation == Operation.AUTH_REPLY:
|
|
|
|
|
# 认证响应
|
|
|
|
|
body = json.loads(body.decode('utf-8'))
|
|
|
|
|
if body['code'] != AuthReplyCode.OK:
|
|
|
|
|
raise AuthError(f"auth reply error, code={body['code']}, body={body}")
|
|
|
|
|
await self._websocket.send_bytes(self._make_packet({}, Operation.HEARTBEAT))
|
|
|
|
|
|
|
|
|
|
else:
|
|
|
|
|
# 未知消息
|
|
|
|
|
logger.warning('room=%d unknown message operation=%d, header=%s, body=%s', self.room_id,
|
|
|
|
|
header.operation, header, body)
|
|
|
|
|
|
2021-12-18 00:50:45 +08:00
|
|
|
|
async def _handle_command(self, command: dict):
|
2021-12-15 21:12:09 +08:00
|
|
|
|
"""
|
2021-12-15 23:44:44 +08:00
|
|
|
|
解析并处理业务消息
|
2021-12-15 21:12:09 +08:00
|
|
|
|
|
|
|
|
|
:param command: 业务消息
|
|
|
|
|
"""
|
2021-12-18 00:50:45 +08:00
|
|
|
|
# 外部代码可能不能正常处理取消,所以这里加shield
|
|
|
|
|
results = await asyncio.shield(
|
|
|
|
|
asyncio.gather(
|
2023-03-25 18:28:32 +08:00
|
|
|
|
*(handler.handle(self, command) for handler in self._handlers), return_exceptions=True
|
|
|
|
|
)
|
2021-12-15 23:44:44 +08:00
|
|
|
|
)
|
|
|
|
|
for res in results:
|
|
|
|
|
if isinstance(res, Exception):
|
2021-12-18 00:50:45 +08:00
|
|
|
|
logger.exception('room=%d _handle_command() failed, command=%s', self.room_id, command, exc_info=res)
|