去年在 B 站创造一个后期超强的 UP 主:修仙不倒大小眼,专出 PDD 这样有名主播的吃鸡精彩集锦,涨粉超快。
于是想怎么做这样的 UP,碰着的第一个问题便是素材,精彩时候须要手动从直播录播中剪辑,很低效。

用户习气

我常常看直播,但是很少发弹幕和送礼物,只有在主播玩出很溜的操作或讲很好玩的事情时,才会发弹幕互动、送礼物支持,常常看直播的室友也是如此。

php读取直播视频B 站直播间数据爬虫 NoSQL

基于这个用户习气,不难推断出在直播间的弹幕高峰或礼物高峰期,主播该当做了些好玩的事情,比如吃到鸡了,或者全队被歼灭之类的…这些时候都可以作为精彩时候的素材。
能写程序自动截取这些素材吗?答案是肯定的。

实现效果

弹幕抓取

数据统计

根据弹幕和礼物高峰天生的精彩剪辑

实现思路

通过爬虫抓取 B 站直播间数据,找出弹幕激增的韶光点,利用 FFmpeg 自动剪辑韶光点前后的视频即可。

本文代码:GitHub

> bilibili-live-crawler $ tree -L 2.├── README.md├── config.php # 配置文件:配置 FFmpeg 可实行文件的位置,录像的保存路径├── const.php # 常量文件:API 地址,定义数据库用户名和密码、弹幕激增的剖断参数等├── crawler.php # 连接并抓取弹幕做事器的数据├── cut_words│ └── seg.php # 分词脚本:将弹幕做分词处理,可用于天生本次直播的词图├── db.sql # 数据存储├── edit.php # 剪辑脚本├── functions.php # 公用函数└── visual_data.php # 直播数据可视化文件脚本准备 API

以 B 站欠王痒局长为例,进入他的 528 直播间,打开 Chrome 的开拓者工具,看 Network 随意马虎找出这些 API:

直播间原始信息

热门主播会有 2 个房间号:易识记的短房间号、原始长房间号,获取主播原始直播间信息的 API:

Resquest: https://api.live.bilibili.com/room/v1/Room/room_init?id=528Response:{ \公众code\公众: 0, \"大众data\公众: { \"大众room_id\公众: 5441, // 开通直播间时的原始房间号,后边会用到 \"大众short_id\"大众: 528, // 短房间号 ... }}

弹幕做事器信息

直播间在加载时,会要求弹幕做事器的地址,即是我们要去爬取数据的做事器:

Request: https://api.live.bilibili.com/api/player?id=cid:5441 // 5441 即原始房间号Response:...<dm_port>2243</dm_port><dm_server>broadcastlv.chat.bilibili.com</dm_server>...

直播推流信息

直播间会有 3~4 个视频推流地址,选用第一个主路线会更稳定:

Request: https://api.live.bilibili.com/room/v1/Room/playUrl?cid=5441Response:{ \"大众code\公众: 0, \"大众data\公众: { \公众durl\"大众: [ { \公众order\"大众: 1, \"大众length\"大众: 0, \"大众url\公众: \"大众https://bvc.live-play.acgvideo.com/live-bvc/671471/live_322892_3999292.flv?wsSecret=55083259fbc34c4227691ca0feb9c4b8&wsTime=1522465545\公众 // flv 视频格式的推流地址 }, ... }协议剖析

B 站和斗鱼一样,为传输直播数据自己设计了协议头部。
需利用 Wireshark 抓包剖析协议的细节,才能将爬虫的要求伪装成浏览器的要求,连接弹幕做事器去爬取直播间的数据。

找出弹幕做事器的 IP 地址:211.159.194.115

查看要求弹幕做事器的数据包:ip.addr == 211.159.194.115

前边三个包是我(10.0.1.34)与弹幕做事器(211.159.194.115)三次握手建立 TCP 连接的包。

要求的打包和解码,我参考 2016.3 的博客:B站直播弹幕协议详解,现在抓到的包协议头与博客中的不一样,B站重新修正过了,不过该当是为了兼容,这种旧协议头还能用。

要求协议头

下边这个是打开进入直播间时,客户端要求弹幕做事器的要求协议头,相应协议头类似:

00000000 00 00 00 35 00 10 00 01 00 00 00 07 00 00 00 01 ...5.... ........ # 数据包长度 # 意义不明 #要求类型:7进入直播间 # 包类型,1是数据包 # 2是心跳包 00000010 7b 22 72 6f 6f 6d 69 64 22 3a 31 30 31 36 2c 22 {\"大众roomid \公众:1016,\"大众00000020 75 69 64 22 3a 31 35 35 39 37 33 36 38 35 37 32 uid\"大众:1559736857200000030 38 31 36 30 7d 8160} # 要求的数据

