ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

油猴脚本实战:为任意动漫网站注入弹幕播放功能

油猴脚本实战:为任意动漫网站注入弹幕播放功能 1. 从零拆解动漫网站弹幕播放脚本到底在解决什么问题1.1 一个真实的使用场景我平时追番的习惯比较杂有些番在A站看有些在B站看还有些老番只有一些小站才有资源。问题就来了小站往往没有弹幕系统或者弹幕池空空荡荡看番的时候总觉得少了点什么。弹幕这个东西看的时候嫌它挡画面一旦没有了又觉得像吃面没放醋——不是不能吃就是差点意思。于是我就琢磨能不能自己写一个油猴脚本把弹幕功能“嫁接”到那些没有弹幕的动漫网站上。这个想法其实不新鲜市面上已经有一些类似的脚本但要么年久失修要么只支持特定站点要么弹幕源单一。我决定自己动手做一个核心目标很明确在任意动漫视频播放页面上注入一个可用的弹幕层弹幕数据从公开的弹幕接口拉取并且支持发送弹幕。这个脚本适合谁用一是像我这样经常在小站追番、又离不开弹幕氛围的人二是想学习油猴脚本开发、想了解弹幕系统实现原理的前端新手。因为整个项目涉及DOM操作、网络请求、Canvas渲染、事件监听等前端核心技能拿来做练手项目非常合适。1.2 为什么选择油猴脚本这个形态油猴脚本的本质是一段注入到目标页面的JavaScript代码它通过浏览器扩展如Tampermonkey、Violentmonkey提供的API在页面加载的不同阶段执行。选择这个形态有几个实实在在的好处无需修改目标网站脚本运行在浏览器端对服务器零侵入不会因为改别人的代码而惹麻烦。跨站通用写一套逻辑通过配置匹配规则可以适配多个动漫站点。开发调试快改完代码刷新页面就能看到效果不需要部署、不需要编译。用户安装门槛低装个扩展点一下安装按钮就完事了。当然油猴脚本也有它的局限。比如它无法解决跨域请求的问题需要依赖目标站点的CORS策略或者使用GM_xmlhttpRequest也无法在页面加载前修改服务器的响应内容。但对于弹幕播放这个需求来说这些限制并不致命。1.3 弹幕播放的核心技术点有哪些要把弹幕跑起来至少需要解决以下几个问题视频元素的定位找到页面中的video标签获取它的位置和尺寸弹幕层要精确覆盖在视频上方。弹幕数据的获取从某个弹幕接口拉取弹幕数据通常返回的是JSON或XML格式需要解析成弹幕对象。弹幕的渲染用Canvas或DOM来绘制弹幕Canvas性能更好DOM更容易做交互。弹幕的调度根据视频当前播放时间决定哪些弹幕应该出现、出现在哪条轨道上、移动速度是多少。弹幕的发送提供一个输入框让用户输入弹幕内容然后提交到弹幕接口。与视频播放状态的同步视频暂停时弹幕也要暂停视频跳转时弹幕要重新调度。这六个点里最核心的是第三和第四点。渲染方式决定了性能上限调度算法决定了弹幕的观感是否自然。1.4 整体架构设计思路我的设计思路是这样的脚本启动后先等待视频元素出现然后在视频上方创建一个绝对定位的Canvas元素尺寸与视频保持一致。接着从配置好的弹幕接口拉取当前视频的弹幕数据存入一个数组。然后启动一个定时器或者用requestAnimationFrame每一帧检查视频的currentTime把时间轴上即将进入视野的弹幕推入渲染队列Canvas每帧清空重绘。弹幕的轨道分配采用“贪心算法”每条弹幕根据其出现时间和移动速度计算出它占据轨道的时间区间然后找到第一条在当前时间点空闲的轨道。如果所有轨道都满了就丢弃这条弹幕或者让它重叠显示。发送弹幕的部分我在视频控制栏旁边注入一个输入框和发送按钮用户输入后通过GM_xmlhttpRequest提交到弹幕接口成功后把弹幕立即加入本地渲染队列实现“秒见”效果。2. 动手之前环境准备与关键工具选型2.1 浏览器扩展的选择油猴脚本需要运行在用户脚本管理器上。目前主流的有Tampermonkey、Violentmonkey和Greasemonkey。我三个都用过最后选了Violentmonkey原因很简单开源、轻量、API兼容性好而且它的GM_xmlhttpRequest实现比较稳定。Tampermonkey功能最全但闭源而且有时候会抽风。Greasemonkey在Firefox上表现不错但Chrome上的支持不如前两者。Violentmonkey在Chrome和Firefox上都能跑安装脚本的方式也很直观。安装好扩展之后在扩展的管理面板里新建一个脚本就可以开始写了。脚本的元数据块Metadata Block是必须的它告诉扩展这个脚本叫什么、在哪些网站上运行、需要哪些权限。// UserScript // name 动漫网站弹幕播放 // namespace http://your-namespace // version 1.0.0 // description 为任意动漫网站添加弹幕播放功能 // author YourName // match *://*.example-anime-site.com/* // grant GM_xmlhttpRequest // grant GM_addStyle // connect danmaku-api.example.com // /UserScript这里有几个关键点需要注意match决定了脚本在哪些页面上运行。你可以用通配符匹配多个站点但不要写得太宽泛否则会在无关页面上执行浪费性能。grant声明需要的API权限。GM_xmlhttpRequest用于跨域请求GM_addStyle用于注入CSS样式。connect声明允许跨域请求的域名。如果不写GM_xmlhttpRequest可能会被拦截。注意不同扩展对connect的支持程度不同。Violentmonkey在未声明connect时也会放行请求但Tampermonkey会严格检查。为了兼容性建议把所有需要请求的域名都列上。2.2 弹幕数据源的选型弹幕数据从哪来这是整个项目最关键的外部依赖。我考虑过几种方案方案优点缺点是否采用自建弹幕服务器完全可控需要服务器和运维成本否使用公开弹幕API零成本、数据丰富接口可能不稳定、有频率限制是本地存储弹幕无网络依赖数据量少、无法共享否爬取其他站点弹幕数据量大法律风险高、技术复杂否最终我选择了使用公开的弹幕API。具体来说我调研了几个提供弹幕服务的平台发现它们的接口格式大同小异通常是通过视频ID或者关键词来查询弹幕。接口返回的JSON结构一般包含弹幕内容、出现时间、颜色、字号等信息。这里要特别说明使用公开API时一定要遵守对方的服务条款控制请求频率不要做恶意爬取。我在脚本里加了缓存机制同一个视频的弹幕只请求一次避免重复请求给服务器造成压力。2.3 渲染方式Canvas还是DOM这是弹幕系统开发中一个经典的选型问题。我两种都实现过说一下实际感受。DOM方式的优点是实现简单每条弹幕就是一个div用CSS动画或者transform来移动。调试也方便打开开发者工具就能看到每条弹幕的元素。但缺点也很明显当弹幕数量多的时候比如同时有几百条DOM节点数量爆炸浏览器渲染压力大容易出现卡顿。Canvas方式的优点是性能好所有弹幕绘制在一个画布上无论多少条弹幕都只有一个DOM节点。缺点是调试麻烦而且要做弹幕的点击交互比如点击弹幕暂停、查看弹幕详情需要自己实现命中检测。考虑到动漫网站的弹幕密度通常比较高我最终选择了Canvas方案。实测下来在同时渲染500条弹幕的情况下Canvas方案的帧率能稳定在60fps而DOM方案会掉到30fps以下。2.4 开发调试环境的搭建油猴脚本的调试和普通前端开发有些不同。你不能用webpack打包也不能用ES模块的import语法除非你用构建工具预处理。脚本就是一个单独的.js文件所有代码都写在一起。我的调试流程是这样的在Violentmonkey中新建脚本粘贴代码。打开目标动漫网站的视频页面。按F12打开开发者工具在Console中查看日志输出。修改代码后在Violentmonkey面板中点击“保存并重新加载”然后刷新页面。为了提高效率我在脚本里加了一个调试开关打开后会在Console中输出详细的日志包括弹幕数量、轨道分配情况、渲染帧率等。这个开关在正式使用时关掉避免性能损耗。const DEBUG false; function log(...args) { if (DEBUG) { console.log([弹幕脚本], ...args); } }3. 核心实现从视频定位到弹幕渲染的完整流程3.1 视频元素的定位与监听动漫网站的视频播放器千奇百怪有的用原生video标签有的用第三方播放器库如DPlayer、Video.js还有的用iframe嵌套。我的策略是优先查找video标签如果找不到再尝试常见的播放器容器选择器。function findVideoElement() { // 直接查找video标签 let video document.querySelector(video); if (video) return video; // 查找常见播放器容器内的video const selectors [ .dplayer video, .video-js video, #player video, .player-container video ]; for (const sel of selectors) { video document.querySelector(sel); if (video) return video; } return null; }找到视频元素后需要监听它的加载状态。因为视频可能是异步加载的脚本执行的时候视频还没出现。我用MutationObserver来监听DOM变化一旦视频元素出现就初始化弹幕层。function waitForVideo(callback) { const video findVideoElement(); if (video) { callback(video); return; } const observer new MutationObserver(() { const v findVideoElement(); if (v) { observer.disconnect(); callback(v); } }); observer.observe(document.body, { childList: true, subtree: true }); }这里有个坑要注意有些网站的视频元素会被动态替换比如切换清晰度时原来的video标签被移除新的video标签被插入。如果只监听一次弹幕层就会失效。我的做法是同时监听video元素的loadedmetadata事件如果视频源变了重新初始化弹幕层。3.2 弹幕层的创建与样式注入弹幕层是一个绝对定位的Canvas元素覆盖在视频上方。关键是要让它的位置和尺寸与视频完全同步。我用getBoundingClientRect()获取视频的位置和尺寸然后设置Canvas的style。function createDanmakuLayer(video) { const canvas document.createElement(canvas); canvas.className danmaku-canvas; canvas.style.position absolute; canvas.style.pointerEvents none; canvas.style.zIndex 9999; // 将canvas插入到视频的父容器中 const parent video.parentElement; parent.style.position relative; parent.appendChild(canvas); // 同步尺寸 function syncSize() { const rect video.getBoundingClientRect(); const parentRect parent.getBoundingClientRect(); canvas.style.left (rect.left - parentRect.left) px; canvas.style.top (rect.top - parentRect.top) px; canvas.style.width rect.width px; canvas.style.height rect.height px; canvas.width rect.width * window.devicePixelRatio; canvas.height rect.height * window.devicePixelRatio; canvas.getContext(2d).scale(window.devicePixelRatio, window.devicePixelRatio); } syncSize(); window.addEventListener(resize, syncSize); return canvas; }pointerEvents: none这个设置很重要它让Canvas不拦截鼠标事件否则用户就没法点击视频控制栏了。但这也意味着弹幕本身无法被点击。如果你需要弹幕交互可以再加一个透明的交互层或者用坐标计算来做命中检测。样式的注入我用GM_addStyle把弹幕相关的CSS统一管理。包括弹幕输入框、发送按钮、弹幕设置面板等UI元素的样式。3.3 弹幕数据的获取与解析弹幕数据的获取是整个脚本中最容易出问题的环节。不同的弹幕API返回格式不同有的返回XML有的返回JSON字段名也五花八门。我写了一个适配层把不同格式的弹幕数据统一转换成内部对象。// 内部弹幕对象格式 // { // text: 弹幕内容, // time: 12.5, // 出现时间秒 // color: #ffffff, // 颜色 // fontSize: 25, // 字号 // mode: 1 // 1滚动 4底部 5顶部 // } function parseDanmakuResponse(response) { const danmakuList []; // 假设返回的是JSON格式 if (typeof response string) { try { response JSON.parse(response); } catch (e) { log(弹幕数据解析失败, e); return danmakuList; } } // 适配不同的字段名 const items response.data || response.danmaku || response.comments || []; for (const item of items) { danmakuList.push({ text: item.text || item.content || item.m, time: parseFloat(item.time || item.t || item.p), color: item.color || #ffffff, fontSize: item.fontSize || item.size || 25, mode: item.mode || item.type || 1 }); } // 按时间排序 danmakuList.sort((a, b) a.time - b.time); return danmakuList; }请求弹幕数据用GM_xmlhttpRequest它不受同源策略限制。但要注意设置合理的超时时间和错误处理。function fetchDanmaku(videoId) { return new Promise((resolve, reject) { GM_xmlhttpRequest({ method: GET, url: https://danmaku-api.example.com/api?vid${videoId}, timeout: 10000, onload: (res) { if (res.status 200) { resolve(parseDanmakuResponse(res.responseText)); } else { reject(new Error(请求失败: res.status)); } }, onerror: reject, ontimeout: () reject(new Error(请求超时)) }); }); }实操心得弹幕接口的稳定性直接决定了脚本的可用性。我在脚本里加了本地缓存用GM_setValue和GM_getValue把弹幕数据存起来同一个视频第二次打开时直接用缓存既加快了加载速度也减少了对接口的压力。缓存有效期设为24小时。3.4 弹幕轨道分配算法轨道分配是弹幕调度的核心。假设弹幕从右向左滚动每条弹幕的速度是固定的比如每秒移动200像素那么一条弹幕从进入屏幕到完全离开屏幕需要的时间是(屏幕宽度 弹幕宽度) / 速度。在这段时间内它占据的轨道不能被其他弹幕使用。我用一个数组来记录每条轨道的“空闲时间”class TrackManager { constructor(trackCount, screenWidth, speed) { this.trackCount trackCount; this.screenWidth screenWidth; this.speed speed; this.trackEndTime new Array(trackCount).fill(0); } // 为一条弹幕分配轨道返回轨道索引如果没有可用轨道返回-1 allocate(danmaku, currentTime) { const textWidth this.measureText(danmaku); const duration (this.screenWidth textWidth) / this.speed; for (let i 0; i this.trackCount; i) { if (this.trackEndTime[i] currentTime) { this.trackEndTime[i] currentTime duration; return i; } } return -1; // 所有轨道都满了 } measureText(danmaku) { // 用离屏Canvas测量文字宽度 const ctx this.measureCtx || (this.measureCtx document.createElement(canvas).getContext(2d)); ctx.font ${danmaku.fontSize}px sans-serif; return ctx.measureText(danmaku.text).width; } }这个算法的优点是简单高效缺点是当弹幕密度很高时后面的弹幕会被丢弃。我加了一个“重叠模式”作为备选如果所有轨道都满了允许弹幕重叠显示但降低透明度避免完全遮挡。轨道的数量根据视频高度和弹幕字号来计算。一般来说每条轨道的高度是字号加上一些间距轨道数 视频高度 / 轨道高度。我通常设置为视频高度的80%用于弹幕显示剩下的20%留空避免弹幕遮挡字幕。3.5 Canvas渲染循环的实现渲染循环用requestAnimationFrame驱动每一帧做三件事清空画布、计算当前时间点应该显示的弹幕、绘制弹幕。class DanmakuRenderer { constructor(canvas, video, danmakuList) { this.canvas canvas; this.ctx canvas.getContext(2d); this.video video; this.danmakuList danmakuList; this.activeDanmaku []; // 当前正在显示的弹幕 this.trackManager new TrackManager(20, canvas.width, 200); this.lastTime 0; this.paused false; } start() { this.video.addEventListener(pause, () { this.paused true; }); this.video.addEventListener(play, () { this.paused false; }); this.loop(); } loop() { requestAnimationFrame(() this.loop()); if (this.paused) return; const currentTime this.video.currentTime; const deltaTime currentTime - this.lastTime; this.lastTime currentTime; // 如果视频跳转清空当前弹幕 if (Math.abs(deltaTime) 1) { this.activeDanmaku []; this.trackManager.reset(); } // 从弹幕列表中取出当前时间点应该出现的弹幕 while (this.danmakuList.length 0 this.danmakuList[0].time currentTime) { const danmaku this.danmakuList.shift(); const track this.trackManager.allocate(danmaku, currentTime); if (track 0) { danmaku.track track; danmaku.startTime currentTime; danmaku.x this.canvas.width; this.activeDanmaku.push(danmaku); } } // 更新和绘制 this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height); for (let i this.activeDanmaku.length - 1; i 0; i--) { const d this.activeDanmaku[i]; const elapsed currentTime - d.startTime; d.x this.canvas.width - elapsed * 200; if (d.x this.measureText(d) 0) { this.activeDanmaku.splice(i, 1); continue; } this.drawDanmaku(d); } } drawDanmaku(d) { const y d.track * 30 20; this.ctx.font ${d.fontSize}px sans-serif; this.ctx.fillStyle d.color; this.ctx.strokeStyle rgba(0,0,0,0.8); this.ctx.lineWidth 2; this.ctx.strokeText(d.text, d.x, y); this.ctx.fillText(d.text, d.x, y); } }这里有几个细节值得展开说。第一deltaTime的计算用于检测视频跳转。如果两次帧之间的时间差超过1秒说明用户拖动了进度条这时候需要清空当前弹幕并重置轨道管理器。第二弹幕的绘制顺序是先描边再填充这样弹幕在浅色背景上也能看清。第三activeDanmaku数组是倒序遍历的因为删除元素时不会影响前面的索引。3.6 弹幕发送功能的实现发送弹幕的UI我放在视频控制栏的下方用一个简单的表单实现。用户输入内容后按回车或者点击发送按钮脚本把弹幕提交到接口同时立即在本地显示。function createSendBox(video, renderer) { const box document.createElement(div); box.className danmaku-send-box; box.innerHTML input typetext placeholder发个弹幕吧... maxlength50 button发送/button ; const input box.querySelector(input); const button box.querySelector(button); async function send() { const text input.value.trim(); if (!text) return; const danmaku { text: text, time: video.currentTime, color: #ffffff, fontSize: 25, mode: 1 }; // 立即在本地显示 renderer.addLocalDanmaku(danmaku); input.value ; // 提交到服务器 try { await postDanmaku(danmaku); } catch (e) { log(弹幕发送失败, e); } } button.addEventListener(click, send); input.addEventListener(keydown, (e) { if (e.key Enter) send(); }); return box; }发送弹幕的接口调用和获取弹幕类似只是用POST方法。这里要注意的是很多弹幕接口需要用户登录或者有频率限制。我在脚本里加了简单的频率控制同一用户10秒内只能发送一条弹幕。4. 踩坑实录常见问题与排查技巧4.1 弹幕层位置偏移怎么办这是最常见的问题。表现是弹幕层没有完全覆盖视频或者随着页面滚动弹幕层和视频错位了。原因通常有两个一是视频的父容器没有设置position: relative导致绝对定位的Canvas相对于更外层的元素定位二是页面滚动时没有重新计算位置。解决方法是在创建弹幕层时确保视频的父容器有position: relative并且监听scroll事件重新同步位置。不过频繁的scroll事件会影响性能我用requestAnimationFrame做了节流。let ticking false; window.addEventListener(scroll, () { if (!ticking) { requestAnimationFrame(() { syncSize(); ticking false; }); ticking true; } });4.2 弹幕不显示或者显示不全如果弹幕完全不显示先检查Console有没有报错。常见的原因包括弹幕接口请求失败、Canvas尺寸为0、弹幕数据解析出错。如果弹幕显示不全比如只显示了一部分就消失了通常是轨道分配的问题。检查trackManager的轨道数量是否足够以及trackEndTime的计算是否正确。还有一个容易被忽略的点Canvas的尺寸设置。如果只设置了CSS的width和height没有设置canvas元素的width和height属性Canvas的实际分辨率会是默认的300x150导致绘制的内容被拉伸或裁剪。正确的做法是同时设置CSS尺寸和canvas属性尺寸并考虑devicePixelRatio。4.3 视频切换后弹幕失效很多动漫网站支持多集切换切换后视频元素会被替换但弹幕层还挂在旧的视频上。解决方法是监听视频元素的loadedmetadata事件每次视频源变化时重新初始化弹幕层。function onVideoChange(video) { // 移除旧的弹幕层 const oldCanvas document.querySelector(.danmaku-canvas); if (oldCanvas) oldCanvas.remove(); // 重新初始化 initDanmaku(video); } video.addEventListener(loadedmetadata, () onVideoChange(video));4.4 弹幕接口请求被拦截用GM_xmlhttpRequest请求弹幕接口时如果返回403或者被重定向通常是因为缺少必要的请求头。有些接口需要Referer或者User-Agent可以在请求参数里手动加上。GM_xmlhttpRequest({ method: GET, url: apiUrl, headers: { Referer: location.href, User-Agent: navigator.userAgent }, // ... });另外如果接口返回的是HTTPS但你的页面是HTTP或者反过来也可能被浏览器拦截。确保请求的协议和页面一致。4.5 性能问题排查弹幕脚本的性能瓶颈通常出现在两个地方一是弹幕数量太多导致渲染卡顿二是频繁的DOM操作导致页面响应变慢。对于渲染卡顿可以限制同时显示的弹幕数量比如最多200条超出的丢弃。另外用requestAnimationFrame代替setInterval让渲染和浏览器的刷新率同步。对于DOM操作尽量把弹幕相关的UI元素一次性创建好避免在循环中反复创建和插入元素。样式的修改也尽量批量进行减少重排和重绘。下面是我整理的一份常见问题速查表问题现象可能原因排查方法解决方案弹幕层不显示Canvas未插入DOM检查DOM中是否有.danmaku-canvas确认插入逻辑检查父容器弹幕位置偏移父容器未设relative检查父容器computed style设置position: relative弹幕不滚动渲染循环未启动检查requestAnimationFrame是否执行确认video已加载弹幕重叠严重轨道数不足检查trackCount和视频高度增加轨道数或降低字号发送弹幕失败接口需要登录查看Network面板的响应添加请求头或提示用户视频切换后失效未监听loadedmetadata检查事件监听重新初始化弹幕层避坑技巧在开发阶段我建议把弹幕速度调慢一点比如从200px/s降到100px/s这样更容易观察弹幕的调度和渲染是否正确。等调试完成后再调回正常速度。另外可以在Canvas上绘制轨道分隔线调试模式下直观地看到每条弹幕占用的轨道。4.6 兼容性问题的处理不同动漫网站的视频播放器实现差异很大有的用Shadow DOM有的用iframe有的把视频放在多层嵌套的容器里。我的经验是不要试图用一个选择器适配所有网站而是为每个网站写一个适配器。const siteAdapters { site-a.com: { findVideo: () document.querySelector(#player video), getVideoId: () new URLSearchParams(location.search).get(vid) }, site-b.com: { findVideo: () document.querySelector(.dplayer video), getVideoId: () location.pathname.split(/).pop() } }; function getAdapter() { const host location.hostname; for (const [domain, adapter] of Object.entries(siteAdapters)) { if (host.includes(domain)) return adapter; } return null; }这种适配器模式的好处是新增一个网站只需要加一条配置不需要改动核心逻辑。而且每个网站的适配逻辑独立互不影响。5. 进阶优化让弹幕体验更接近原生5.1 弹幕防遮挡与透明度调节弹幕遮挡视频内容是一个老生常谈的问题。我的做法是提供几个选项让用户自己调节弹幕透明度0.3到1.0、弹幕显示区域全屏、上半屏、下半屏、弹幕字号缩放。这些设置用GM_setValue持久化下次打开自动生效。UI上用一个简单的设置面板点击齿轮图标展开。function applySettings(renderer) { const opacity GM_getValue(danmakuOpacity, 0.9); const area GM_getValue(danmakuArea, full); const scale GM_getValue(danmakuScale, 1.0); renderer.ctx.globalAlpha opacity; renderer.area area; renderer.scale scale; }5.2 弹幕屏蔽与关键词过滤有些弹幕内容重复或者引战影响观看体验。我加了一个简单的关键词过滤功能用户可以在设置里添加屏蔽词包含这些词的弹幕会被自动过滤掉。function shouldFilter(danmaku, blockedWords) { for (const word of blockedWords) { if (danmaku.text.includes(word)) return true; } return false; }这个功能实现简单但效果很好。我自己的屏蔽词列表里有“前排”、“火钳刘明”、“第一”这些常见的无意义弹幕。5.3 弹幕数据缓存与离线观看前面提到过用GM_setValue缓存弹幕数据。这里再展开说一下缓存的策略以视频ID为key存储弹幕数组和缓存时间。每次请求前先检查缓存如果缓存未过期24小时内直接使用缓存数据。function getCachedDanmaku(videoId) { const cacheKey danmaku_ videoId; const cached GM_getValue(cacheKey); if (!cached) return null; const { data, timestamp } cached; const now Date.now(); if (now - timestamp 24 * 60 * 60 * 1000) { GM_deleteValue(cacheKey); return null; } return data; }这个缓存机制不仅加快了加载速度还让脚本在弹幕接口暂时不可用的时候也能正常工作用旧缓存。5.4 移动端适配的注意事项虽然油猴脚本主要在桌面浏览器上使用但有些用户也会在移动端浏览器如Kiwi Browser、Yandex Browser上安装扩展。移动端的适配要注意几点触摸事件代替鼠标事件输入框的弹出可能会遮挡视频。屏幕尺寸小弹幕字号要适当缩小轨道数也要减少。性能相对较弱需要降低弹幕密度和渲染帧率。我在脚本里加了一个简单的设备检测如果是移动端自动调整参数。const isMobile /Android|iPhone|iPad|iPod/i.test(navigator.userAgent); if (isMobile) { defaultFontSize 18; maxTracks 10; maxDanmaku 100; }5.5 脚本更新与版本管理油猴脚本的更新机制是通过version和updateURL实现的。当你在脚本托管平台上发布了新版本扩展会定期检查更新。我建议在脚本里加一个版本检查的逻辑如果发现新版本在Console里提示用户。// UserScript // version 1.0.0 // updateURL https://example.com/danmaku-script.meta.js // downloadURL https://example.com/danmaku-script.user.js // /UserScript版本号遵循语义化版本规范主版本号.次版本号.修订号。修复bug时递增修订号新增功能时递增次版本号不兼容的改动递增主版本号。5.6 安全性与合规性考量最后说一下安全性和合规性。油猴脚本运行在用户的浏览器上理论上可以读取和修改页面上的任何内容。所以脚本的代码必须透明不能包含恶意逻辑。我建议把脚本开源让用户可以看到完整的代码。另外弹幕内容的发送和获取要遵守目标平台的服务条款。不要用脚本做批量发送、刷屏等行为。我在脚本里加了发送频率限制和内容长度限制既是为了合规也是为了避免被接口封禁。提示如果你打算把脚本发布到公开的脚本托管平台一定要仔细阅读平台的审核规则。有些平台不允许脚本包含跨域请求有些平台对弹幕类脚本有额外的要求。发布前先确认清楚避免脚本被下架。这个脚本我从最初的一个简单原型到现在稳定使用了大半年中间迭代了十几个版本。最大的体会是弹幕系统的核心不在于技术有多复杂而在于细节的打磨。轨道分配的时机、弹幕消失的边界条件、视频跳转时的状态重置这些看似不起眼的地方恰恰决定了最终体验的好坏。如果你也在做类似的项目我的建议是先跑通最小可用版本然后在实际使用中不断发现问题、优化细节。
返回列表