From acc1a308ef74d4923e386190a147c4f1baf0d9fd Mon Sep 17 00:00:00 2001 From: czp3009 Date: Mon, 25 Mar 2019 14:23:55 +0800 Subject: [PATCH] =?UTF-8?q?=E5=AE=9E=E7=8E=B0=E7=9B=B4=E6=92=AD=E5=BC=B9?= =?UTF-8?q?=E5=B9=95=E5=8F=91=E9=80=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 25 +++++++++- .../com/hiczp/bilibili/api/BilibiliClient.kt | 3 +- .../com/hiczp/bilibili/api/danmaku/Danmaku.kt | 4 +- .../com/hiczp/bilibili/api/live/LiveAPI.kt | 47 +++++++++++++++++++ .../bilibili/api/live/websocket/LiveClient.kt | 35 ++++++++++++-- .../bilibili/api/live/websocket/Parser.kt | 4 +- .../hiczp/bilibili/api/test/LiveClientTest.kt | 1 + .../bilibili/api/test/SendLiveMessageTest.kt | 14 ++++++ 8 files changed, 123 insertions(+), 10 deletions(-) create mode 100644 src/test/kotlin/com/hiczp/bilibili/api/test/SendLiveMessageTest.kt diff --git a/README.md b/README.md index 7b2e4f5..b67f0bf 100644 --- a/README.md +++ b/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 diff --git a/src/main/kotlin/com/hiczp/bilibili/api/BilibiliClient.kt b/src/main/kotlin/com/hiczp/bilibili/api/BilibiliClient.kt index 010606d..dc9de89 100644 --- a/src/main/kotlin/com/hiczp/bilibili/api/BilibiliClient.kt +++ b/src/main/kotlin/com/hiczp/bilibili/api/BilibiliClient.kt @@ -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 ) diff --git a/src/main/kotlin/com/hiczp/bilibili/api/danmaku/Danmaku.kt b/src/main/kotlin/com/hiczp/bilibili/api/danmaku/Danmaku.kt index 5222052..8ff968c 100644 --- a/src/main/kotlin/com/hiczp/bilibili/api/danmaku/Danmaku.kt +++ b/src/main/kotlin/com/hiczp/bilibili/api/danmaku/Danmaku.kt @@ -19,10 +19,10 @@ data class Danmaku( val time: Long, /** - * 弹幕类型 + * 弹幕模式 * (1从右至左滚动弹幕|6从左至右滚动弹幕|5顶端固定弹幕|4底端固定弹幕|7高级弹幕|8脚本弹幕) */ - val type: Int, + val mode: Int, /** * 字号 diff --git a/src/main/kotlin/com/hiczp/bilibili/api/live/LiveAPI.kt b/src/main/kotlin/com/hiczp/bilibili/api/live/LiveAPI.kt index 67bb3ec..cda22b4 100644 --- a/src/main/kotlin/com/hiczp/bilibili/api/live/LiveAPI.kt +++ b/src/main/kotlin/com/hiczp/bilibili/api/live/LiveAPI.kt @@ -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 + + /** + * 发送弹幕(直播) + * + * @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 + + /** + * 用于确认客户端在看直播的心跳包(与弹幕推送无关) + * 每五分钟发送一次 + */ + @POST("/mobile/userOnlineHeart") + @FormUrlEncoded + @Headers(Header.FORCE_QUERY) + fun userOnlineHeart( + @Field("room_id") roomId: Long, + @Field("scale") scale: String = "xxhdpi" + ): Deferred } diff --git a/src/main/kotlin/com/hiczp/bilibili/api/live/websocket/LiveClient.kt b/src/main/kotlin/com/hiczp/bilibili/api/live/websocket/LiveClient.kt index b70a2fc..1d22dae 100644 --- a/src/main/kotlin/com/hiczp/bilibili/api/live/websocket/LiveClient.kt +++ b/src/main/kotlin/com/hiczp/bilibili/api/live/websocket/LiveClient.kt @@ -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) } diff --git a/src/main/kotlin/com/hiczp/bilibili/api/live/websocket/Parser.kt b/src/main/kotlin/com/hiczp/bilibili/api/live/websocket/Parser.kt index d14e816..d48726d 100644 --- a/src/main/kotlin/com/hiczp/bilibili/api/live/websocket/Parser.kt +++ b/src/main/kotlin/com/hiczp/bilibili/api/live/websocket/Parser.kt @@ -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 /** diff --git a/src/test/kotlin/com/hiczp/bilibili/api/test/LiveClientTest.kt b/src/test/kotlin/com/hiczp/bilibili/api/test/LiveClientTest.kt index 86e5af9..08c5c2e 100644 --- a/src/test/kotlin/com/hiczp/bilibili/api/test/LiveClientTest.kt +++ b/src/test/kotlin/com/hiczp/bilibili/api/test/LiveClientTest.kt @@ -17,6 +17,7 @@ class LiveClientTest { runBlocking { bilibiliClient.liveClient( roomId = 3, + sendUserOnlineHeart = true, onConnect = { println("Connected") }, diff --git a/src/test/kotlin/com/hiczp/bilibili/api/test/SendLiveMessageTest.kt b/src/test/kotlin/com/hiczp/bilibili/api/test/SendLiveMessageTest.kt new file mode 100644 index 0000000..2b6911d --- /dev/null +++ b/src/test/kotlin/com/hiczp/bilibili/api/test/SendLiveMessageTest.kt @@ -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() + } + } +}