// ==UserScript==
// @name 直播小工具
// @namespace https://github.com/isma123HH/bilibili_live-assistant
// @version 2.8.3
// @description 一个直播小工具,功能包括但不限于获取直播流、获取直播封面
// @TODO 无
// @tips v2.8.3:修复了【获取主播名称失败】的问题,删除了【录制直播】功能,预计将在不久后清理相关代码
// @tips v2.8.2:删除了重连功能,因为在实际使用中根本没有任何的用,还会消耗性能。以及在获取直播流时添加了菜单,现在可以自选分辨率了
// @tips v2.8.1:新增删除B站专栏(https://*.bilibili.com/read/*)复制后带出处的功能。
// @tips v2.8.0:由于B站的限制,搜索api无法被调用,所以删除了点击sc进主页的功能,以及优化及修复了一堆大小问题
// @tips v2.8.0:替换了pako.js的cdn,并修复了多开直播间导致的网络问题
// @tips v2.7.9:修复一些时候无法连接ws服务器
// @tips v2.7.8:修复了一点小问题,以及新增心跳包统计(可以根据这个来推测观看时长,每30秒发送一次心跳包)
// @tips v2.7.6:新增舰长数统计,以及统计数据导出为json格式,并且添加了打开sc可以显示对应的人民币
// @tips v2.7.6:更新了打开super chat可以点击目标用户的用户名跳转到他的个人主页,以及修复了弹幕发送时间显示
// @tips v2.7.5:更新了直播录制(acfun),以及修改了录制完毕后的操作
// @tips v2.7.4:更新了直播录制,位置:小功能->录制直播,停止录制同理
// @waring-tips v2.7.4的警告:录制时清晰度请勿选择"原画PRO" "超清PRO"等PRO清晰度,会导致无法录制。
// @waring-tips v2.7.4的警告:录制时不要静音播放器,或作出影响正常播放的行为。
// @tips v2.7.3:详细信息请前往github的Releases查看
// @tips v2.7.0:现已支持B站直播间wss连接,以及支持了Acfun的直播流获取
// @author isma
// @license MIT
// @match https://live.bilibili.com/*
// @match https://*.bilibili.com/read/*
// @match https://live.acfun.cn/live/*
// @match https://live.douyin.com/*
// @icon https://i1.hdslb.com/bfs/live/83f48bf72165be6ed8d59ac249aec58e48360575.png
// @grant GM_setValue
// @grant GM_getValue
// @grant GM_listValues
// @require https://unpkg.com/sweetalert2@11.4.17/dist/sweetalert2.all.min.js
// @require https://unpkg.com/sweetalert@2.1.2/dist/sweetalert.min.js
// @require https://unpkg.com/jquery@3.2.1/dist/jquery.min.js
// @require https://unpkg.com/xgplayer@2.31.2/browser/index.js
// @require https://unpkg.com/xgplayer-hls.js@2.1.1/browser/index.js
// @require https://unpkg.com/xgplayer-flv.js@2.1.2/browser/index.js
// @require https://unpkg.com/pako@2.0.4/dist/pako.min.js
// @run-at document-end
// ==/UserScript==
window.onload = function () { // 重构为加载时再进行各种操作
'use strict';
if (top.location != self.location) {
return; // 防止在iframe中再加载
}
class tools_log_class { // 封装提示class
constructor() {
console.warn("[live_tools]初始化class")
}
error(msg) {
console.error("[live_tools_error]" + msg)
}
normal(msg) {
console.log("[live_tools]" + msg)
}
warn(msg) {
console.warn("[live_tools_warn]" + msg)
}
}
var live_tools_log = new tools_log_class() // 初始化
// 全局通用函数
function send_toast(icon, title, text, time, pos) { // 将提示封装成函数以便调用
const Toast = Swal.mixin({
toast: true,
position: pos,
showConfirmButton: false,
timer: time,
text: text,
timerProgressBar: true,
didOpen: (toast) => {
toast.addEventListener('mouseenter', Swal.stopTimer)
toast.addEventListener('mouseleave', Swal.resumeTimer)
},
});
Toast.fire({
icon: icon,
title: title,
})
}
function timestamptotime(timestamp) { // 时间戳解析
return new Date(parseInt(timestamp) * 1000).toLocaleString().replace(/年|月/g, "-").replace(/日/g, " ");
}
function time_stamp_ten(tm) { // 转换为10位时间戳,做这个函数才不是因为只写了解析10位时间戳呢!
var tma = tm.toString()
var tmp = tma.substr(0, 10)
return tmp
}
function file_download(content, name, types) {
var eleLink = document.createElement("a");
eleLink.download = name + '.json';
eleLink.style.display = "none";
// 字符内容转变成blob地址
var data = JSON.stringify(content, undefined, 4);
var blob = new Blob([data], { type: types });
eleLink.href = URL.createObjectURL(blob);
// 触发点击
document.body.appendChild(eleLink);
eleLink.click();
// 然后移除
document.body.removeChild(eleLink);
}
// 检测网站
switch (window.location.host) {
case 'live.acfun.cn':
acfun_run()
break;
case 'live.bilibili.com':
bilibili_run()
break;
case 'live.douyin.com':
douyin_run()
break;
case 'www.bilibili.com':
if (window.location.pathname.indexOf('read') != -1) {
bilibili_zhuanlan_run()
}
}
function douyin_run() {
init_douyin()
const ids = {
LIVE__GET_STREAM_LINK_FLV: '#get_stream_link_flv',
LIVE__GET_STREAM_LINK_MU: '#get_stream_link_mu'
}
var dy_room_id // 储存一下抖音直播间id,方便以后使用
function init_douyin() {
dy_room_id = window.location.pathname.replace('/', '') // 获取网址,例如 https://live.douyin.com/801196266504 = /801196266504
send_toast('success', 'html注入成功!享用脚本', '', 3000, 'top')
}
const wrapperObserver = new MutationObserver((mutationsList) => { // 监听变动
for (const mutation of mutationsList) {
if (mutation.type === 'childList') {// 子元素变动,也有characterData(节点内容或节点文本),attributes(属性变动),subtree(所有下属节点的变动)
[...mutation.addedNodes].map(item => {// 在新增的节点 返回数组(map),并且带上item
// mmsn_log('非目标变更', item);
if (typeof item.innerHTML == 'string') {
if (item.parentNode.type == 'button') {
if (item.children[0].innerHTML == '举报直播间') {
attack_sgd(item)
}
}
}
})
}
}
});
wrapperObserver.observe(document.body, { attributes: true, childList: true, subtree: true }); // 设置监听参数
function attack_sgd(item) {
// flv
var nodef = document.createElement('li')
nodef.id = 'get_stream_link_flv'
nodef.classList.add(item.firstChild.className)
nodef.appendChild(document.createTextNode('获取flv直播流'))
// m3u8
var nodem = document.createElement('li')
nodem.id = 'get_stream_link_mu'
nodem.classList.add(item.firstChild.className)
nodem.appendChild(document.createTextNode('获取m3u8直播流'))
// 插入
item.append(nodef)
item.append(nodem)
var live_data = JSON.parse(decodeURIComponent(document.getElementById("RENDER_DATA").innerText));
// flv
document.querySelector(ids.LIVE__GET_STREAM_LINK_FLV).addEventListener('click', function () {
navigator.clipboard.writeText(live_data.initialState.roomStore.roomInfo.room.stream_url.flv_pull_url.FULL_HD1)
send_toast('success', '已复制直播流链接', '', 2000, 'top')
})
// m3u8
document.querySelector(ids.LIVE__GET_STREAM_LINK_MU).addEventListener('click', function () {
navigator.clipboard.writeText(live_data.initialState.roomStore.roomInfo.room.stream_url.hls_pull_url_map.FULL_HD1)
send_toast('success', '已复制直播流链接', '', 2000, 'top')
})
}
}
function acfun_run() {
const ids = {
LIVE__MENU_ID: '#get_stream_link',
LIVE__COVER_ID: '#get_cover_link',
PLUGIN_MENU_ID: '#plugin_menu',
LIVE__REC_ID: '#get_live_rec'
}
init_acfun()
get_live_info()
function get_live_info() {
$.ajax(
{
url: "https://id.app.acfun.cn/rest/app/visitor/login",
type: 'post',
xhrFields: { withCredentials: true },
contentType: 'application/x-www-form-urlencoded',
data: 'sid=acfun.api.visitor',
success: function (data) {
anonymous_uid = data.userId
var visitor_st = data['acfun.api.visitor_st']
$.ajax(
{
url: "https://api.kuaishouzt.com/rest/zt/live/web/startPlay?subBiz=mainApp&kpn=ACFUN_APP&kpf=PC_WEB&userId=" + anonymous_uid + '&did=H5_&acfun.api.visitor_st=' + visitor_st,
type: 'post',
xhrFields: { withCredentials: true },
contentType: 'application/x-www-form-urlencoded',
data: 'authorId=' + ac_room_id + '&pullStreamType=FLV',
success: function (data) {
live_data = data
}
}
)
},
}
)
}
var ac_room_id // 房间号
var acfun_video = null
var anonymous_uid = null // 匿名id
var live_data = null
// 直播录制
var is_rec = false
var mediaRecorder
var video_arr = []
var rec_time_for
var rec_time_total = 0
function init_acfun() {
ac_room_id = window.location.pathname.replace('/live/', '') // 获取网址,例如 https://live.acfun.cn/live/38382871 = /live/38382871
send_toast('success', 'html注入成功!享用脚本', '', 3000, 'top')
}
const wrapperObserver = new MutationObserver((mutationsList) => { // 监听变动
for (const mutation of mutationsList) {
if (mutation.type === 'childList') {// 子元素变动,也有characterData(节点内容或节点文本),attributes(属性变动),subtree(所有下属节点的变动)
[...mutation.addedNodes].map(item => {// 在新增的节点 返回数组(map),并且带上item
//mmsn_log('非目标变更', item);
if (item.classList?.contains('btn-lab')) {
attack_player_player()
}
})
}
}
});
wrapperObserver.observe(document.body, { attributes: true, childList: true, subtree: true }); // 设置监听参数
function attack_player_player() { // 由于acfun的播放器比较特殊,和B站不一样,没有打开菜单就没有div,所以监听打开事件再插入
$('.context-menu')[0].insertBefore($('<li id="get_stream_link">获取直播流</li>')[0], document.querySelector('.context-menu').childNodes[6]);
$('.context-menu')[0].insertBefore($('<li id="get_cover_link">获取直播封面</li>')[0], document.querySelector('.context-menu').childNodes[7]);
// $('.context-menu')[0].insertBefore($('<li id="get_live_rec">录制直播</li>')[0], document.querySelector('.context-menu').childNodes[8]);
if (is_rec == true) {
document.querySelector(ids.LIVE__REC_ID).innerText = '停止录制' // 更改按钮名
}
// 直播封面
document.querySelector(ids.LIVE__COVER_ID).addEventListener('click', function () {
Swal.fire({
title: '直播间封面',
text: '右键或点击下方按钮即可复制链接!',
imageUrl: 'https://ali2.a.kwimgs.com/bs2/ztlc/cover_' + live_data.data.liveId + '_raw.jpg',
confirmButtonText: '复制',
}).then((result) => {
if (result.isConfirmed) {
navigator.clipboard.writeText('https://ali2.a.kwimgs.com/bs2/ztlc/cover_' + live_data.data.liveId + '_raw.jpg')
send_toast('success', '已复制图片链接', '', 2000, 'top')
}
})
})
// 直播流
document.querySelector(ids.LIVE__MENU_ID).addEventListener('click', function () {
var stlk_json = JSON.parse(live_data.data.videoPlayRes) // stlk=stream link
navigator.clipboard.writeText(stlk_json.liveAdaptiveManifest[0].adaptationSet.representation[stlk_json.liveAdaptiveManifest[0].adaptationSet.representation.length - 1].url)
send_toast('success', '已复制直播流链接', '', 2000, 'top')
})
// 录制直播
document.querySelector(ids.LIVE__REC_ID).addEventListener('click', function () {
if (is_rec == true & document.querySelector(ids.LIVE__REC_ID).innerText == '停止录制') {
is_rec = false; live_tools_log.warn('正在停止录制'); document.querySelector(ids.LIVE__REC_ID).innerText = '录制直播' // 更改按钮名
mediaRecorder.stop()
var web_m = new Blob(video_arr, { type: "video/webm" }); // 新建Blob对象,类型为webm
send_toast('success', '录制完毕', '共录制了' + rec_time_total + '秒,1秒后将自动跳转', 2000, 'top')
document.querySelector('#show_rec_time').remove(); clearInterval(rec_time_for); rec_time_total = 0 // 各种销毁
setTimeout(function () {
Swal.fire({
showConfirmButton: false, width: 1280, html: '<div id="video_run_rec"></div>', showCloseButton: true, // 显示关闭框
willClose: () => {
video_player.destroy(true) // 销毁播放器
},
})
var video_player = new Player({
id: 'video_run_rec', url: URL.createObjectURL(web_m), width: 1200, height: 700, autoplay: true, download: true, playbackRate: [0.5, 0.75, 1, 1.5, 2, 5, 10], defaultPlaybackRate: 1 // 注意的是也设置了倍数播放
})
// open(URL.createObjectURL(web_m))
}, 1500);
}
else if (is_rec == false & document.querySelector(ids.LIVE__REC_ID).innerText == '录制直播') {
is_rec = true // 设置一下状态
mediaRecorder = new MediaRecorder(document.querySelector('.container-video').childNodes[1].captureStream(), {
mimeType: "video/webm;codecs=vp8" // 目前看来只支持webm
})
video_arr = [] // 新建数组
new Promise((resolve, reject) => { // 监听将要发生的事件
mediaRecorder.onstop = resolve;
mediaRecorder.onerror = reject;
mediaRecorder.ondataavailable = (event) => {
video_arr.push(event.data); // 将数据存入数组
// console.log(video_arr) // 未来的计划是video_arr.length > 5000的时候分组
}
mediaRecorder.start(1); // 不加1的话大概率不会成功运行
})
setTimeout(function () {
rec_time_for = setInterval(function () { // 每隔1秒钟
rec_time_total++ // 记录一下已录制的时长
document.querySelector('#show_rec_time').innerText = '已经录制了' + rec_time_total + '秒' // 然后在播放器里面修改
}, 1000)
}, 1)
document.querySelector(ids.LIVE__REC_ID).innerText = '停止录制' // 更改按钮名
var rec_time_show = '<div class="share"> <span class="shareCount" id="show_rec_time">又是一个播放时间占位! By isma</span> </div>'
$(rec_time_show).insertAfter($('.live-tips')[0]) // 1:播放器的显目提示 2.类似高能榜提醒的时间统计
send_toast('info', '正在录制直播', '不要给播放器静音,会导致录制的视频没有声音 \n 以及也不要在录制时刷新,数据不会保存', 2500, 'top')
}
})
}
// 直播菜单
window.setTimeout(function () {
$('.author-interactive-area')[0].insertBefore($('<div class="follow-up not-follow" id="plugin_menu"><div class="follow-status">我是</div> <div class="follow-count">插件菜单</div></div>')[0], $('.author-interactive-area .more-action')[0])
document.querySelector(ids.PLUGIN_MENU_ID).addEventListener('click', function () {
Swal.fire({
title: '插件菜单',
showConfirmButton: false,
showDenyButton: true,
denyButtonText: '直播流播放器',
}).then((result) => {
if (result.isDenied) {
let is_m3u8, is_flv = false;
Swal.fire({
title: '直播流播放',
input: 'text', // 允许输入text,但没有任何原生检测
inputLabel: '注意!播放器已经同时支持m3u8与flv播放!',
inputPlaceholder: '在这里粘贴直播流链接',
confirmButtonText: '继续',
inputValidator: (value) => {
if (!value) {
return '请输入直播流链接!'
}
if (value.indexOf(".m3u8") != -1) {
is_m3u8 = true
}
if (value.indexOf(".flv") != -1) {
is_flv = true
}
if (value.indexOf(".m3u8") != -1 & value.indexOf(".flv") != -1) {
return '既有.m3u8又有.flv,你这个链接不对吧?'
}
},
}).then((result) => {
if (result.isConfirmed) {
var find_video_id = false
try {
acfun_video = document.querySelector('.container-video').childNodes[1] // 播放器控件最后一个节点
acfun_video.muted = true; // 对播放器静音
find_video_id = true
}
catch {
find_video_id = false
}
Swal.fire({ // 如果通过video.js来播放m3u8视频。请在将要关闭时使用dispose(),也就是删除播放器的所有事件、元素,完美符合我们"重新创建标签"的需求
showConfirmButton: false,
width: 1280,
html: // 这里就写html代码,其实我更想是找到swal窗口用innerHTML插入的,库本身支持的方法更好,唯一的缺点就是换行需要+
'<div id="video_run_m3u8"></div>',
showCloseButton: true, // 显示关闭框
willClose: () => {
if (find_video_id == true) {
acfun_video.muted = false; // 取消对acfun播放器的静音
video_player.destroy(true)
}
if (find_video_id == false) {
video_player.destroy(true)
}
},
})
var video_player = null
if (is_m3u8) {
video_player = new HlsJsPlayer({
id: 'video_run_m3u8', url: result.value,
isLive: true, width: 1200, height: 700,
autoplay: true, pip: true,
})
}
else if (is_flv) {
video_player = new FlvJsPlayer({
id: 'video_run_m3u8', url: result.value,
isLive: true, width: 1200, height: 700,
autoplay: true, pip: true, hasVideo: true, hasAudio: true,
})
}
}
})
}
})
})
}, 500)
}
// ***
// 哔哩哔哩专栏相关函数
// ***
function bilibili_zhuanlan_run() { // 所有人都恨附加信息!
// 因为专栏的主要内容在article-content内,并且B站的信息添加也是针对该元素的。
// 所以仅需阻止默认事件以及冒泡事件(不确定是否有效,先阻止就对了),最后再将选中的内容copy即可。
// 这里不用.toString()是因为会将html元素也一起复制,并且B站本身也不用.toString()进行复制。
document.querySelector('.article-content').addEventListener("copy", (e) => {
e.preventDefault()
e.stopPropagation()
navigator.clipboard.writeText(window.getSelection())
})
}
// ***
// 哔哩哔哩相关函数
// ***
function bilibili_run() {
const ids = {
MENU__SETTING_ID: '#plugins_setting',
MENU__LIVE_TOALS_ID: '#totals_menu',
RIGHT_MENU__CLICK_GET_STREAM_LINK_ID: '#right_click_menu_getstreamlink',
RIGHT_MENU__CLICK_GET_STREAM_LINK_FLV_ID: '#right_click_menu_getstreamlink_flv',
RIGHT_MENU__CLICK_GET_STREAM_COVER_ID: '#right_click_menu_getstreamcover',
RIGHT_MENU__CLICK_NO_IN_SHOW: '#right_click_menu_no_in_show',
RIGHT_MENU__CLICK_300S: '#right_click_menu_300s',
RIGHT_MENU__CLICK_180S: '#right_click_menu_180s',
RIGHT_MENU__CLICK_60S: '#right_click_menu_60s',
RIGHT_MENU__CLICK_30S: '#right_click_menu_30s',
RIGHT_MENU__CLICK_15S: '#right_click_menu_15s',
LIVE_TOALS_SHOW_HIGH: '#high_people',
RIGHT_MENU__CLICK_REC_LIVE: '#right_click_menu_rec_live',
}
var room_total = {
// 各种统计
high_people: 0,
entry_people: 0,
boat_guy_entry: 0,
follow_people: 0,
block_guys: 0,
danmu_total: 0,
// 付费相关
silver: 0,
free_gift: 0,
free_gift_silver: 0,
pay_gift: 0,
// sc相关
super_chat_total: 0,
super_chat_rmb: 0,
boat_add: 0,
hearts: 0,
}
function getCertification(json) { // 这里的代码来源:https://blog.csdn.net/yyznm/article/details/116543107,非常感谢这位博主。
var bytes = str2bytes(json); //字符串转bytes
var n1 = new ArrayBuffer(bytes.length + 16)
var i = new DataView(n1);
i.setUint32(0, bytes.length + 16), //封包总大小
i.setUint16(4, 16), // 头部长度
i.setUint16(6, 1), // 协议版本
i.setUint32(8, 7), // 操作码 7表示认证并加入房间
i.setUint32(12, 1); // 就1
for (var r = 0; r < bytes.length; r++) {
i.setUint8(16 + r, bytes[r]); //把要认证的数据添加进去
}
return i; //返回
}
function str2bytes(str) {
const bytes = []
let c
const len = str.length
for (let i = 0; i < len; i++) {
c = str.charCodeAt(i)
if (c >= 0x010000 && c <= 0x10FFFF) {
bytes.push(((c >> 18) & 0x07) | 0xF0)
bytes.push(((c >> 12) & 0x3F) | 0x80)
bytes.push(((c >> 6) & 0x3F) | 0x80)
bytes.push((c & 0x3F) | 0x80)
} else if (c >= 0x000800 && c <= 0x00FFFF) {
bytes.push(((c >> 12) & 0x0F) | 0xE0)
bytes.push(((c >> 6) & 0x3F) | 0x80)
bytes.push((c & 0x3F) | 0x80)
} else if (c >= 0x000080 && c <= 0x0007FF) {
bytes.push(((c >> 6) & 0x1F) | 0xC0)
bytes.push((c & 0x3F) | 0x80)
} else {
bytes.push(c & 0xFF)
}
}
return bytes
}
// 文本解码器
var textDecoder = new TextDecoder('utf-8');
// 从buffer中读取int
const readInt = function (buffer, start, len) {
let result = 0
for (let i = len - 1; i >= 0; i--) {
result += Math.pow(256, len - i - 1) * buffer[start + i]
}
return result
}
// blob blob数据
// call 回调 解析数据会通过回调返回数据
function decode(blob, call) {
let reader = new FileReader();
reader.onload = function (e) {
let buffer = new Uint8Array(e.target.result)
let result = {}
result.packetLen = readInt(buffer, 0, 4)
result.headerLen = readInt(buffer, 4, 2)
result.ver = readInt(buffer, 6, 2)
result.op = readInt(buffer, 8, 4)
result.seq = readInt(buffer, 12, 4)
if (result.op == 5) {
result.body = []
let offset = 0;
while (offset < buffer.length) {
let packetLen = readInt(buffer, offset + 0, 4)
let headerLen = 16 // readInt(buffer,offset + 4,4)
let data = buffer.slice(offset + headerLen, offset + packetLen);
let body = "{}"
if (result.ver == 2) {
body = textDecoder.decode(pako.inflate(data)); //协议版本为 2 时代表数据有进行压缩,通过pako.js进行解压
} else {
body = textDecoder.decode(data); //协议版本为 0 时,代表数据没有进行压缩
}
if (body) {
const group = body.split(/[\x00-\x1f]+/); // 同一条消息中可能存在多条信息,用正则筛出来
group.forEach(item => {
try {
result.body.push(JSON.parse(item));
} catch (e) {
// 忽略非JSON字符串,通常情况下为分隔符
}
});
}
offset += packetLen;
}
}
call(result); //回调
}
reader.readAsArrayBuffer(blob);
}
function bili_toast(type, left, top, message, time) { // type可用:success、error、info、caution
var send_msg = '<div id="bili_toast" class="link-toast ' + type + '" style="left:' + left + 'px; top: ' + top + 'px;"><span class="toast-text">' + message + '</span></div>'
$('body').append(send_msg)
setTimeout(function () {
$('#bili_toast').remove()
}, time)
}
function get_stream_link(type, qn) {
return new Promise((res, rej) => { // type可用:h5(hls,m3u8),web(flv) qn: 80:流畅 150:高清 400:蓝光 10000:原画 20000:4K 30000:杜比
$.get('https://api.live.bilibili.com/room/v1/Room/playUrl?cid=' + room_id + '&platform=' + type + '&qn=' + qn + '', function (data) {
res(data.data.durl[0].url)
})
})
}
//初始化
var is_rec = false
var ls_stream = null; // 加上&tmshift=xxx可以看直播回放(单位秒)
var uid = null; // 用户uid
var anchor = null; // 主播
var anchor_uid = null; // 主播uid
var room_id = null; // 房间号
var room_real_id = null; // 真正的房间号,因为有些房间是短号
var rdm_id = null; // 随机id,用在 _context-menu-item_ + rdm_id
var room_title = null;
var room_init_res = null; // 房间初始化信息
var send_time_show = null; // 时间显示的html
var data_v = null; // 一种data-v
// var data_v1 = null; // 第二种data-v,但和data_v2一起使用 没必要了所有就注释了
// var data_v2 = null // 第二种data-v,和v1一起使用
var wss_timer = null; // 将定时器声明为全局变量,因为在丢失wss连接后要清除定时器
var load_time = null; // 加载好本脚本的时间戳
var now_time = null; // 现在时间
var ws_content = null; // wss连接,方便在任何地方调用
get_room_id() // 运行初始化函数
function get_room_id() {
if (window.location.pathname == '/') { // 由于在主页也会加载,所以先判断一下pathname是不是/,如果是就代表在主页,不必进行其他操作,否则会损耗用户性能
return
}
room_id = window.location.pathname.replace('/', '') // 获取房间号,例如 https://live.bilibili.com/213 = /213 -> 213
if (room_id.indexOf('blanc') != -1) { // 如果有blanc
room_id = room_id.replace('/', '') // 那就继续解析!
room_id = room_id.replace('blanc', '')
}
if (window.location.pathname.indexOf('blackboard') != -1) {
setTimeout(function () {
window.location.href = document.querySelector('#player-ctnr').firstChild.firstChild.src
}, 1500)
}
if (document.querySelector('.t-background-image') != null) { // 观察于2022/9/14 似乎B站升级了,现在地址栏没有blackboard了,就用该方法判断是否特殊的直播间
setTimeout(() => { // 想留在特殊页面也可以,注释掉这个if就行了,对功能没有影响,但控制台会输入一堆报错
// 2022/9/23 根据本人观察,有些特殊直播间点进去后网址的房间号不变,但实际上显示的是其他直播间(例如22年的高能电玩节,点击进入C酱直播间,但实际上是“下班被游戏打”的直播间,也就是活动主办方。)
// 所以在这里解析出播放器url,如果播放器url的roomid不是当前网址的roomid,则重新跳转。
// 但我推测是bug,期待官方未来修复。
window.location.href = document.querySelector('#player-ctnr').firstChild.firstChild.src
// if(document.querySelector('#player-ctnr').firstChild.firstChild.src.split('/')[4].split('?')[0] != window.location.pathname.replace('/', '')){
// window.location.href = 'https://live.bilibili.com/blanc/' + window.location.pathname.replace('/', '')
// }else{
// window.location.href = document.querySelector('#player-ctnr').firstChild.firstChild.src
// }
}, 1000)
}
setTimeout(() => {
if (document.querySelector('.kv-box') != null) {
console.log('https://live.bilibili.com/blanc/' + window.location.pathname.replace('/', ''))
window.location.href = 'https://live.bilibili.com/blanc/' + window.location.pathname.replace('/', '')
}
if (document.querySelector('.era-container') != null) { // 不知道傻逼B站加那么多B乱七八糟的东西干嘛
setTimeout(() => {
window.location.href = document.querySelector('#player-ctnr').firstChild.firstChild.src
}, 1000)
}
}, 1500)
init() // 继续初始化
wss_get() // websocket连接
}
function init() {
try {
data_v = document.querySelector('.follow-ctnr .left-part').getAttributeNames()[0] // 获取data-v
}
catch {
// 在一些特殊的直播间会获取不到data-v,但如果想做一个正常的样式,data-v是必须存在的。2022/9/14 大部分直播间没有该情况
setTimeout(function () { // 我在LIVE__MENU_INJECT里内置了圆角边框和字体居中,如果没有data-v也能模仿其他按钮的样式
data_v = document.querySelector('.follow-ctnr').getAttributeNames()[0]
document.querySelector('#totals_menu').setAttribute(data_v, ''); document.querySelector('#plugins_setting').setAttribute(data_v, '')
document.querySelector('#totals_menu').firstChild.setAttribute(data_v, ''); document.querySelector('#plugins_setting').firstChild.setAttribute(data_v, '')
}, 1000)
}
//注入
load_time = time_stamp_ten(Date.now()) // 给加载时间复制
live_tools_log.warn(timestamptotime(load_time) + '时加载脚本')
send_toast('success', 'html注入成功!享用脚本', '', 3000, 'top') //调用示例 第一个参数是提示图标,可以在sweetalert2官网查询;第二个参数是标题;第三个参数是内容,不填则无;第四个参数是显示时间,毫秒为单位;第五个为显示位置,同样在sweetalert2官网查询。
}
const htmls = {
LIVE__TOTALS_MENU: '<div id="totals_menu" ' + data_v + '="" style="margin-right:5px;border-radius:5px;" role="button" aria-label="数据统计" title="点击打开数据统计菜单" class="left-part live-skin-highlight-bg live-skin-button-text dp-i-block pointer p-relative"><span ' + data_v + '="" class="follow-text v-middle d-inline-block" style="text-align: center;line-height: 20px;">数据统计</span></div>',
LIVE__MENU_INJECT: '<div id="plugins_setting" ' + data_v + '="" style="margin-right:5px;border-radius:5px;" role="button" aria-label="插件菜单" title="点击打开插件菜单" class="left-part live-skin-highlight-bg live-skin-button-text dp-i-block pointer p-relative"><span ' + data_v + '="" class="follow-text v-middle d-inline-block" style="text-align: center;line-height: 20px;">插件菜单</span></div>'
};
// 菜单注入
window.setTimeout(function bili_live_menu_inject() {
try {
document.querySelector('.follow-ctnr').insertBefore($(htmls.LIVE__MENU_INJECT)[0], $('.follow-ctnr .left-part')[0]) // 将"直播菜单"注入
document.querySelector('.follow-ctnr').insertBefore($(htmls.LIVE__TOTALS_MENU)[0], $('.follow-ctnr .left-part')[0]) // 将"数据统计"注入
} catch {
setTimeout(() => {
bili_live_menu_inject()
}, 1000)
return
}
// var high_people_show = '<div title="" '+data_v1+'="" '+data_v2+'="" class="live-skin-normal-a-text pointer not-hover" style="line-height: 16px;"><i '+data_v1+'="" style="font-size: 16px;"></i><span '+data_v1+'="" class="action-text v-middle" id="high_people" style="font-size: 12px;">高能榜占位</span></div>'
// document.querySelector('.right-ctnr').insertBefore($(high_people_show)[0],document.querySelector('.right-ctnr').childNodes[5]) // 注入高能榜
document.querySelector(ids.MENU__SETTING_ID).addEventListener('click', function () {
Swal.fire({
title: '插件菜单',
showConfirmButton: false,
showCancelButton: true,
showDenyButton: true,
cancelButtonText: '退出',
denyButtonText: '直播流播放器',
}).then((result) => {
if (result.isDenied) {
let is_m3u8, is_flv = false;
Swal.fire({
title: '直播流播放',
input: 'text', // 允许输入text,但没有任何原生检测
inputLabel: '注意!播放器已经同时支持m3u8与flv播放!',
inputPlaceholder: '在这里粘贴直播流链接',
confirmButtonText: '继续',
inputValidator: (value) => {
if (!value) {
return '请输入直播流链接!'
}
if (value.indexOf(".m3u8") != -1) {
is_m3u8 = true
}
if (value.indexOf(".flv") != -1) {
is_flv = true
}
if (value.indexOf(".m3u8") != -1 & value.indexOf(".flv") != -1) {
return '既有.m3u8又有.flv,你这个链接不对吧?'
}
},
}).then((result) => {
if (result.isConfirmed) {
var find_video_id = false
try {
document.querySelector('video').muted = true; // 对播放器静音
find_video_id = true
}
catch {
find_video_id = false
}
Swal.fire({ // 如果通过video.js来播放m3u8视频。请在将要关闭时使用dispose(),也就是删除播放器的所有事件、元素,完美符合我们"重新创建标签"的需求
showConfirmButton: false,
width: 1280,
html: // 这里就写html代码,其实我更想是找到swal窗口用innerHTML插入的,库本身支持的方法更好,唯一的缺点就是换行需要+
'<div id="video_run_m3u8"></div>',
showCloseButton: true, // 显示关闭框
willClose: () => {
if (find_video_id == true) {
document.querySelector('video').muted = false; // 取消对B站播放器的静音
video_player.destroy(true) // 销毁播放器
}
if (find_video_id == false) {
video_player.destroy(true) // 销毁播放器
}
},
})
var video_player = null
if (is_m3u8 == true) {
video_player = new HlsJsPlayer({
id: 'video_run_m3u8',
url: result.value,
isLive: true,
width: 1200,
height: 700,
autoplay: true,
pip: true,
definitionActive: 'hover', // 设置清晰度需要悬停在按钮上才能修改
})
}
else if (is_flv == true) {
video_player = new FlvJsPlayer({
id: 'video_run_m3u8',
url: result.value,
isLive: true,
width: 1200,
height: 700,
autoplay: true,
pip: true,
hasVideo: true,
hasAudio: true,
definitionActive: 'hover', // 设置清晰度需要悬停在按钮上才能修改
})
}
video_player.emit('resourceReady', [{ name: '流畅', url: 'zanwei' }, { name: '高清', url: 'zanwei' }, { name: '原画', url: 'zanwei' }]);
video_player.on('definitionChange', function (e) { // 监听清晰度更改
if (e.to == '原画' & is_flv == true) { // 监听更改并劫持修改
$.get('https://api.live.bilibili.com/room/v1/Room/playUrl?cid=' + room_id + '&quality=4', function (data) {
video_player.src = data.data.durl[0].url
})
}
else if (e.to == '原画' & is_m3u8 == true) {
$.get('https://api.live.bilibili.com/room/v1/Room/playUrl?cid=' + room_id + '&platform=h5&quality=4', function (data) {
video_player.src = data.data.durl[0].url
})
}
if (e.to == '流畅' & is_flv == true) {
$.get('https://api.live.bilibili.com/room/v1/Room/playUrl?cid=' + room_id + '&quality=2', function (data) {
video_player.src = data.data.durl[0].url
})
}
else if (e.to == '流畅' & is_m3u8 == true) {
$.get('https://api.live.bilibili.com/room/v1/Room/playUrl?cid=' + room_id + '&platform=h5&quality=2', function (data) {
video_player.src = data.data.durl[0].url
})
}
if (e.to == '高清' & is_flv == true) {
$.get('https://api.live.bilibili.com/room/v1/Room/playUrl?cid=' + room_id + '&quality=3', function (data) {
video_player.src = data.data.durl[0].url
})
}
else if (e.to == '高清' & is_m3u8 == true) {
$.get('https://api.live.bilibili.com/room/v1/Room/playUrl?cid=' + room_id + '&platform=h5&quality=3', function (data) {
video_player.src = data.data.durl[0].url
})
}
})
video_player.once('canplay', function (e) { // 视频准备好之后就设置一下清晰度切换的样式
document.querySelector('.xgplayer-definition').lastChild.setAttribute('style', 'left:0px')
document.querySelector('.xgplayer-definition').lastChild.innerText = '清晰度'
})
}
})
}
})
})
document.querySelector(ids.MENU__LIVE_TOALS_ID).addEventListener('click', function () { // 监听点击"数据菜单"
now_time = timestamptotime(time_stamp_ten(Date.now())) // 获取现在时间
Swal.fire({
title: '<font size=5>' + timestamptotime(load_time) + '到<br>' + now_time + '的统计</font>',
html:
'<h3>房间号' + room_id + ',真实房间号' + room_real_id + ',主播:' + anchor + '<br>' +
'新增了' + room_total.follow_people + '个关注<br>有' + room_total.entry_people + '个普通用户和' + room_total.boat_guy_entry + '个大航海用户进入直播间<br>共新增了' + room_total.boat_add + '个大航海<br>已经接收了' + room_total.danmu_total + '条弹幕<br>' + '共收到' + room_total.pay_gift + '个付费礼物,价值' + room_total.silver / 100 + '电池,等同于' + String(room_total.silver / 100).slice(0, String(room_total.silver / 100).length - 1) + '人民币<br>共收到' + room_total.free_gift + '个免费礼物,价值' + room_total.free_gift_silver + '银瓜子<br>共收到了' + room_total.super_chat_total + '条SuperChat,总价值' + room_total.super_chat_rmb + '人民币<br>共禁言了' + room_total.block_guys + '位用户<br>共发送了' + room_total.hearts + '个心跳包',
showCloseButton: true,
showCancelButton: true,
showConfirmButton: false,
showDenyButton: true,
cancelButtonText: '退出',
// confirmButtonText: '下载txt格式的统计数据',
denyButtonText: '下载json格式的统计数据'
}).then((result) => {
if (result.isDenied) {
var total_json_data = {
'room_id': room_id,
'room_title': room_title,
'up': anchor,
'up_uid': anchor_uid,
'start_time': timestamptotime(load_time),
'export_time': now_time,
'data': {
'into_room': { // 进入直播间
'normal_user': room_total.entry_people,
'boat_user': room_total.boat_guy_entry,
},
'new_follow': room_total.follow_people, // 新增关注
'new_boat': room_total.boat_add, // 新增大航海
'danmu_total': room_total.danmu_total,
'gift': { // 礼物统计
'free_gifts': room_total.free_gift,
'free_gift_silver_total': room_total.free_gift_silver,
'pays_gifts': room_total.pay_gift,
'pays_gift_gold_total': room_total.silver / 100, // 金瓜子
'pays_gift_rmb': String(room_total.silver / 100).slice(0, String(room_total.silver / 100).length - 1),
},
'superchat': { // SuperChat统计
'superchat_total': room_total.super_chat_total,
'superchat_rmb': room_total.super_chat_rmb,
},
'ban_total': room_total.block_guys, // 禁言统计
'send_heart-bags': room_total.hearts // 共发送的心跳包数量
},
}
file_download(total_json_data, '[' + anchor + ']' + room_title + '-' + now_time, "text/json")
}
})
})
attack_player()
}, 800);
function attack_player() {
// 获取一些东西
try {
anchor = document.querySelector('.upper-row .left-ctnr').childNodes[0].text; // fix in 2023/6/19 1:11
anchor_uid = document.querySelector('#iframe-popup-area').firstChild.src.split('uid=')[1]
} catch {
setTimeout(() => {
try {
anchor = document.querySelector('.upper-row .left-ctnr').childNodes[0].text;
anchor_uid = document.querySelector('#iframe-popup-area').firstChild.src.split('uid=')[1]
} catch {
anchor = "can_find"
anchor_uid = "can_find"
}
}, 1200);
}
room_title = document.querySelector('.flex-wrap').firstChild.innerText
// 注入部分
try {
rdm_id = document.querySelector('.live-player-mounter').childNodes[5].className.split('_')[2] // 获取随机的id,分割_得到id
} catch { // 如果无法获取就在一秒后重试
setTimeout(() => {
attack_player()
}, 1000)
return
}
// <li class="_context-sub-menu-item_' + rdm_id + '" id="right_click_menu_rec_live">录制直播</li>
var LIVE__PLAYER_MENU = '<li class="_context-menu-item_' + rdm_id + '"><span class="_context-menu-text_' + rdm_id + '">小功能</span><div class="_context-menu-right-arrow_' + rdm_id + '"></div><ul class="_context-sub-menu_' + rdm_id + '"><li class="_context-sub-menu-item_' + rdm_id + '" id="right_click_menu_getstreamlink">获取m3u8直播流</li><li class="_context-sub-menu-item_' + rdm_id + '" id="right_click_menu_getstreamlink_flv">获取flv直播流</li><li class="_context-sub-menu-item_' + rdm_id + '" id="right_click_menu_getstreamcover">获取直播封面</li></ul></li>'
var LIVE__QC_MENU = '<li class="_context-menu-item_' + rdm_id + '"><span class="_context-menu-text_' + rdm_id + '">直播切片</span><div class="_context-menu-right-arrow_' + rdm_id + '"></div><ul class="_context-sub-menu_' + rdm_id + '"> <li class="_context-sub-menu-item_' + rdm_id + ' _disabled_' + rdm_id + '">仅在某些直播间可用!</li> <li class="_context-sub-menu-item_' + rdm_id + '" id="right_click_menu_300s">300秒(5分钟)回放</li> <li class="_context-sub-menu-item_' + rdm_id + '" id="right_click_menu_180s">180秒(3分钟)回放</li> <li class="_context-sub-menu-item_' + rdm_id + '" id="right_click_menu_60s">60秒回放</li> <li class="_context-sub-menu-item_' + rdm_id + '" id="right_click_menu_30s">30秒回放</li> <li class="_context-sub-menu-item_' + rdm_id + '" id="right_click_menu_15s">15秒回放</li> </ul></li>'
var inject_live_player_menu_here = $('._web-player-context-menu_' + rdm_id + '') // 这里就有必要声明一个变量了
var inject_live_player_here = document.querySelectorAll('.live-player-mounter ._context-menu-item_' + rdm_id + '')[3]
inject_live_player_menu_here[0].insertBefore($(LIVE__PLAYER_MENU)[0], inject_live_player_here); // 向播放器注入"小功能"菜单
inject_live_player_menu_here[0].insertBefore($(LIVE__QC_MENU)[0], inject_live_player_here); // 向播放器注入"直播切片"菜单
// 复制m3u8直播流链接 // todo:修复问题
document.querySelector(ids.RIGHT_MENU__CLICK_GET_STREAM_LINK_ID).addEventListener('click', function () {
// $.get('https://api.live.bilibili.com/room/v1/Room/playUrl?cid=' + room_id + '&platform=h5&ts='+time_stamp_ten(Date.now()), function (data) {
// $.get('https://api.live.bilibili.com/room/v1/Room/playUrl?cid=' + room_id + '&platform=h5&ts='+time_stamp_ten(Date.now()), function (data) {
// console.log(data.data)
// navigator.clipboard.writeText(data.data.durl[0].url)
// send_toast('success', '已复制直播流链接', '', 2000, 'top')
// })
swal("选择流分辨率", "点击按钮选择!", "info", {
buttons: {
HD: {
text: "高清",
value: "HD",
},
// defeat: true,
blue: {
text: "蓝光",
value: "blue",
},
original: {
text: "原画",
value: "original",
},
fourK: {
text: "4K",
value: "fourK",
}
},
}).then((value) => {
switch (value) {
case "HD":
get_stream_link("h5", "150").then(res => {
navigator.clipboard.writeText(res)
send_toast('success', '已复制直播流链接', '', 2000, 'top')
})
break;
case "blue":
get_stream_link("h5", "400").then(res => {
navigator.clipboard.writeText(res)
send_toast('success', '已复制直播流链接', '', 2000, 'top')
})
break;
case "original":
get_stream_link("h5", "10000").then(res => {
navigator.clipboard.writeText(res)
send_toast('success', '已复制直播流链接', '', 2000, 'top')
})
break;
case "fourK":
get_stream_link("h5", "20000").then(res => {
navigator.clipboard.writeText(res)
send_toast('success', '已复制直播流链接', '', 2000, 'top')
})
break;
// default:
// swal("安全离开!");
}
});
document.getElementsByClassName('_web-player-context-menu_' + rdm_id + '')[0].setAttribute('style', 'opacity : 0;')
});
// 复制flv直播流链接
document.querySelector(ids.RIGHT_MENU__CLICK_GET_STREAM_LINK_FLV_ID).addEventListener('click', function () {
swal("选择流分辨率", "点击按钮选择!", "info", {
buttons: {
HD: {
text: "高清",
value: "HD",
},
// defeat: true,
blue: {
text: "蓝光",
value: "blue",
},
original: {
text: "原画",
value: "original",
},
fourK: {
text: "4K",
value: "fourK",
}
},
}).then((value) => {
switch (value) {
case "HD":
get_stream_link("web", "150").then(res => {
navigator.clipboard.writeText(res)
send_toast('success', '已复制直播流链接', '', 2000, 'top')
})
break;
case "blue":
get_stream_link("web", "400").then(res => {
navigator.clipboard.writeText(res)
send_toast('success', '已复制直播流链接', '', 2000, 'top')
})
break;
case "original":
get_stream_link("web", "10000").then(res => {
navigator.clipboard.writeText(res)
send_toast('success', '已复制直播流链接', '', 2000, 'top')
})
break;
case "fourK":
get_stream_link("web", "20000").then(res => {
navigator.clipboard.writeText(res)
send_toast('success', '已复制直播流链接', '', 2000, 'top')
})
break;
// default:
// swal("安全离开!");
}
});
document.getElementsByClassName('_web-player-context-menu_' + rdm_id + '')[0].setAttribute('style', 'opacity : 0;')
});
// 获取直播间封面
document.querySelector(ids.RIGHT_MENU__CLICK_GET_STREAM_COVER_ID).addEventListener('click', function () {
$.get('https://api.live.bilibili.com/room/v1/Room/get_info?id=' + room_id, function (data) {
Swal.fire({
title: '直播间封面',
text: '右键或点击下方按钮即可复制链接!',
imageUrl: data.data.user_cover,
confirmButtonText: '复制',
}).then((result) => {
if (result.isConfirmed) {
navigator.clipboard.writeText(data.data.user_cover)
send_toast('success', '已复制图片链接', '', 2000, 'top')
}
})
})
document.getElementsByClassName('_web-player-context-menu_' + rdm_id + '')[0].setAttribute('style', 'opacity : 0;')
});
try {
// 其实这段直接就能 window.__NEPTUNE_IS_MY_WAIFU__ 获取,但不知道为什么在脚本里不行
room_init_res = JSON.parse(document.getElementsByClassName('script-requirement')[0].firstChild.innerHTML.replace(/window.__NEPTUNE_IS_MY_WAIFU__=/, ''))
try { // 除chrome外的浏览器(例如edge)不支持fmp4,所以在这里会报错。
ls_stream = room_init_res.roomInitRes.data.playurl_info.playurl.stream[1].format[1].codec[0].url_info[0].host + room_init_res.roomInitRes.data.playurl_info.playurl.stream[1].format[1].codec[0].base_url + room_init_res.roomInitRes.data.playurl_info.playurl.stream[1].format[1].codec[0].url_info[0].extra
} catch {
ls_stream = room_init_res.roomInitRes.data.playurl_info.playurl.stream[1].format[0].codec[0].url_info[0].host + room_init_res.roomInitRes.data.playurl_info.playurl.stream[1].format[0].codec[0].base_url + room_init_res.roomInitRes.data.playurl_info.playurl.stream[1].format[0].codec[0].url_info[0].extra
}
uid = room_init_res.userLabInfo.data.uid // 获取一下uid
live_tools_log.normal("当前用户uid:" + uid)
}
catch {
live_tools_log.error("无法获取到房间初始化信息")
uid = 0
$.get('https://api.live.bilibili.com/room/v1/Room/playUrl?cid=' + room_id + '&platform=h5&quality=4', function (data) {
ls_stream = data.data.durl[0].url
})
}
// 录制直播 已完成.
var mediaRecorder
var video_arr = []
var rec_time_for
var rec_time_total = 0
document.querySelector(ids.RIGHT_MENU__CLICK_REC_LIVE).addEventListener('click', function () {
if (is_rec == true & document.querySelector(ids.RIGHT_MENU__CLICK_REC_LIVE).innerText == '停止录制') {
is_rec = false; live_tools_log.warn('正在停止录制'); document.querySelector(ids.RIGHT_MENU__CLICK_REC_LIVE).innerText = '录制直播' // 更改按钮名
mediaRecorder.stop(); var web_m = new Blob(video_arr, { type: "video/webm" }); // 新建Blob对象,类型为webm
mediaRecorder.release()
send_toast('success', '录制完毕', '共录制了' + rec_time_total + '秒,1秒后将自动跳转', 2000, 'top')
document.querySelector('.web-player-icon-rec_now').remove(); document.querySelector('#rec_time_show').remove(); clearInterval(rec_time_for); rec_time_total = 0 // 各种销毁
document.getElementsByClassName('_web-player-context-menu_' + rdm_id + '')[0].setAttribute('style', 'opacity : 0;')
setTimeout(function () {
Swal.fire({
showConfirmButton: false, width: 1280, html: '<div id="video_run_rec"></div>', showCloseButton: true, // 显示关闭框
willClose: () => {
video_player.destroy(true) // 销毁播放器
},
})
var video_player = new Player({
id: 'video_run_rec', url: URL.createObjectURL(web_m), width: 1200, height: 700, autoplay: true, download: true, playbackRate: [0.5, 0.75, 1, 1.5, 2, 5, 10], defaultPlaybackRate: 1 // 注意的是也设置了倍数播放
})
// open(URL.createObjectURL(web_m))
}, 1500);
}
else if (is_rec == false & document.querySelector(ids.RIGHT_MENU__CLICK_REC_LIVE).innerText == '录制直播') {
is_rec = true // 设置一下状态
var video_stream = document.getElementsByTagName('video')[0].captureStream()
mediaRecorder = new MediaRecorder(video_stream, {
mimeType: "video/webm" // 目前看来只支持webm
})
video_arr = [] // 新建数组
new Promise((resolve, reject) => { // 监听将要发生的事件
mediaRecorder.onstop = resolve;
mediaRecorder.onerror = reject;
mediaRecorder.ondataavailable = (event) => {
video_arr.push(event.data); // 将数据存入数组
// console.log(video_arr) // 未来的计划是video_arr.length > 5000的时候分组
}
mediaRecorder.start(100); // 不加1的话大概率不会成功运行
})
setTimeout(function () {
rec_time_for = setInterval(function () { // 每隔1秒钟
rec_time_total++ // 记录一下已录制的时长
document.querySelector('#show_rec_time').innerText = '已经录制了' + rec_time_total + '秒' // 然后在播放器里面修改
}, 1000)
}, 1)
live_tools_log.warn(now_time + '开始录制直播')
document.querySelector(ids.RIGHT_MENU__CLICK_REC_LIVE).innerText = '停止录制' // 更改按钮名
var rec_now = '<div class="web-player-icon-rec_now" style="position: absolute; left: ' + document.querySelector('.live-player-mounter').getBoundingClientRect().width / 2 + 'px; top: 0px; z-index: 2; pointer-events: none; width: 150px; height: 35px; opacity: 100; background: none;"> <span id="player_show_rec_now" style="vertical-align: middle"><font size=3>正在录制</font></span> <svg viewBox="0 0 1024 1024" style="vertical-align: middle;color:#CC3300" xmlns="http://www.w3.org/2000/svg" data-v-78e17ca8="" width=20px height=20px><path fill="currentColor" d="M704 768V256H128v512h576zm64-416 192-96v512l-192-96v128a32 32 0 0 1-32 32H96a32 32 0 0 1-32-32V224a32 32 0 0 1 32-32h640a32 32 0 0 1 32 32v128zm0 71.552v176.896l128 64V359.552l-128 64zM192 320h192v64H192v-64z"></path></svg></div>'
var rec_time_show = '<div id="rec_time_show" ' + document.querySelector('.left-ctnr .dp-i-block').getAttributeNames()[0] + '="" ' + document.querySelector('.left-ctnr .dp-i-block').getAttributeNames()[1] + '="" class="dp-i-block info-section"><div ' + document.querySelector('.left-ctnr .dp-i-block').getAttributeNames()[0] + '="" class="hot-rank-wrap"><div ' + document.querySelector('.left-ctnr .dp-i-block').getAttributeNames()[0] + '="" class="hot-rank-text rank-desc"><span id="show_rec_time" ' + document.querySelector('.left-ctnr .dp-i-block').getAttributeNames()[0] + '="">播放时间占位By isma</span></div></div></div>'
$('.live-player-mounter')[0].insertBefore($(rec_now)[0], $('.web-player-controller-wrap')[0]); $(rec_time_show).insertAfter($('.upper-row .left-ctnr .dp-i-block')[0]) // 1:播放器的显目提示 2.类似高能榜提醒的时间统计
send_toast('info', '正在录制直播', '不要静音播放器,会导致录制的视频没有声音 \n 也不要在录制时刷新,录制数据不会保存', 2500, 'top'); document.getElementsByClassName('_web-player-context-menu_' + rdm_id + '')[0].setAttribute('style', 'opacity : 0;')
}
})
// 这里用 ls_stream 因为该链接已鉴权,使用 get_stream_link 获取的链接,由于没有权限所以无法使用tmshift
// 直播流300秒(5分钟)切片
document.querySelector(ids.RIGHT_MENU__CLICK_300S).addEventListener('click', function () {
navigator.clipboard.writeText(ls_stream + '&tmshift=300')
send_toast('success', '已复制直播流链接', '', 2000, 'top'); document.getElementsByClassName('_web-player-context-menu_' + rdm_id + '')[0].setAttribute('style', 'opacity : 0;')
});
// 直播流180秒(3分钟)切片
document.querySelector(ids.RIGHT_MENU__CLICK_180S).addEventListener('click', function () {
navigator.clipboard.writeText(ls_stream + '&tmshift=180')
send_toast('success', '已复制直播流链接', '', 2000, 'top'); document.getElementsByClassName('_web-player-context-menu_' + rdm_id + '')[0].setAttribute('style', 'opacity : 0;')
});
// 直播流60秒切片
document.querySelector(ids.RIGHT_MENU__CLICK_60S).addEventListener('click', function () {
navigator.clipboard.writeText(ls_stream + '&tmshift=60')
send_toast('success', '已复制直播流链接', '', 2000, 'top'); document.getElementsByClassName('_web-player-context-menu_' + rdm_id + '')[0].setAttribute('style', 'opacity : 0;')
});
// 直播流30秒切片
document.querySelector(ids.RIGHT_MENU__CLICK_30S).addEventListener('click', function () {
navigator.clipboard.writeText(ls_stream + '&tmshift=30')
send_toast('success', '已复制直播流链接', '', 2000, 'top'); document.getElementsByClassName('_web-player-context-menu_' + rdm_id + '')[0].setAttribute('style', 'opacity : 0;')
});
// 直播流15秒切片
document.querySelector(ids.RIGHT_MENU__CLICK_15S).addEventListener('click', function () {
navigator.clipboard.writeText(ls_stream + '&tmshift=15')
send_toast('success', '已复制直播流链接', '', 2000, 'top'); document.getElementsByClassName('_web-player-context-menu_' + rdm_id + '')[0].setAttribute('style', 'opacity : 0;')
});
$('.web-player-icon-roomStatus').remove()
// var player_show_high_guy = '<div class="web-player-icon-high_guy_show" style="position: absolute; left: 10px; top: 10px; z-index: 2; pointer-events: none; width: 200px; height: 43px; opacity: 100; background: none;"> <font size=3><span id="player_show_high-people">B站直播小工具加载成功啦,但这里是占位</span></font> </div>'
// <div class="web-player-round-title" style="z-index: 2; position: absolute; right: 20px; bottom: 20px; pointer-events: none; color: rgb(170, 170, 170); font-size: 14px;">BV1Di4y1N7LV-【直播录屏】3.7嘉然生日会 完整录屏-P1</div>
// 固定到右下角示例
// $('.live-player-mounter')[0].insertBefore($(player_show_high_guy)[0], $('.web-player-controller-wrap')[0])
}
function wss_get() {
// data_v1 = document.querySelector('.right-ctnr').childNodes[2].getAttributeNames()[0] // data-v
// data_v2 = document.querySelector('.right-ctnr').childNodes[2].getAttributeNames()[1] // 也是data-v
$.get('https://api.live.bilibili.com/room/v1/Room/room_init?id=' + room_id, function (rddata) { // 获取真实的房间号,因为有些房间是短号,而ws服务器是根据真实房间号来做广播的
room_real_id = rddata.data.room_id // 有些房间是短号,还有长一点的id
ws_content = new WebSocket('wss://broadcastlv.chat.bilibili.com/sub') //(不必了) 这里还有有个host_list[0]可以用,但相关的事件信息会较少 以及拼接wss链接
ws_content.onopen = function () { // 在ws连接成功后
send_toast('success', '与wss服务器连接成功!', '', 3000, 'top')
live_tools_log.warn(timestamptotime(time_stamp_ten(Date.now())) + '时连接websocket服务器成功') // 注意!必须要在5秒内发送正确的验证包,不然会被服务器断开wss连接
var auth_bag = {
"uid": uid, // 用户uid,非必要可不填
'roomid': room_real_id, // 房间id,必填参数
'protover': 1, // 协议版本,我这里填1,填其他的或许会有错误吧
"platform": "web", // 播放平台
"clientver": "1.4.0", // 连接客户端版本
}
ws_content.send(getCertification(JSON.stringify(auth_bag)).buffer) // 发送请求,已经在getCertification处理好了,但我也看不懂
wss_timer = setInterval(function () { // 定时每30秒发送一次心跳包,不然会被服务端断开连接
var n1 = new ArrayBuffer(16) // 心跳包结构比较简单,直接写
var i = new DataView(n1);
i.setUint32(0, 0), // 封包总大小
i.setUint16(4, 16), // 头部长度
i.setUint16(6, 1), // 协议版本
i.setUint32(8, 2), // 操作码,2为心跳包
i.setUint32(12, 1); // 就1
ws_content.send(i.buffer); //发送
// live_tools_log.warn(timestamptotime(time_stamp_ten(Date.now())) + '发送了一次心跳包')
room_total.hearts++
}, 30000) //30秒
}
ws_content.onmessage = function (event) {
decode(event.data, function (packet) { // 调用函数来解码
//解码成功回调
if (packet.op == 5) {
//会同时有多个数发过来 所以要循环
for (let i = 0; i < packet.body.length; i++) {
var element = packet.body[i];
// console.log(element); // 打印
switch (element.cmd) {
case "DANMU_MSG": // 弹幕事件
room_total.danmu_total++;
if (element.info[2][0] == uid) {
var send_pos = document.querySelector('#chat-control-panel-vm').getBoundingClientRect()
bili_toast('success', send_pos.left, send_pos.top, '你的弹幕发送成功了~', 3000)
//var send_ok_toast = '<div id="bili_toast" class="link-toast success " style="left: '+send_pos.left+'px; top: '+send_pos.top+'px;"><span class="toast-text">弹幕发送成功~</span></div>'
}
break;
case "ROOM_CHANGE": // 直播信息更改
break;
case "SEND_GIFT": // 礼物事件
if (element.data.coin_type != "silver") {
room_total.pay_gift = room_total.pay_gift + element.data.num
room_total.silver = room_total.silver + element.data.price
}
else {
room_total.free_gift = room_total.free_gift + element.data.num
room_total.free_gift_silver = room_total.free_gift_silver + element.data.price
}
break;
case "SUPER_CHAT_MESSAGE": // SuperChat事件
room_total.super_chat_total++;
room_total.super_chat_rmb = room_total.super_chat_rmb + element.data.price
break;
case "ONLINE_RANK_COUNT": // 高能榜
room_total.high_people = element.data.count
// document.querySelector(ids.LIVE_TOALS_SHOW_HIGH).innerText = '高能榜人数:'+room_total.high_people
// document.querySelector('#player_show_high-people').innerText = '高能榜人数:'+room_total.high_people
document.querySelector(".tab-list").firstChild.innerText = '高能榜 共' + room_total.high_people + ' 人'
// live_tools_log.normal(timestamptotime(time_stamp_ten(Date.now())) + ' 直播高能榜更新为'+element.data.count)
break;
case "INTERACT_WORD": // 进场事件为1,关注事件为2
if (element.data.msg_type == 1) {
room_total.entry_people++;
}
if (element.data.msg_type == 2) {
room_total.follow_people++;
}
break;
case "ENTRY_EFFECT": // 进场特效
room_total.boat_guy_entry++;
break;
case "ROOM_BLOCK_MSG": // 禁言事件
room_total.block_guys++;
live_tools_log.error(element.data.uname + '被禁言了')
break;
case "GUARD_BUY":
room_total.boat_add++;
break;
}
}
}
});
}
ws_content.onclose = function (e) {
live_tools_log.error('断开了wss连接,错误代码' + e.code + '断开原因' + e.reason + ' 是否正常断开' + e.wasClean)
send_toast('error', '断开了wss连接,请刷新页面重连', '', 3000, 'top')
}
})
}
// 变动后执行函数
function dm_timeshow(wrapper) {
var insert_here = wrapper.childNodes[1]
if (wrapper.getAttribute('data-danmaku') != undefined) { // 区分普通弹幕和礼物、系统提示
if (wrapper.getAttribute('data-image') != undefined) {
setTimeout(function () {
var dm_send_time = timestamptotime(wrapper.getAttribute('data-ts')) // 获取弹幕发送时间戳
send_time_show = '<span id="time_menu" style="color:#00D1F1;">' + dm_send_time + '</span>' // 时间戳显示
$(send_time_show).insertAfter(wrapper); // 附加上去
}, 1)
return 0
}
if (wrapper.getAttribute('data-ts') == "0") { // 自己发的弹幕是没有时间戳的
send_time_show = '<span id="time_menu" style="color:#00D1F1;"><br>你应该知道自己是在什么时候发的弹幕吧!</span>'
$(send_time_show).insertAfter(insert_here); // 附加上去
}
else {
var dm_send_time = timestamptotime(wrapper.getAttribute('data-ts')) // 获取弹幕发送时间戳
send_time_show = '<span id="time_menu" style="color:#00D1F1;"><br>' + dm_send_time + '</span>' // 时间戳显示
$(send_time_show).insertAfter(insert_here); // 附加上去
}
}
}
function superchat_event(target) {
setTimeout(function () { // 这里必须设置定时,如果不设置定时,设置属性的步骤会比获取target先执行
target.querySelector('.name').setAttribute('id', 'go_sc_tp')
// target.querySelector('.name').innerText = target.querySelector('.name').innerText + ' 点击前往主页'
var sc_price = target.querySelector('.price').innerText.split('电池')[0] // 10000
var sc_price_rmb = sc_price.slice(0, sc_price.length - 1) + '.00' // 1000 -> 1000.0
target.querySelector('.price').innerText = sc_price + '电池 ' + sc_price_rmb + '人民币' // 10000电池 1000.0人民币
}, 100)
}
// 观察变动
const wrapperObserver = new MutationObserver((mutationsList) => { // 监听变动
for (const mutation of mutationsList) {
if (mutation.type === 'childList') { // 子元素变动,也有characterData(节点内容或节点文本),attributes(属性变动),subtree(所有下属节点的变动)
[...mutation.addedNodes].map(item => { // 在新增的节点 返回数组(map),并且带上item
//mmsn_log('非目标变更', item);
if (item.classList?.contains('chat-item')) { // 聊天框
// mmsn_log('目标变更', item);
dm_timeshow(item)
}
if (item.classList?.contains('mode-roll')) { // 弹幕
//mmsn_log('目标变更', item);
}
if (item.classList?.contains('detail-info')) { // 打开sc
superchat_event(item)
}
})
}
// if(mutation.type === 'attributes'){ // 属性变动,因为某些直播间弹幕较多,哔哩哔哩面对较多的弹幕会在原有的100个div基础上修改,而不是继续添加,影响性能
// [mutation.target].map(item => { // 如果要发送一个新弹幕,不会新建一个div而是在原有的div基础上修改弹幕内容来达到想要的效果
// })
// }
}
});
// attributeFilter:['style'],attributeOldValue:true,
wrapperObserver.observe(document.body, { attributes: true, childList: true, subtree: true }); // 设置监听参数
}
};