- // ==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();
- }
- })();