模仿UP主,用Python實(shí)現(xiàn)一個(gè)彈幕控制的直播間欲诺!

靈感來(lái)源

之前在B站看到一個(gè)有意思的視頻:

【B站】【亦】終極云游戲抄谐!五千人同開一輛車,復(fù)現(xiàn)經(jīng)典群體智慧實(shí)驗(yàn)

image

大家可以看看扰法,很有意思蛹含。

up主通過(guò)代碼實(shí)現(xiàn)了實(shí)時(shí)讀取直播間里的彈幕內(nèi)容,進(jìn)而控制自己的電腦塞颁,把彈幕翻譯成指令操控《賽博朋克2077》游戲浦箱。

觀眾也越來(lái)越多,最后甚至還把直接間搞崩了(當(dāng)然祠锣,其實(shí)是因?yàn)槟翘霣站全站崩了)酷窥。

我十分好奇到底是怎么做到的。

外行看熱鬧伴网,內(nèi)行看門道蓬推,作為半個(gè)內(nèi)行,我們就模仿UP主的想法澡腾,自己做一個(gè)沸伏。

所以今天我的目標(biāo)就是復(fù)刻一個(gè) 通過(guò)彈幕控制直播間 的代碼,并且最終在自己的直播間開播动分。

先給大家看看最終我的成品小視頻:

【B站】模仿UP主毅糟,做一個(gè)彈幕控制的直播間!

看起來(lái)是不是很像樣了澜公。

初版設(shè)計(jì)思路

首先在腦海里規(guī)劃一個(gè)大致的思路姆另,如下圖:

img

這個(gè)思路看起來(lái)很簡(jiǎn)單,不過(guò)還是得解釋一下,首先我們要搞清楚蜕青,彈幕的內(nèi)容是怎么抓到的苟蹈。

大部分我們常見的直播平臺(tái),在瀏覽器端右核,彈幕都是通過(guò)WebSocket來(lái)推送給觀眾的慧脱。在手機(jī)平板等客戶端(非Web端),可能會(huì)有一些更加復(fù)雜的TCP進(jìn)行彈幕的推送贺喝。

關(guān)于TCP的消息投遞菱鸥,有個(gè)很好的文章,就是美團(tuán)的這個(gè):美團(tuán)終端消息投遞服務(wù)Pike的演進(jìn)之路

歸根結(jié)底,這些彈幕都是通過(guò)在客戶端和服務(wù)端建立長(zhǎng)鏈接來(lái)實(shí)現(xiàn)的躏鱼。

所以氮采,我們需要做的就是用代碼作為客戶端,與直播平臺(tái)進(jìn)行長(zhǎng)鏈接染苛。這樣就能拿到彈幕鹊漠。

我們只是需要實(shí)現(xiàn)整個(gè)彈幕控制的流程,所以彈幕的抓取也不是本文的重點(diǎn)茶行,我們來(lái)淘一個(gè)現(xiàn)成的輪子躯概!在Github上一頓找,找到了一個(gè)非常不錯(cuò)的開源庫(kù)畔师,里面能夠獲取很多直播平臺(tái)的彈幕:

https://github.com/wbt5/real-url

獲取斗魚&虎牙&嗶哩嗶哩&抖音&快手等 58 個(gè)直播平臺(tái)的真實(shí)流媒體地址(直播源)和彈幕娶靡,直播源可在 PotPlayer、flv.js 等播放器中播放看锉。

我們把代碼clone下來(lái)姿锭,運(yùn)行main函數(shù),隨便輸入一個(gè)Bilibili直播間地址伯铣,就能拿到直播間實(shí)時(shí)的彈幕流:

image-20211122225149043

代碼里把獲取到的一條條彈幕(包括用戶名)直接打印在了控制臺(tái)呻此。

他是如何做到的呢?核心的Python代碼如下(不熟悉Python腔寡?不要緊焚鲜,就當(dāng)做偽代碼,很容易看懂):

wss_url = 'wss://broadcastlv.chat.bilibili.com/sub'
heartbeat = b'\x00\x00\x00\x1f\x00\x10\x00\x01\x00\x00\x00\x02\x00\x00\x00\x01\x5b\x6f\x62\x6a\x65\x63\x74\x20' \
                b'\x4f\x62\x6a\x65\x63\x74\x5d '
  heartbeatInterval = 60

