1
0
mirror of https://github.com/wbt5/real-url.git synced 2025-08-01 14:48:01 +08:00

新增acfun直播弹幕

This commit is contained in:
wbt5 2020-08-18 21:49:12 +08:00
parent c75c75713f
commit 3cfbdcd5d0
3 changed files with 4397 additions and 0 deletions

437
danmu/danmaku/acfun.proto Normal file
View File

@ -0,0 +1,437 @@
syntax = "proto2";
package AcFunPack;
message RegisterRequest {
optional AppInfo appInfo = 1;
optional DeviceInfo deviceInfo = 2;
optional EnvInfo envInfo = 3;
optional PresenceStatus presenceStatus = 4;
optional ActiveStatus appActiveStatus = 5;
optional bytes appCustomStatus = 6;
optional PushServiceToken pushServiceToken = 7;
optional int64 instanceId = 8;
repeated PushServiceToken pushServiceTokenList = 9;
optional int32 keepaliveIntervalSec = 10;
optional ZtCommonInfo ztCommonInfo = 11;
enum PresenceStatus {
kPresenceOffline = 0;
kPresenceOnline = 1;
}
enum ActiveStatus {
kInvalid = 0;
kAppInForeground = 1;
kAppInBackground = 2;
}
}
message RegisterResponse {
optional AccessPointsConfig accessPointsConfig = 1;
optional bytes sessKey = 2;
optional int64 instanceId = 3;
optional SdkOption sdkOption = 4;
optional AccessPointsConfig accessPointsConfigIpv6 = 5;
}
message AccessPointsConfig {
repeated AccessPoint optimalAps = 1;
repeated AccessPoint backupAps = 2;
repeated uint32 availablePorts = 3;
optional AccessPoint forceLastConnectedAp = 4;
}
message AccessPoint {
optional AddressType addressType = 1;
optional uint32 port = 2;
optional fixed32 ipV4 = 3;
optional bytes ipV6 = 4;
optional string domain = 5;
enum AddressType {
kIPV4 = 0;
kIPV6 = 1;
kDomain = 2;
}
}
message SdkOption {
optional int32 reportIntervalSeconds = 1;
optional string reportSecurity = 2;
optional int32 lz4CompressionThresholdBytes = 3;
repeated string netCheckServers = 4;
}
message ZtLiveCsEnterRoom {
optional bool isAuthor = 1;
optional uint32 reconnectCount = 2;
optional uint32 lastErrorCode = 3;
optional string enterRoomAttach = 4;
optional string clientLiveSdkVersion = 5;
}
message ZtLiveCsHeartbeat {
optional uint64 clientTimestampMs = 1;
optional bool sequence = 2;
}
message CsCmd {
optional string cmdType = 1;
optional bytes payload = 2;
optional string ticket = 3;
optional string liveId = 4;
}
message AppInfo {
optional string appName = 1;
optional string appVersion = 2;
optional string appChannel = 3;
optional string sdkVersion = 4;
map < string,
string > extensionInfo = 5;
}
message DeviceInfo {
optional PlatformType platformType = 1;
optional string osVersion = 2;
optional string deviceModel = 3;
optional bytes imeiMd5 = 4;
optional string deviceId = 5;
optional string softDid = 6;
optional string kwaiDid = 7;
optional string manufacturer = 8;
optional string deviceName = 9;
enum PlatformType {
kInvalid = 0;
kAndroid = 1;
kiOS = 2;
kWindows = 3;
WECHAT_ANDROID = 4;
WECHAT_IOS = 5;
H5 = 6;
H5_ANDROID = 7;
H5_IOS = 8;
H5_WINDOWS = 9;
H5_MAC = 10;
kPlatformNum = 11;
}
}
message EnvInfo {
optional NetworkType networkType = 1;
optional bytes apnName = 2;
enum NetworkType {
kInvalid = 0;
kWIFI = 1;
kCellular = 2;
}
}
message PushServiceToken {
optional PushType pushType = 1;
optional bytes token = 2;
optional bool isPassThrough = 3;
enum PushType {
kPushTypeInvalid = 0;
kPushTypeAPNS = 1;
kPushTypeXmPush = 2;
kPushTypeJgPush = 3;
kPushTypeGtPush = 4;
kPushTypeOpPush = 5;
kPushTypeVvPush = 6;
kPushTypeHwPush = 7;
kPushTypeFcm = 8;
}
}
message ZtCommonInfo {
optional string kpn = 1;
optional string kpf = 2;
optional int64 uid = 4;
optional string did = 5;
}
message PingResponse {
optional sfixed32 serverTimestamp = 1;
optional fixed32 clientIp = 2;
optional fixed32 redirectIp = 3;
optional uint32 redirectPort = 4;
}
message PingRequest {
optional PingType pingType = 1;
optional uint32 pingRound = 2;
enum PingType {
kInvalid = 0;
kPriorRegister = 1;
kPostRegister = 2;
}
}
message UpstreamPayload {
optional string command = 1;
optional int64 seqId = 2;
optional uint32 retryCount = 3;
optional bytes payloadData = 4;
optional UserInstance userInstance = 5;
optional int32 errorCode = 6;
optional SettingInfo settingInfo = 7;
optional RequsetBasicInfo requestBasicInfo = 8;
optional string subBiz = 9;
optional FrontendInfo frontendInfo = 10;
optional string kpn = 11;
optional bool anonymouseUser = 12;
}
message DownstreamPayload {
optional string command = 1;
optional int64 seqId = 2;
optional int32 errorCode = 3;
optional bytes payloadData = 4;
optional string errorMsg = 5;
optional bytes errorData = 6;
optional string subBiz = 7;
}
message UserInstance {
optional User user = 1;
optional int64 instanceId = 2;
}
message User {
optional int32 appId = 1;
optional int64 uid = 2;
}
message SettingInfo {
optional string locale = 1;
optional sint32 timezone = 2;
}
message RequsetBasicInfo {
optional DeviceInfo.PlatformType clientType = 1;
optional string deviceId = 2;
optional string clientIp = 3;
optional string appVersion = 4;
optional string channel = 5;
optional AppInfo appInfo = 6;
optional DeviceInfo deviceInfo = 7;
optional EnvInfo envInfo = 8;
optional int32 clientPort = 9;
optional string location = 10;
optional string kpf = 11;
}
message FrontendInfo {
optional string ip = 1;
optional int32 port = 2;
}
message PacketHeader {
optional int32 appId = 1;
optional int64 uid = 2;
optional int64 instanceId = 3;
optional uint32 flags = 4;
optional EncodingType encodingType = 6;
optional uint32 decodedPayloadLen = 7;
optional EncryptionMode encryptionMode = 8;
optional TokenInfo tokenInfo = 9;
optional int64 seqId = 10;
repeated Feature features = 11;
optional string kpn = 12;
enum Flags {
option allow_alias = true;
kDirUpstream = 0;
kDirDownstream = 1;
kDirMask = 1;
}
enum EncodingType {
kEncodingNone = 0;
kEncodingLz4 = 1;
}
enum EncryptionMode {
kEncryptionNone = 0;
kEncryptionServiceToken = 1;
kEncryptionSessionKey = 2;
}
enum Feature {
kReserve = 0;
kCompressLz4 = 1;
}
}
message TokenInfo {
optional TokenType tokenType = 1;
optional bytes token = 2;
enum TokenType {
kInvalid = 0;
kServiceToken = 1;
}
}
message KeepAliveRequest {
optional RegisterRequest.PresenceStatus presenceStatus = 1;
optional RegisterRequest.ActiveStatus appActiveStatus = 2;
optional PushServiceToken pushServiceToken = 3;
repeated PushServiceToken pushServiceTokenList = 4;
repeated int32 keepaliveIntervalSec = 5;
}
message ZtLiveScMessage {
optional string messageType = 1;
optional int32 compressionType = 2;
optional bytes payload = 3;
optional string liveId = 4;
optional string ticket = 5;
optional uint64 serverTimestampMs = 6;
}
message ZtLiveScNotifySignal {
repeated ZtLiveNotifySignalItem item = 1;
message ZtLiveNotifySignalItem {
optional string signalType = 1;
repeated bytes payload = 2;
}
}
message ZtLiveScActionSignal {
repeated ZtLiveActionSignalItem item = 1;
message ZtLiveActionSignalItem {
optional string signalType = 1;
repeated bytes payload = 2;
}
}
message ZtLiveScStateSignal {
repeated ZtLiveStateSignalItem item = 1;
message ZtLiveStateSignalItem {
optional string signalType = 1;
repeated bytes payload = 2;
}
}
message ZtLiveScStatusChanged {
optional int32 type = 1;
optional uint64 maxRandomDelayMs = 2;
optional BannedInfo bannedInfo = 3;
message BannedInfo {
optional string banReason = 1;
}
}
message CommonActionSignalComment {
optional string content = 1;
optional uint64 sendTimeMs = 2;
optional ZtLiveUserInfo userInfo = 3;
}
message CommonActionSignalLike {
optional ZtLiveUserInfo userInfo = 1;
optional uint64 sendTimeMs = 2;
}
message CommonActionSignalGift {
optional ZtLiveUserInfo userInfo = 1;
optional uint64 sendTimeMs = 2;
optional uint64 giftId = 3;
optional uint32 batchSize = 4;
optional uint32 comboCount = 5;
optional uint64 rank = 6;
optional string comboKey = 7;
optional uint64 slotDisplayDurationMs = 8;
optional uint64 expireDurationMs = 9;
}
message CommonStateSignalDisplayInfo {
optional string watchingCount = 1;
optional string likeCount = 2;
optional uint32 likeDelta = 3;
}
message CommonStateSignalTopUsers {
repeated TopUser topUser = 1;
message TopUser {
optional ZtLiveUserInfo userInfo = 1;
optional string customWatchingListData = 2;
optional string displaySendAmount = 3;
optional bool anonymousUser = 4;
}
}
message CommonActionSignalUserEnterRoom {
optional ZtLiveUserInfo userInfo = 1;
optional uint64 sendTimeMs = 2;
}
message CommonActionSignalUserFollowAuthor {
optional ZtLiveUserInfo userInfo = 1;
optional uint64 sendTimeMs = 2;
}
message CommonNotifySignalKickedOut {
optional string reason = 1;
}
message CommonNotifySignalViolationAlert {
optional string violationContent = 1;
}
message CommonStateSignalCurrentRedpackList {
}
message CommonStateSignalRecentComment {
optional CommonActionSignalComment comment = 1;
}
message CommonStateSignalChatReady {
optional string chatId = 1;
optional ZtLiveUserInfo guestUserInfo = 2;
optional int32 mediaType = 3;
}
message CommonStateSignalChatEnd {
optional string chatId = 1;
optional int32 endType = 2;
}
message AcfunActionSignalThrowBanana {
optional UserInfo visitor = 1;
optional int32 count = 2;
optional uint64 sendTimeMs = 3;
}
message AcfunStateSignalDisplayInfo {
optional string bananaCount = 1;
}
message ZtLiveUserInfo {
optional uint64 userId = 1;
optional string nickname = 2;
optional ImageCdnNode avatar = 3;
}
message ImageCdnNode {
optional string cdn = 1;
optional string url = 2;
optional string urlPattern = 3;
}
message UserInfo {
optional uint64 userId = 1;
optional string name = 2;
}

