2019-02-15 18:14:27 +08:00
|
|
|
# Bilibili API JVM 调用库
|
|
|
|
该项目提供 Bilibili API 的 JVM 调用, 协议来自 Bilibili Android APP 的逆向工程以及截包分析.
|
2017-11-07 16:48:53 +08:00
|
|
|
|
2019-02-26 17:49:31 +08:00
|
|
|
使用一台虚拟的 `Pixel 2` 设备来截取数据包, 一些固定参数可能与真实设备不一致.
|
|
|
|
|
2019-02-21 18:59:53 +08:00
|
|
|
# 技术说明
|
|
|
|
`BilibiliClient` 类表示一个模拟的客户端, 实例化此类即表示打开了 Bilibili APP.
|
2019-02-20 14:55:07 +08:00
|
|
|
|
2019-02-21 18:59:53 +08:00
|
|
|
所有调用从这个类开始, 包括登陆以及访问其他各种 API.
|
2019-02-20 00:05:53 +08:00
|
|
|
|
2019-02-21 18:59:53 +08:00
|
|
|
使用协程来实现异步, 由于 [kotlin coroutines](https://kotlinlang.org/docs/reference/coroutines-overview.html) 为编译器实现, 因此并非所有 JVM 语言都能正确调用 `suspend` 方法.
|
2019-02-20 00:05:53 +08:00
|
|
|
|
2019-02-21 18:59:53 +08:00
|
|
|
本项目尽可能的兼容其他 JVM 语言和 Android, 不要问, 问就没测试过.
|
|
|
|
|
|
|
|
`BilibiliClient` 实例化时会记录一些信息, 例如初始化的事件, 用于更逼真的模拟真实客户端发送的请求. 因此请不要每次都实例化一个新的 `BilibiliClient` 实例, 而应该保存其引用.
|
|
|
|
|
|
|
|
一个客户端下各种不同类型的 API (代理类)都是惰性初始化的, 并且只初始化一次, 因此不需要保存 API 的引用, 例如以下代码是被推荐的:
|
2019-02-20 00:05:53 +08:00
|
|
|
|
2019-02-21 18:59:53 +08:00
|
|
|
```kotlin
|
|
|
|
runBlocking {
|
|
|
|
val bilibiliClient = BilibiliClient().apply {
|
|
|
|
login(username, password)
|
2019-02-20 00:05:53 +08:00
|
|
|
}
|
2019-02-21 18:59:53 +08:00
|
|
|
val myInfo = bilibiliClient.appAPI.myInfo().await()
|
|
|
|
val reply = bilibiliClient.mainAPI.reply(oid = 44154463).await()
|
|
|
|
}
|
|
|
|
```
|
2019-02-20 00:05:53 +08:00
|
|
|
|
2019-02-21 18:59:53 +08:00
|
|
|
如果一个请求的返回内容中的 `code`(code 是 BODY 的内容, 并非 HttpStatus) 不为 0, 将抛出异常 `BilibiliApiException`, 通过以下代码来获取服务器原始返回的 `code`:
|
2019-02-20 00:05:53 +08:00
|
|
|
|
2019-02-21 18:59:53 +08:00
|
|
|
```kotlin
|
|
|
|
val code = bilibiliApiException.commonResponse.code
|
|
|
|
```
|
2019-02-20 00:05:53 +08:00
|
|
|
|
2019-02-21 18:59:53 +08:00
|
|
|
一个错误返回的原始 `JSON` 如下所示:
|
2019-02-20 00:05:53 +08:00
|
|
|
|
2019-02-21 18:59:53 +08:00
|
|
|
```json
|
|
|
|
{
|
|
|
|
"code": -629,
|
|
|
|
"message": "用户名与密码不匹配",
|
|
|
|
"ts": 1550730464
|
|
|
|
}
|
|
|
|
```
|
2019-02-20 00:05:53 +08:00
|
|
|
|
2019-02-21 18:59:53 +08:00
|
|
|
每种不同的 API 在错误时返回的 `code` 丰富多彩(确信), 可能是正数也可能是负数, 可能上万也可能是个位数, 不要问, 问就是你菜.
|
2019-02-20 00:05:53 +08:00
|
|
|
|
2019-02-21 18:59:53 +08:00
|
|
|
# 登录和登出
|
|
|
|
(Bilibili oauth2 v3)
|
2019-02-20 00:05:53 +08:00
|
|
|
|
2019-02-21 18:59:53 +08:00
|
|
|
登陆和登出均为异步方法, 需要在协程上下文中执行.
|
2019-02-20 00:05:53 +08:00
|
|
|
|
2019-02-24 21:26:49 +08:00
|
|
|
如果所使用的语言无法正确调用 `suspend` 方法, 可以使用 `loginFuture` 方法来替代, 它会返回一个 Java8 `CompletableFuture`.
|
|
|
|
|
|
|
|
`logoutFuture` 同理.
|
|
|
|
|
2019-02-21 18:59:53 +08:00
|
|
|
```kotlin
|
|
|
|
runBlocking {
|
|
|
|
BilibiliClient().run {
|
|
|
|
login(username, password)
|
|
|
|
logout()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
```
|
2019-02-20 00:05:53 +08:00
|
|
|
|
2019-02-21 18:59:53 +08:00
|
|
|
`login` 方法返回一个 `LoginResponse` 实例, 下次可以直接赋值到没有登陆的 `BilibiliClient` 实例中来恢复登陆状态.
|
2019-02-20 00:05:53 +08:00
|
|
|
|
2019-02-21 18:59:53 +08:00
|
|
|
```kotlin
|
|
|
|
BilibiliClient().apply {
|
|
|
|
this.loginResponse = loginResponse
|
|
|
|
}
|
|
|
|
```
|
2019-02-20 00:05:53 +08:00
|
|
|
|
2019-02-21 18:59:53 +08:00
|
|
|
`LoginResponse` 继承 `Serializable`, 可被序列化(JVM 序列化).
|
2019-02-20 00:05:53 +08:00
|
|
|
|
2019-02-21 18:59:53 +08:00
|
|
|
可能的错误返回有两种:
|
2019-02-20 00:05:53 +08:00
|
|
|
|
2019-02-21 18:59:53 +08:00
|
|
|
-629 用户名与密码不匹配
|
|
|
|
-105 验证码错误
|
2019-02-20 18:24:47 +08:00
|
|
|
|
2019-02-21 18:59:53 +08:00
|
|
|
如果仅使用用户名与密码进行登陆并且得到了 `-105` 的结果, 那么说明需要验证码(通常是由于多次错误的登陆尝试导致的).
|
2019-02-20 18:24:47 +08:00
|
|
|
|
2019-02-21 18:59:53 +08:00
|
|
|
原始返回如下所示
|
2019-02-20 18:24:47 +08:00
|
|
|
|
2019-02-21 18:59:53 +08:00
|
|
|
{"ts":1550569982,"code":-105,"data":{"url":"https://passport.bilibili.com/register/verification.html?success=1>=b6e5b7fad7ecd37f465838689732e788&challenge=9a67afa4d42ede71a93aeaaa54a4b6fe&ct=1&hash=105af2e7cc6ea829c4a95205f2371dc5"},"message":"验证码错误!"}
|
2019-02-20 14:55:07 +08:00
|
|
|
|
2019-02-21 18:59:53 +08:00
|
|
|
自行访问 `commonResponse.data.obj.url.string` 打开一个极验弹窗, 完成滑动验证码后再次调用登陆接口:
|
2019-02-20 14:55:07 +08:00
|
|
|
|
2019-02-21 18:59:53 +08:00
|
|
|
```kotlin
|
|
|
|
login(username, password, challenge, secCode, validate)
|
|
|
|
```
|
2019-02-20 14:55:07 +08:00
|
|
|
|
2019-02-21 18:59:53 +08:00
|
|
|
`challenge` 为本次极验的唯一标识(在一开始给出的 url 中)
|
2019-02-20 14:55:07 +08:00
|
|
|
|
2019-02-21 18:59:53 +08:00
|
|
|
`validate` 为极验返回值
|
2019-02-20 18:24:47 +08:00
|
|
|
|
2019-02-21 18:59:53 +08:00
|
|
|
`secCode` 为 `"$validate|jordan"`
|
|
|
|
|
|
|
|
(注意, 极验会根据滑动的轨迹来识别人机, 所以要为最终用户打开一个 WebView 来进行真人操作而不能自动完成. 极验最终返回的是一个 jsonp, 里面包含以上三个参数, 详见极验接入文档).
|
2019-02-20 00:05:53 +08:00
|
|
|
|
2019-02-21 18:59:53 +08:00
|
|
|
注意, `BilibiliClient` 不能严格保证线程安全, 如果在登出的同时进行登录操作可能引发错误.
|
|
|
|
|
|
|
|
登陆后, 可以访问全部 API.
|
2019-02-21 00:09:25 +08:00
|
|
|
|
2019-02-21 18:59:53 +08:00
|
|
|
# 访问 API
|
2019-02-25 23:31:18 +08:00
|
|
|
不要问文档, 用自动补全(心)来感受. 以下给出几个示例
|
|
|
|
|
|
|
|
## 获取个人信息
|
|
|
|
(首先要登陆)
|
2019-02-21 00:09:25 +08:00
|
|
|
|
2019-02-21 18:59:53 +08:00
|
|
|
```kotlin
|
|
|
|
val myInfo = bilibiliClient.appAPI.myInfo().await()
|
|
|
|
```
|
2019-02-21 00:09:25 +08:00
|
|
|
|
2019-02-25 23:31:18 +08:00
|
|
|
返回用户 ID, vip 信息等.
|
|
|
|
|
|
|
|
## 获取视频播放地址
|
|
|
|
获取视频实际播放地址的 API 比较特殊, 被单独分了出来, 示例如下
|
|
|
|
|
|
|
|
```kotlin
|
|
|
|
val videoPlayUrl = bilibiliClient.playerAPI.videoPlayUrl(aid = 41517911, cid = 72913641).await()
|
|
|
|
```
|
|
|
|
|
|
|
|
`aid` 即 av 号, 只能表示视频播放的那个页面, 如果一个视频有多个 `p`, 那么每个 `p` 都有单独的 `cid`.
|
|
|
|
|
|
|
|
在 Web 端, URL 通常是这样的
|
|
|
|
|
|
|
|
https://www.bilibili.com/video/av44541340/?p=2
|
|
|
|
|
|
|
|
实际上就是选择了该 `aid` 下的第二个 `cid`.
|
|
|
|
|
|
|
|
简单的来说, `aid` 和 `cid` 加在一起才能表示一个视频流(为什么 `cid` 不能直接表示一个视频我也不知道).
|
|
|
|
|
|
|
|
因此无论是获取视频播放地址, 还是获取弹幕列表, 都要同时传入 `aid` 与 `cid`.
|
|
|
|
|
|
|
|
而 `cid` 在哪里获得呢, 如下所示
|
|
|
|
|
|
|
|
```kotlin
|
|
|
|
val view = bilibiliClient.appAPI.view(aid = 41517911).await()
|
|
|
|
```
|
|
|
|
|
|
|
|
该接口返回对一个视频页面的描述信息(甚至包含广告和推荐), 客户端根据这些信息生成视频页面.
|
|
|
|
|
2019-02-26 14:58:14 +08:00
|
|
|
其中 `data.cid` 为默认 `p` 的 `cid`. `data.pages[n].cid` 为每个 `p` 的 `cid`. 如果只有一个 `p` 那么说明视频没有分 `p`.
|
2019-02-25 23:31:18 +08:00
|
|
|
|
|
|
|
请求视频地址将访问如下结构的内容
|
|
|
|
|
|
|
|
```json
|
|
|
|
{
|
|
|
|
"code": 0,
|
|
|
|
"data": {
|
|
|
|
"accept_description": [
|
|
|
|
"高清 1080P+",
|
|
|
|
"高清 1080P",
|
|
|
|
"高清 720P",
|
|
|
|
"清晰 480P",
|
|
|
|
"流畅 360P"
|
|
|
|
],
|
|
|
|
"accept_format": "hdflv2,flv,flv720,flv480,flv360",
|
|
|
|
"accept_quality": [
|
|
|
|
112,
|
|
|
|
80,
|
|
|
|
64,
|
|
|
|
32,
|
|
|
|
16
|
|
|
|
],
|
|
|
|
"dash": {
|
|
|
|
"audio": [
|
|
|
|
{
|
|
|
|
"bandwidth": 319173,
|
|
|
|
"base_url": "http://upos-hz-mirrorks3u.acgvideo.com/upgcxcode/18/58/77995818/77995818-1-30280.m4s?e=ig8euxZM2rNcNbdlhoNvNC8BqJIzNbfqXBvEuENvNC8aNEVEtEvE9IMvXBvE2ENvNCImNEVEIj0Y2J_aug859r1qXg8xNEVE5XREto8GuFGv2U7SuxI72X6fTr859IB_&deadline=1551113319&gen=playurl&nbs=1&oi=3670888782&os=ks3u&platform=android&trid=925269b941bf4883ac9ec92c6ab5af4e&uipk=5&upsig=33273eaf403739d9f51304509f55589e",
|
|
|
|
"codecid": 0,
|
|
|
|
"id": 30280
|
|
|
|
},
|
|
|
|
{
|
|
|
|
"bandwidth": 67326,
|
|
|
|
"base_url": "http://upos-hz-mirrorkodou.acgvideo.com/upgcxcode/18/58/77995818/77995818-1-30216.m4s?e=ig8euxZM2rNcNbdlhoNvNC8BqJIzNbfqXBvEuENvNC8aNEVEtEvE9IMvXBvE2ENvNCImNEVEIj0Y2J_aug859r1qXg8xNEVE5XREto8GuFGv2U7SuxI72X6fTr859IB_&deadline=1551113319&gen=playurl&nbs=1&oi=3670888782&os=kodou&platform=android&trid=925269b941bf4883ac9ec92c6ab5af4e&uipk=5&upsig=3d1f9b836430bb8033b2f318faf42f9b",
|
|
|
|
"codecid": 0,
|
|
|
|
"id": 30216
|
|
|
|
}
|
|
|
|
],
|
|
|
|
"video": [
|
|
|
|
{
|
|
|
|
"bandwidth": 376693,
|
|
|
|
"base_url": "http://upos-hz-mirrorks3u.acgvideo.com/upgcxcode/18/58/77995818/77995818-1-30015.m4s?e=ig8euxZM2rNcNbdlhoNvNC8BqJIzNbfqXBvEuENvNC8aNEVEtEvE9IMvXBvE2ENvNCImNEVEIj0Y2J_aug859r1qXg8xNEVE5XREto8GuFGv2U7SuxI72X6fTr859IB_&deadline=1551113319&gen=playurl&nbs=1&oi=3670888782&os=ks3u&platform=android&trid=925269b941bf4883ac9ec92c6ab5af4e&uipk=5&upsig=82bc845bce9f22b731b062bf83fa000f",
|
|
|
|
"codecid": 7,
|
|
|
|
"id": 16
|
|
|
|
},
|
|
|
|
...
|
|
|
|
{
|
|
|
|
"bandwidth": 2615324,
|
|
|
|
"base_url": "http://upos-hz-mirrorcosu.acgvideo.com/upgcxcode/18/58/77995818/77995818-1-30080.m4s?e=ig8euxZM2rNcNbdlhoNvNC8BqJIzNbfqXBvEuENvNC8aNEVEtEvE9IMvXBvE2ENvNCImNEVEIj0Y2J_aug859r1qXg8xNEVE5XREto8GuFGv2U7SuxI72X6fTr859IB_&deadline=1551113319&dynamic=1&gen=playurl&oi=3670888782&os=cosu&platform=android&rate=0&trid=925269b941bf4883ac9ec92c6ab5af4e&uipk=5&uipv=5&um_deadline=1551113319&um_sign=22fef3c0efa0d23388429f6926fad298&upsig=c4768c036beb667ba4648369770f8de8",
|
|
|
|
"codecid": 7,
|
|
|
|
"id": 80
|
|
|
|
}
|
|
|
|
]
|
|
|
|
},
|
|
|
|
"fnval": 16,
|
|
|
|
"fnver": 0,
|
|
|
|
"format": "flv480",
|
|
|
|
"from": "local",
|
|
|
|
"quality": 32,
|
|
|
|
"result": "suee",
|
|
|
|
"seek_param": "start",
|
|
|
|
"seek_type": "offset",
|
|
|
|
"timelength": 175332,
|
|
|
|
"video_codecid": 7,
|
|
|
|
"video_project": true
|
|
|
|
},
|
|
|
|
"message": "0",
|
|
|
|
"ttl": 1
|
|
|
|
}
|
|
|
|
```
|
|
|
|
|
|
|
|
(由于内容太长, 去除了一部分内容)
|
|
|
|
|
|
|
|
注意, 视频下载地址有好几个(以上返回内容中被折叠成了两个), 但是实际上他们都是一样的内容, 只是清晰度不同. `data.dash.video.id` 实际上代表 `data.accept_quality`.
|
|
|
|
|
|
|
|
视频和音频是分开的, 视频和音频都返回 `m4s` 文件, 将其合并即可得到完整的 `mp4` 文件.
|
|
|
|
|
|
|
|
`data.quality` 指默认选择的清晰度, 通常情况下移动网络会自动选择 `32`, 即 "清晰 480P"(在 `data.accept_description` 中对应).
|
|
|
|
|
|
|
|
对于番剧来说, 也使用 `aid` 与 `cid` 来获得播放地址
|
|
|
|
|
|
|
|
```kotlin
|
|
|
|
val bangumiPlayUrl = bilibiliClient.playerAPI.bangumiPlayUrl(aid = 42714241, cid = 74921228).await()
|
|
|
|
```
|
|
|
|
|
|
|
|
返回内容差不多是一个原理, 这里就不赘述了.
|
|
|
|
|
|
|
|
如何获得番剧的 `aid` 与 `cid` 呢. 我们都知道, 实际上番剧那个页面的唯一标识是 "季", 同一个番的不同 "季" 其实是不同的东西.
|
|
|
|
|
|
|
|
我们在番剧搜索页面可以得到番剧的 `season`, 这代表了一个番剧的某一季的页面.
|
|
|
|
|
|
|
|
然后我们用 `season` 来打开番剧页面.
|
|
|
|
|
|
|
|
```kotlin
|
|
|
|
val season = bilibiliClient.mainAPI.season(seasonId = 25617).await()
|
|
|
|
```
|
|
|
|
|
|
|
|
返回值中的 `result.seasons[n].season_id` 为该番所有季的 id(包含用来作为查询条件的 `seasonId`).
|
|
|
|
|
|
|
|
该 API 还可以用 `episodeId` 作为查询条件, 即以集为条件打开一个番剧页面(会跳转到对应的季).
|
|
|
|
|
|
|
|
返回值中的 `result.episodes` 包含了当前所选择的季的全部集的 `aid` 与 `cid`.
|
|
|
|
|
|
|
|
搜索视频和番剧的功能绝赞咕咕咕中.
|
|
|
|
|
|
|
|
## 查看视频下面的评论
|
|
|
|
看完了视频当然要看一下傻吊网友都在说些什么. 使用以下 API 获取一个视频的评论.
|
|
|
|
|
|
|
|
```kotlin
|
|
|
|
val reply = bilibiliClient.mainAPI.reply(oid = 44154463).await()
|
|
|
|
```
|
|
|
|
|
|
|
|
这里的 `oid` 指 `aid`(其他一些 API 中 `oid` 也可能指 `cid` 详见方法上面的注释).
|
|
|
|
|
|
|
|
评论是不分 `p` 的, 所有评论都是在一起的.
|
|
|
|
|
2019-02-26 14:58:14 +08:00
|
|
|
可以额外使用一个 `next` 参数来指定返回的起始楼层(即翻页).
|
|
|
|
|
|
|
|
楼层是越翻越小的, 所以 `next` 也要越来越小.
|
|
|
|
|
2019-02-25 23:31:18 +08:00
|
|
|
看到了傻吊网友们的评论是不够的, 我们还想看到杠精与其隔着屏幕对喷的场景, 因此我们要获取评论的子评论, 即评论的评论
|
|
|
|
|
|
|
|
```kotlin
|
|
|
|
val childReply = bilibiliClient.mainAPI.childReply(oid = 16622855, root = 1405602348).await()
|
|
|
|
```
|
|
|
|
|
2019-02-26 18:21:58 +08:00
|
|
|
其中的 `root` 表示根评论的 id.
|
2019-02-25 23:31:18 +08:00
|
|
|
|
2019-02-26 18:21:58 +08:00
|
|
|
每个评论都有自己的 `replyId`, `parentId` 以及 `rootId`.
|
2019-02-25 23:31:18 +08:00
|
|
|
|
2019-02-26 18:21:58 +08:00
|
|
|
假如一个人在一个评论的子评论里发布了一个评论并且 at 了其他人发的评论, 那么其 `parentId` 是他所 at 的评论, 其 `rootId` 为所在的根评论.
|
|
|
|
|
2019-02-27 00:05:58 +08:00
|
|
|
如果不满足对应的层级逻辑关系(例如本身为根评论), `parentId` 或 `rootId` 可能为 0.
|
2019-02-25 23:31:18 +08:00
|
|
|
|
2019-02-26 14:58:14 +08:00
|
|
|
用额外的 `minId` 参数来指定返回的起始子楼层.
|
|
|
|
|
|
|
|
注意, 子楼层是越翻越大的.
|
|
|
|
|
2019-02-26 18:21:58 +08:00
|
|
|
如果一个根评论下面有很多个喷子在互喷, 会导致看不清, 客户端上有一个按钮 "查看对话" 就是解决这个问题的.
|
|
|
|
|
|
|
|
```kotlin
|
|
|
|
val chatList = bilibiliClient.mainAPI.chatList(oid = 34175504, root = 1136310360, dialog = 1136351035).await()
|
|
|
|
```
|
|
|
|
|
|
|
|
`root` 为根评论 ID, `dialog` 为父评论 ID.
|
|
|
|
|
|
|
|
用 `minFloor` 控制分页, 原理同上.
|
|
|
|
|
2019-02-26 14:58:14 +08:00
|
|
|
番剧下面的评论用一样的方式获取.
|
2019-02-25 23:31:18 +08:00
|
|
|
|
|
|
|
## 获得一个视频的弹幕
|
|
|
|
看评论自然不够刺激, 我们想看到弹幕!
|
|
|
|
|
2019-02-26 14:58:14 +08:00
|
|
|
获取弹幕非常简单
|
|
|
|
|
|
|
|
```kotlin
|
|
|
|
val danmakuFile = bilibiliClient.danmakuAPI.list(aid = 810872, oid = 1176840).await()
|
|
|
|
```
|
|
|
|
|
|
|
|
弹幕是一个文件, 可能非常大, 里面是二进制内容.
|
|
|
|
|
|
|
|
为了解析弹幕, 我们要用到另一个类
|
|
|
|
|
|
|
|
```kotlin
|
|
|
|
val (flagMap, danmakuList) = DanmakuParser.parser(danmakuFile.byteStream())
|
|
|
|
```
|
|
|
|
|
|
|
|
`flagMap` 类型为 `Map<Long, Int>` 键和值分别表示 弹幕ID 与 弹幕等级.
|
|
|
|
|
|
|
|
弹幕等级在区间 \[1, 10\] 内, 低于客户端设置的 "弹幕云屏蔽等级" 的弹幕将不会显示出来.
|
|
|
|
|
|
|
|
`danmakuList` 类型为 `List<Danmaku>`, 内含所有解析得到的弹幕.
|
|
|
|
|
|
|
|
使用以下代码来输出全部弹幕的内容
|
|
|
|
|
|
|
|
```kotlin
|
|
|
|
danmakuList.forEach {
|
|
|
|
println(it.content)
|
|
|
|
}
|
|
|
|
```
|
|
|
|
|
|
|
|
客户端的弹幕屏蔽设置是对弹幕中的 `user` 属性做的. 而实际上 `danmaku.user` 是一个字符串.
|
|
|
|
|
|
|
|
这个字符串是 用户ID 的 `CRC32` 的校验和.
|
|
|
|
|
|
|
|
众所周知, 一切 hash 算法都有冲突的问题. 这也就意味着, 屏蔽一个用户的同时可能屏蔽掉了多个与该用户 hash 值相同的用户.
|
|
|
|
|
|
|
|
在另一方面, 通过这个 `CRC32` 校验和进行用户 ID 反查, 将查询到多个可能的用户, 因此无法确定一条弹幕到底是哪个用户发送的.
|
|
|
|
|
2019-03-04 00:02:56 +08:00
|
|
|
如果想获得发送这条弹幕的所有可能的用户的 ID, 可以通过以下方法:
|
|
|
|
|
|
|
|
```kotlin
|
|
|
|
val possibleUserIds = danmaku.calculatePossibleUserIds()
|
|
|
|
```
|
|
|
|
|
|
|
|
返回一个 `List<Int>`, 内容为所有可能的用户 ID(至少有一个).
|
|
|
|
|
|
|
|
注意, 第一次使用 `CRC反查` 功能将花费大约 `300ms` 来生成彩虹表, 如果想手动初始化请使用以下代码
|
|
|
|
|
|
|
|
```kotlin
|
|
|
|
Crc32Cracker
|
|
|
|
```
|
|
|
|
|
|
|
|
(`Crc32Cracker` 是一个惰性初始化的单例)
|
|
|
|
|
|
|
|
通常情况下, 一次 `CRC反查` 耗时大约 `1ms`.
|
|
|
|
|
|
|
|
由于这是一个比较耗时的操作, 请不要每条弹幕都如此操作(相比较 6000 条弹幕的解析只需要 `150ms`).
|
|
|
|
|
2019-02-26 14:58:14 +08:00
|
|
|
番剧的弹幕同理.
|
2018-03-02 16:31:44 +08:00
|
|
|
|
2019-02-28 23:42:45 +08:00
|
|
|
## 发送弹幕
|
|
|
|
光看不发憋着慌, 我们来发送一条视频弹幕:
|
|
|
|
|
|
|
|
```kotlin
|
|
|
|
bilibiliClient.mainAPI.sendDanmaku(aid = 40675923, cid = 71438168, progress = 2297, message = "2333").await()
|
|
|
|
```
|
|
|
|
|
|
|
|
其中 `progress` 是播放器时间, 其他观众将看到你的弹幕在视频的此处出现, 单位为毫秒.
|
|
|
|
|
|
|
|
`message` 应该是有长度限制的, 但是没有测过.
|
|
|
|
|
|
|
|
如果不确定视频的长度, 需要从[视频播放地址的 API](#获取视频播放地址) 中的 `data.timelength` 来获得, 单位也是毫秒.
|
|
|
|
|
2017-11-07 16:48:53 +08:00
|
|
|
# License
|
|
|
|
GPL V3
|