"S"キーでYouTubeのスクリーンショット保存
// ==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(); } })();