十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

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

Bilibili-Evolved 视频截图功能源码解析:从控制栏按钮到带弹幕 PNG 的完整实现

Bilibili-Evolved 视频截图功能源码解析:从控制栏按钮到带弹幕 PNG 的完整实现 Bilibili-Evolved 视频截图功能源码解析从控制栏按钮到带弹幕 PNG 的完整实现【免费下载链接】Bilibili-Evolved强大的哔哩哔哩增强脚本项目地址: https://gitcode.com/gh_mirrors/bi/Bilibili-Evolved导读本文以 Bilibili-Evolved 增强脚本的「启用视频截图」组件位于 registry/lib/components/video/player/screenshot为核心深入解析其文档、入口、截图核心逻辑、缩略图交互界面与快捷键插件的完整实现链路。读完本文你将掌握该功能的启用方式、控制栏按钮注册机制、Canvas 截图与弹幕叠加的底层原理以及复制、保存、批量打包等交互细节可直接对照源码继续深入。功能概述文档定义的组件行为组件官方文档index.md对功能做了精炼定义启用视频快速截图将在播放器的时间右边增加一个截图按钮。装有快捷键扩展时支持键盘快捷键CtrlAltC。从这行描述可以提炼出三个核心事实触发入口播放器控制栏的时间显示右侧会出现一个「截图」按钮扩展能力截图支持键盘快捷键但前提是安装了「快捷键扩展」组件即 registry/lib/components/utils/keymap使用范围该组件仅在播放器页面生效对应源码中的urlInclude: playerUrlsindex.ts即 B 站播放页相关 URL 才会注入。说明文档中「快捷键扩展」组件本身未在本文覆盖范围内但其与截图组件的交互方式keymap.actions/keymap.presets数据注入会在下文详细展开。入口实现控制栏按钮的注册与生命周期组件入口定义在 index.ts核心逻辑分为三部分控制栏按钮注册、页面退出保护、插件数据注入。通过 addControlBarButton 注册按钮组件通过addControlBarButton向播放器控制栏注册按钮addControlBarButton({ name: takeScreenshot, displayName: 截图, icon: mdi-camera, order: 0, action: async (e: MouseEvent) { const { playerAgent } await import(/components/video/player-agent) const video await playerAgent.query.video.element() if (video instanceof HTMLVideoElement) { const screenshot takeScreenshot(video, e.shiftKey) if (!screenShotsList) { screenShotsList mountVueComponent(ScreenshotContainer) document.body.insertAdjacentElement(beforeend, screenShotsList.$el) } screenShotsList.screenshots.unshift(screenshot) } else { const { logError } await import(/core/utils/log) logError(视频截图失败: 无法定位视频元素, 请尝试右击视频两次后另存为图片, 或将播放策略改为 AV1 或 AVC.) } }, })按钮字段含义如下字段值说明nametakeScreenshot按钮唯一标识渲染为data-name属性供快捷键插件选择器定位displayName截图按钮显示名称iconmdi-cameraMDI 图标相机由/ui的VIcon渲染order0排序权重控制按钮在扩展控制栏中的位置action异步函数点击回调接收MouseEvent按住Shift点击可截取带弹幕的画面e.shiftKeyaddControlBarButton本身定义在 src/components/video/video-control-bar.ts它采用懒初始化策略首次调用时才通过initControlBar()内部使用lodash.once保证只执行一次在videoChange事件触发后把 VideoControlBar.vue 挂载到播放器时间控件的后面time.insertAdjacentElement(afterend, instance.$el)扩展栏的根选择器为.be-video-control-bar-extend每个按钮的data-name绑定item.nameVideoControlBar.vue。截图失败的用户提示当无法定位到video元素时例如 DPlayer 视频元素尚未就绪组件会通过/core/utils/log的logError输出一条可操作的提示建议尝试「右击视频两次后另存为图片」或将播放策略改为 AV1 或 AVC。这说明该功能对视频元素的 DOM 结构有依赖失败时也给出了降级方案。生命周期与页面退出保护组件注册了完整的reload/unload生命周期index.tsunload时向document.body添加video-screenshot-disable类此时截图缩略图容器会通过 CSS 被隐藏display: none见 VideoScreenshotContainer.vuereload时移除该类并重新挂载beforeunload处理器页面退出保护exitConfirmHandler会在beforeunload时检查screenShotsList.screenshots.length 0若存在尚未保存/丢弃的截图则调用e.preventDefault()触发浏览器离开确认弹窗防止误关页面丢失截图。截图核心Screenshot 类与 Canvas 绘制原理核心逻辑位于 screenshot.ts入口函数takeScreenshot读取视频当前时间并构造Screenshot实例export const takeScreenshot (video: HTMLVideoElement, withDanmaku false) { const time video.currentTime return new Screenshot(video, time, withDanmaku) }Screenshot类关键成员成员类型说明mimeTypeimage/png输出格式固定为 PNGurlstringURL.createObjectURL(blob)生成的临时对象 URL用于展示与下载blobBlobPNG 二进制数据用于复制到剪贴板与打包下载videoTimenumber截图时视频的currentTime秒timeStampnumber截图时刻的时间戳参与文件名与id生成withDanmakuboolean是否包含弹幕层画布尺寸的计算区分横竖屏黑边createUrl()是核心绘制方法其画布尺寸计算考虑了播放器画面与视频原始画面比例不一致的两种情况if (this.withDanmaku) { const videoWrapper dq(playerAgent.query.video.wrap.selector) as HTMLElement const rect videoWrapper.getBoundingClientRect() const playerRatio rect.width / rect.height const videoRatio this.video.videoWidth / this.video.videoHeight if (playerRatio videoRatio) { // 竖屏视频(两侧黑边) canvas.height this.video.videoHeight canvas.width this.video.videoHeight * playerRatio } else { // 横屏视频(上下黑边) canvas.width this.video.videoWidth canvas.height this.video.videoWidth / playerRatio } } else { canvas.width this.video.videoWidth canvas.height this.video.videoHeight }不带弹幕画布直接取视频原始分辨率videoWidth × videoHeight保证截图无损带弹幕画布尺寸扩展为播放器显示区域的分辨率比例——竖屏视频在播放器中两侧有黑边playerRatio videoRatio时画布宽度按videoHeight * playerRatio扩展横屏视频上下有黑边时画布高度按videoWidth / playerRatio扩展。这样弹幕层的坐标才能与视频画面完整对齐。视频帧与弹幕层的叠加绘制绘制分两步进行绘制视频帧context.drawImage(this.video, videoLeft, videoTop)其中偏移量(canvas.width - videoWidth) / 2、(canvas.height - videoHeight) / 2将视频帧在扩展画布中居中放置黑边区域留空叠加弹幕层从播放器 DOM 中查找弹幕画布选择器canvas.bilibili-player-video-danmaku或canvas.dm-canvas调用context.drawImage(danmakuCanvas, 0, 0, canvas.width, canvas.height)将弹幕层完整铺满画布。随后通过canvas.toBlob(blob { ... }, image/png)异步生成 PNG Blob并用URL.createObjectURL生成可展示、可下载的临时 URL。异常场景的错误处理createUrl()针对三种异常给出了明确的日志提示异常提示文案getContext(2d)返回null视频截图失败: canvas 未创建或创建失败.toBlob回调收到null视频截图失败: 创建 blob 失败.toBlob抛出异常捕获视频截图失败: 操作被浏览器阻止. 这通常发生于电影的试看片段, 请在正片尝试使用截图功能.其中「操作被浏览器阻止」通常对应 DRM 保护内容如电影试看片段此时 Canvas 读取会被浏览器安全策略拦截toBlob会抛错。这是从源码可以确认的实现事实也是用户遇到截图失败时最需要了解的场景。文件名与时间格式Screenshot提供三个 getterget filename() { return ${getFriendlyTitle()} ${this.time.replace(/:/g, -)} ${this.timeStamp.toString()}.png }文件名{视频友好标题} {HH-MM-SS} {时间戳}.png冒号替换为连字符以兼容文件系统时间显示timegetter 将videoTime格式化为MM:SS.xx或HH:MM:SS.xx小时为 0 时省略小时位如02:13.50秒保留两位小数唯一 idvideoTime timeStamp字符串作为 Vue 列表:key使用资源释放revoke()调用URL.revokeObjectURL(this.url)在丢弃截图时释放对象 URL避免内存泄漏。缩略图交互保存、复制、丢弃与批量操作截图列表由两个 Vue 组件构成容器 VideoScreenshotContainer.vue 与单项 VideoScreenshot.vue。单项缩略图VideoScreenshot.vue每张截图渲染为240px × 135px的黑色背景缩略图加载完成前objectUrl为空显示旋转加载动画spinner加载完成后 hover 显示半透明遮罩遮罩内提供三个圆形按钮与时间标签按钮图标mdi行为保存mdi-content-save-outline触发隐藏的a download链接点击浏览器下载该 PNG 后自动丢弃该项复制mdi-content-copy/mdi-check通过navigator.clipboard.writeClipboardItem将 PNG Blob 写入剪贴板成功后图标切换为对勾并 1 秒后复原丢弃mdi-delete-forever-outline向容器发出discard事件删除该项并revoke()释放 URL复制功能的实现依赖浏览器剪贴板 APIawait navigator.clipboard.write([ new ClipboardItem({ [screenshot.mimeType]: screenshot.blob }), ])这要求页面处于安全上下文HTTPS/localhost且用户授予剪贴板权限是使用「复制」功能时需要知晓的前提条件。批量操作VideoScreenshotContainer.vue容器将截图列表渲染在屏幕右上角position: fixedz-index: 20000列表最大高度限制为calc(100% - 3 * 12px - 37px)带transition-group进出场滑动动画。当截图数量 2时底部浮现批量操作栏全部保存使用DownloadPackage来自 src/core/download.ts将每张截图以filename blob 时间戳日期打包输出为${getFriendlyTitle()}.zip下载完成后清空列表全部丢弃遍历revoke()后清空screenshots数组。快捷键扩展CtrlAltC 的实现机制组件通过plugin字段向快捷键系统注入数据index.ts这是「装有快捷键扩展时支持 CtrlAltC」这一文档描述的代码实现plugin: { displayName: 视频截图 - 快捷键支持, setup: ({ addData }) { addData(keymap.actions, (actions: Recordstring, KeyBindingAction) { actions.takeScreenshot { displayName: 视频截图, run: context { const { clickElement } context return clickElement( .be-video-control-bar-extend [data-nametakeScreenshot], context, ) }, } }) addData(keymap.presets, (presetBase: Recordstring, string) { presetBase.takeScreenshot ctrl [shift] alt c }) }, },其工作机制可以总结为两点动作注册keymap.actions注册名为takeScreenshot的动作动作执行时通过clickElement模拟点击控制栏扩展区中data-nametakeScreenshot的按钮——即复用鼠标点击的完整流程包括 Shift 修饰键的透传快捷键预设中的[shift]表示按住 Shift 触发时等同按住 Shift 点击从而截取带弹幕画面默认按键keymap.presets将默认组合键设为ctrl [shift] alt c即CtrlAltC可加Shift截带弹幕图用户可在「快捷键扩展」的设置面板中自定义。从源码结构看该 plugin 是可选的增强即便没有安装快捷键扩展控制栏按钮截图功能依然完整可用只有启用快捷键扩展时CtrlAltC的按键映射才会生效。这与文档中「装有快捷键扩展时支持」的表述完全一致。完整工作链路总结综合上述源码一次「启用视频截图」的完整链路如下用户点击控制栏「截图」按钮或按 CtrlAltC → index.ts 的 action 回调 → playerAgent 查询 video 元素 → takeScreenshot(video, shiftKey) 读取 currentTime 并构造 Screenshot → createUrl(): Canvas 按是否带弹幕计算尺寸 → drawImage 绘制视频帧居中→ 带弹幕时叠加弹幕 Canvas → canvas.toBlob 生成 PNG → createObjectURL 生成临时 URL → 挂载/复用 ScreenshotContainer 并 unshift 插入列表 → 缩略图展示 → 保存 / 复制 / 丢弃 / 全部保存(打包 zip) / 全部丢弃 → 未处理完前刷新页面时 beforeunload 拦截提醒涉及的核心文件一览组件入口与插件registry/lib/components/video/player/screenshot/index.ts截图核心逻辑registry/lib/components/video/player/screenshot/screenshot.ts缩略图单项registry/lib/components/video/player/screenshot/VideoScreenshot.vue缩略图容器registry/lib/components/video/player/screenshot/VideoScreenshotContainer.vue控制栏按钮注入机制src/components/video/video-control-bar.ts、src/components/video/VideoControlBar.vue快捷键扩展插件数据registry/lib/components/utils/keymap组件元数据定义registry/lib/components/define.ts结语「启用视频截图」组件是一个麻雀虽小、五脏俱全的典型范例它借助addControlBarButton与keymap插件数据注入两套扩展机制把「截图」这个简单需求做成了支持带弹幕截图、剪贴板复制、批量 zip 下载、页面退出保护、快捷键触发与失败降级提示的完整功能。对照 index.ts 与 screenshot.ts 阅读可以清晰看到 Bilibili-Evolved 中一个视频类组件从注册到用户交互的完整实现范式。【免费下载链接】Bilibili-Evolved强大的哔哩哔哩增强脚本项目地址: https://gitcode.com/gh_mirrors/bi/Bilibili-Evolved创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表