bilibili b站 视频 旋转

bilibili 视频 旋转 插件

// ==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,
  })
})()