实现直播弹幕发送

This commit is contained in:
czp3009 2019-03-25 14:23:55 +08:00
parent 759327f12a
commit acc1a308ef
8 changed files with 123 additions and 10 deletions

View File

@ -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

View File

@ -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
)

View File

@ -19,10 +19,10 @@ data class Danmaku(
val time: Long,
/**
* 弹幕类型
* 弹幕模式
* (1从右至左滚动弹幕|6从左至右滚动弹幕|5顶端固定弹幕|4底端固定弹幕|7高级弹幕|8脚本弹幕)
*/
val type: Int,
val mode: Int,
/**
* 字号

View File

@ -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>
}

View File

@ -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)
}

View File

@ -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
/**

View File

@ -17,6 +17,7 @@ class LiveClientTest {
runBlocking {
bilibiliClient.liveClient(
roomId = 3,
sendUserOnlineHeart = true,
onConnect = {
println("Connected")
},

View File

@ -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()
}
}
}