@staticmethod
async def get_ws_info(url):
    url = 'https://api.live.bilibili.com/room/v1/Room/room_init?id=' + url.split('/')[-1]
    reg_datas = []
    async with aiohttp.ClientSession() as session:
        async with session.get(url) as resp:
            room_json = json.loads(await resp.text())
            room_id = room_json['data']['room_id']
            data = json.dumps({
                'roomid': room_id,
                'uid': int(1e14 + 2e14 * random.random()),
                'protover': 1
            }, separators=(',', ':')).encode('ascii')
            data = (pack('>i', len(data) + 16) + b'\x00\x10\x00\x01' +
                    pack('>i', 7) + pack('>i', 1) + data)
            reg_datas.append(data)

    return Bilibili.wss_url, reg_datas

它連上了Bilibili的直播彈幕WSS地址蹬蚁,也就是WebSocket地址,然后偽裝成客戶端郑兴,接受彈幕推送犀斋。

OK,做完了第一步情连,下一步就是用消息隊(duì)列將彈幕發(fā)送出來(lái)叽粹。開啟單獨(dú)的消費(fèi)者接收彈幕。

為了實(shí)現(xiàn)上盡量簡(jiǎn)單,就不上那些專業(yè)的消息隊(duì)列了虫几,這里用了redis的list作為隊(duì)列锤灿,將彈幕內(nèi)容放進(jìn)去。

發(fā)送者核心代碼如下:

# 鏈接Redis
def init_redis():
    r = redis.Redis(host='localhost', port=6379, decode_responses=True)
    return r

# 消息發(fā)送者
async def printer(q, redis):
    while True:
        m = await q.get()
        if m['msg_type'] == 'danmaku':
            print(f'{m["name"]}:{m["content"]}')
            list_str = list(m["content"])
            print("彈幕拆分:", list_str)
            for char in list_str:
                if char.lower() in key_list:
                    print('推送隊(duì)列:', char.lower())
                    redis.rpush(list_name, char.lower())

完成了彈幕內(nèi)容的發(fā)送后辆脸,需要寫一個(gè)消費(fèi)者但校,消費(fèi)這些彈幕,把里面的指令都提取出來(lái)啡氢。

并且状囱,在消費(fèi)者收到彈幕后,如何消費(fèi)呢倘是?我們需要一個(gè)能夠用代碼指令控制電腦的辦法亭枷。

咱繼續(xù)本著不造輪子的原則,找到了一個(gè)Python的自動(dòng)化控制庫(kù)PyAutoGUI

PyAutoGUI is a cross-platform GUI automation Python module for human beings. Used to programmatically control the mouse & keyboard.

安裝上這個(gè)庫(kù)搀崭,在代碼中引入叨粘,便可以通過(guò)他的API控制電腦鼠標(biāo)和鍵盤執(zhí)行對(duì)應(yīng)的操作。簡(jiǎn)直是完美傲龆谩升敲!

消費(fèi)者(控制電腦)核心Python代碼如下:

# 鏈接Redis
def init_redis():
    r = redis.Redis(host='localhost', port=6379, decode_responses=True)
    return r

# 消費(fèi)者
def control(key_name):
    print("key_name =", key_name)
    if key_name == None:
        print("本次無(wú)指令發(fā)出")
        return
    key_name = key_name.lower()
    # 控制電腦指令
    if key_name in key_list:
        print("發(fā)出指令", key_name)
        pyautogui.keyDown(key_name)
        time.sleep(press_sec)
        pyautogui.keyUp(key_name)
        print("結(jié)束指令", key_name)


if __name__ == '__main__':
    r = init_redis()
    print("開始監(jiān)聽彈幕消息, loop_sec =", loop_sec)
    while True:
        key_name = r.lpop(list_name)
        control(key_name)
        time.sleep(loop_sec)

