CapTube

"S"キーでYouTubeのスクリーンショット保存

Aby zainstalować ten skrypt, wymagana jest instalacje jednego z następujących rozszerzeń: Tampermonkey, Greasemonkey lub Violentmonkey.

You will need to install an extension such as Tampermonkey to install this script.

Aby zainstalować ten skrypt, wymagana jest instalacje jednego z następujących rozszerzeń: Tampermonkey, Violentmonkey.

Aby zainstalować ten skrypt, wymagana będzie instalacja rozszerzenia Tampermonkey lub Userscripts.

You will need to install an extension such as Tampermonkey to install this script.

Aby zainstalować ten skrypt, musisz zainstalować rozszerzenie menedżera skryptów użytkownika.

(Mam już menedżera skryptów użytkownika, pozwól mi to zainstalować!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

Będziesz musiał zainstalować rozszerzenie menedżera stylów użytkownika, aby zainstalować ten styl.

Będziesz musiał zainstalować rozszerzenie menedżera stylów użytkownika, aby zainstalować ten styl.

Musisz zainstalować rozszerzenie menedżera stylów użytkownika, aby zainstalować ten styl.

(Mam już menedżera stylów użytkownika, pozwól mi to zainstalować!)

// ==UserScript==
// @name        CapTube
// @namespace   https://github.com/segabito/
// @description "S"キーでYouTubeのスクリーンショット保存
// @include     https://www.youtube.com/*
// @include     https://www.youtube.com/embed/*
// @include     https://youtube.com/*
// @version     0.0.10
// @grant       none
// @license     public domain
// ==/UserScript==

(function() {

  let previewContainer = null, meterContainer = null;
  const addStyle = function(styles, id) {
    var elm = document.createElement('style');
    elm.type = 'text/css';
    if (id) { elm.id = id; }

    var text = styles.toString();
    text = document.createTextNode(text);
    elm.appendChild(text);
    var head = document.getElementsByTagName('head');
    head = head[0];
    head.appendChild(elm);
    return elm;
  };

  const createWebWorker = function(func) {
    const src = func.toString().replace(/^function.*?\{/, '').replace(/}$/, '');
    const blob = new Blob([src], {type: 'text\/javascript'});
    const url = URL.createObjectURL(blob);

    return new Worker(url);
  };

  const callOnIdle = function(func) {
    if (window.requestIdleCallback) {
      window.requestIdleCallback(func);
    } else {
      setTimeout(func, 0);
    }
  };

  const DataUrlConv = (function() {
    const sessions = {};

    const func = function(self) {
      self.onmessage = function(e) {
        const dataURL   = e.data.dataURL;
        const sessionId = e.data.sessionId;

        const bin = atob(dataURL.split(',')[1]);
        const buf = new Uint8Array(bin.length);

        for (let i = 0, len = buf.length; i < len; i++) {
          buf[i] = bin.charCodeAt(i);
        }

        const blob = new Blob([buf.buffer], {type: 'image/png'});
        const objectURL = URL.createObjectURL(blob);
    
        self.postMessage({objectURL, sessionId});
      };
    };

    const worker = createWebWorker(func);
    worker.addEventListener('message', (e) => {
      const sessionId = e.data.sessionId;
      if (!sessions[sessionId]) { return; }

      (sessions[sessionId])(e.data.objectURL);
      delete sessions[sessionId];
    });

    return {
      toObjectURL: function(dataURL) {
        return new Promise(resolve => {
          const sessionId = 'id:' + Math.random();
          sessions[sessionId] = resolve;
          worker.postMessage({dataURL, sessionId});
        });
      }
    };
  })();


  const __css__ = (`
    #CapTubePreviewContainer {
      position: fixed;
      padding: 16px 0 0 16px;
      width: 90%;
      bottom: 100px;
      left: 5%;
      z-index: 10000;
      pointer-events: none;
      transform: translateZ(0);
      /*background: rgba(192, 192, 192, 0.4);*/
      border: 1px solid #ccc;
      -webkit-user-select: none;
      user-select: none;
    }

    #CapTubePreviewContainer:empty {
      display: none;
    }
      #CapTubePreviewContainer canvas {
        display: inline-block;
        width: 256px;
        margin-right: 16px;
        margin-bottom: 16px;
        outline: solid 1px #ccc;
        outline-offset: 4px;
        transform: translateZ(0);
        transition:
          1s opacity      linear,
          1s margin-right linear;
      }

      #CapTubePreviewContainer canvas.is-removing {
        opacity: 0;
        margin-right: -272px;
        /*width: 0;*/
      }

    #CapTubeMeterContainer {
      pointer-events: none;
      position: fixed;
      width: 26px;
      bottom: 100px;
      left: 16px;
      z-index: 10000;
      border: 1px solid #ccc;
      transform: translateZ(0);
      -webkit-user-select: none;
      user-select: none;
     }

     #CapTubeMeterContainer::after {
       content: 'queue';
       position: absolute;
       bottom: -2px;
       left: 50%;
       transform: translate(-50%, 100%);
       color: #666;
     }

    #CapTubeMeterContainer:empty {
      display: none;
    }

      #CapTubeMeterContainer .memory {
        display: block;
        width: 24px;
        height: 8px;
        margin: 1px 0 0;
        background: darkgreen;
        opacity: 0.5;
        border: 1px solid #ccc;
      }

  `).trim();

  addStyle(__css__);

  const getVideoId = function() {
    var id = '';
    location.search.substring(1).split('&').forEach(function(item){
      if (item.split('=')[0] === 'v') { id = item.split('=')[1]; }
    });
    return id;
  };

  const toSafeName = function(text) {
    text = text.trim()
      .replace(/</g, '<')
      .replace(/>/g, '>')
      .replace(/\?/g, '?')
      .replace(/:/g, ':')
      .replace(/\|/g, '|')
      .replace(/\//g, '/')
      .replace(/\\/g, '¥')
      .replace(/"/g, '”')
      .replace(/\./g, '.')
      ;
    return text;
  };

  const getVideoTitle = function(params = {title, videoId, author}) {
    var prefix = localStorage['CapTube-prefix']  || '';
    var videoId = params.videoId || getVideoId();
    var title = document.querySelector('.title yt-formatted-string') || document.querySelector('.watch-title') || {textContent: document.title};
    var authorName = toSafeName(
      params.author || document.querySelector('#owner-container yt-formatted-string').textContent || '');
    var titleText = toSafeName(params.title || title.textContent);
    titleText = prefix + titleText + ' - by ' + authorName + ' (v=' + videoId + ')';

    return titleText;
  };

  const createCanvasFromVideo = function(video) {
    console.time('createCanvasFromVideo');
    const width = video.videoWidth;
    const height = video.videoHeight;
    const canvas = document.createElement('canvas');
    canvas.width = width;
    canvas.height = height;
    const context = canvas.getContext('2d');
    context.drawImage(video, 0, 0);


    const thumbnail = document.createElement('canvas');
    thumbnail.width = 256;
    thumbnail.height = canvas.height * (256 / canvas.width);
    thumbnail.getContext('2d').drawImage(canvas, 0, 0, thumbnail.width, thumbnail.height);
    console.timeEnd('createCanvasFromVideo');

    return {canvas, thumbnail};
  };

  const getFileName = function(video, params = {title, videoId, author}) {
    const title = getVideoTitle(params);
    const currentTime = video.currentTime;
    const min = Math.floor(currentTime / 60);
    const sec = (currentTime % 60 + 100).toString().substr(1, 6);
    const time = `${min}_${sec}`;

    return `${title}@${time}.png`;
  };
  /*
  const createBlobLinkElement = function(canvas, fileName) {
    console.time('createBlobLinkElement');

    console.time('canvas.toDataURL');
    const dataURL = canvas.toDataURL('image/png');
    console.timeEnd('canvas.toDataURL');

    console.time('createObjectURL');
    const bin = atob(dataURL.split(',')[1]);
    const buf = new Uint8Array(bin.length);
    for (let i = 0, len = buf.length; i < len; i++) { buf[i] = bin.charCodeAt(i); }
    const blob = new Blob([buf.buffer], {type: 'image/png'});
    const url = window.URL.createObjectURL(blob);
    console.timeEnd('createObjectURL');

    const link = document.createElement('a');
    link.setAttribute('download', fileName);
    link.setAttribute('target', '_blank');
    link.setAttribute('href', url);

    console.timeEnd('createBlobLinkElement');
    return link;
  };
  */

  const createBlobLinkElementAsync = function(canvas, fileName) {
    //console.time('createBlobLinkElement');

    console.time('canvas to DataURL');
    const dataURL = canvas.toDataURL('image/png');
    console.timeEnd('canvas to DataURL');

    console.time('dataURL to objectURL');

    return DataUrlConv.toObjectURL(dataURL).then(objectURL => {
      console.timeEnd('dataURL to objectURL');

      const link = document.createElement('a');
      link.setAttribute('download', fileName);
      //link.setAttribute('target', '_blank');
      link.setAttribute('href', objectURL);

      //console.timeEnd('createBlobLinkElement');
      return Promise.resolve(link);
    });
   };

  const saveScreenShot = function(params = {title, videoId, author}) {
    const video = document.querySelector('.html5-main-video');
    if (!video) { return; }

    const meter = document.createElement('div');
    if (meterContainer) {
      meter.className = 'memory';
      meterContainer.appendChild(meter);
    }

    const {canvas, thumbnail} = createCanvasFromVideo(video);
    const fileName = getFileName(video, params);

    const create = () => {
      createBlobLinkElementAsync(canvas, fileName).then(link => {
        document.body.appendChild(link);
        link.click();
        setTimeout(() => {
          link.remove();
          meter.remove();
          URL.revokeObjectURL(link.getAttribute('href'));
        }, 1000);
      });
    };

    callOnIdle(create);

    if (!previewContainer) { return; }
    previewContainer.appendChild(thumbnail);
    setTimeout(() => {
      thumbnail.classList.add('is-removing');
      setTimeout(() => { thumbnail.remove(); }, 2000);
    }, 1500);
  };

  const setPlaybackRate = function(v) {
    const video = document.querySelector('.html5-main-video');
    if (!video) { return; }
    video.playbackRate = v;
  };

  const togglePlay = function() {
    const video = document.querySelector('.html5-main-video');
    if (!video) { return; }

    if (video.paused) {
      video.play();
    } else {
      video.pause();
    }
  };

  const seekBy = function(v) {
    const video = document.querySelector('.html5-main-video');
    if (!video) { return; }

    const ct = Math.max(video.currentTime + v, 0);
    video.currentTime = ct;
  };

  let isVerySlow = false;
  const onKeyDown = (e) => {
    const key = e.key.toLowerCase();
    switch (key) {
      case 'd':
        setPlaybackRate(0.1);
        isVerySlow = true;
        break;
      case 's':
        saveScreenShot({});
        break;
    }
  };

  const onKeyUp = (e) => {
    //console.log('onKeyUp', e);
    const key = e.key.toLowerCase();
    switch (key) {
      case 'd':
        setPlaybackRate(1);
        isVerySlow = false;
        break;
    }
  };

  const onKeyPress = (e) => {
    const key = e.key.toLowerCase();
    switch (key) {
      case 'w':
        togglePlay();
        break;
      case 'a':
        seekBy(isVerySlow ? -0.5 : -5);
        break;
    }
  };


  const initDom = function() {
    const div = document.createElement('div');
    div.id = 'CapTubePreviewContainer';
    document.body.appendChild(div);
    previewContainer = div;

    meterContainer = document.createElement('div');
    meterContainer.id = 'CapTubeMeterContainer';
    document.body.appendChild(meterContainer);
   };

  const HOST_REG = /^[a-z0-9]*\.nicovideo\.jp$/;

  const parseUrl = (url) => {
    const a = document.createElement('a');
    a.href = url;
    return a;
  };


  const initialize = function() {
    initDom();
    window.addEventListener('keydown',  onKeyDown);
    window.addEventListener('keyup',    onKeyUp);
    window.addEventListener('keypress', onKeyPress);
  };

  const initializeEmbed = function() {
    let parentHost = parseUrl(document.referrer).hostname;
    if (!HOST_REG.test(parentHost)) {
      window.console.log('disable bridge');
      return;
    }

    console.log('%cinit embed CapTube', 'background: lightgreen;');
    window.addEventListener('message', event =>  {
      if (!HOST_REG.test(parseUrl(event.origin).hostname)) { return; }
      let data = JSON.parse(event.data), command = data.command;

      switch (command) {
        case 'capture':
          saveScreenShot({
            title: data.title,
            videoId: data.videoId,
            author: data.author
          });
          break;
      }
    });

  };

  if (window.top !== window && location.pathname.indexOf('/embed/') === 0) {
    initializeEmbed();
  } else {
    initialize();
  }
})();