Greasy Fork is available in English.
B站截图、获取封面、逐帧前进/后退;移除宽屏功能,兼容新版播放器控制栏
// ==UserScript==
// @name bilibili plus fixed
// @version 0.5.0
// @description B站截图、获取封面、逐帧前进/后退;移除宽屏功能,兼容新版播放器控制栏
// @namespace local.bilibili.plus.fixed
// @author 化猫之宿 / fixed by Codex
// @license MIT
// @homepageURL https://greasyfork.org/zh-CN/scripts/373172-bilibili-plus
// @encoding utf-8
// @match *://www.bilibili.com/video/*
// @match *://www.bilibili.com/bangumi/play/*
// @match *://www.bilibili.com/blackboard/*
// @compatible chrome 54+
// @compatible firefox 49+
// @grant none
// ==/UserScript==
(function () {
"use strict";
const FPS = 29.97;
const ID = "bili-plus-fixed-controls";
const STYLE_ID = "bili-plus-fixed-style";
const SELECTORS = {
controlRoots: [
".bpx-player-dm-root",
".bpx-player-control-bottom-center .bpx-player-dm-root",
".bpx-player-control-bottom-left",
".bilibili-player-video-control-bottom-center",
".squirtle-controller-wrap"
],
playerArea: [
".bpx-player-container",
"#bilibili-player",
".bilibili-player",
".squirtle-video-wrap"
]
};
const icons = {
prev: '<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M11 6.5 5.5 12 11 17.5V14h8v-4h-8V6.5Z"/></svg>',
next: '<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M13 6.5V10H5v4h8v3.5l5.5-5.5L13 6.5Z"/></svg>',
cover: '<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M5 5h14a2 2 0 0 1 2 2v10a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V7a2 2 0 0 1 2-2Zm0 11.5 4.2-4.2 2.8 2.8 3.8-4.8L19 14.2V7H5v9.5Zm4.2-6.7a1.6 1.6 0 1 0 0-3.2 1.6 1.6 0 0 0 0 3.2Z"/></svg>',
shot: '<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M8.3 5 7 7H4a2 2 0 0 0-2 2v8a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2V9a2 2 0 0 0-2-2h-3l-1.3-2H8.3ZM12 17a4 4 0 1 1 0-8 4 4 0 0 1 0 8Zm0-2a2 2 0 1 0 0-4 2 2 0 0 0 0 4Z"/></svg>'
};
function first(selectors, root = document) {
for (const selector of selectors) {
const el = root.querySelector(selector);
if (el) return el;
}
return null;
}
function ensureStyle() {
if (document.getElementById(STYLE_ID)) return;
const style = document.createElement("style");
style.id = STYLE_ID;
style.textContent = `
#${ID} {
display: inline-flex;
align-items: center;
gap: 4px;
height: 100%;
margin-right: 8px;
color: #61666d;
fill: currentColor;
flex: 0 0 auto;
}
#${ID} .bpf-btn {
width: 28px;
height: 28px;
display: inline-flex;
align-items: center;
justify-content: center;
padding: 0;
border: 0;
outline: 0;
appearance: none;
-webkit-appearance: none;
background: transparent;
color: inherit;
fill: currentColor;
cursor: pointer;
position: relative;
opacity: .9;
line-height: 1;
font: inherit;
transition: color .15s ease, opacity .15s ease;
}
#${ID} .bpf-btn:hover {
color: #00aeec;
opacity: 1;
}
#${ID} svg {
width: 22px;
height: 22px;
display: block;
pointer-events: none;
}
#${ID} .bpf-btn::after {
content: attr(data-tip);
position: absolute;
left: 50%;
bottom: 34px;
transform: translateX(-50%);
white-space: nowrap;
background: rgba(0, 0, 0, .82);
color: #fff;
font-size: 12px;
line-height: 1;
padding: 7px 9px;
border-radius: 4px;
pointer-events: none;
opacity: 0;
visibility: hidden;
transition: opacity .12s ease;
}
#${ID} .bpf-btn:hover::after {
opacity: 1;
visibility: visible;
}
.bpx-player-container[data-screen=full] #${ID},
.bpx-player-container[data-screen=web] #${ID},
.bpx-player-container[data-screen=wide] #${ID} {
color: hsla(0, 0%, 100%, .86);
}
@media screen and (max-width: 860px) {
#${ID} {
gap: 3px;
margin-right: 3px;
}
#${ID} .bpf-btn {
width: 24px;
}
#${ID} svg {
width: 19px;
height: 19px;
}
}
`;
document.head.appendChild(style);
}
function getVideo() {
return document.querySelector("video");
}
function pauseVideo(video) {
if (!video) return;
video.pause();
}
function openUrl(url, width = 960, height = 600) {
const win = window.open(url, "_blank", `width=${width},height=${height}`);
if (!win) alert("弹窗被浏览器拦截了,请允许此页面弹窗。");
}
function getCoverUrl() {
const metaSelectors = [
'meta[itemprop="image"]',
'meta[itemprop="thumbnailUrl"]',
'meta[property="og:image"]',
'meta[name="thumbnail"]'
];
for (const selector of metaSelectors) {
const content = document.querySelector(selector)?.getAttribute("content");
if (content) return content.replace(/^\/\//, `${location.protocol}//`);
}
const scripts = Array.from(document.scripts, script => script.textContent || "");
const scriptText = scripts.find(text => text.includes('"pic"') || text.includes('"cover"'));
const match = scriptText && scriptText.match(/"(?:pic|cover)"\s*:\s*"([^"]+)"/);
return match ? match[1].replace(/\\\//g, "/").replace(/^\/\//, `${location.protocol}//`) : "";
}
function openCover() {
const url = getCoverUrl();
if (!url) {
alert("未找到封面。");
return;
}
openUrl(url, 960, 600);
}
function screenshot() {
const video = getVideo();
if (!video || !video.videoWidth || !video.videoHeight) {
alert("未找到可截图的视频画面。");
return;
}
const canvas = document.createElement("canvas");
canvas.width = video.videoWidth;
canvas.height = video.videoHeight;
try {
canvas.getContext("2d").drawImage(video, 0, 0, canvas.width, canvas.height);
pauseVideo(video);
const url = canvas.toDataURL("image/png");
openImageDocument(url, canvas.width, canvas.height);
} catch (error) {
console.error("[bilibili plus fixed] screenshot failed:", error);
alert("截图失败:当前视频画面可能被浏览器跨域限制。");
}
}
function openImageDocument(dataUrl, width, height) {
const win = window.open("", "_blank", `width=${Math.min(width, 1280)},height=${Math.min(height, 800)}`);
if (!win) {
alert("弹窗被浏览器拦截了,请允许此页面弹窗。");
return;
}
win.document.title = "Bilibili Screenshot";
win.document.body.innerHTML = "";
const style = win.document.createElement("style");
style.textContent = "html,body{margin:0;width:100%;height:100%;background:#222;display:flex;align-items:center;justify-content:center}img{max-width:100%;max-height:100%;object-fit:contain}";
const img = win.document.createElement("img");
img.src = dataUrl;
img.alt = "screenshot";
win.document.head.appendChild(style);
win.document.body.appendChild(img);
win.focus();
}
function seekFrame(direction) {
const video = getVideo();
if (!video) return;
pauseVideo(video);
const offset = direction === "next" ? 1 / FPS : -1 / FPS;
video.currentTime = Math.max(0, video.currentTime + offset);
}
function makeButton(className, title, icon, handler) {
const btn = document.createElement("button");
btn.type = "button";
btn.className = `bpf-btn ${className}`;
btn.dataset.tip = title;
btn.title = title;
btn.innerHTML = icon;
btn.addEventListener("click", event => {
event.preventDefault();
event.stopPropagation();
handler();
});
return btn;
}
function makeControls() {
const controls = document.createElement("div");
controls.id = ID;
controls.append(
makeButton("bpf-cover", "封面", icons.cover, openCover),
makeButton("bpf-shot", "截图", icons.shot, screenshot),
makeButton("bpf-prev", "逐帧后退 <", icons.prev, () => seekFrame("prev")),
makeButton("bpf-next", "逐帧前进 >", icons.next, () => seekFrame("next"))
);
return controls;
}
function mountControls() {
ensureStyle();
const root = first(SELECTORS.controlRoots);
if (!root) return false;
const old = document.getElementById(ID);
if (old && old.parentElement === root) return true;
if (old) old.remove();
root.prepend(makeControls());
return true;
}
function bindHotkeys() {
if (window.__biliPlusFixedHotkeys) return;
window.__biliPlusFixedHotkeys = true;
document.addEventListener("keydown", event => {
const tag = event.target?.tagName?.toLowerCase();
if (tag === "input" || tag === "textarea" || event.target?.isContentEditable) return;
if (!getVideo()) return;
if (event.code === "Comma") {
event.preventDefault();
seekFrame("prev");
}
if (event.code === "Period") {
event.preventDefault();
seekFrame("next");
}
}, true);
}
function boot() {
bindHotkeys();
mountControls();
const observerTarget = first(SELECTORS.playerArea) || document.body;
const observer = new MutationObserver(() => {
window.clearTimeout(boot.timer);
boot.timer = window.setTimeout(mountControls, 200);
});
observer.observe(observerTarget, { childList: true, subtree: true });
let tries = 0;
const timer = window.setInterval(() => {
if (mountControls() || ++tries > 40) window.clearInterval(timer);
}, 500);
}
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", boot, { once: true });
} else {
boot();
}
})();