.NET鬥魚直播彈幕客戶端(2019上)

2021-02-14 DotNet騷操作
前言

現在直播平臺由於彈幕的存在,主播與觀眾可以更輕鬆地進行互動,非常受年輕群眾的歡迎。鬥魚TV就是一款非常流行的直播平臺,彈幕更是非常火爆。看到有不少主播接入 彈幕語音播報器、 彈幕點歌等模塊,這都需要首先連接鬥魚彈幕。

經常看到其它程式語言的開發者,分享了他們鬥魚彈幕客戶端的代碼。.NET當然也能做,還能做得更好(只是不知為何很少見人分享😂)。

本文將包含以下內容:

我將使用鬥魚TV官方公開的彈幕PDF文檔,使用 Socket/ TcpClient連續鬥魚彈幕;

分析如何利用 .NET強大的 ValueTask特性,在保持代碼簡潔的同時,輕鬆享受高性能異步代碼的快樂;

然後將使用 ReactiveExtensions( RX),演示如何將一系列複雜的彈幕接入操作,就像寫 HelloWorld一般容易;

用我自製的「準遊戲引擎」 FlysEngine,只需少量代碼,即可將鬥魚TV的彈幕顯示左右飛過的效果;

本文內容可能比較多,因此分上、下兩篇闡述,上篇將具體聊聊第1、2點,第3、4點將在下篇進行,整篇完成後,最終效果如下:

鬥魚直播API

現在網上可以輕鬆找到 鬥魚彈幕伺服器第三方接入協議v1.6.2.pdf(網上搜索該關鍵字即可找到)。文檔提到,第三方接入彈幕服務的伺服器為 openbarrage.douyutv.com:8601,我們可以使用 TcpClient來方便連接:

using (var client = new TcpClient())

{

client.ConnectAsync("openbarrage.douyutv.com", 8601).Wait();

Stream stream = client.GetStream();

// do other works

}

該文檔中提到所有數據包格式如下:

注意前兩個4位元組的消息長度是完全一樣的,可以使用 Debug.Assert進行斷言。

其中所有數字都為小端整數,剛好 .NET的 BinaryWriter類默認都以小端整數進行轉換。可以利用起來。

因此,讀取一個消息包的完整代碼如下:

using (var reader = new BinaryReader(stream, Encoding.UTF8, true))

{

var fullMsgLength = reader.ReadInt32();

var fullMsgLength2 = reader.ReadInt32();

Debug.Assert(fullMsgLength == fullMsgLength2);

var length = fullMsgLength - 1 - 4 - 4;

var packType = reader.ReadInt16();

Debug.Assert(packType == ServerSendToClient);

var encrypted = reader.ReadByte();

Debug.Assert(encrypted == Encrypted);

var reserved = reader.ReadByte();

Debug.Assert(reserved == Reserved);

var bytes = reader.ReadBytes(length);

var zero = reader.ReadByte();

Debug.Assert(zero == ByteZero);

}

其中 bytes既是數據部分,根據 pdf文檔中的規定,該部分為 UTF-8編碼,在 C#中使用 Encoding.UTF8.GetString()即可獲取其字符串,該字符串長這樣子:

type@=chatmsg/rid@=633019/ct@=1/uid@=124155/nn@=夜科揚羽/txt@=這不壓個蜥蜴/cid@=602c7f1becf2419962a6520300000000/ic@=avatar@S000@S12@S41@S55_avatar/level@=21/sahf@=0/cst@=1570891500125/bnn@=賊開心/bl@=8/brid@=5789561/hc@=21ebd5b2c86c01e0565453e45f14ca5b/el@=/lk@=/urlev@=10/

該格式不是 JSON/ XML等,但仔細分析又確實有邏輯,有層次感,根據文檔,該格式為所謂的 STT序列化,該格式包含鍵值對、數組等多種格式。雖然不懂為什麼不用 JSON。還好協議簡單,我可以通過寥寥幾行代碼,即可轉換為 Json.NET的 JToken格式:

public static JToken DecodeStringToJObject(string str)