ok,大功告成默蚌,我們打開彈幕發(fā)送隊(duì)列和消費(fèi)者冻晤,這個(gè)不斷循環(huán)消費(fèi)的隊(duì)列就開始運(yùn)行了。一旦彈幕中有wsad這種控制游戲常用的按鍵绸吸,電腦就會(huì)自己給自己發(fā)出指令鼻弧。

image-20211123000340404

初版運(yùn)行中的問(wèn)題

我興沖沖的打開自己的B站直播間,開始調(diào)試锦茁,結(jié)果發(fā)現(xiàn)我還是太天真了攘轩。這個(gè)初版代碼暴露了非常多的問(wèn)題。我們一個(gè)個(gè)來(lái)說(shuō)下是什么問(wèn)題码俩,我是如何解決的度帮。

指令不人性化

水友們其實(shí)很喜歡發(fā)送類似www dddd這類重復(fù)單詞(疊詞),但初版的實(shí)現(xiàn)只支持單個(gè)字幕稿存,水友們發(fā)現(xiàn)不得勁笨篷,沒(méi)有作用后,就從直播間走了瓣履。

這點(diǎn)很容易解決率翅,把彈幕內(nèi)容拆分成每個(gè)單詞,然后再推送給隊(duì)列袖迎。

解決方法:拆解彈幕冕臭,把DDD腺晾,拆成D,D,D,發(fā)送個(gè)消費(fèi)者辜贵。

危險(xiǎn)指令

首先是玩家的指令超出了應(yīng)該有的范圍悯蝉。

在我把賽博朋克游戲打開,讓彈幕觀眾控制游戲里的開車時(shí)托慨,有個(gè)神秘觀眾進(jìn)了直播間鼻由,默默發(fā)了個(gè)“F”,然后榴芳。嗡靡。。

然后游戲里的V(主角名)就從車?yán)锵聛?lái)了窟感,淦讨彼,我是讓你們開車的,不是讓你們下來(lái)和警察斗毆的柿祈。哈误。。

解決方法:添加彈幕過(guò)濾器躏嚎。

# 將彈幕進(jìn)行拆分蜜自,只發(fā)送指定的指令給消費(fèi)者
key_list = ('w', 's', 'a', 'd', 'j', 'k', 'u', 'i', 'z', 'x', 'f', 'enter', 'shift', 'backspace')
list_str = list(m["content"])
            print("彈幕拆分:", list_str)
            for char in list_str:
                if char.lower() in key_list:
                    print('推送隊(duì)列:', char.lower())
                    redis.rpush(list_name, char.lower())

上面兩個(gè)問(wèn)題解決后,發(fā)送者就像下面這樣運(yùn)行了:

image-20211123000321183

彈幕指令堆積

這是個(gè)很大的問(wèn)題卢佣,如果處理所有水友發(fā)送的全部彈幕指令重荠,一定會(huì)存在消費(fèi)不過(guò)來(lái)的問(wèn)題。

解決方法:需要固定時(shí)間處理彈幕虚茶,其他拋棄戈鲁。

if __name__ == '__main__':
    r = init_redis()
    print("開始監(jiān)聽彈幕消息, loop_sec =", loop_sec)
    while True:
        key_name = r.lpop(list_name)
        # 每次只取出一個(gè)指令,然后把list清空嘹叫,也就是這個(gè)時(shí)間窗口內(nèi)其他彈幕都扔掉婆殿!
        r.delete(list_name)
        control(key_name)
        time.sleep(loop_sec)

彈幕從發(fā)出到觀眾看到結(jié)果有延遲

在最開始的視頻里,你們也能感受到了罩扇,從觀眾的指令發(fā)出婆芦,到最終被觀眾看到,大概要經(jīng)歷5秒的延遲喂饥。其中消约,起碼有3秒,都是網(wǎng)絡(luò)直播流的延遲员帮,這一點(diǎn)或粮,很難去優(yōu)化。

回爐重造后的版本

經(jīng)過(guò)一系列調(diào)優(yōu)和涉及集侯,我們的版本也算是從V0.1到了V0.2了被啼。猛虎落淚。

下面是重構(gòu)后的結(jié)構(gòu)圖:

img

后記

