Greasy Fork is available in English.

倍速播放

HTML5播放器,倍速|最高10倍|计时器掌控者|视频跳过广告|视频广告加速

// ==UserScript==
// @name         倍速播放
// @namespace    https://gitee.com/yellownacl
// @version      1.0
// @description  HTML5播放器,倍速|最高10倍|计时器掌控者|视频跳过广告|视频广告加速
// @author       黄盐
// @include      *:*
// @grant        none
// @run-at       document-end
// @require      https://greasyfork.org/scripts/420615-cangshi-everything-hook/code/Cangshi-Everything-Hook.js?version=893832
// @require      https://greasyfork.org/scripts/420618-cangshi-timerhooker/code/Cangshi-TimerHooker.js?version=893838
// ==/UserScript==
/* jshint esversion: 6 */
const defaltLocalStorage = {speed: 1, position: {left: 50, top:100}, speedArray: [1, 2, 3, 15]}; // localStorage 默认值
// 因为外部资源脚本不允许 @grant GM_getValue 和 GM_setValue,暂时只能用本地存储替代扩展存储数据。
function GM_getValue(key, defaultValue){
  let res = localStorage.getItem('tampermonkeySpeedy')
  if(res){
    res = JSON.parse(res)
  }else{
    res = Object.assign({}, JSON.parse(JSON.stringify(defaltLocalStorage)))
  }
  if(res[key]){
    return res[key]
  }else{
    return defaultValue
  }
}
function GM_setValue(key,value){
  let res = localStorage.getItem('tampermonkeySpeedy')
  if(res){
    res = JSON.parse(res)
  }else{
    res = Object.assign({}, JSON.parse(JSON.stringify(defaltLocalStorage)))
  }
  res[key] = value
  localStorage.setItem('tampermonkeySpeedy', JSON.stringify(res))
}

function isNumber(obj){
  // 这个方法网络上找来的,对于整数,浮点数返回true,对于NaN或可转成NaN的值返回false。
  return obj === +obj
}

function querySelectorAll(parentNode, selector){
  let elements = parentNode.querySelectorAll(selector)
  elements = Array.prototype.slice.call(elements || [])
  return elements
}

function changeSpeed(rate, isInit=false){
  if(!isNumber(rate)){
    rate = parseFloat(rate)
    if(!isNumber(rate)) {
      log('不能转为速度')
      return false
    }
  }
  timer.change(1/rate)
  if(!isInit){GM_setValue('speed', rate)}
  return rate
}

function log(message,msgType="normal"){
  let style = {
    hint: `background:#ff0; border-left: 5px solid #333; padding:1px 3px; font-weight:bold;`,
    normal: `background:lightgreen;padding:1px 5px 1px 2px; border-right: 3px solid darkblue;`,
    warning: `background:#FFFBE5;padding:1px 5px 1px 2px; border-right: 3px solid darkblue;`,
    error: `background:red;padding:1px 5px 1px 2px; border-right: 3px solid darkblue;`
  };
  console.log("%c SpeedyPlay Info: %c"+message, style.hint, style[msgType]);
}

