mirror of
https://github.com/czp3009/bilibili-api.git
synced 2025-02-19 20:50:28 +08:00
实现直播弹幕发送
This commit is contained in:
parent
759327f12a
commit
acc1a308ef
25
README.md
25
README.md
@ -364,7 +364,7 @@ Crc32Cracker
|
||||
|
||||
番剧的弹幕同理.
|
||||
|
||||
## 发送弹幕
|
||||
## 发送视频弹幕
|
||||
光看不发憋着慌, 我们来发送一条视频弹幕:
|
||||
|
||||
```kotlin
|
||||
@ -472,5 +472,28 @@ onClose = { liveClient, closeReason ->
|
||||
|
||||
如果数据包被中间人修改, 那么可能不会触发 `onConnect` 回调, 但是会触发 `onClose`.
|
||||
|
||||
如果手动取消了执行 `start()` 方法的协程将不会触发 `onClose`.
|
||||
|
||||
## 发送直播弹幕
|
||||
在直播间里发送弹幕也非常简单(必须先登陆)
|
||||
|
||||
```kotlin
|
||||
liveClient.sendMessage("我上我也行").await()
|
||||
```
|
||||
|
||||
注意, 除了弹幕超长(普通用户为 20 个 Unicode 字符, 老爷, 会员可以额外加长)会导致抛出异常, 其他情况都会正常返回.
|
||||
|
||||
完全正常返回时, 返回内容中的 `message` 为一个空字符串.
|
||||
|
||||
如果不为空字符串, 则表示不完全正常
|
||||
|
||||
例如返回内容的 `message` 为 "msg repeat" 则表示短时间重复发送相同的弹幕而被服务器拒绝, 但是返回的 `code` 为 0.
|
||||
|
||||
其他情况诸如包含特殊字符, 包含不文明词语等均会导致不完全正常的返回.
|
||||
|
||||
正常返回时, 客户端都会将这条弹幕显示到屏幕上, 如果不是完全正常的, 那么这条弹幕就只有自己能看见(刷新后也会消失).
|
||||
|
||||
所以请额外判断返回的 `message` 是否为空字符串.
|
||||
|
||||
# License
|
||||
GPL V3
|
||||
|
@ -248,12 +248,13 @@ class BilibiliClient(
|
||||
fetchRoomId: Boolean = true,
|
||||
fetchDanmakuConfig: Boolean = true,
|
||||
doEntryRoomAction: Boolean = false,
|
||||
sendUserOnlineHeart: Boolean = false,
|
||||
onConnect: (LiveClient) -> Unit,
|
||||
onPopularityPacket: (LiveClient, Int) -> Unit,
|
||||
onCommandPacket: (LiveClient, JsonObject) -> Unit,
|
||||
onClose: (LiveClient, CloseReason?) -> Unit
|
||||
) = LiveClient(
|
||||
this, roomId, fetchRoomId, fetchDanmakuConfig, doEntryRoomAction,
|
||||
this, roomId, fetchRoomId, fetchDanmakuConfig, doEntryRoomAction, sendUserOnlineHeart,
|
||||
onConnect, onPopularityPacket, onCommandPacket, onClose
|
||||
)
|
||||
|
||||
|
@ -19,10 +19,10 @@ data class Danmaku(
|
||||
val time: Long,
|
||||
|
||||
/**
|
||||
* 弹幕类型
|
||||
* 弹幕模式
|
||||
* (1从右至左滚动弹幕|6从左至右滚动弹幕|5顶端固定弹幕|4底端固定弹幕|7高级弹幕|8脚本弹幕)
|
||||
*/
|
||||
val type: Int,
|
||||
val mode: Int,
|
||||
|
||||
/**
|
||||
* 字号
|
||||
|
@ -2,8 +2,10 @@ package com.hiczp.bilibili.api.live
|
||||
|
||||
import com.hiczp.bilibili.api.live.model.*
|
||||
import com.hiczp.bilibili.api.retrofit.CommonResponse
|
||||
import com.hiczp.bilibili.api.retrofit.Header
|
||||
import kotlinx.coroutines.Deferred
|
||||
import retrofit2.http.*
|
||||
import kotlin.random.Random
|
||||
|
||||
/**
|
||||
* 直播站 API
|
||||
@ -183,4 +185,49 @@ interface LiveAPI {
|
||||
@Query("page_size") pageSize: Int = 30,
|
||||
@Query("sort_type") sortType: String? = null
|
||||
): Deferred<RoomList>
|
||||
|
||||
/**
|
||||
* 发送弹幕(直播)
|
||||
*
|
||||
* @param bubble 气泡, 不明确含义
|
||||
* @param cid 房间号
|
||||
* @param mid 发送者的用户 ID
|
||||
* @param message 弹幕内容
|
||||
* @param random 随机数, 不包括符号位有 9 位 或者 10 位
|
||||
* @param mode 弹幕模式, 可能与视频弹幕的模式含义相同, 可能需要特殊身份才能使用额外模式, 下同
|
||||
* @param pool 弹幕池
|
||||
* @param type 固定为 "json"
|
||||
* @param color 弹幕颜色
|
||||
* @param fontSize 弹幕字号
|
||||
* @param playTime 不明确
|
||||
*/
|
||||
@Suppress("SpellCheckingInspection")
|
||||
@POST("/api/sendmsg")
|
||||
@FormUrlEncoded
|
||||
@Headers(Header.FORCE_QUERY)
|
||||
fun sendMessage(
|
||||
@Field("bubble") bubble: Int = 0,
|
||||
@Field("cid") cid: Long,
|
||||
@Field("mid") mid: Long,
|
||||
@Field("msg") message: String,
|
||||
@Field("rnd") random: Int = (if (Random.nextBoolean()) 1 else -1) * Random.nextInt(100000000, Int.MAX_VALUE),
|
||||
@Field("mode") mode: Int = 1,
|
||||
@Field("pool") pool: Int = 0,
|
||||
@Field("type") type: String = "json",
|
||||
@Field("color") color: Int = 16777215,
|
||||
@Field("fontsize") fontSize: Int = 25,
|
||||
@Field("playTime") playTime: Float = 0.0f
|
||||
): Deferred<CommonResponse>
|
||||
|
||||
/**
|
||||
* 用于确认客户端在看直播的心跳包(与弹幕推送无关)
|
||||
* 每五分钟发送一次
|
||||
*/
|
||||
@POST("/mobile/userOnlineHeart")
|
||||
@FormUrlEncoded
|
||||
@Headers(Header.FORCE_QUERY)
|
||||
fun userOnlineHeart(
|
||||
@Field("room_id") roomId: Long,
|
||||
@Field("scale") scale: String = "xxhdpi"
|
||||
): Deferred<CommonResponse>
|
||||
}
|
||||
|
@ -14,6 +14,7 @@ import io.ktor.util.InternalAPI
|
||||
import io.ktor.util.KtorExperimentalAPI
|
||||
import io.ktor.util.decodeString
|
||||
import kotlinx.coroutines.ObsoleteCoroutinesApi
|
||||
import kotlinx.coroutines.cancelAndJoin
|
||||
import kotlinx.coroutines.channels.consumeEach
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
@ -26,6 +27,7 @@ import kotlinx.io.errors.IOException
|
||||
* @param fetchRoomId 是否在连接前先获取房间号(长号)
|
||||
* @param fetchDanmakuConfig 是否在连接前先获取弹幕推送服务器地址
|
||||
* @param doEntryRoomAction 是否产生直播间观看历史记录
|
||||
* @param sendUserOnlineHeart 是否发送 rest 心跳包, 这会增加观看直播的时长, 用于服务端统计(与弹幕推送无关)
|
||||
* @param onConnect 回调函数, 连接成功时触发
|
||||
* @param onPopularityPacket 回调函数, 接收到人气值数据包时触发
|
||||
* @param onCommandPacket 回调函数, 接收到 Command 数据包时触发
|
||||
@ -38,6 +40,7 @@ class LiveClient(
|
||||
private val fetchRoomId: Boolean = true,
|
||||
private val fetchDanmakuConfig: Boolean = true,
|
||||
private val doEntryRoomAction: Boolean = false,
|
||||
private val sendUserOnlineHeart: Boolean = false,
|
||||
private val onConnect: (LiveClient) -> Unit,
|
||||
private val onPopularityPacket: (LiveClient, Int) -> Unit,
|
||||
private val onCommandPacket: (LiveClient, JsonObject) -> Unit,
|
||||
@ -50,7 +53,8 @@ class LiveClient(
|
||||
private set
|
||||
|
||||
/**
|
||||
* 开启连接, 注意此方法是 suspend 的
|
||||
* 开启连接
|
||||
* 注意此方法将 suspend 所在协程直到连接关闭
|
||||
*/
|
||||
@UseExperimental(KtorExperimentalAPI::class, ObsoleteCoroutinesApi::class, InternalAPI::class)
|
||||
suspend fun start() {
|
||||
@ -78,7 +82,7 @@ class LiveClient(
|
||||
|
||||
//产生历史记录
|
||||
@Suppress("DeferredResultUnused")
|
||||
if (doEntryRoomAction) liveAPI.roomEntryAction(roomId)
|
||||
if (doEntryRoomAction && bilibiliClient.isLogin) liveAPI.roomEntryAction(roomId)
|
||||
|
||||
//开启 websocket
|
||||
HttpClient(CIO).config { install(WebSockets) }.wss(host = host, port = port, path = "/sub") {
|
||||
@ -98,8 +102,22 @@ class LiveClient(
|
||||
close(IOException("Receive unreadable server response"))
|
||||
}
|
||||
|
||||
//发送心跳包
|
||||
launch {
|
||||
//发送 rest 心跳包
|
||||
val restHeartBeatJob = if (sendUserOnlineHeart && bilibiliClient.isLogin) {
|
||||
launch {
|
||||
val scale = bilibiliClient.billingClientProperties.scale
|
||||
while (true) {
|
||||
@Suppress("DeferredResultUnused")
|
||||
liveAPI.userOnlineHeart(roomId, scale)
|
||||
delay(300_000)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
//发送 websocket 心跳包
|
||||
val websocketHeartBeatJob = launch {
|
||||
while (true) {
|
||||
send(PresetPacket.heartbeatPacket())
|
||||
delay(30_000)
|
||||
@ -120,6 +138,9 @@ class LiveClient(
|
||||
}
|
||||
}
|
||||
|
||||
restHeartBeatJob?.cancelAndJoin()
|
||||
websocketHeartBeatJob.cancelAndJoin()
|
||||
|
||||
launch {
|
||||
val closeReason = closeReason.await()
|
||||
try {
|
||||
@ -138,4 +159,10 @@ class LiveClient(
|
||||
websocketSession = null
|
||||
close(CloseReason(CloseReason.Codes.NORMAL, "user close"))
|
||||
} ?: Unit
|
||||
|
||||
/**
|
||||
* 发送弹幕
|
||||
*/
|
||||
fun sendMessage(message: String) =
|
||||
liveAPI.sendMessage(cid = roomId, mid = bilibiliClient.userId ?: 0, message = message)
|
||||
}
|
||||
|
@ -26,10 +26,10 @@ inline class DanmakuMessage(val data: JsonObject) {
|
||||
get() = basicInfo[0].int
|
||||
|
||||
/**
|
||||
* 弹幕类型, 可能和视频弹幕一致
|
||||
* 弹幕模式, 可能和视频弹幕一致
|
||||
* (1从右至左滚动弹幕|6从左至右滚动弹幕|5顶端固定弹幕|4底端固定弹幕|7高级弹幕|8脚本弹幕)
|
||||
*/
|
||||
inline val type
|
||||
inline val mode
|
||||
get() = basicInfo[1].int
|
||||
|
||||
/**
|
||||
|
@ -17,6 +17,7 @@ class LiveClientTest {
|
||||
runBlocking {
|
||||
bilibiliClient.liveClient(
|
||||
roomId = 3,
|
||||
sendUserOnlineHeart = true,
|
||||
onConnect = {
|
||||
println("Connected")
|
||||
},
|
||||
|
@ -0,0 +1,14 @@
|
||||
package com.hiczp.bilibili.api.test
|
||||
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
class SendLiveMessageTest {
|
||||
@Test
|
||||
fun sendMessage() {
|
||||
runBlocking {
|
||||
bilibiliClient.liveAPI
|
||||
.sendMessage(cid = 29434, mid = 20293030, message = "自动捧场机器人").await()
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user