您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
在直播串流時將聊天室的訊息轉換成彈幕發送
当前为
// ==UserScript== // @name YT 彈幕 // @namespace http://tampermonkey.net/ // @version 0.0.3 // @description 在直播串流時將聊天室的訊息轉換成彈幕發送 // @author JayHuang // @match https://www.youtube.com/* // @icon https://www.youtube.com/s/desktop/b5305900/img/favicon.ico // @grant none // @license MIT // ==/UserScript== const canCtrlConfig = { showUser: false, // 是否顯示使用者 fontFamily: "Arial", // 字型 speed: 2, // 每幀移動 px 量 bufferDistance: 20, // 開始位置增量(px),防止突兀的出現 size: 36, // 字體大小 weight: 800, // 字體粗細 (normal | bold | bolder | lighter | 100 | 200 | 300 | 400 | 500 | 600 | 700 | 800 | 900) alive: 5, // 存活秒數(若 `fixed` 為 `true` 時才會生效) }; const defaultConfig = Object.assign({ showUser: false, // 是否顯示使用者 color: "white", // 字體顏色 fontFamily: "Arial", // 字型 size: 30, // 字體大小 weight: 800, // 字體粗細 at: "top", // 上下半部( "top" | "bottom" ) from: "right", // 從左到右 或 從右到左 ( "right" | "left" ) speed: 2, // 每幀移動 px 量 bufferDistance: 20, // 開始位置增量(px),防止突兀的出現 fixed: false, // 是否固度位置 alive: 5, // 存活秒數(若 `fixed` 為 `true` 時才會生效), }, canCtrlConfig); var SceneInitSatae; (function (SceneInitSatae) { SceneInitSatae[SceneInitSatae["\u670D\u52D9\u5DF2\u7D50\u675F"] = 0] = "\u670D\u52D9\u5DF2\u7D50\u675F"; SceneInitSatae[SceneInitSatae["\u670D\u52D9\u521D\u59CB\u5316"] = 1] = "\u670D\u52D9\u521D\u59CB\u5316"; SceneInitSatae[SceneInitSatae["\u670D\u52D9\u5DF2\u555F\u52D5"] = 2] = "\u670D\u52D9\u5DF2\u555F\u52D5"; })(SceneInitSatae || (SceneInitSatae = {})); function theLog(...message) { console.log("[danmu]::", ...message); } function sleep(millisecond = 400) { return new Promise((resolve) => { setTimeout(resolve, millisecond); }); } class DanMuManager { get ratio() { return window.devicePixelRatio; } get isFreeze() { return document.visibilityState === "hidden"; } get isActive() { return !this.isFreeze; } constructor(elementRef) { this.elementRef = elementRef; theLog("DanMuManager Constructor"); this.canvas = document.createElement("canvas"); const ctx = this.canvas.getContext("2d"); if (ctx === null) { throw new Error("Not Support Canvas Context"); } else { this.ctx = ctx; this.danmuSet = new Set(); this.__injectCanvas(); this.loop = this.loop.bind(this); this.loodID = requestAnimationFrame(this.loop); } } loop() { this.__setCanvas(); const width = this.canvas.width; const height = this.canvas.height; this.ctx.clearRect(0, 0, width, height); const willDelete = []; this.danmuSet.forEach((danmu) => { const obj = danmu.update(); if (obj) { const { size, color, weight, message, x, y } = obj; this.ctx.font = `${weight} ${size}px Arial`; this.ctx.fillStyle = color; this.ctx.fillText(message, x, y); } else { willDelete.push(danmu); } }); willDelete.forEach((danmu) => { this.danmuSet.delete(danmu); }); this.loodID = requestAnimationFrame(this.loop); } addDanmu(message, user, option) { if (this.isActive) { const width = this.canvas.width; const height = this.canvas.height; const danmu = new DanMu(message, user, { width, height }, this, option); this.danmuSet.add(danmu); } } onDestroy() { this.canvas.remove(); this.danmuSet.clear(); cancelAnimationFrame(this.loodID); theLog("DanMuManager onDestroy"); } __injectCanvas() { var _a; (_a = this.elementRef.parentElement) === null || _a === void 0 ? void 0 : _a.append(this.canvas); } __setCanvas() { this.canvas.style.position = "absolute"; this.canvas.style.width = this.elementRef.style.width; this.canvas.style.height = this.elementRef.style.height; this.canvas.style.top = this.elementRef.style.top; this.canvas.style.left = this.elementRef.style.left; this.canvas.width = this.elementRef.clientWidth * this.ratio; this.canvas.height = this.elementRef.clientHeight * this.ratio; } } class DanMu { get width() { return this.elementRef.width; } get height() { return this.elementRef.height; } constructor(message, user, elementRef, manager, config) { this.message = message; this.user = user; this.elementRef = elementRef; this.manager = manager; const { showUser, color, size, weight, fixed, at, from, bufferDistance, speed, alive, fontFamily, } = Object.assign(Object.assign({}, defaultConfig), config); this.config = { showUser, color, size, weight, fontFamily, fixed, at, from, speed: fixed ? 0 : from === "left" ? speed : speed * -1, alive: alive * 1000, bufferDistance, }; this.textWidth = this.__computerWidth(); const ref_x = (this.textWidth + bufferDistance) * 1.5; this.rangeX = [ref_x * -1, this.width + ref_x]; this.locate = this.__getInitLoate(); this.currXaxis = this.locate.x; this.isEnd = false; this.time = Date.now(); } update() { if (this.isEnd) { return null; } else { const { size, weight, color, showUser, speed, alive, fixed } = this.config; const message = showUser ? `${this.user} 說: ${this.message}` : this.message; this.currXaxis += speed; if (fixed) { const timeGap = Date.now() - this.time; if (timeGap >= alive) { this.isEnd = true; } } else if (this.currXaxis < this.rangeX[0] || this.currXaxis > this.rangeX[1]) { this.isEnd = true; } return { size, weight, color, message, x: this.currXaxis, y: this.locate.y, }; } } __computerWidth() { const { weight, size } = this.config; this.manager.ctx.font = `${weight} ${size}px Arial`; const textMetrics = this.manager.ctx.measureText(this.message); return textMetrics.width; } __getInitLoate() { const x = this.__getXaxis(); const y = this.__getYaxis(); return { x, y }; } __getXaxis() { const { fixed, from } = this.config; if (fixed) { return (this.width - this.textWidth) / 2; } else if (from === "left") { return this.rangeX[0]; } else { return this.rangeX[1]; } } __getYaxis() { const { at } = this.config; const padding = 20; const rangeY = [padding, this.height - padding]; if (at === "bottom") { rangeY[0] = this.height / 2; } else if (at === "top") { rangeY[1] = this.height / 2; } return Math.floor(Math.random() * (rangeY[1] - rangeY[0] + 1)) + rangeY[0]; } } class ChatObserver { get ChatListElement() { var _a, _b; return (_b = (_a = this.iframeBody) === null || _a === void 0 ? void 0 : _a.querySelector("#items")) !== null && _b !== void 0 ? _b : null; } constructor(containerEl, manager) { this.manager = manager; theLog("ChatObserver Constructor"); if (containerEl instanceof HTMLIFrameElement) { this.containerEl = containerEl; containerEl.onload = () => { var _a; this.iframeBody = (_a = containerEl.contentDocument) === null || _a === void 0 ? void 0 : _a.body; this.__addObs(); }; } else { throw new Error("ChatObserver Error"); } } __addObs() { if (this.iframeBody && this.iframeBody.children.length > 0) { const targetEl = this.ChatListElement; if (targetEl && this.stateObs === undefined) { theLog("add observer"); const stateObs = new MutationObserver((entire) => { entire.forEach((record) => { record.addedNodes.forEach((node) => { var _a, _b; const element = node; const authorEl = element.querySelector("#author-name"); const isMember = Array.from((_a = authorEl === null || authorEl === void 0 ? void 0 : authorEl.classList) !== null && _a !== void 0 ? _a : []).includes("member"); const userName = authorEl === null || authorEl === void 0 ? void 0 : authorEl.textContent; const message = (_b = element.querySelector("#message")) === null || _b === void 0 ? void 0 : _b.textContent; if (userName && message) { this.manager.addDanmu(message, userName, isMember ? { color: "red", size: 36, fixed: true } : {}); } }); }); }); stateObs.observe(targetEl, { childList: true }); } } } __removeObs() { var _a; (_a = this.stateObs) === null || _a === void 0 ? void 0 : _a.disconnect(); this.stateObs = undefined; theLog("remove observer"); } onDestroy() { this.__removeObs(); theLog("ChatObserver onDestroy"); } } class Scene { constructor() { theLog("Scene Constructor"); this.prevUrl = ""; this.destroy = () => { }; this.state = SceneInitSatae.服務已結束; } changeScene(url) { var _a; if (this.prevUrl !== url) { this.prevUrl = url; const isVideo = Scene.videoRegExp.test(url); if (isVideo) { switch (this.state) { case SceneInitSatae.服務已結束: { theLog("啟動服務"); this.__onInit(); break; } case SceneInitSatae.服務初始化: theLog("啟動中....."); break; case SceneInitSatae.服務已啟動: theLog("關閉服務並重新啟動"); this.__onDesroy().then(() => { this.__onInit(); }); break; } } else { (_a = this.controller) === null || _a === void 0 ? void 0 : _a.abort(); this.__onDesroy(); } } } async __onInit() { try { this.state = SceneInitSatae.服務初始化; this.controller = new AbortController(); const signal = this.controller.signal; const [video, container] = await Promise.all([ this.__getElement(".video-stream.html5-main-video", { signal }), this.__getElement("#chat-container #chatframe", { signal }), ]); theLog({ video, container }); const manager = new DanMuManager(video); const obs = new ChatObserver(container, manager); this.controller = undefined; this.state = SceneInitSatae.服務已啟動; this.destroy = () => { manager.onDestroy(); obs.onDestroy(); }; } catch (e) { theLog("Error occur", e); this.state = SceneInitSatae.服務已結束; this.destroy = () => { }; } } async __onDesroy() { return new Promise((resolve) => { this.destroy(); this.state = SceneInitSatae.服務已結束; resolve(); }); } async __getElement(selectors, option = {}) { var _a, _b; let element = null; const signal = (_a = option.signal) !== null && _a !== void 0 ? _a : { aborted: false }; let limitRetry = (_b = option.limitRetry) !== null && _b !== void 0 ? _b : 400; while (element === null && limitRetry >= 0) { if (signal.aborted) { throw new Error(`Search aborted for selector: ${selectors} and Reason: ${signal.reason}`); } element = document.querySelector(selectors); limitRetry -= 1; await sleep(); } if (element) { return element; } else { throw new Error("Not Found Element: " + selectors); } } } Scene.videoRegExp = new RegExp(/^https:\/\/www.youtube.com\/watch\?v=(\S+)/); (function () { "use strict"; theLog("Loaded Script"); const scene = new Scene(); const observer = new MutationObserver(() => { scene.changeScene(window.location.href); }); const titleElement = document.querySelector("title"); if (titleElement) { observer.observe(titleElement, { childList: true }); } })();