Masked Watch

ビリビリのアレをリアルタイムでやってみる

目前為 2019-08-23 提交的版本,檢視 最新版本

// ==UserScript==
// @name        Masked Watch
// @namespace   https://github.com/segabito/
// @description ビリビリのアレをリアルタイムでやってみる
// @match       *://www.nicovideo.jp/*
// @match       *://live.nicovideo.jp/*
// @match       *://anime.nicovideo.jp/*
// @exclude     *://ads*.nicovideo.jp/*
// @exclude     *://www.upload.nicovideo.jp/*
// @exclude     *://www.nicovideo.jp/watch/*?edit=*
// @exclude     *://ch.nicovideo.jp/tool/*
// @exclude     *://flapi.nicovideo.jp/*
// @exclude     *://dic.nicovideo.jp/p/*
// @exclude     *://ext.nicovideo.jp/thumb/*
// @exclude     *://ext.nicovideo.jp/thumb_channel/*
// @version     0.0.1
// @grant       none
// @author      名無しさん
// @license     public domain
// ==/UserScript==
/* eslint-disable */


// chrome://flags/#enable-experimental-web-platform-features
(() => {
  const PRODUCT = 'MaskedWatch';

  const monkey = (PRODUCT) => {
    'use strict';
    const INTERVAL = 30;

    var VER = '0.0.1';
    //@version
    const ENV = 'STABLE';

    //@environment
    const createWorker = (func, options = {}) => {
      const src = `(${func.toString()})(self);`;
      const blob = new Blob([src], {type: 'text/javascript'});
      const url = URL.createObjectURL(blob);
      return new Worker(url, options);
    };

    const 業務 = function(self) {
      const FASTMODE = false; // true 精度重視  false 速度重視

      let canvas, ctx;
      const init = params => {
        ({canvas} = params);
        const {width, height} = params;
        ctx = canvas.getContext('2d');
        ctx.fillStyle = 'rgba(255, 255, 255, 0.5)';
        canvas.width = width || 320;
        canvas.height = height || 180;
      };

      const detector = new (self || window).FaceDetector({fastMode: FASTMODE});
      const detect = async ({bitmap}) => {
        // console.time('detect');
        const faces = await detector.detect(bitmap);
        // console.timeLog('detect', 'detector.detect');
        ctx.beginPath();
        ctx.fillRect(0, 0, canvas.width, canvas.height);
        for (const face of faces) {
          const {x, y , width, height} = face.boundingBox;
          ctx.clearRect(x / 2 - 10, y / 2  -16 , width / 2 + 20, height / 2 + 32);
        }
        // console.timeLog('detect', 'draw');
        const dataURL = await toDataURL(canvas);
        // console.timeEnd('detect');
        return dataURL;
      };

      const reader = new FileReader();
      const toDataURL = async (canvas, type = 'image/png') => {
        const blob = await canvas.convertToBlob({type});
        return new Promise((ok, ng) => {
          reader.onload = () => { ok(reader.result); };
          reader.onerror = ng;
          reader.readAsDataURL(blob);
        });
      };

      self.onmessage = async e => {
        const {command, params} = e.data.body;
        switch (command) {
          case 'init':
            init(params);
            self.postMessage({body: {command: 'init', params: {}, status: 'ok'}});
            break;
          case 'detect': {
            const dataURL = await detect(params);
            self.postMessage({body: {command: 'data', params: {dataURL}, status: 'ok'}});
          }
            break;
        }
      };
    };

    const createFaceDetector = ({video, layer, interval}) => {
      const worker = createWorker(業務, {name: 'Facelook'});
      const width = 640, height = 360;
      const transferCanvas = new OffscreenCanvas(width, height);
      const ctx = transferCanvas.getContext('2d', {alpha: false});

      const workCanvas = document.createElement('canvas');
      workCanvas.width = width;
      workCanvas.height = height;
      // for debug
      // Object.assign(workCanvas.style, {
      //   border: '1px solid #888',
      //   left: 0,
      //   bottom: 0,
      //   position:'fixed',
      //   zIndex:'100000',
      //   width:`${width / 2}px`,
      //   height:`${height / 2}px`,
      //   opacity: 0.8,
      //   filter: 'drop-shadow(2px 2px 2px black)',
      //   pointerEvents: 'none',
      //   userSelect: 'none'
      // });
      // document.body.append(workCanvas);
      const offscreenCanvas = workCanvas.transferControlToOffscreen();
      worker.postMessage({body: {command: 'init', params: {canvas: offscreenCanvas, width: width / 2, height: height / 2}}}, [offscreenCanvas]);
      let currentTime = video.currentTime;

      let isBusy = true;
      worker.addEventListener('message', e => {
        const {command, params} = e.data.body;
        switch (command) {
          case 'init':
            console.log('initialized');
            isBusy = false;
            break;
          case 'data': {
            const url = `url('${params.dataURL}')`;
            layer.style.maskImage = url;
            layer.style.webkitMaskImage = url;
            isBusy = false;
          }
           break;
        }
      });
      interval = interval || INTERVAL;
      const onTimer = () => {
        if (currentTime === video.currentTime || isBusy) {
          return;
        }

        currentTime = video.currentTime;
        const vw = video.videoWidth, vh = video.videoHeight;
        const ratio = Math.min(width / vw, height / vh);
        const dw = vw * ratio, dh = vh * ratio;

        ctx.drawImage(video, (width - dw) / 2, (height - dh) / 2, dw, dh);
        const bitmap = transferCanvas.transferToImageBitmap();
        isBusy = true;
        worker.postMessage({body: {command: 'detect', params: {bitmap}}}, [bitmap]);
      };
      let timer = setInterval(onTimer, interval);

      return {
        start: () => timer = setInterval(onTimer, interval),
        stop: () => timer = clearInterval(timer),
      };
    };

    const vmap = new WeakMap();
    const watch = () => {
      [...document.querySelectorAll('video, zenza-video')]
        .filter(video => !video.paused && !vmap.has(video))
        .forEach(video => {
          // 対応プレイヤー増やすならココ
          let layer, type = 'UNKNOWN';
          if (video.closest('#MainVideoPlayer')) {
            layer = document.querySelector('.CommentRenderer');
            type = 'NICO VIDEO';
          } else if (video.closest('.zenzaPlayerContainer')) {
            layer = document.querySelector('.commentLayerFrame');
            type = 'ZenzaWatch';
          } else if (video.closest('[class*="__leo"]')) {
            layer = document.querySelector('#comment-layer-container canvas');
            type = 'NICO LIVE';
          }
          console.log('%ctype: "%s"', 'font-weight: bold', type);
          Object.assign(layer.style, {
            backgroundSize:     'contain',
            maskSize:           'contain',
            webkitMaskSize:     'contain',
            maskRepeat:         'no-repeat',
            webkitMaskRepeat:   'no-repeat',
            maskPosition:       'center center',
            webkitMaskPosition: 'center center'
          });
          vmap.set(video,
            layer ?
            createFaceDetector({video: video.drawableElement || video, layer, interval: INTERVAL}) :
            type
          );
        });
    };
    setInterval(watch, 1000);

    console.log('%cMasked Watch', 'font-size: 200%;', `ver ${VER}`);
  };

  const loadGm = () => {
    const script = document.createElement('script');
    script.id = `${PRODUCT}Loader`;
    script.setAttribute('type', 'text/javascript');
    script.setAttribute('charset', 'UTF-8');
    script.append(`
    (() => {
      (${monkey.toString()})("${PRODUCT}");
    })();`);
    (document.head || document.documentElement).append(script);
  };

  loadGm();
})();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址