进入直播间,打包天生连接做事器的协议头:

// $roomID 是直播间的长房间号// $uid 是当前登任命户的 uid,游客的是随机数function packMsg($roomID, $uid) { $data = json_encode(['roomid' => $roomID, 'uid' => $uid]); // 大端字节序,利用参数 N (4字节) 和 n(2字节) 打包要求 // 占4字节的数据包长度:16字节协议头长度 + 要求数据长度 // 占2字节意义不明:00 10 // 占2字节意义不明:00 01 // 占4字节的要求类型:00 00 00 07 // 占4字节的包类型:00 00 00 01 return pack('NnnNN', 16 + strlen($data), 16, 1, 7, 1) . $data;}

相应数据

做事返回的 JSON 数据包的协议头如下:

00000835 7b 22 69 6e 66 6f 22 3a 5b 5b 30 2c 31 2c 32 35 {\公众info\"大众: [[0,1,2500000845 2c 31 36 37 37 37 32 31 35 2c 31 34 35 37 39 35 ,1677721 5,145795...000008E5 5d 2c 22 63 6d 64 22 3a 22 44 41 4e 4d 55 5f 4d ],\公众cmd\"大众: \"大众DANMU_M000008F5 53 47 22 7d SG\"大众}

解码相应的数据体:

function decodeMessage($socket) { while (socket_last_error($socket)) { while ($out = socket_read($socket, 16)) { $res = @unpack('N', $out); if ($res[1] != 16) { break; } } $message = @socket_read($socket, $res[1] - 16); $resp = json_decode($message, true); switch ($resp['cmd']) { case 'DANMU_MSG': // 弹幕 // info[1] 弹幕内容 // info[2][1] 发送者昵称 echo $resp['info'][2][1] . \"大众 : \"大众 . $resp['info'][1] . PHP_EOL; break; case 'SEND_GIFT': // 直播间送礼物信息 $data = $resp['data']; // uname 发送者的昵称 // giftName 赠予的礼物名称 // unum 一次赠予的数量 // price 礼物的代价 echo $data['uname'] . ' 赠予' . $data['num'] . '份' . $data['giftName'] . PHP_EOL; break; case 'WELCOME': // 直播间欢迎信息 break; default: // 未知的类型 } } socket_close($socket);}

心跳包

如果客户端涌现溘然断网等非常情形,做事端依旧会连续推送数据,掩护这种半打开的 TCP 连接将会摧残浪费蹂躏做事器的资源。
客户端可以每隔一小段韶光给做事端发送心跳包来保活,如果做事端一定超时时间内没收到某个客户真个心跳包,就主动断开连接。

B 站的弹幕做事器也有类似的机制,随便打开一个未开播的直播间,抓包将看到每隔 30s 旁边会给做事端发送一个心跳包,协议头第四部分的值从 7 修正为 2 即可。
如果不发送心跳包,弹幕做事器将在 1~2min 内主动断开连接。

// 发送心跳包function sendHeartBeatPkg($socket) { // 包类型从数据包的 7 修正为心跳包的 2 $str = pack('NnnNN', 16, 16, 1, 2, 1); socket_write($socket, $str, strlen($str));}录播并剪辑精彩时候

录播:直策应用 FFmpeg 保存推流地址的视频即可

剪辑:根据 每分钟 的弹幕数量变革情形,如果涌现峰值,取峰值 前后的一分钟 作为精彩部分。

峰值的判断标准:

对痒局长这样的大主播:直播间人很多,玩出甩狙瞬狙这种骚操作弹幕会激增很多,比如是前一分钟的三倍

对小主播:人一样平常比较少,弹幕数量颠簸不大,涌现精彩操作时也涨幅也不大

以上加粗的剖断粒度、剖断标准都可根据自己喜好的主播修正,详细参考 edit.php 的实现。
还可以为精彩时候加上弹幕剖断,比如剖析是否有大量的 666、233、基本操作、学不来之类的词集中涌现等等。

弹幕剖析

参考 结巴分词 的算法,可用于天生直播的词图、剖析粉丝的习惯用语等等。
我参考的教程:

结巴分词1—结巴分词系统先容

结巴分词2--基于前缀词典及动态方案实现分词

总结

开拓碰着了两个难点

协议头部:参考的博客里边逆向 B 站官方 C# 版客户端代码,剖析协议组成,感谢博主 lyyyuna

分词算法:参考的是结巴分词的前缀词典与动态方案算法,算法能力待提升 :(

再看看人家斗鱼,有开放利用的 《斗鱼弹幕做事器第三方接入协议》,协议也不会修正,兴趣使然写了 B 站的爬虫,这种根据弹幕峰值剪辑视频的想法运用在斗鱼上,估计会更有代价 :)