{

if (str.Contains("//")) // 數組

{

var result = new JArray();

foreach (var field in str.Split(new[] { "//" }, StringSplitOptions.RemoveEmptyEntries))

{

result.Add(DecodeStringToJObject(field));

}

return result;

}

if (str.Contains("@=")) // 對象

{

var result = new JObject();

foreach (var field in str.Split(new[] { '/' }, StringSplitOptions.RemoveEmptyEntries))

{

var tokens = field.Split(new[] { "@=" }, StringSplitOptions.None);

var k = tokens[0];

var v = UnscapeSlashAt(tokens[1]);

result[k] = DecodeStringToJObject(v);

}

return result;

}

else if (str.Contains("@A=")) // 鍵值對

{

return DecodeStringToJObject(UnscapeSlashAt(str));

}

else

{

return UnscapeSlashAt(str); // 值

}

}

static string EscapeSlashAt(string str)

{

return str

.Replace("/", "@S")

.Replace("@", "@A");

}

static string UnscapeSlashAt(string str)

{

return str

.Replace("@S", "/")

.Replace("@A", "@");

}

這樣一來,即可將 STT格式轉換為 JSON格式,因此只需像 JSON格式取出 nn欄位和 txt欄位即可,還有一個 col欄位,可以用來確定彈幕顏色,我可以將其轉換為 RGB的 int32值:

Color = (x["col"] ?? new JValue(0)).Value<int>() switch

{

1 => 0xff0000, // 紅

2 => 0x1e87f0, // 淺藍

3 => 0x7ac84b, // 淺綠

4 => 0xff7f00, // 橙色

5 => 0x9b39f4, // 紫色

6 => 0xff69b4, // 洋紅

_ => 0xffffff, // 默認,白色

}

該代碼使用了 C# 8.0的 switchexpression功能,可以一個表達式轉成整個顏色轉換,比 if/else和 switch/case語句都精簡不少,可謂一氣呵成。

支持異步/ ValueTask/ Memory<T>優化

C# 5.0提供了強大的異步 API—— async/await,通過異步API,以前難以用編程實現的操作現在可以像寫串行代碼一樣輕鬆完成,還能輕鬆加入取消任務操作。

然後 C# 7.0發布了 ValueTask, ValueTask是值類型,因此在頻繁調用異步操作(如使用 Stream讀取字節)時,不會因為創建過多的 Task而分配沒必要的內存。這裡,我確實是使用 TCP連接流讀取字節,是使用 ValueTask的最佳時機。

這裡我們將嘗試將代碼切換為 ValueTask版本。

首先第一個問題是 BinaryReader類,該類提供了便利的字節操作方式,且能確保字節端為小端,但該類不提供異步 API,因此需要作一些特殊處理:

public static async Task<string> RecieveAsync(Stream stream, CancellationToken cancellationToken)

{

int fullMsgLength = await ReadInt32().ConfigureAwait(false);

int fullMsgLength2 = await ReadInt32().ConfigureAwait(false);

Debug.Assert(fullMsgLength == fullMsgLength2);

int length = fullMsgLength - 1 - 4 - 4;

short packType = await ReadInt16().ConfigureAwait(false);

Debug.Assert(packType == ServerSendToClient);

short encrypted = await ReadByte().ConfigureAwait(false);

Debug.Assert(encrypted == Encrypted);

short reserved = await ReadByte().ConfigureAwait(false);

Debug.Assert(reserved == Reserved);

Memory<byte> bytes = await ReadBytes(length).ConfigureAwait(false);

byte zero = await ReadByte().ConfigureAwait(false);

Debug.Assert(zero == ByteZero);

return Encoding.UTF8.GetString(bytes.Span);

}

如代碼所示,我封裝了 ReadInt16()和 ReadInt32()兩個方法,

var intBuffer = new byte[4];

var int32Buffer = new Memory<byte>(intBuffer, 0, 4);

async ValueTask<int> ReadInt32()

{

var memory = int32Buffer;

int read = 0;

while (read < 4)

{

read += await stream.ReadAsync(memory.Slice(read), cancellationToken).ConfigureAwait(false);

}

Debug.Assert(read == memory.Length);

return

(intBuffer[0] << 0) +

(intBuffer[1] << 8) +

(intBuffer[2] << 16) +

(intBuffer[3] << 24);

}