在寫完這個(gè)項(xiàng)目后棠枉,我在直播間試了很多次浓体,體驗(yàn)已經(jīng)無(wú)限接近UP主當(dāng)時(shí)的視頻了。我開播掛在那邊好久辈讶,但是命浴,人氣最高的時(shí)候,也只有20幾個(gè)人贱除,寥寥十幾條彈幕生闲,還有很多是我發(fā)的。我還期望著觀眾能夠拉更多人進(jìn)來(lái)一起玩呢月幌,事與愿違啊碍讯。

由此可得出結(jié)論,我扯躺,先得有粉絲捉兴,才能玩得起來(lái)啊,嗚嗚嗚嗚录语。大家要是不介意倍啥,可以關(guān)注下我的B站賬號(hào),也叫:蠻三刀醬澎埠。我會(huì)偶爾抽風(fēng)發(fā)點(diǎn)有趣的技術(shù)視頻的虽缕。

本文實(shí)現(xiàn)的全部代碼已經(jīng)開源在了Github上,大家可以在自己的直播間里試試呀:

https://github.com/qqxx6661/live_comment_control_stream

我是在阿里搬磚的工程師 @蠻三刀醬

持續(xù)的更新優(yōu)質(zhì)文章蒲稳,離不開你的點(diǎn)贊氮趋,轉(zhuǎn)發(fā)和分享!

全網(wǎng)唯一技術(shù)公眾號(hào):后端技術(shù)漫談

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末弟塞,一起剝皮案震驚了整個(gè)濱河市凭峡,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌决记,老刑警劉巖摧冀,帶你破解...
    沈念sama閱讀 206,013評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異系宫,居然都是意外死亡索昂,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,205評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門扩借,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)椒惨,“玉大人,你說(shuō)我怎么就攤上這事潮罪】底唬” “怎么了领斥?”我有些...
    開封第一講書人閱讀 152,370評(píng)論 0 342
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)沃暗。 經(jīng)常有香客問(wèn)我月洛,道長(zhǎng),這世上最難降的妖魔是什么孽锥? 我笑而不...
    開封第一講書人閱讀 55,168評(píng)論 1 278
  • 正文 為了忘掉前任嚼黔,我火速辦了婚禮,結(jié)果婚禮上惜辑,老公的妹妹穿的比我還像新娘唬涧。我一直安慰自己,他們只是感情好盛撑,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,153評(píng)論 5 371
  • 文/花漫 我一把揭開白布碎节。 她就那樣靜靜地躺著,像睡著了一般抵卫。 火紅的嫁衣襯著肌膚如雪钓株。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 48,954評(píng)論 1 283
  • 那天陌僵,我揣著相機(jī)與錄音轴合,去河邊找鬼。 笑死碗短,一個(gè)胖子當(dāng)著我的面吹牛受葛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播偎谁,決...
    沈念sama閱讀 38,271評(píng)論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼总滩,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了巡雨?” 一聲冷哼從身側(cè)響起闰渔,我...
    開封第一講書人閱讀 36,916評(píng)論 0 259
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎铐望,沒(méi)想到半個(gè)月后冈涧,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,382評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡正蛙,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,877評(píng)論 2 323
  • 正文 我和宋清朗相戀三年督弓,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片乒验。...
    茶點(diǎn)故事閱讀 37,989評(píng)論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡愚隧,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出锻全,到底是詐尸還是另有隱情狂塘,我是刑警寧澤录煤,帶...
    沈念sama閱讀 33,624評(píng)論 4 322
  • 正文 年R本政府宣布,位于F島的核電站荞胡,受9級(jí)特大地震影響辐赞,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜硝训,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,209評(píng)論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望新思。 院中可真熱鬧窖梁,春花似錦、人聲如沸夹囚。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,199評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)荸哟。三九已至假哎,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間鞍历,已是汗流浹背舵抹。 一陣腳步聲響...
    開封第一講書人閱讀 31,418評(píng)論 1 260
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留劣砍,地道東北人惧蛹。 一個(gè)月前我還...
    沈念sama閱讀 45,401評(píng)論 2 352
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像刑枝,于是被迫代替她去往敵國(guó)和親香嗓。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,700評(píng)論 2 345

推薦閱讀更多精彩內(nèi)容