CapTube

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

目前为 2016-11-11 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name CapTube
  3. // @namespace https://github.com/segabito/
  4. // @description "S"キー連打でYouTubeのスクリーンショット保存
  5. // @include https://www.youtube.com/*
  6. // @include https://youtube.com/*
  7. // @version 0.0.2
  8. // @grant none
  9. // @license public domain
  10. // @noframes
  11. // ==/UserScript==
  12.  
  13. (function() {
  14.  
  15. let previewContainer = null;
  16. const addStyle = function(styles, id) {
  17. var elm = document.createElement('style');
  18. elm.type = 'text/css';
  19. if (id) { elm.id = id; }
  20.  
  21. var text = styles.toString();
  22. text = document.createTextNode(text);
  23. elm.appendChild(text);
  24. var head = document.getElementsByTagName('head');
  25. head = head[0];
  26. head.appendChild(elm);
  27. return elm;
  28. };
  29.  
  30. const createWebWorker = function(func) {
  31. const src = func.toString().replace(/^function.*?\{/, '').replace(/}$/, '');
  32. const blob = new Blob([src], {type: 'text\/javascript'});
  33. const url = URL.createObjectURL(blob);
  34.  
  35. return new Worker(url);
  36. };
  37.  
  38. const DataUrlConv = (function() {
  39. const sessions = {};
  40.  
  41. const func = function(self) {
  42. self.onmessage = function(e) {
  43. const dataURL = e.data.dataURL;
  44. const sessionId = e.data.sessionId;
  45.  
  46. const bin = atob(dataURL.split(',')[1]);
  47. const buf = new Uint8Array(bin.length);
  48.  
  49. for (let i = 0, len = buf.length; i < len; i++) {
  50. buf[i] = bin.charCodeAt(i);
  51. }
  52.  
  53. const blob = new Blob([buf.buffer], {type: 'image/png'});
  54. const objectURL = URL.createObjectURL(blob);
  55. self.postMessage({objectURL, sessionId});
  56. };
  57. };
  58.  
  59. const worker = createWebWorker(func);
  60. worker.addEventListener('message', (e) => {
  61. const sessionId = e.data.sessionId;
  62. if (!sessions[sessionId]) { return; }
  63.  
  64. (sessions[sessionId])(e.data.objectURL);
  65. });
  66.  
  67. return {
  68. toObjectURL: function(dataURL) {
  69. return new Promise(resolve => {
  70. const sessionId = 'id:' + Math.random();
  71. sessions[sessionId] = resolve;
  72. worker.postMessage({dataURL, sessionId});
  73. });
  74. }
  75. };
  76. })();
  77.  
  78.  
  79. const __css__ = (`
  80. #CapTubePreviewContainer {
  81. position: fixed;
  82. padding: 16px 0 0 16px;
  83. width: 90%;
  84. bottom: 100px;
  85. left: 5%;
  86. z-index: 10000;
  87. pointer-events: none;
  88. transform: translateZ(0);
  89. /*background: rgba(192, 192, 192, 0.4);*/
  90. border: 1px solid #ccc;
  91. -webkit-user-select: none;
  92. user-select: none;
  93. }
  94.  
  95. #CapTubePreviewContainer:empty {
  96. display: none;
  97. }
  98. #CapTubePreviewContainer canvas {
  99. display: inline-block;
  100. width: 256px;
  101. margin-right: 16px;
  102. margin-bottom: 16px;
  103. outline: solid 1px #ccc;
  104. outline-offset: 4px;
  105. transform: translateZ(0);
  106. transition:
  107. 1s opacity linear,
  108. 1s margin-right linear;
  109. }
  110.  
  111. #CapTubePreviewContainer canvas.is-removing {
  112. opacity: 0;
  113. margin-right: -272px;
  114. /*width: 0;*/
  115. }
  116.  
  117. `).trim();
  118.  
  119. addStyle(__css__);
  120.  
  121. const getVideoId = function() {
  122. var id = '';
  123. location.search.substring(1).split('&').forEach(function(item){
  124. if (item.split('=')[0] === 'v') { id = item.split('=')[1]; }
  125. });
  126. return id;
  127. };
  128.  
  129. const toSafeName = function(text) {
  130. text = text.trim()
  131. .replace(/</g, '<')
  132. .replace(/>/g, '>')
  133. .replace(/\?/g, '?')
  134. .replace(/:/g, ':')
  135. .replace(/\|/g, '|')
  136. .replace(/\//g, '/')
  137. .replace(/\\/g, '¥')
  138. .replace(/"/g, '”')
  139. .replace(/\./g, '.')
  140. ;
  141. return text;
  142. };
  143.  
  144. const getVideoTitle = function() {
  145. var videoId = getVideoId();
  146. var title = document.querySelector('.watch-title');
  147. var authorName = toSafeName(document.querySelector('.yt-user-info a').text);
  148. var titleText = toSafeName(title.textContent);
  149. titleText = titleText + ' - by ' + authorName + ' (v=' + videoId + ')';
  150.  
  151. return titleText;
  152. };
  153.  
  154. const createCanvasFromVideo = function(video) {
  155. console.time('createCanvasFromVideo');
  156. const width = video.videoWidth;
  157. const height = video.videoHeight;
  158. const canvas = document.createElement('canvas');
  159. canvas.width = width;
  160. canvas.height = height;
  161. const context = canvas.getContext('2d');
  162. context.drawImage(video, 0, 0);
  163. console.timeEnd('createCanvasFromVideo');
  164. return canvas;
  165. };
  166.  
  167. const getFileName = function(video) {
  168. const title = getVideoTitle();
  169. const currentTime = video.currentTime;
  170. const min = Math.floor(currentTime / 60);
  171. const sec = (currentTime % 60 + 100).toString().substr(1, 6);
  172. const time = `${min}_${sec}`;
  173.  
  174. return `${title}@${time}.png`;
  175. };
  176. /*
  177. const createBlobLinkElement = function(canvas, fileName) {
  178. console.time('createBlobLinkElement');
  179.  
  180. console.time('canvas.toDataURL');
  181. const dataURL = canvas.toDataURL('image/png');
  182. console.timeEnd('canvas.toDataURL');
  183.  
  184. console.time('createObjectURL');
  185. const bin = atob(dataURL.split(',')[1]);
  186. const buf = new Uint8Array(bin.length);
  187. for (let i = 0, len = buf.length; i < len; i++) { buf[i] = bin.charCodeAt(i); }
  188. const blob = new Blob([buf.buffer], {type: 'image/png'});
  189. const url = window.URL.createObjectURL(blob);
  190. console.timeEnd('createObjectURL');
  191.  
  192. const link = document.createElement('a');
  193. link.setAttribute('download', fileName);
  194. link.setAttribute('target', '_blank');
  195. link.setAttribute('href', url);
  196.  
  197. console.timeEnd('createBlobLinkElement');
  198. return link;
  199. };
  200. */
  201.  
  202. const createBlobLinkElementAsync = function(canvas, fileName) {
  203. //console.time('createBlobLinkElement');
  204.  
  205. console.time('canvas to DataURL');
  206. const dataURL = canvas.toDataURL('image/png');
  207. console.timeEnd('canvas to DataURL');
  208.  
  209. console.time('dataURL to objectURL');
  210.  
  211. return DataUrlConv.toObjectURL(dataURL).then(objectURL => {
  212. console.timeEnd('dataURL to objectURL');
  213.  
  214. const link = document.createElement('a');
  215. link.setAttribute('download', fileName);
  216. link.setAttribute('target', '_blank');
  217. link.setAttribute('href', objectURL);
  218.  
  219. //console.timeEnd('createBlobLinkElement');
  220. return Promise.resolve(link);
  221. });
  222. };
  223.  
  224. const saveScreenShot = function() {
  225. const video = document.querySelector('.html5-main-video');
  226. if (!video) { return; }
  227.  
  228. const canvas = createCanvasFromVideo(video);
  229. const fileName = getFileName(video);
  230.  
  231. const create = () => {
  232. createBlobLinkElementAsync(canvas, fileName).then(link => {
  233. document.body.appendChild(link);
  234. setTimeout(() => {
  235. link.click();
  236. setTimeout(() => { link.remove(); }, 1000);
  237. }, 0);
  238. });
  239. };
  240.  
  241. if (window.requestIdleCallback) {
  242. window.requestIdleCallback(create);
  243. } else {
  244. setTimeout(create, 0);
  245. }
  246.  
  247. if (previewContainer) {
  248. previewContainer.appendChild(canvas);
  249. setTimeout(() => {
  250. canvas.classList.add('is-removing');
  251. setTimeout(() => { canvas.remove(); }, 2000);
  252. }, 1500);
  253. }
  254. };
  255.  
  256. const setPlaybackRate = function(v) {
  257. const video = document.querySelector('.html5-main-video');
  258. if (!video) { return; }
  259. video.playbackRate = v;
  260. };
  261.  
  262. const togglePlay = function() {
  263. const video = document.querySelector('.html5-main-video');
  264. if (!video) { return; }
  265.  
  266. if (video.paused) {
  267. video.play();
  268. } else {
  269. video.pause();
  270. }
  271. };
  272.  
  273. const seekBy = function(v) {
  274. const video = document.querySelector('.html5-main-video');
  275. if (!video) { return; }
  276.  
  277. const ct = Math.max(video.currentTime + v, 0);
  278. video.currentTime = ct;
  279. };
  280.  
  281. let isVerySlow = false;
  282. const onKeyDown = (e) => {
  283. const key = e.key.toLowerCase();
  284. switch (key) {
  285. case 'd':
  286. setPlaybackRate(0.1);
  287. isVerySlow = true;
  288. break;
  289. case 's':
  290. saveScreenShot();
  291. break;
  292. }
  293. };
  294.  
  295. const onKeyUp = (e) => {
  296. //console.log('onKeyUp', e);
  297. const key = e.key.toLowerCase();
  298. switch (key) {
  299. case 'd':
  300. setPlaybackRate(1);
  301. isVerySlow = false;
  302. break;
  303. }
  304. };
  305.  
  306. const onKeyPress = (e) => {
  307. const key = e.key.toLowerCase();
  308. switch (key) {
  309. case 'w':
  310. togglePlay();
  311. break;
  312. case 'a':
  313. seekBy(isVerySlow ? -0.5 : -5);
  314. break;
  315. }
  316. };
  317.  
  318.  
  319. const initDom = function() {
  320. const div = document.createElement('div');
  321. div.id = 'CapTubePreviewContainer';
  322. document.body.appendChild(div);
  323. previewContainer = div;
  324. };
  325.  
  326. const initialize = function() {
  327. initDom();
  328. window.addEventListener('keydown', onKeyDown);
  329. window.addEventListener('keyup', onKeyUp);
  330. window.addEventListener('keypress', onKeyPress);
  331. };
  332.  
  333. initialize();
  334. })();