准备工作
确保电脑上安装了 Electron vue-cli3 并且有一定Vue基础
我是基于Electron+Vue开发的,当然React等也是可行的,这里只展示Vue部分。
为了方便,我提供了一个获取B站弹幕的npm包,目前还不是非常完善,还靠各位一起开发了。
安装
$ npm i bili-danmu-loader-yl --save
使用
import DanmuLoader from 'bili-danmu-loader-yl';
const loader = new DanmuLoader();
//设置房间号
loader.setRoomID(123456);
//弹幕回调
loader.onadd = i => {
//有新弹幕加入的钩子,i为一个数组,包含了新接收的弹幕
console.log(i);
}
loader.startLoader( () => {
//启动加载弹幕成功
//succeced
} , () => {
//成功
//failed
});
//关闭弹幕加载
loader.closeLoader();
进入正题
这件事还要从一只蝙蝠说起(不是,在家里待久了实在没啥事做,那就整个弹幕姬吧?
首先我们开始装prebuilt,如果装了就不用管了
这里推荐用淘宝的镜像
npm install -g cnpm --registry=https://registry.npm.taobao.org
然后是开始装
cnpm install electron-prebuilt -g
然后是初始化项目,后面那个YeuolyDanmu填自己的项目名
vue init simulatedgreg/electron-vue YeuolyDanmu
再安装项目
npm init //安装
npm run dev //开发者模式启动
好了现在项目装好了,我们可以正式开始写代码了
先一通操作删掉原本Electron-Vue的界面,只留下App.vue,界面和路由啥的就大家自己定义叭,反正能用就行,我这里只负责提供最核心的部分
我定义了我自己的路由,如下
import Vue from 'vue'
import Router from 'vue-router'
Vue.use(Router)
export default new Router({
routes: [
{
path: '/',
name: 'index',
component: require('@/views/Index.vue').default,
meta : {
title : '默认目录'
},
children : [
{
name : 'log',
path : '/index/log',
component: require('@/components/items/Logger.vue').default,
meta : {
title : '日志'
}
},
{
name : 'task',
path : '/index/task',
component: require('@/views/Task.vue').default,
meta : {
title : '任务'
}
},
{
name : 'room-settings',
path : '/index/room-settings',
component: require('@/views/RoomSettings.vue').default,
meta : {
title : '房间设置'
}
}
]
},
{
path: '*',
redirect: '/index/log'
}
]
})
我一共定义了3个界面,一个用来放日志,一个用来当控制台,还有个设置界面,我们来看核心的控制台部分
想一想我们脑子的弹幕姬,应该不止一个窗口叭?应该还有个副窗口用来显示窗口叭?所以我决定在启动获取弹幕后启动第二个窗口,这里咱们用个插件,有个大佬给我们提供了一个快捷创建窗口的插件 链接
npm i -S electron-vue-windows
具体的api就去仔细看看作者的文档吧,这里就不细说了
好了,现在定义我们的打开窗口方法,在任务界面内定义
const win = this.$Win.openWin({
width: 300,
height: 700,
useContentSize: true,
webPreferences : {
webSecurity : false
},
resizable: true,
frame: false,
titleBarStyle: false,
windowConfig : {
router : '/danmu',
name : '弹幕窗口',
}
});
这里提一嘴,windowConfig中的router为一个路由的path,有什么用呢?
新建的窗口会使用这个路由的组件作为其的Index,也就是当新窗口已创建它就会显示出这个路由的组件
所以我们要加一条路由
{
path : '/danmu',
name : 'danmu',
component: () => import('@/views/DanmuDialog.vue')
},
好了窗口也建好了,下面来实现窗口通讯,因为无论怎么样都要向弹幕窗口发送弹幕过去的,不然人家也拿不到弹幕,这里使用ipc通讯,这是electron提供的通讯方式,electron允许主进程和渲染进程之间的通讯,而我们的弹幕窗口和主窗口都是渲染进程,所以我们要通过主进程这一个桥梁来实现通讯
这里需要说一下,可能有的人没了解过(完全没了解过electron的
在渲染进程中,也就是 ***.vue啊这些地方,我们通过
const ipc = require('electron').ipcRenderer;
来获取通讯实例,ipc.send发出去的信息都是发到主进程中,但可以用ipc.sendTo来发向指定窗口,我这里用了第一种
在main/index.js中
let mainWindow;
let danmuWindowID;
然后这里就有个很蛋疼的事情,我们手里有主窗口的实例,因为它就建立在main.js里,我们插件建立的窗口我们还没拿到实例呢,这咋整啊,然后我想到同样可以使用通讯,只不过是传个ID的事情
DanmuDialog.vue
<template>
<div id="handle" class="danmu-dialog">
<div id="cover" ref="cover">
<Danmu v-for="(i, key) in danmus" :key="key" :Danmu="i" :parent="$refs.cover"></Danmu>
</div>
</div>
</template>
<script>
import Danmu from '../components/items/Danmu';
const drag = require('electron-drag');
const ipc = require('electron').ipcRenderer;
const win_id = require('electron').remote.getCurrentWindow().id;
require('electron').remote.getCurrentWindow().setAlwaysOnTop(true);
//十万的时候清理一下,100000个在沙月的直播间大概要5 * 1000分钟,完全够了
//就算弹幕速度快5倍也能用1000分钟,一天就24 * 60分钟
const danmu_max_len = 100000;
const delete_time = 5;
export default {
name : 'DanmuDialog',
components : { Danmu },
data: () => ({
danmus : [],
}),
methods: {
//装载弹幕接收钩子
setupRevDanmu(){
//由于这个窗口是在渲染进程中进行的,主进程不能直接获取窗口ID,所以在窗口挂载时向主进程发送窗口ID
ipc.send('danmu-mounted',win_id);
//用于接收所有传输到弹幕窗口的信息
ipc.on('to-danmu', ( sender, channel, msg ) => {
if(channel === 'trans-danmu'){
//传过来的弹幕都是数组
msg.forEach( e => {
this.danmus.push(e);
});
if(this.danmus.length > danmu_max_len){
this.clear();
}
}else if(channel === 'clear'){
this.clear()
}
});
//向主窗口发送成功消息
ipc.send('to-main','trans-info',
{ block : '[DanmuDialog]', info : '弹幕窗口连接成功', color : 'green'} );
},
//当弹幕太多了的时候清除顶部的几个
clear(){
this.danmus = []
}
},
mounted() {
//linux等平台的窗口拖动
const clear = drag('#cover');
//windows和mac的窗口拖动
if(!drag.supported){
document.querySelector('#cover').style['-webkit-app-region'] = 'drag';
}
this.setupRevDanmu();
},
}
</script>
<style>
/* 各位自己弄样式叭~ */
</style>
可以看我在窗口加载完之后通过
require('electron').remote.getCurrentWindow().id;
获取了窗口ID,然后通过
ipc.send('danmu-mounted',win_id);
向主进程发送了我们弹幕窗口的ID,那我们就可以开始搭建窗口沟通的桥梁了呀
main/index.js
//获取弹幕窗口
const ipc = require('electron').ipcMain;
ipc.on('danmu-mounted',function(sender,id){
danmu_win_id = id;
});
以上是获取窗口ID,下面是建立通讯
main/index.js
ipc.on('to-danmu',function(sender,channel,msg){
const DanmuWin = BrowserWindow.fromId(danmu_win_id);
if(DanmuWin){
DanmuWin.webContents.send('to-danmu',channel,msg);
}
});
我把发向主进程的to-danmu消息直接转发到了弹幕窗口中,通过一个channel参数来分开不同类型的消息(其实就是懒,不想弄一堆转发
我们再回头看DanmuDialog.vue
ipc.on('to-danmu', ( sender, channel, msg ) => {
if(channel === 'trans-danmu'){
//传过来的弹幕都是数组
msg.forEach( e => {
this.danmus.push(e);
});
if(this.danmus.length > danmu_max_len){
this.clear();
}
}else if(channel === 'clear'){
this.clear()
}
});
我写了这么几行在里面用来收取发来的消息,然后分别处理不同channel的消息
这接消息的搞完了,得发消息了
任务界面中定义一个方法
transDanmus(danmus){
//向弹幕窗口发送新弹幕
ipc.send('to-danmu','trans-danmu',danmus);
},
这不就把弹幕都发到弹幕窗口里了
好,这通讯的问题都解决了,接下来是获取弹幕了,这又咋整啊,是我的插件登场的时候了(
npm i bili-danmu-loader-yl --save
回到任务界面中
我们可不只需要一个openDanmuDialog来打开窗口,还需要一个来启动弹幕加载
那就来吧
//启动弹幕加载
startDanmuLoader(){
//首先打开弹幕窗口,初始化窗口信息
this.openDanmuDialog();
const room_id = this.$store.getters.getRoomID;
//把弹幕传输钩子挂到Loader上
this.DanmuLoader.onadd = danmus => {
this.transDanmus(danmus);
}
this.DanmuLoader.setRoomID(room_id);
this.DanmuLoader.startLoader(() => {
//成功后的回调
});
},
好,这就大功告成了,我们只需要调用一下startDanmuLoader()就可以启动弹幕姬了
剩下的无非就是一些美工活了,诸位可以自己定义样式,把弹幕姬弄成自己喜欢的样子,还可以通过onadd接口实现一些弹幕小游戏,统计一段时间内的弹幕信息分析特征啥的。
一些后话
老实说最开始做的时候被B站弹幕传输的数据给整得有点想吐,B站用的是二进制数据,Socket传输数据,拿到数据后我一看,嗯?不是Json,好吧那就分析一下数据吧,花了一段时间分离出来信息的头部和主体,封装了一下数据处理类,结果一拿出来用,嗯?怎么少了几条弹幕?这个时候我还是懵的,一次偶然瞄到数据长度比头部的提供的packet_len要长,嗯?这么长?再把数据拿出来一看发现B站把一堆弹幕都弄到一起去了,也就是说我还要拆数据,把不同的信息给分开,最后总算是做完了,感想就是,老老实实用Json它不想嘛。
后来我搜了一下,发现有挺多人做了B站直播弹幕抓取分析的,但好像都没有提到数据分割这茬。
这就算是做完了,如果各位有兴趣的话可以尝试一下,或者说想白嫖的话可以去我的项目https://github.com/Yeuoly/YeuolyDanmuJi看看,但这里有个蛋疼的事情,我需要弹幕姬拥有获取用户头像的能力,跨域的问题倒不是问题,但因为一个origin的原因,我的请求被B站403了(悲,现在的解决方案是搭建一个本地虚拟服务器,由它作为一个中转站去伪造origin获取用户公共信息,我使用的是php(原生的那种,连websocket都不行,端口号是32867,各位不嫌麻烦就用吧
这就又很蛋疼了,谁平时会开着个php服务器啊,装swoole感觉也不大好,和electron又不好整合,那能咋办啊?我现在的想法是到时候用C++写一个中转站,tcp接electron的请求,然后再去B站拿数据,就不需要搭建php服务器了,但我C菜啊(悲,性能搞不好会很差,这里很需要各位的帮助,实在没有就只能自己写了。
再讲一下为什么要用electron吧,首先一个是跨平台,而且弹幕姬这东西你总不能给它放网页里吧,所以就用electron了。第二个一个是html+css+js对自定义很友好,我到时候准备开放一些接口出来给用户自己定义弹幕样式,通过一些vue可视化编辑插件来完成这个工作,用户只需要拖一拖dom,改一改颜色,调一调大小,加一加图片就可以实现自己理想的弹幕姬了,有能力的用户还可以直接修改js和css。