// ==UserScript==
// @author yukinotech
// @namespace yukinotech
// @github https://github.com/yukinotech/bili-rotate
// @name bilibili b站 视频 旋转
// @name:en bilibili player rotate
// @version 1.0.7
// @description bilibili 视频 旋转 插件
// @description:en bilibili b站 player rotate plugin
// @include http*://*.bilibili.com/video/*
// @license MIT
// ==/UserScript==
;(async function () {
console.log("rotate init start xxxxx")
// ****** utils 函数 ******
let waitToGet = (fn, time) => {
return new Promise((resolve, reject) => {
let c = () => {
setTimeout(() => {
let leftAgs = [...arguments].slice(2)
let rtn = fn(...leftAgs)
if (rtn) {
resolve(rtn)
} else {
c()
}
}, time)
}
c()
})
}
let getNumFromPx = (pxStr) => {
return Number(pxStr.replace("px", ""))
}
let numToPx = (num) => {
return String(num.toFixed(2)) + "px"
}
// ****** 全局初始化 ******
let playerStyleTag = await waitToGet(() => {
return document.getElementById("setSizeStyle")
}, 600)
// ****** video 旋转处理部分 ******
// 使用全局变量,因为会出现页面内刷新的情况,变量需要实时指向最新的dom标签,便于赋值刷新
// 页面层级结构: 1、变量名:dom名 2、外层->里层
// video:div -> realVideo:video || realVideo:bwp-video
// video:包裹player的中层div
let video
// realVideo:实际video标签,或者bwp-video标签
let realVideo
// deg:旋转角度
let deg
// realVideo_H_W_Ratio:视频原始高比宽
let realVideo_H_W_Ratio
// video逻辑初始化部分
let videoInit = async () => {
video = await waitToGet(() => {
return (
document.getElementsByClassName("bilibili-player-video")?.[0] ||
document.getElementsByClassName("bpx-player-video-wrap")?.[0]
)
}, 600)
realVideo = video.childNodes[0]
video.style.height = "100%"
video.style.width = "100%"
video.style.display = "flex"
video.style["justify-content"] = "center"
realVideo.style.margin = "0"
realVideo.style.padding = "0"
realVideo.style["object-fit"] = "contain"
realVideo.style.height = "auto"
realVideo.style.width = "auto"
realVideo.style.transform = "none"
let { height: videoContainerHeight, width: videoContainerWidth } =
window.getComputedStyle(video)
let { height: realVideoHeight, width: realVideoWidth } =
window.getComputedStyle(realVideo)
realVideo_H_W_Ratio =
getNumFromPx(realVideoHeight) / getNumFromPx(realVideoWidth)
if (realVideo_H_W_Ratio >= 1) {
// 原始视频是竖屏
realVideo.style["max-height"] = "none"
} else {
// 原始视频是横屏
realVideo.style["width"] = "100%"
}
console.log("realVideoHeight", realVideoHeight)
console.log("realVideoWidth", realVideoWidth)
console.log("videoContainerHeight", videoContainerHeight)
console.log("videoContainerWidth", videoContainerWidth)
// deg 标记旋转角度
deg = 0
}
// 旋转时回调函数
let rotate = () => {
deg = (deg + 90) % 360
resetHW()
}
// 重置宽高
let resetHW = () => {
let { height: videoContainerHeight, width: videoContainerWidth } =
window.getComputedStyle(video)
let videoContainerHeightNum = getNumFromPx(videoContainerHeight)
let videoContainerWidthNum = getNumFromPx(videoContainerWidth)
// deg 为当前角度状态
if (deg === 90 || deg === 270) {
// console.log("realVideo_H_W_Ratio", realVideo_H_W_Ratio)
if (realVideo_H_W_Ratio < 1) {
// 原始视频是横屏
realVideo.style.transform = `rotate(${deg}deg)`
realVideo.style.width = videoContainerHeight
} else {
// 原始视频是竖屏
realVideo.style.height = videoContainerWidth
realVideo.style.width = numToPx(
videoContainerWidthNum / realVideo_H_W_Ratio
)
let offsetY = numToPx(
(videoContainerWidthNum - videoContainerHeightNum) / -2
)
realVideo.style.transform = `translate(0,${offsetY}) rotate(${deg}deg)`
}
} else {
if (realVideo_H_W_Ratio < 1) {
// 原始视频是横屏
realVideo.style.width = videoContainerWidth
realVideo.style.transform = `rotate(${deg}deg)`
} else {
// 原始视频是竖屏
realVideo.style.height = "auto"
realVideo.style.width = "auto"
realVideo.style.transform = `rotate(${deg}deg)`
}
}
}
// 按钮初始化部分
let buttonInit = async () => {
// 找到播放底栏父元素
let controlRight = await waitToGet(() => {
return (
document.getElementsByClassName(
"bilibili-player-video-control-bottom-right"
)?.[0] ||
document.getElementsByClassName("bpx-player-control-bottom-right")?.[0]
)
}, 600)
// 调试用代码 begin :强制底栏常驻
// let controlBottom = await waitToGet(() => {
// return document.getElementsByClassName(
// "bilibili-player-video-control-bottom"
// )?.[0]
// }, 300)
// controlBottom.style.opacity = "1"
// controlBottom.style.visibility = "visible"
// 调试用代码 end :强制底栏常驻
// 构造button div,绑定事件,并插入文档
let buttonSvg = `<svg viewBox="0 0 1536 1536" aria-labelledby="rwsi-awesome-repeat-title" id="si-awesome-repeat" width="100%" height="100%"><title id="rwsi-awesome-repeat-title">icon repeat</title><path d="M1536 128v448q0 26-19 45t-45 19h-448q-42 0-59-40-17-39 14-69l138-138Q969 256 768 256q-104 0-198.5 40.5T406 406 296.5 569.5 256 768t40.5 198.5T406 1130t163.5 109.5T768 1280q119 0 225-52t179-147q7-10 23-12 14 0 25 9l137 138q9 8 9.5 20.5t-7.5 22.5q-109 132-264 204.5T768 1536q-156 0-298-61t-245-164-164-245T0 768t61-298 164-245T470 61 768 0q147 0 284.5 55.5T1297 212l130-129q29-31 70-14 39 17 39 59z"></path></svg>`
let buttonDiv = document.createElement("div")
buttonDiv.style.width = "17px"
buttonDiv.style.height = "17px"
buttonDiv.style.fill = "#fff"
buttonDiv.style.margin = "3px 6px"
buttonDiv.style.cursor = "pointer"
buttonDiv.innerHTML = buttonSvg
buttonDiv.id = "rotate-button"
if (!document.getElementById("rotate-button")) {
controlRight.insertBefore(buttonDiv, controlRight.childNodes[6])
}
buttonDiv.addEventListener("click", rotate)
console.log("rotate init end")
setTimeout(() => {
if (!document.getElementById("rotate-button")) {
// 存在b站在脚本的初始化之后执行,覆盖脚本,加一次兜底
controlRight.insertBefore(buttonDiv, controlRight.childNodes[6])
}
}, 5000)
}
// ****** 第一次实际执行部分 ******
await videoInit()
await buttonInit()
// ****** 监听部分 ******
// 播放器父元素的大小发生变化时,处理宽高 回调
let observer = new MutationObserver((mutationList) => {
// console.log("播放器的大小change", mutationList)
// 这里b站会自动重置css,包括video的宽高,可以加延迟解决
if (mutationList.length !== 2) {
// === 2时,为播放列表切换视频,应该走视频初始化流程
console.log("handle size change")
setTimeout(() => {
resetHW()
}, 100)
}
// 调试用代码 begin
// let { height: videoContainerHeight, width: videoContainerWidth } =
// window.getComputedStyle(video)
// let { height: realVideoHeight, width: realVideoWidth } =
// window.getComputedStyle(realVideo)
// console.log("realVideoHeight", realVideoHeight)
// console.log("realVideoWidth", realVideoWidth)
// console.log("videoContainerHeight", videoContainerHeight)
// console.log("videoContainerWidth", videoContainerWidth)
// 调试用代码 end
})
// 监听播放器父元素的大小发生变化
observer.observe(playerStyleTag, {
childList: true,
attributes: true,
subtree: true,
})
// 播放列表里切换视频时,b站会初始化播放器。此处为初始化播放器的回调
let videoSwitchObserver = new MutationObserver((mutationList) => {
// console.log("视频切换change", mutationList?.["1"]?.type)
// 页面往下滑,会触发画中画功能,造成初始化误判,增加条件判断
if (mutationList?.["1"]?.type === "childList") {
// 增加一个同步重置, 避免闪烁
realVideo.style.transform = "none"
;(async () => {
console.log("**** handle init ****")
await videoInit()
await buttonInit()
})()
} else if (mutationList?.["1"]?.type === "attributes") {
// 触发画中画功能,回来后需要重置宽高
console.log("触发画中画功能,回来后需要重置宽高")
setTimeout(() => {
resetHW()
}, 100)
}
})
// 监听播放器是否初始化
videoSwitchObserver.observe(document.getElementById("bilibili-player"), {
childList: true,
attributes: true,
})
})()