310
danmu/danmaku/acfun.py Normal file
View File

@ -0,0 +1,310 @@
# AcFun直播现在属于快手旗下了其JS源码中也可以看到很多名为'kuaishou'的变量所以和快手直播弹幕的获取方法有点类似都使用了protobuf压缩数据。
# 在mplayer-live.xxx.js中可以看到所有websocket过程。奇怪的是其序列化和反序列化时代码并不统一可能有啥"特殊意义"我没看懂。
# ws请求过程Register--EnterRoom--Heartbeat,其中Register后返回的第一条数据里有sessionkey和instanceid。
# 在Chrome调试时发现客户端一直会向服务器发送每1秒一次ping每10秒一次keepalive模拟实现时不发送也正常。
import base64
import gzip
import struct
import time
import requests
from Crypto import Random
from Crypto.Cipher import AES
from Crypto.Util.Padding import pad
from . import acfun_pb2 as pb
class AcFun:
ws_url = 'wss://link.xiatou.com/'
def __init__(self, rid):
self.encryptionmode = 0 # 加密模式0不加密1时key=ssecurity2时key=sessionkey
self.seqId = 0 # 客户端发送请求时都会加1
self.payload_len = 0
self.sessionkey = b''
self.instanceid = 0 # 初始值0
self.gift = { # 有些直播间的礼物编号不一样实际打开直播间页面会有一次XHR请求
1: '香蕉',
17: '快乐水',
9: '告白',
35: '氧气瓶',
4: '牛啤',
33: '情书',
8: '星蕉雨',
31: '金坷垃',
34: '狗粮',
2: '吃瓜',
12: '打Call',
32: '变身腰带',
15: 'AC机娘',
16: '猴岛',
10: '666',
11: '菜鸡',
30: '鸽鸽',
13: '立FLAG',
6: '魔法棒',
7: '好人卡',
14: '窜天猴',
21: '生日快乐',
5: '手柄',
29: '大触'
}
# 下面获取一些加入房间所需注册参数
headers = {
'content-type': 'application/x-www-form-urlencoded',
'cookie': '_did=H5_',
'referer': 'https://m.acfun.cn/'
}
url = 'https://id.app.acfun.cn/rest/app/visitor/login'
form_data = 'sid=acfun.api.visitor'
with requests.Session() as s:
res = s.post(url, data=form_data, headers=headers).json()
status, *d = res.values()
if status == 0:
# 未登陆访客获取的3个参数
acsecurity, self.uid, visitor_st = d
self.ssecurity = base64.b64decode(acsecurity)
self.token = visitor_st.encode()
else:
raise Exception('token 获取错误')
url = 'https://api.kuaishouzt.com/rest/zt/live/web/startPlay'
params = {
'subBiz': 'mainApp',
'kpn': 'ACFUN_APP',
'kpf': 'OUTSIDE_IOS_H5',
'userId': self.uid,
'did': 'H5_',
'acfun.api.visitor_st': visitor_st
}
data = 'authorId={}&pullStreamType=SINGLE_HLS'.format(rid)
res = s.post(url, params=params, data=data, headers=headers).json()
if res['result'] == 1:
data = res['data']
# 获取直播间的几个参数
self.availabletickets, *_ = data['availableTickets']
self.enterroomattach = data['enterRoomAttach']
self.liveid = data['liveId']
else:
raise Exception('直播已关闭')
@staticmethod
# aes加密iv随机key值取决去encryptionmode
def aes_encode(t, key):
t = pad(t, AES.block_size)
iv = Random.new().read(AES.block_size)
mode = AES.MODE_CBC
c = AES.new(key, mode, iv)
res = c.encrypt(t)
return iv + res # 把iv加到头部
@staticmethod
# 这里传入的待解密数据不用补全一直都是16的整数倍。key值取决去encryptionmode。pkcs7模式去掉最后的填充padding。
def aes_decode(t, key):
iv = t[:16]
n = t[16:]
# n = pad(n, AES.block_size)
mode = AES.MODE_CBC
c = AES.new(key, mode, iv)
res = c.decrypt(n)
length = len(res)
padding = res[length - 1]
res = res[0:length - padding]
return res
def register(self):
# 注册
p = pb.RegisterRequest()
p.appActiveStatus = 1
p.appInfo.appName = 'link-sdk'
p.appInfo.sdkVersion = '1.2.1'
p.deviceInfo.deviceModel = 'h5'
p.deviceInfo.platformType = 6
p.instanceId = 0
p.presenceStatus = 1
p.ztCommonInfo.kpn = 'ACFUN_APP'
p.ztCommonInfo.kpf = 'OUTSIDE_IOS_H5'
p.ztCommonInfo.uid = self.uid
register_data = p.SerializeToString()
self.encryptionmode = 1
self.seqId = 1
return register_data
def keepalive(self):
# keepalive
p = pb.KeepAliveRequest()
p.appActiveStatus = 1
p.presenceStatus = 1
keepalive_data = p.SerializeToString()
self.encryptionmode = 2
self.seqId += 1
return keepalive_data
def ping(self):
# ping
p = pb.PingRequest()
# p.pingType = 2
ping_data = p.SerializeToString()
self.seqId += 1
self.encryptionmode = 2
return ping_data
def ztlivecsenterroom(self):
return self.cscmd('ZtLiveCsEnterRoom')
def ztlivecsheartbeat(self):
# 心跳数据
return self.cscmd('ZtLiveCsHeartbeat')
def cscmd(self, payload_type):
p = getattr(pb, payload_type)()
if payload_type == 'ZtLiveCsEnterRoom':
p.isAuthor = False
p.reconnectCount = 0
p.enterRoomAttach = self.enterroomattach
p.clientLiveSdkVersion = 'kwai-acfun-live-link'
elif payload_type == 'ZtLiveCsHeartbeat':
p.sequence = self.seqId
p.clientTimestampMs = int(time.time() * 1000)
payload = p.SerializeToString()
p = pb.CsCmd()
p.cmdType = payload_type
p.ticket = self.availabletickets
p.payload = payload
p.liveId = self.liveid
cscmd_data = p.SerializeToString()
self.seqId += 1
self.encryptionmode = 2
return cscmd_data
def encode_payload(self, payload_type):
# 要发送的原始数据都是先序列化再拼接
c = {
'keepalive': 'Basic.KeepAlive',
'register': 'Basic.Register',
'ping': 'Basic.Ping',
'ztlivecsenterroom': 'Global.ZtLiveInteractive.CsCmd',
'ztlivecsheartbeat': 'Global.ZtLiveInteractive.CsCmd'
}
p = pb.UpstreamPayload()
p.command = c[payload_type]
p.retryCount = 1
p.payloadData = getattr(self, payload_type)()
p.seqId = self.seqId
p.subBiz = 'mainApp'
e = p.SerializeToString()
key = self.ssecurity if self.encryptionmode == 1 else self.sessionkey
payload_data = AcFun.aes_encode(e, key) if self.encryptionmode != 0 else e
self.payload_len = len(e)
return payload_data
def encode_head(self):
# 头部数据,里面有原始数据长度
p = pb.PacketHeader()
p.appId = 13
p.decodedPayloadLen = self.payload_len
p.encryptionMode = self.encryptionmode
p.instanceId = self.instanceid
p.kpn = 'ACFUN_APP'
p.seqId = self.seqId
if self.encryptionmode == 1:
p.tokenInfo.tokenType = 1
p.tokenInfo.token = self.token
p.uid = self.uid
head = p.SerializeToString()
return head
def encode_packet(self, payload_type):
# 数据组成:固定数据 + 头部长度 + 原始数据长度 + 头部 + 原始数据
body = self.encode_payload(payload_type)
head = self.encode_head()
data = struct.pack('!HHII', 43981, 1, len(head), len(body))
data += head + body
return data
def decode_packet(self, data):
# 数据解包
msgs = [{'name': '', 'content': '', 'msg_type': 'other'}]
head_length, body_length = struct.unpack('!II', data[4:12])
if 12 + head_length + body_length != len(data):
raise Exception('downstream message size is not correct')
# 头部解包
e = data[12:head_length + 12]
c = pb.PacketHeader()
c.ParseFromString(e)
encryptionmode = c.encryptionMode # 根据加密模式确定解密的key
# body解包
h = data[head_length + 12:]
key = self.ssecurity if encryptionmode == 1 else self.sessionkey
n = AcFun.aes_decode(h, key) if encryptionmode != 0 else h
u = pb.DownstreamPayload()
u.ParseFromString(n)
# header = c
# body = u
payload = u.payloadData
command = u.command # 根据command确定返回数据类型
# print(command)
if command == 'Basic.Register':
# websocket第一次返回数据,确定sessKey和instanceId
p = pb.RegisterResponse()
p.ParseFromString(payload)
self.sessionkey = p.sessKey
self.instanceid = p.instanceId
elif command == 'Push.ZtLiveInteractive.Message': # 'Push.ZtLiveInteractive.Message'
a = pb.ZtLiveScMessage()
a.ParseFromString(payload)
o = a.messageType
s = a.payload
if a.compressionType == 2:
s = gzip.decompress(s)
if o == 'ZtLiveScTicketInvalid':
raise Exception('ZtLiveScTicketInvalid')
# o可为'ZtLiveScNotifySignal' 'ZtLiveScActionSignal' 等,弹幕礼物入场点赞等在ZtLiveScActionSignal中
elif o == 'ZtLiveScActionSignal':
p = getattr(pb, o)()
p.ParseFromString(s)
for i in p.item:
p = getattr(pb, i.signalType)()
# signalType:
# CommonActionSignalComment 评论
# CommonActionSignalGift 礼物
# CommonActionSignalUserEnterRoom 入场
# CommonActionSignalLike 点赞
# CommonActionSignalUserFollowAuthor 关注主播
# 等等等
u = {
'CommonActionSignalUserEnterRoom': '进入直播间',
'CommonActionSignalUserFollowAuthor': '关注了主播',
'CommonActionSignalComment': '',
'CommonActionSignalLike': '点赞了❤',
'CommonActionSignalGift': ''
}
for a_payload in i.payload: # i.payload 是 repeated 类型
p.ParseFromString(a_payload)
if i.signalType in u.keys():
user = p.userInfo.nickname
content = u[i.signalType]
if i.signalType == 'CommonActionSignalComment':
content = p.content
elif i.signalType == 'CommonActionSignalGift':
content = '送出 ' + self.gift.get(p.giftId, '')
msg = {'name': user, 'content': content, 'msg_type': 'danmaku'}
msgs.append(msg.copy())
return msgs

3650
danmu/danmaku/acfun_pb2.py Normal file

File diff suppressed because one or more lines are too long