function getSideSpeed(){
  return GM_getValue('speed', 1.0)
}
function getSitePosition(){
  return GM_getValue('position', {left: 50, top: 100}) ;
}
function saveSpeed(speed){
  if(!isNumber(speed)){
    log(`速度值设置错误,应该提供数值类型,${speed} 是 ${typeof speed} 类型!`, 'error');
    return false;
  }
  GM_setValue('speed', speed);
  return true;
}
function savePosition(left, top){
  try {
    if(typeof left != 'number' || typeof top != 'number'){
      log(`位置值设置错误,应该提供数值类型,现在 left:${typeof left},top: ${typeof top}`, 'error');
      return false;
    }
    GM_setValue('position', {left: left,top: top});
    log(left+'--'+top);
    return true;
  } catch (error) {}
  log("savePosition", left, top)
}
function saveSpeedArray(speedArray){
  GM_setValue('speedArray', speedArray);
}
function initSpeedy(){
  const speedData = {
    currentSpeed: GM_getValue('speed'),
    speedArray: GM_getValue('speedArray'), // 常用速度列表,比如2x,3x, 常规观看 10x,15x用来跳过广告。最多6个
    editArray: false, // 速度状态是否处于可编辑装填,如果是,点击就没有反应,如果不是,点击就改变速度
    lastTimeSpeed: 1, // 上次的速度,方便在2种速度之间切换,区分站点存储,
    position: GM_getValue('position'), // 控件的位置,区分站点存储
  }
  const speedyHandler = {
    set(target, prop, value){
      switch(prop){
        case "currentSpeed" :
          target["lastTimeSpeed"] = target["currentSpeed"]
          target[prop] = value
          currentSpeedElem.value = value
          speedText.innerHTML = value
          currentSpeedElem.setAttribute("style", `background-size:${value*10}% 100%`)
          changeSpeed(value)
          break;
        case "editArray":
          target[prop] = value
          if(speedState.editArray){
            speedArrayElems.forEach(element=>{
              element.setAttribute("contenteditable", "true")
            })
            editArrayButton.innerHTML = "保存"
          } else {
            let tmp = []
            speedArrayElems.forEach(element=>{
              element.removeAttribute("contenteditable")
              tmp.push(parseFloat(element.dataset.value))
            })
            editArrayButton.innerHTML = "编辑"
            speedState.speedArray = tmp
            saveSpeedArray(speedState.speedArray)
          }
          break;
        default:
          target[prop] = value
          // log(prop +' → '+ value.toString())
          break;
      }
    }
  }
  // 创建元素
  const node = document.createElement("DIV")
  let nodeHTML = `
    <div id="speedCtrl" style="left:${speedData.position.left}px;top:${speedData.position.top}px;">
      <div>
      <button id="speedText">${speedData.currentSpeed}</button>
      <input id="currentSpeed" type="range" min="0.1" max="10" step="0.1" value="${speedData.currentSpeed}"
        style="background-size:${speedData.currentSpeed*10}% 100%"
      >
      </div>
      <div>
        <span class="usualSpeed" data-value="${speedData.speedArray[0]}" data-index="0">${speedData.speedArray[0]}</span>
        <span class="usualSpeed" data-value="${speedData.speedArray[1]}" data-index="1">${speedData.speedArray[1]}</span>
        <span class="usualSpeed" data-value="${speedData.speedArray[2]}" data-index="2">${speedData.speedArray[2]}</span>
        <span class="usualSpeed" data-value="${speedData.speedArray[3]}" data-index="3">${speedData.speedArray[3]}</span>
        <button id="editArray" class="usualfn">编辑</button>
        <!--
        <button id="expandCtrl" class="usualfn">展开</button>
        -->
      </div>
    </div>
    <style>
    #speedCtrl{
      opacity: 0.1;
      width: 50px;
      height: 50px;
      overflow: hidden;
      display: block;
      position: fixed;
      top: 100px;
      left: 50px;
      display: grid;
      grid-template-rows: 50px 50px;
      z-index: 99999999;
      background: #ffff0010;
      border-radius: 5px;
    }
    #speedCtrl:hover{
      opacity: 1;
      background: #ffff0080;
      width: auto;
      height: auto;
    }
    #speedCtrl div{
      display: flex;
      align-items: center;
      justify-content: space-evenly;
    }
    #speedText{
      cursor: move;
      display: inline-flex;
      justify-content: center;
      align-items: center;
      width: 50px;
      height: 50px;
      border-radius: 25px;
      border: 2px solid #FDC02F;
      font-size: 30px;
      text-align: center;
      font-weight: bold;
      background: yellow;
    }
    .usualfn, .usualSpeed{
      display: inline-flex;
      justify-content: center;
      align-items: center;
      width: 50px;
      height: 30px;
      background: yellow;
      border: 2px solid #FDC02F;
      border-radius: 5px;
      font-family: "微软雅黑", consolas;
    }
    .usualfn{
      cursor: pointer;
    }
    /*横条样式*/
    #speedCtrl input[type='range'] {
      -webkit-appearance: none;
      /*清除系统默认样式*/
      border: 1px solid #FDC02F;
      border-radius: 10px;
      width: 300px;
      background: -webkit-linear-gradient(#ff0, #ff0) no-repeat, #999;
      /*设置左边颜色为#ff0,右边颜色为#999*/
      /* background-size: 75% 100%;   设置左右宽度比例 */
      height: 20px;
      /*横条的高度*/
    }

    /*拖动块的样式*/
    #speedCtrl input[type=range]::-webkit-slider-thumb {
      -webkit-appearance: none;
      /*清除系统默认样式*/
      height: 40px;
      /*拖动块高度*/
      width: 40px;
      /*拖动块宽度*/
      background: #fff;
      /*拖动块背景*/
      border-radius: 50%;
      /*外观设置为圆形*/
      border: solid 2px cyan;
      /*设置边框*/
      cursor: pointer;
    }
    </style>`;
  node.innerHTML = nodeHTML;
  document.body.appendChild(node)

  function move(e){
    let moveTarget = document.getElementById("speedCtrl");        //获取目标元素
    //算出鼠标相对元素的位置
    let disX = e.clientX - moveTarget.offsetLeft;
    let disY = e.clientY - moveTarget.offsetTop;
    let left, top;
    document.onmousemove = (e)=>{       //鼠标按下并移动的事件
      //用鼠标的位置减去鼠标相对元素的位置,得到元素的位置
      left = e.clientX - disX;    
      top = e.clientY - disY;
      
      speedState.top = top;
      speedState.left = left;
      
      //移动当前元素
      moveTarget.style.left = left + 'px';
      moveTarget.style.top = top + 'px';
    };
    document.onmouseup = (e) => {
      savePosition(left, top)
      document.onmousemove = null;
      document.onmouseup = null;
    };
  }

  const speedState = new Proxy(speedData, speedyHandler)
  const speedText = document.getElementById("speedText")
  const currentSpeedElem = document.getElementById("currentSpeed")
  const speedArrayElems = querySelectorAll(document, ".usualSpeed")
  const editArrayButton = document.getElementById("editArray")

  speedText.addEventListener('dblclick', e=>{
    speedState.currentSpeed = speedState.lastTimeSpeed
  })
  speedText.addEventListener("mousedown", move)
  currentSpeedElem.addEventListener('input', e=>{
    speedState.currentSpeed = e.target.value
  })
  speedArrayElems.forEach(element => {
    element.addEventListener('click', e=>{
      if(!speedState.editArray){
        speedState.currentSpeed = e.target.dataset.value
      }
    })
    element.addEventListener('blur', e=>{
      let rate = parseFloat(e.target.textContent)
      if(isNumber(rate)){
        e.target.dataset.value = rate.toFixed(1)
        e.target.textContent = rate.toFixed(1)
      }else{
        let input = prompt("输入不正确,请输入数字")
        if(isNumber(input)){
          e.target.dataset.value = rate.toFixed(1)
          e.target.textContent = rate.toFixed(1)
        }else{
          e.target.textContent = e.target.dataset.value
        }
      }
    })
  });
  editArrayButton.addEventListener("click", e=>{
    speedState.editArray = !speedState.editArray 
  })

  changeSpeed(GM_getValue('speed'), true)


}
function isReady(){
  let startStamp = new Date().getTime()
  window.checkVideoTimer = setInterval(()=>{
    let videos = querySelectorAll(document, "video");
    let nowStamp = new Date().getTime()
    if(videos.length){
      clearInterval(checkVideoTimer);
      initSpeedy()
    }else if((nowStamp - startStamp) > 30000){
      clearInterval(checkVideoTimer);
    }else{
      log('waiting...');
    }
  },1000);
}

isReady();