如圖,我還使用了一個 while語句,因為不像 BinaryReader,如果一次無法讀取所需的字節數(4個字節), stream.ReadAsync()並不會堵塞線程。然後需要將 int32Buffer轉換為 int類型。

注意:此處我沒有使用 BitConverter.ToInt32(),也不能使用該方法,因為該方法不像 BinaryReader,它在大端/小端的 CPU上會有不同的行為。(其中在大端 CPU上將有錯誤的行為)涉及二進位序列化需要傳輸的,不能使用 BitConverter類。

同樣的,寫 TCP流也需要有相應的變化:

static async Task SendAsync(Stream stream, byte[] body, CancellationToken cancellationToken)

{

var buffer = new byte[4];

await stream.WriteAsync(GetBytesI32(4 + 4 + body.Length + 1), cancellationToken).ConfigureAwait(false);

await stream.WriteAsync(GetBytesI32(4 + 4 + body.Length + 1), cancellationToken).ConfigureAwait(false);

await stream.WriteAsync(GetBytesI16(ClientSendToServer), cancellationToken).ConfigureAwait(false);

await stream.WriteAsync(new byte[] { Encrypted}, cancellationToken).ConfigureAwait(false);

await stream.WriteAsync(new byte[] { Reserved}, cancellationToken).ConfigureAwait(false);

await stream.WriteAsync(body, cancellationToken).ConfigureAwait(false);

await stream.WriteAsync(new byte[] { ByteZero}, cancellationToken).ConfigureAwait(false);

Memory<byte> GetBytesI32(int v)

{

buffer[0] = (byte)v;

buffer[1] = (byte)(v >> 8);

buffer[2] = (byte)(v >> 16);

buffer[3] = (byte)(v >> 24);

return new Memory<byte>(buffer, 0, 4);

}

Memory<byte> GetBytesI16(short v)

{

buffer[0] = (byte)v;

buffer[1] = (byte)(v >> 8);;

return new Memory<byte>(buffer, 0, 2);

}

}

總結

最終運行效果如下:

這一篇【DotNet騷操作】文章介紹了如何使用鬥魚tv開放彈幕 API,下篇將會:

敬請期待!「刷一波666🚀🚀🚀」

喜歡的朋友請關注我的微信公眾號:【DotNet騷操作】

