- // ==UserScript==
- // @name CapTube
- // @namespace https://github.com/segabito/
- // @description "S"キー連打でYouTubeのスクリーンショット保存
- // @include https://www.youtube.com/*
- // @include https://youtube.com/*
- // @version 0.0.2
- // @grant none
- // @license public domain
- // @noframes
- // ==/UserScript==
-
- (function() {
-
- let previewContainer = 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 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);
- });
-
- 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;*/
- }
-
- `).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() {
- var videoId = getVideoId();
- var title = document.querySelector('.watch-title');
- var authorName = toSafeName(document.querySelector('.yt-user-info a').text);
- var titleText = toSafeName(title.textContent);
- titleText = 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);
- console.timeEnd('createCanvasFromVideo');
- return canvas;
- };
-
- const getFileName = function(video) {
- const title = getVideoTitle();
- 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() {
- const video = document.querySelector('.html5-main-video');
- if (!video) { return; }
-
- const canvas = createCanvasFromVideo(video);
- const fileName = getFileName(video);
-
- const create = () => {
- createBlobLinkElementAsync(canvas, fileName).then(link => {
- document.body.appendChild(link);
- setTimeout(() => {
- link.click();
- setTimeout(() => { link.remove(); }, 1000);
- }, 0);
- });
- };
-
- if (window.requestIdleCallback) {
- window.requestIdleCallback(create);
- } else {
- setTimeout(create, 0);
- }
-
- if (previewContainer) {
- previewContainer.appendChild(canvas);
- setTimeout(() => {
- canvas.classList.add('is-removing');
- setTimeout(() => { canvas.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;
- };
-
- const initialize = function() {
- initDom();
- window.addEventListener('keydown', onKeyDown);
- window.addEventListener('keyup', onKeyUp);
- window.addEventListener('keypress', onKeyPress);
- };
-
- initialize();
- })();