相關焦點

  • .NET鬥魚直播彈幕客戶端(2019下)
    前言在上篇文章中,我們提到了如何使用 .NET連接鬥魚TV直播彈幕的基本操作。
  • .NET鬥魚直播彈幕客戶端(2021)
    .NET鬥魚直播彈幕客戶端(2021)離之前更新的兩篇《.NET鬥魚直播彈幕客戶端》已經有一段時間,近期有許多客戶向我反饋剛好有這方面的需求,但之前的代碼不能用了——但網上許多流傳的Node.js、Python腳本卻可以用,這豈能忍?(剛好我終於找回了我的發布密碼😂)因此我有動力重新對此進行好(xie)好(xie)研(bo)究(ke)。
  • 下載鬥魚TV客戶端 隨時隨地看直播
    現在的我們,習慣了手機購物、習慣了吃飯拍照,同樣習慣了通過手機看直播……手機正逐漸改變著我們的生活。  為了滿足更多朋友對手機的需求,鬥魚TV特推出了手機客戶端,android、ios的用戶都可以下載鬥魚TV客戶端,通過手機收看鬥魚的精彩直播。最新的直播訊息,最勁爆的活動,隨時隨地隨著指尖呈現。
  • 鬥魚:當遊戲直播遇上彈幕
    鬥魚作為遊戲直播平臺,未來也會專注在兩個方向上。一方面是電子競技,一方面是非常娛樂化的遊戲直播。而這也正與鬥魚的寓意相契合,王巖說:「鬥魚的鬥,代表競技;鬥魚的魚,代表娛樂」。從 Dota 解說到中國版 Twitch王巖很早就開始接觸了電子競技,而且在 2008 年就開始做 Dota 的遊戲解說,然而那時的遊戲直播其實是比較痛苦的一件事。
  • Python鬥魚虎牙直播平臺視頻彈幕
    基本開發環境 Python 3.6 Pycharm 相關模塊的使用 鬥魚直播視頻彈幕爬取分析
  • 彈幕視頻直播新方向 鬥魚TV打造多元化平臺
    目前,遊戲直播是鬥魚TV直播平臺的主要項目,並且囊括了Dota、LOL、爐石傳說、風暴英雄等市面上所有的主流遊戲直播,只要是你喜歡的遊戲,基本上都能在鬥魚找到有相同愛好的主播以及觀眾群體。  鬥魚TV為希望成為主播的遊戲愛好者提供了一個展示自己的平臺,通過直播,他們不僅能迅速累積粉絲,還能培養固定粉絲群體,打造粉絲文化,成為遊戲直播中的超級明星。就目前而言,鬥魚TV部分人氣主播的「明星影響力」絲毫不亞於傳統明星,他們開播時直播間觀看人數動輒便能達到百萬級別,影響力可見一斑。
  • 鬥魚直播簡介 - 鬥魚TV手機客戶端_鬥魚TV手機客戶端下載【最新...
    鬥魚直播 音樂視頻 大小: 64.47MB 版本: 6.3.8
  • 鬥魚2019魚樂盛典倒計時 遊戲直播行業「奧斯卡」來了!
    1月11日,「鬥魚2019魚樂盛典暨鬥魚六周年慶典」(以下簡稱「魚樂盛典」)將在上海靜安體育中心隆重舉行。作為行業規模最大、影響力最強的年度盛會,魚樂盛典向來被認為是直播屆的「奧斯卡」。本屆魚樂盛典上,鬥魚將隆重頒發年度十大巔峰主播、鬥魚6周年十大星耀主播、年度最佳分區主播、年度最佳遊戲公會、年度最佳娛樂公會等重磅獎項。
  • 鬥魚年度十大彈幕背後 梗文化之下的遊戲社區
    相關數據顯示,這一彈幕曾經連續三年榮登鬥魚年度十大彈幕榜首位。僅2019年666就被鬥魚水友使用了超過2.16億次。裂開了這一彈幕更是直接誕生於鬥魚,最早是鬥魚CSGO主播冬瓜的口頭禪,此後經由包括PDD等主播的引用而開始火爆出圈。微信表情對這兩個梗的引用從側面印證了鬥魚在彈幕文化上的沉澱,而彈幕文化向來是鬥魚的核心競爭力之一。
  • 鬥魚主播尾隨陳冠希上熱搜,彈幕還原事情真相
    導語  繼上次狗粉絲約架陳冠希,陳冠希被放鴿子後  陳冠希又因為被鬥魚主播跟拍上了熱搜。
  • 鬥魚主播跟拍陳冠希惹眾怒事後直播間道歉 從彈幕數據還願過程
    導語繼上次狗粉絲約架陳冠希,陳冠希被放鴿子後陳冠希又因為被鬥魚主播跟拍上了熱搜。還原事件發生過程4月1日中午11點57分,鬥魚主播「目黑川的櫻井翔太」在東京街頭偶遇陳冠希父女,主播想要上前合影,但陳冠希因為帶著女兒選擇婉拒。陳冠希剛開始態度是很好的,笑著說「我跟小孩子一起啊,不好意思啊今天」。
  • 鬥魚TV成直播界的「發明家」扛把子,一條彈幕背後隱藏了50項專利!!
    但是,你知道嗎你在鬥魚直播看到的一條彈幕背後就隱藏著50項專利?鬥魚智慧財產權總監覃波說,特別是「彈幕」,顏色、外觀設計、速度快慢、發射的角度等都有很大差別,已為個性化的核心「彈幕」先後申請了50多項專利。
  • 「鬥魚2019魚樂盛典」圓滿收官 年度十大巔峰主播揭曉
    在來自全國各地線上線下數千萬鬥魚水友的見證下,2019年度十大巔峰主播親臨現場,領走了屬於自己的年度榮耀獎盃。主持人華少和主播餘霜擔綱盛典主持,胡彥斌也作為表演嘉賓傾情獻唱,與最新出爐的2019十大巔峰主播一起慶祝鬥魚六周歲生日。
  • 「神曲」作者邁特怡直播慘遭彈幕噴哭?鬥魚超管戰術破解瞬間逆轉
    就在如今雲頂之弈的熱度不斷高漲之下,鬥魚主播邁特怡卻在直播衝分之中遭遇到糟心的事情了。#雲頂之弈[超話]# 打工都是人上人!」這一首洗腦神曲吧?這首神曲的作者正是邁特怡,不僅如此,邁特怡還在近期應粉絲要求給大司馬傾情製作了一首《下飯人》的歌曲,這首歌曲一出瞬間引爆圈內,就連大司馬本人都會有「很強」!鬥魚官方直接點讚「下飯」!
  • 鬥魚2020十大彈幕出爐,這些遊戲黑話你都知道嗎?
    2021年1月6日,國內領先的遊戲直播平臺鬥魚發布了2020年度十大彈幕。「『就這?』、針不戳、蕪湖起飛、上流、痛苦面具、我接受不了、『歪比歪比、歪比巴卜』、找個班上吧、究極長痛、肉蛋蔥雞」當選年度彈幕TOP10。據統計,鬥魚水友2020年共計發送了超過109億條彈幕,其中「就這?」發送超過4000萬次。
  • 鬥魚直播下架怎麼回事 鬥魚為什麼直播下架背後真相揭秘
    在蘋果ios手機客戶端搜索鬥魚直播APP,只能看到其它直播產品的競品,完全看不到鬥魚APP的身影。在安卓手機客戶端搜索鬥魚直播APP,則會出現鬥魚極速版。好端端的,鬥魚怎麼就玩消失了那?在虎撲上,有網友發帖詢問關於鬥魚下架一事,一位暱稱為「鬥魚TV小助手」的網友回復到:「由於蘋果商店問題目前暫時下線了鬥魚App。我們正在積極協調中,請您耐心等待後續重新上線。
  • 央視六一晚會節目單/北京超高清發展計劃/B站、YY、鬥魚等關閉彈幕功能一周|資訊
    創建8K超高清視頻「制播雲」系統,培育超高清視頻「雲上共享」等產業新業態、新模式,鼓勵企事業單位開展跨屏幕、跨網絡業務推送服務,推動4K/8K超高清技術在智慧廣電、智慧城市等領域示範應用,打造全球領先的超高清視頻產業集群。
  • 鬥魚2020年度十大彈幕出爐,第一名簡直絕了!
    又到了一年一總結的時候,經歷比往年多一點的2020年,就連彈幕數據也比往年多了不止一點點!繼「666」「下飯」「到胃」等彈幕之後,又出現了不少炸屏的彈幕詞。2021年1月6日,鬥魚對外公布了2020年度十大彈幕排行,全年彈幕使用量高達109億次,位於彈幕排行榜第一「就這」的使用頻次達到了4000萬次。
  • 2019CFPL秋季賽鬥魚抽獎活動官方網址
    小編今天給各位玩家朋友們帶來的是2019CFPL秋季賽鬥魚抽獎活動官方網址,2019CFPL的抽獎活動已經正式開始了,抽獎入口在哪呢?小編給大家整理了一下,還不知道抽獎入口的玩家朋友們快來跟小編一起往下看看吧!最新的官方網址就在下方哦!
  • 鬥魚發布2020年度十大彈幕,大司馬和粉絲成爆梗王!
    2020年1月7日,國內領先遊戲直播平臺鬥魚發布了2020年度十大彈幕。據悉,在去年,數億水友在鬥魚實現了最真實的自我表達,共刷出了10931764154條彈幕。、針不戳、蕪湖起飛、上流、痛苦面具、我接受不了、歪比歪比、歪比巴卜、找個班上吧、究極長痛、肉蛋蔥雞當選鬥魚年度彈幕TOP10。看來,電競圈一半的流行語,幾乎都出自鬥魚,這些極具「電競特色」的彈幕詞,不僅成為了鬥魚主播直播時心路歷程的表現,也展現出了屏幕外觀眾們觀賽的緊張心情,基於鬥魚PUGC的平臺屬性,經由用戶的二次創作,得以傳播開來。